Skip to content

syarul/requrse

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

93 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

reQurse

Lightweight driven query language

NPM Version requrse CI Coverage Status

What is reQurse

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!

Features

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);

Usage

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' } } }

Advance usage

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
//   }
// ]

More Samples

You can check samples folder to see more usage cases with Mongoose, Redis and the Starwars examples.

About

Lightweight driven query language

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages