Lightweight driven query language
reQurse introduces an innovative approach that overcomes the complexities of CRUD operations. The focus is on delivering a streamlined and efficient CRUD library solution, simplifying API development, and effortlessly handling complex data management tasks. reQurse utilized JSON-based queries, allows multi-tenant API sources, avoid writing lengthy procedural APIs and truly embrace Javascript core philosophy as OOP language. This approach promotes a modular and streamlined code structure, retaining the complexity of Object tree while enhancing flexibility and maintainability.
Here's the first example to get you started. Try it here—no build step required!
| Feature | reQurse |
|---|---|
| Query Syntax | âś… JSON object-based queries |
| Schema & Typing | ❌ No type enforcement (support GraphQL schema) |
| Resolvers | âś… methods functions resolve keys |
| API Federation | âś… Can compose multiple instance of rq() into one federated API |
| Caching | âś… Implemented (pass in options) |
| Use Cases | âś… Full APIs, Lightweight, data orchestration |
| Runtime | âś… Client-server over HTTP/In-process library calls |
| Complexity | âś… Very light; no server setup |
As direct comparison with graphQL using the RandomDie sample
// graphQL
const RandomDie = new GraphQLObjectType({
name: "RandomDie",
fields: () => {
const fields = {
numSides: {
type: new GraphQLNonNull(GraphQLInt),
resolve: (die) => die.numSides,
},
rollOnce: {
type: new GraphQLNonNull(GraphQLInt),
resolve: (die) => 1 + Math.floor(Math.random() * die.numSides),
},
roll: {
type: new GraphQLList(GraphQLInt),
args: {
numRolls: { type: new GraphQLNonNull(GraphQLInt) },
},
resolve: (die, { numRolls }, ctx, info) => {
const rollOnceResolver = fields.rollOnce.resolve;
const output = [];
for (let i = 0; i < numRolls; i++) {
output.push(rollOnceResolver(die, {}, ctx, info));
}
return output;
},
},
};
return fields;
},
});
const QueryType = new GraphQLObjectType({
name: "Query",
fields: {
getDie: {
type: RandomDie,
args: {
numSides: { type: GraphQLInt },
},
resolve: (_, { numSides }) => {
return { numSides: numSides || 6 };
},
},
},
});
const schema = new GraphQLSchema({
query: QueryType,
});
const query = `
{
getDie(numSides: 6) {
numSides
rollOnce
roll(numRolls: 3)
}
}
`;
graphql({
schema,
source: query,
}).then(console.log);
// {
// "data": {
// "getDie": {
// "numSides": 6,
// "rollOnce": 1,
// "roll": [
// 2,
// 5,
// 1
// ]
// }
// }
// }// rq
import { RqExtender } from "requrse";
class RandomDie extends RqExtender {
constructor() {
super();
}
rollOnce(die) {
return 1 + Math.floor(Math.random() * die.numSides);
}
roll(die, { numRolls }) {
const output = [];
for (let i = 0; i < numRolls; i++) {
// reuse rollOnce here
// context is using rq context
// {
// query, // combine queryResult
// computes, // computed fields
// }
output.push(this.computes.rollOnce(die));
}
return output;
}
}
const getDie = new RandomDie();
const query = {
data: {
getDie: {
$params: {
numSides: 6,
},
numSides: 1,
rollOnce: 1,
roll: {
$params: {
numRolls: 3,
},
},
},
},
};
getDie.compute(query).then(console.log);
// {
// "data": {
// "getDie": {
// "numSides": 6,
// "rollOnce": 2,
// "roll": [
// 5,
// 5,
// 2
// ]
// }
// }
// }You also can pass graphQL query instead (i.e. using graphQL client)
const query = `
{
getDie(numSides: 6) {
numSides
rollOnce
roll(numRolls: 3)
}
}
`;
getDie.compute(query, { rootKey: "data" }).then(console.log);A basic usage of reQurse.
import rq, { RqExtender } from "requrse";
rq(query, { methods, config, dataUrl, rootKey, cache, cacheDir });- query: (object) required JSON like query.
- methods: (object) required define methods/computed fields that exist in the query.
- config: (object) optional extend and added parameterize control over methods.
- dataUrl: (string) optional resolve result to data url path.
- rootKey: (string) optional graphQL root key if using graphQL query, default to 'data' if not given.
- cache: (number) optional cache result in second(s).
- cacheDir: (string) optional custom caching directory default is '.tmp'.
await rq(
{
Test: {
test: {
greeting: "*",
},
},
},
{
methods: {
greeting() {
return "hello world";
},
},
},
).then(console.log, console.error);
// { Test: { test: { greeting: 'hello world' } } }A proper query should do more, to demystify the capability of this library, create a few data samples, you can imagine this as a setup that your database may have.
const acolyte = { id: "0", progression: ["1", "4"], name: "Acolyte" };
const priest = { id: "1", progression: ["4"], name: "Priest" };
const squire = { id: "2", progression: ["3", "4"], name: "Squire" };
const paladin = { id: "3", progression: ["4"], name: "Paladin" };
const inquisitor = { id: "4", progression: [], name: "Inquisitor" };
// we also create the relations between them
const supportData = {
0: acolyte,
1: priest,
4: inquisitor,
};
const vanguardData = {
2: squire,
3: paladin,
4: inquisitor,
};Then the helper functions to access these data
/**
* Helper function to get a class by ID.
*/
function getClass(id) {
// Returning a promise just to illustrate query support.
return Promise.resolve(supportData[id] || vanguardData[id]);
}
/**
* Allows us to query for a classes's progression.
*/
function getProgression(classes) {
return classes.progression.map((id) => getClass(id));
}
/**
* Allows us to query for the support class with the given id.
*/
function getSupport(id) {
return supportData[id];
}
/**
* Allows us to query for the vanguard class with the given id.
*/
function getVanguard(id) {
return vanguardData[id];
}
/**
* Allows us to query for the player class by gameId.
*/
function getPlayer(gameId) {
if (gameId === 0) {
return acolyte;
}
return inquisitor;
}Then configure reQurse to use these methods
const confParams = {
getPlayer,
getClass,
getProgression,
getSupport,
getVanguard,
};
const config = (param) => confParams[param];
const methods = {
player: "getPlayer",
class: "getClass",
progression: "getProgression",
support: "getSupport",
vanguard: "getVanguard",
};Simple usage
await rq(
{
PlayerClass: {
player: {
name: 1,
},
},
},
{ methods, config },
).then(console.log);
// { PlayerClass: { player: { name: 'Inquisitor' } } }Use $params to filter result
await rq(
{
PlayerClass: {
player: {
$params: { gameId: 0 },
name: 1,
},
},
},
{ methods, config },
).then(console.log);
// { PlayerClass: { player: { name: 'Acolyte' } } }Optimize your query by writing efficient methods, i.e., here progression return the class next progression seamlessly
await rq(
{
PlayerClass: {
player: {
$params: { gameId: 0 },
id: 1,
name: 1,
progression: {
name: 1,
},
},
},
},
{ methods, config },
).then(console.log);
// {
// PlayerClass: {
// player: {
// id: '0',
// name: 'Acolyte',
// progression: [
// { name: 'Priest' },
// { name: 'Inquisitor' }
// ]
// }
// }
// }You can have multiple same dataset key name by / naming
await rq(
{
vanguard: {
"vanguard/paladin": {
$params: { id: 3 },
name: 1,
},
"vanguard/inquisitor": {
$params: { id: 4 },
name: 1,
},
},
},
{ methods, config },
).then(console.log);
// {
// vanguard: {
// 'vanguard/paladin': { name: 'Paladin' },
// 'vanguard/inquisitor': { name: 'Inquisitor' }
// }
// }Now we expand the dataset to the inventory of the player
const healingPotion = {
id: "0",
effect: "heal",
dmg: 4,
name: "Healing Potion",
};
const bandage = { id: "1", effect: "heal", dmg: 1, name: "Bandage" };
const holyWater = { id: "2", effect: "cleansing", dmg: 2, name: "Holy Water" };
// add relations to the inventory data
const itemData = {
0: healingPotion,
1: bandage,
2: holyWater,
};
// add relations to how many each class have these items in their inventory
const inventoryData = {
0: [7, 1, 0],
1: [3, 2, 2],
2: [0, 5, 0],
3: [1, 6, 2],
4: [0, 0, 10],
};Demonstrate usage of method/computed field to return value that you need, in this case count which came from a relational collection that store the value only, you can use such logic to build a powerful query for your api.
/**
* Helper function to get an item by ID.
*/
function getItem(count, id) {
// Returning a promise just to illustrate query support.
return Promise.resolve({ ...itemData[id], count });
}
/**
* Allows us to query for the player class inventoryData.
*/
function getInventory({ id }) {
return inventoryData[id].map(getItem);
}Extends the reQurse methods/config
const extConfig = {
methods: {
...methods,
item: "getItem",
inventory: "getInventory",
},
config: (param) =>
({
...confParams,
getItem,
getInventory,
})[param],
};Now see how it perform!
await rq(
{
PlayerClass: {
player: {
$params: { gameId: 0 },
name: 1,
inventory: {
id: 1,
name: 1,
count: 1,
},
},
},
},
extConfig,
).then(console.log);
// {
// PlayerClass: {
// player: {
// name: "Acolyte",
// inventory: [
// {
// id: "0",
// name: "Healing Potion",
// count: 7
// },
// {
// id: "1",
// name: "Bandage",
// count: 1
// },
// {
// id: "2",
// name: "Holy Water",
// count: 0
// }
// ]
// }
// }
// }You can also return as dataUrl
await rq(
{
PlayerClass: {
player: {
$params: { gameId: 0 },
name: 1,
inventory: {
id: 1,
name: 1,
count: 1,
},
},
},
},
{ ...extConfig, dataUrl: "PlayerClass/player/inventory" },
).then(console.log);
// [
// {
// id: '0',
// name: 'Healing Potion',
// count: 7
// },
// {
// id: '1',
// name: 'Bandage',
// count: 1
// },
// {
// id: '2',
// name: 'Holy Water',
// count: 0
// }
// ]You can check samples folder to see more usage cases with Mongoose, Redis and the Starwars examples.