diff --git a/.gitignore b/.gitignore index e434e5f..6bed957 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ node_modules .env .npmignore coverage -.npmrc \ No newline at end of file +.npmrc +.tmp +test/data.json \ No newline at end of file diff --git a/README.md b/README.md index 6e3bbe2..135e9c4 100644 --- a/README.md +++ b/README.md @@ -12,46 +12,51 @@ Lightweight driven query language Here's the first example to get you started. [Try it here](https://codepen.io/syarul/pen/xxmLMVP)—no build step required! -## Comparison to graphQL +## Features -| Feature | GraphQL | reQurse | -| --------------- | --------------------------------------------------------- | ----------------------------------------------------- | -| Query Syntax | ✅ Declarative queries as strings (SDL) | ✅ JSON object-based queries | -| Schema & Typing | ✅ Strongly typed schemas | ❌ No type enforcement (support GraphQL schema) | -| Resolvers | ✅ Defined server-side logic for each field | ✅ `methods` functions resolve keys | -| API Federation | ✅ Built-in with tools like Apollo Federation | ✅ Can compose multiple `rq()` into one federated API | -| Use Cases | ✅ Full APIs, strongly validated, efficient data fetching | ✅ Full APIs, Lightweight, data orchestration | -| Runtime | ✅ Client-server over HTTP | ✅ Client-server over HTTP/In-process library calls | -| Complexity | ❌ Requires schema + resolvers + server setup | ✅ Very light; no server setup | +| 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 example using the [RandomDie](https://www.graphql-js.org/docs/object-types/) sample +As direct comparison with graphQL using the [RandomDie](https://www.graphql-js.org/docs/object-types/) sample ```js // graphQL const RandomDie = new GraphQLObjectType({ name: "RandomDie", - 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) }, + fields: () => { + const fields = { + numSides: { + type: new GraphQLNonNull(GraphQLInt), + resolve: (die) => die.numSides, }, - resolve: (die, { numRolls }) => { - const output = []; - for (let i = 0; i < numRolls; i++) { - output.push(1 + Math.floor(Math.random() * die.numSides)); - } - return output; + 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; }, }); @@ -117,7 +122,7 @@ class RandomDie extends RqExtender { for (let i = 0; i < numRolls; i++) { // reuse rollOnce here // context is using rq context - // { + // { // query, // combine queryResult // computes, // computed fields // } @@ -184,12 +189,16 @@ A basic usage of reQurse. ```javascript import rq from "requrse"; -rq(query, { methods, config }); +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'. ```js await rq( diff --git a/README_OLD.md b/README_OLD.md deleted file mode 100644 index 835ec41..0000000 --- a/README_OLD.md +++ /dev/null @@ -1,255 +0,0 @@ -# reQurse -Lightweight driven query language - -[![NPM Version](https://img.shields.io/npm/v/requrse.svg)](https://www.npmjs.com/package/requrse) -[![requrse CI](https://github.com/syarul/requrse/actions/workflows/main-ci.yml/badge.svg)](https://github.com/syarul/requrse/actions/workflows/main-ci.yml) -[![Coverage Status](https://coveralls.io/repos/github/syarul/requrse/badge.svg?branch=main)](https://coveralls.io/github/syarul/requrse?branch=main) - -## 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](https://codepen.io/syarul/pen/xxmLMVP)—no build step required! - -> This library take some inspirations from NextQL and GraphQL - -## Usage -You can check [samples](https://github.com/syarul/requrse/blob/main/samples) folder to see usage cases with [Mongoose](https://github.com/syarul/requrse/blob/main/samples/mongoose), [Redis](https://github.com/syarul/requrse/blob/main/samples/redis) and the [Starwars](https://github.com/syarul/requrse/blob/main/samples/starwars) examples. - -A basic usage of reQurse. -```javascript -import rq from 'requrse' - -rq(query, { methods, config }) -``` -- **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. -```js -await rq({ - Test: { - test: { - greeting: '*' - } - } -}, -{ - methods: { - greeting () { - return 'hello world' - } - } -}).then(console.log, console.error) -// { Test: { test: { greeting: 'hello world' } } } -``` - -By default methods will automatically resolve promises. -```js -await rq({ - Test: { - test: { - person: { - name: 1 - } - } - } -}, -{ - methods: { - person () { - return Promise.resolve({ name: 'Foo', age: 12 }) - } - } -}).then(console.log, console.error) -// { Test: { test: { person: { name: 'Foo' } } } } -``` - -You can pass arguments using `$params` parameter. -```js -await rq({ - Test: { - test: { - person: { - $params: { name: 'Bar', age: 30 }, - name: 1, - age: 1 - } - } - } -}, -{ - methods: { - person (name, age) { - return { name, age } - } - } -}).then(console.log, console.error) -// { Test: { test: { person: { name: 'Bar', age: 30 } } } } -``` -Not limited to database queries, you can also manage API endpoints too -```js -rq({ - Test: { - test: { - request: { - $params: { - url: 'https://api.github.com/users/douglascrockford' - }, - status: 1, - data: { - id: 1, - login: 1 - } - }, - } - } -}, -{ - methods: { - request: (url) => axios.get(url) - } -}).then(console.log, console.error) -// { -// Test: { -// test: { -// request: { status: 200, data: { id: 262641, login: 'douglascrockford' } } -// } -// } -// } -``` - -You can add options `config` to map methods with different name. This allow a consistent structure of the query. -```js -await rq({ - Test: { - test: { - person: { - $params: { age: 30 }, - name: 1, - age:1 - } - } - } -}, -{ - methods: { - person: 'getPerson' - }, - config: (param) => ({ - getPerson (age) { - return { name: 'Foo', age } - } - })[param] -}).then(console.log, console.error) -// { Test: { test: { person: { name: 'Foo', age: 30 } } } } -``` - -With `config` you can specify custom parameter to map result or use it as input for your methods. -```js -await rq({ - Test: { - test: { - occupation: 1, - person: { - $params: { age: 30 }, - name: 1, - age:1, - occupation: 1 - } - } - } -}, -{ - methods: { - occupation(){ - return { type: 'Copywriter', started: '2020', city: 'NY' } - }, - // 'type' is custom key where methods have access to them as arguments, you can add multiple keys with '|' - person: 'getPerson,type' - }, - config: (param) => ({ - getPerson (occupation, { age }, [ $param ]) { - return { - name: 'Foo', - age, - occupation: { - [$param]: occupation[$param] - } - } - } - })[param] -}).then(console.log, console.error) -// { -// Test: { -// test: { -// occupation: 1, -// person: { name: 'Foo', age: 30, occupation: { type: 'Copywriter' } } -// } -// } -// } -``` - -The query tree is resolve recursively, so you can have very complex query structure. -```js -await rq({ - Test: { - test: { - person: { - $params: { name: 'Foo' }, - name: 1, - age:1, - birth: { - year: 1, - area: { - city: 1 - } - }, - occupation: { - type: 1, - }, - } - } - } -}, -{ - methods: { - area: 'area', - occupation: 'occupation', - person: 'getPerson', - birth: 'birth' - }, - config: (param) => ({ - area() { - return { city: 'NY' } - }, - occupation () { - return { type: 'CT0' } - }, - birth () { - return { year: '1981' } - }, - getPerson (name) { - return { name, age: 42 } - } - })[param] -}).then(console.log, console.error) -// { -// Test: { -// test: { -// person: { -// name: 'Foo', -// age: 42, -// birth: { year: '1981', area: { city: 'NY' } }, -// occupation: { type: 'CT0' } -// } -// } -// } -// } -``` - -## Sample usage with lookup table and model caching - -Using **reQurse** for lookup queries offers greater flexibility and memory efficiency compared to the standard database lookup method. With reQurse, you can avoid resource exhaustion issues like timeouts, especially when dealing with complex data structures and custom projections. To see an implementation example, check out the [Mongoose Lookup](https://github.com/syarul/requrse/blob/main/samples/mongoose/mongoose-lookup.test.mjs) sample. - -Additionally, **reQurse** provides support for model caching, eliminating the need to repeatedly declare the model for each query. This caching feature streamlines your querying process. For a practical use case, refer to the [Mongoose Middleware](https://github.com/syarul/requrse/blob/main/samples/mongoose/mongoose.middleware.js) query for usage case. - -> P/S: If your Entity-Relationship Diagram (ERD) is massive and you anticipate having more, it's better to convert your queries into JSON format and store them elsewhere, for example, in a Redis database or AWS DynamoDB. \ No newline at end of file diff --git a/libs/arrayToObject.cjs b/libs/arrayToObject.cjs index bc4ce47..51e84ab 100644 --- a/libs/arrayToObject.cjs +++ b/libs/arrayToObject.cjs @@ -7,7 +7,9 @@ const checkEntry = require("./checkEntry.cjs"); * @returns {Boolean} */ const checkUniq = (arr) => { - const check = arr.map((i) => i && i?.[0]).filter((f) => f !== undefined); + const check = arr + .map((/** @type {any[]} */ i) => i && i?.[0]) + .filter((/** @type {any} */ f) => f !== undefined); return check.length === [...new Set(check)].length; }; @@ -17,7 +19,10 @@ const checkUniq = (arr) => { * @returns */ const reducer = (unique) => { - return (acc, item) => { + return ( + /** @type {Record} */ acc, + /** @type {any[]} */ item, + ) => { if (item && item[0]) { if (unique) { acc[item[0]] = arrayToObject(item[1]); diff --git a/libs/buildArgs.cjs b/libs/buildArgs.cjs index afeea11..ea757ad 100644 --- a/libs/buildArgs.cjs +++ b/libs/buildArgs.cjs @@ -3,11 +3,12 @@ /** * Checks if an object is an associative object (not an array) and removes undefined values. * - * @param {object} obj - The object to check. - * @returns {object} A new object with undefined values removed. + * @param {Record} obj - The object to check. + * @returns {object | undefined} A new object with undefined values removed. */ const chk = (obj) => { if (obj instanceof Object && !(obj instanceof Array)) { + /** @type {Record} */ const newObj = {}; Object.keys(obj).forEach((key) => { if (obj[key] !== undefined) { @@ -28,8 +29,8 @@ const isObj = (obj) => !(obj instanceof Array) && typeof obj === "object"; /** * Reduces an array of objects into a single object, removing undefined values. * - * @param {object[]} arr - The array of objects to reduce. - * @returns {object | object[]} A single object or an array of objects with undefined values removed. + * @param {any[] | null} arr - The array of objects to reduce. + * @returns {any} A single object or an array of objects with undefined values removed. */ const reducer = (arr) => { if (arr instanceof Array && !arr.some((a) => typeof a !== "object")) { @@ -71,14 +72,15 @@ const deepMerge = (target, ...sources) => { /** * Combines and prepares arguments for a function call. - * + * @this {import("./executeQuery.cjs").MergeQuery} * @param {object | undefined} $vParams - Additional parameters. * @param {object} params - Main parameters. - * @param {...object} currentQuery - Current query objects. + * @param {...object | null} currentQuery - Current query objects. * @returns {object[]} An array of arguments for the function call. */ const buildArgs = function ($vParams, params, ...currentQuery) { - const args = [].concat(reducer(currentQuery)); + /** @type {any[]} */ + const args = currentQuery && [].concat(reducer(currentQuery)); if ($vParams) { args.push($vParams); if (args.length === 1) { diff --git a/libs/cache.cjs b/libs/cache.cjs new file mode 100644 index 0000000..b3adfdc --- /dev/null +++ b/libs/cache.cjs @@ -0,0 +1,69 @@ +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); + +/** + * Generates a hash for the given parsed query object. + * @param {object} parseQuery + * @returns {string} + */ +function getHash(parseQuery) { + return crypto + .createHash("sha256") + .update(JSON.stringify(parseQuery)) + .digest("hex"); +} + +/** + * Handles caching for parsed queries. + * @param {'create' | 'get'} action + * @param {object} parseQuery + * @param {import("./executor.cjs").QueryOptions} options + * @param {*} [processed] + * @returns {Promise|undefined} + */ +function cache(action, parseQuery, options, processed) { + if (!options.cache) return; + + const hash = getHash(parseQuery); + const tmpDir = options.cacheDir || path.join(__dirname, "../.tmp"); + const tmpFile = path.join(tmpDir, `${hash}.tmp`); + const tmpFileExpiry = path.join(tmpDir, `${hash}.expire.tmp`); + + if (!fs.existsSync(tmpDir)) { + fs.mkdirSync(tmpDir); + } + + if (action === "create" && processed !== undefined) { + try { + fs.writeFileSync(tmpFile, JSON.stringify(processed), "utf8"); + fs.writeFileSync( + tmpFileExpiry, + `${Math.floor(Date.now() / 1000)}`, + "utf8", + ); + } catch {} + return Promise.resolve({}); + } + + if (fs.existsSync(tmpFileExpiry)) { + try { + const expiryTime = parseInt(fs.readFileSync(tmpFileExpiry, "utf8"), 10); + if (Math.floor(Date.now() / 1000) - expiryTime > options.cache) { + fs.unlinkSync(tmpFileExpiry); + if (fs.existsSync(tmpFile)) { + fs.unlinkSync(tmpFile); + } + } + } catch {} + } + + if (fs.existsSync(tmpFileExpiry) && fs.existsSync(tmpFile)) { + try { + const cached = fs.readFileSync(tmpFile, "utf8"); + return Promise.resolve(JSON.parse(cached)); + } catch {} + } +} + +module.exports = cache; diff --git a/libs/executeQuery.cjs b/libs/executeQuery.cjs index c94e903..5526dd7 100644 --- a/libs/executeQuery.cjs +++ b/libs/executeQuery.cjs @@ -14,7 +14,7 @@ const mapResult = require("./mapResult.cjs"); * @property {CurrentQuery} currentQuery * @property {ResultQuery} resultQuery * @property {MergeQuery} mergeQuery - * @property {Boolean | undefined} failedComputed + * @property {Boolean | undefined} [failedComputed] */ /** @@ -166,11 +166,10 @@ function handleOther({ mergeQuery, config, $vParams, - params, }) { currentQuery = compute; // resolve recurrence - if (typeof currentQuery === "string" && config(currentQuery)) { + if (config && typeof currentQuery === "string" && config(currentQuery)) { currentQuery = config(currentQuery); } if (!currentQuery && $vParams) { @@ -244,7 +243,7 @@ async function valueIsObject({ /** * @typedef Result - * @property {Promise} currentQuery + * @property {CurrentQuery} currentQuery * @property {ResultQuery} resultQuery * @property {Boolean | undefined} [computed] * @property {Boolean | undefined} [failedComputed] @@ -314,43 +313,33 @@ async function getResult({ const processHandler = ({ key, compute, - $params, currentQuery, - resultQuery, mergeQuery, - $vParams, - params, - args, - config, - methods, + $params, + ...res }) => { mergeQuery.key = mergeQuery?.key || key; // model cache support mergeQuery = merge(mergeQuery, currentQuery); - const checks = { - 0: - typeof compute === "function" && - $params.currentQuery && - !$params.currentQuery[key], - 1: typeof compute === "function", - }; - const handlers = { - 0: handleComputeParams, - 1: handleCompute, - }; - - const match = Object.keys(checks).find((key) => checks[parseInt(key)]); + let handler; + if ( + typeof compute === "function" && + $params.currentQuery && + !$params.currentQuery[key] + ) { + handler = handleComputeParams; + } else if (typeof compute === "function") { + handler = handleCompute; + } else { + handler = handleOther; + } - return (handlers?.[match] || handleOther)({ + return handler({ + key, compute, currentQuery, - resultQuery, mergeQuery, - $vParams, - params, - key, - args, - config, - methods, + $params, + ...res, }); }; @@ -443,7 +432,14 @@ function handleResult({ * @returns {function} */ function chainReducer(options) { - return ({ buildEntries, resultQuery, currentQuery, mergeQuery }) => { + return ( + /** @type {ChainReducerPayload} */ { + buildEntries, + resultQuery, + currentQuery, + mergeQuery, + }, + ) => { const params = genParams(currentQuery, options); const res = processHandler({ ...options, @@ -481,7 +477,10 @@ function chainReducer(options) { * @returns */ function entryReducer(options) { - return (promiseChain, [_key, value]) => + return ( + /** @type {any} */ promiseChain, + /** @type {[string, any]} */ [_key, value], + ) => promiseChain.then( chainReducer({ _key, @@ -524,6 +523,8 @@ const handleEntries = ({ query, currentQuery, mergeQuery, options }) => /** @typedef {Record} MergeQuery */ /** @typedef {any[]} BuildEntries */ +/** @typedef {{ buildEntries: BuildEntries, resultQuery: ResultQuery, currentQuery: CurrentQuery, mergeQuery: MergeQuery }} ChainReducerPayload */ + /** * Executes a query with the provided configuration. * @@ -539,6 +540,9 @@ const executeQuery = async (query, currentQuery, options, mergeQuery = {}) => currentQuery, mergeQuery, options, - }).then(({ buildEntries }) => buildEntries); + }).then( + (/** @type {{ buildEntries: BuildEntries }} */ { buildEntries }) => + buildEntries, + ); module.exports = executeQuery; diff --git a/libs/executor.cjs b/libs/executor.cjs index d1d2832..8beeff3 100644 --- a/libs/executor.cjs +++ b/libs/executor.cjs @@ -3,6 +3,8 @@ const executeQuery = require("./executeQuery.cjs"); const arrayToObject = require("./arrayToObject.cjs"); const dataPath = require("./dataPath.cjs"); const gqlToJson = require("./gqlToJson.cjs"); +const copy = require("copy-props"); +const cache = require("./cache.cjs"); /** * Using waterfall structure for better readability @@ -19,15 +21,21 @@ function parser(query, options) { const result = {}; const current = result; - query.computes.reduce((acc, item) => { - if (typeof item === "string") { - acc[item] = {}; - return acc[item]; - } else if (typeof item === "object" && item !== null) { - Object.assign(acc, item); - return acc; - } - }, current); + query.computes.reduce( + ( + /** @type {{ [x: string]: any; }} */ acc, + /** @type {string | number | null} */ item, + ) => { + if (typeof item === "string") { + acc[item] = {}; + return acc[item]; + } else if (typeof item === "object" && item !== null) { + Object.assign(acc, item); + return acc; + } + }, + current, + ); return { [query.name]: result, @@ -41,13 +49,21 @@ function parser(query, options) { * Options for query execution. * * @typedef {object} QueryOptions - * @property {object} methods - Methods configuration. - * @property {object} [config] - Optional, configuration settings. + * @property {Record} methods - Methods configuration. + * @property {Function} [config] - Optional, configuration settings. * @property {string} [dataUrl] - Optional, data url path. * @property {string} [rootKey] - Optional, graphQL root key name of Query if using graphQL query payload instead of JSON. + * @property {number} [cache] - how long the cache lives in seconds + * @property {string} [cacheDir] - Optional, custom caching directory default is '.tmp'. */ +/** + * + * @param {QueryOptions} options + * @returns + */ function postProcessing(options) { + /** @param {import("./executeQuery.cjs").BuildEntries} result */ return (result) => { if (options.dataUrl) { return dataPath(arrayToObject(result), options.dataUrl); @@ -65,28 +81,51 @@ function postProcessing(options) { */ const rq = (query, options) => { const parseQuery = parser(query, options); - return executeQuery(parseQuery, null, options).then(postProcessing(options)); + const cached = cache("get", parseQuery, options); + if (cached) { + return cached; + } + return executeQuery(copy(parseQuery, {}), null, options).then((result) => { + const processed = postProcessing(options)(result); + if (options.cache) { + cache("create", parseQuery, options, processed); + } + return processed; + }); }; +/** @typedef {{ [key: string]: Function | any }} Method */ + class RqExtender { constructor() { this.methods = {}; } + /** + * + * @param {object} query + * @param {QueryOptions} options + * @returns + */ compute(query, options) { if (Object.keys(this.methods).length) { return rq(query, { + ...options, methods: this.methods, + /** @param {string} param */ config: (param) => this.getMethodsMap()[param], - ...options, }); } else { return rq(query, { - methods: this.getMethodsMap(), ...options, + methods: this.getMethodsMap(), }); } } + /** + * @this {Method} + * @returns {Method} + */ getMethodsMap() { const prototype = Object.getPrototypeOf(this); const methodNames = Object.getOwnPropertyNames(prototype).filter( @@ -96,6 +135,7 @@ class RqExtender { name !== "compute" && name !== "getMethodsMap", ); + /** @type {Method} */ const methodsMap = {}; for (const name of methodNames) { methodsMap[name] = this[name]; // use rq context { query, computes } @@ -104,11 +144,12 @@ class RqExtender { } } -global.rq = rq; +/** @type {any} */ +(global).rq = rq; exports.rq = rq; module.exports = rq; module.exports.default = rq; - -global.RqExtender = RqExtender; +/** @type {any} */ +(global).RqExtender = RqExtender; exports.RqExtender = RqExtender; module.exports.RqExtender = RqExtender; diff --git a/libs/getCurrentQueryArgs.cjs b/libs/getCurrentQueryArgs.cjs index 3368fc2..a5136df 100644 --- a/libs/getCurrentQueryArgs.cjs +++ b/libs/getCurrentQueryArgs.cjs @@ -10,7 +10,7 @@ /** * Prepares arguments and parameters. * - * @param {object} value - The value to process. + * @param {Record} value - The value to process. * @param {object | null} currentQuery - The current query object. * @param {string | undefined} alias - Indicates whether an alias is used. * @param {string[] | string | undefined} params - An array of parameter names. diff --git a/libs/iterate.cjs b/libs/iterate.cjs index 6ab9b49..d76f105 100644 --- a/libs/iterate.cjs +++ b/libs/iterate.cjs @@ -1,8 +1,11 @@ // @ts-check const reducer = - (query, index) => - (acc, [key, value]) => { + (/** @type {Record} */ query, /** @type {number} */ index) => + ( + /** @type {Record} */ acc, + /** @type {[string, any]} */ [key, value], + ) => { if (value === 1 && query[key] !== undefined) { acc[key] = query[key]; } else if (value[index] !== undefined) { @@ -17,24 +20,30 @@ const reducer = * Iterates through result and currentQuery and merge currentQuery properties into result. * * @param {Array<[string, any]>} result - The base query to iterate through. - * @param {object} currentQuery - The query result to iterate through. + * @param {import("./executeQuery.cjs").CurrentQuery} currentQuery - The query result to iterate through. * @returns {Array<[string, any]>} The iterated result. */ const iterate = (result, currentQuery) => { - return currentQuery - .map((query, i) => { - query = query instanceof Array ? iterate(result, query) : query; - if (query instanceof Array) { - return query; - } - if (result.length) { - const e = Object.entries(result.reduce(reducer(query, i), {})); - return e.length === 1 ? e.flat() : e; - } else { - return query; - } - }) - .filter((f) => (f instanceof Array && f.length) || typeof f !== "object"); + return ( + currentQuery && + currentQuery + .map((/** @type {object} */ query, /** @type {number} */ i) => { + query = query instanceof Array ? iterate(result, query) : query; + if (query instanceof Array) { + return query; + } + if (result.length) { + const e = Object.entries(result.reduce(reducer(query, i), {})); + return e.length === 1 ? e.flat() : e; + } else { + return query; + } + }) + .filter( + (/** @type {object} */ f) => + (f instanceof Array && f.length) || typeof f !== "object", + ) + ); }; module.exports = iterate; diff --git a/libs/mapResult.cjs b/libs/mapResult.cjs index f4f6dfe..b455e5a 100644 --- a/libs/mapResult.cjs +++ b/libs/mapResult.cjs @@ -5,7 +5,7 @@ * * @param {object} query - The query object. * @param {Array<[string, any]>} result - The result to be mapped. - * @param {object} currentQuery - The current query object. + * @param {{ [key: string]: any }} currentQuery - The current query object. * @returns {Array<[string, any]>} The mapped result. */ const mapResult = (query, result, currentQuery) => { diff --git a/libs/mergeQuery.cjs b/libs/mergeQuery.cjs index c943f95..9686380 100644 --- a/libs/mergeQuery.cjs +++ b/libs/mergeQuery.cjs @@ -1,7 +1,10 @@ // @ts-check -const queryReducer = (acc, ql) => { - return (query) => { +const queryReducer = ( + /** @type {object} */ acc, + /** @type {import("./executeQuery.cjs").CurrentQuery} */ ql, +) => { + return (/** @type {Query} */ query) => { return { ...acc, ...mergeQuery(query, ql), @@ -9,16 +12,18 @@ const queryReducer = (acc, ql) => { }; }; +/** @typedef {object} Query */ + /** * Merges two query objects. * - * @param {object} query - The original query object. - * @param {object | object[]} nextQuery - The query object(s) to merge with the original. + * @param {Query} query - The original query object. + * @param {import("./executeQuery.cjs").CurrentQuery} nextQuery - The query object(s) to merge with the original. * @returns {object} The merged query object. */ const mergeQuery = (query, nextQuery) => { if (Array.isArray(nextQuery)) { - return mergeQuery(query, nextQuery.reduce(queryReducer(query), {})); + return mergeQuery(query, nextQuery.reduce(queryReducer(query, {}), {})); } else { return { ...query, diff --git a/libs/resolvePromises.cjs b/libs/resolvePromises.cjs index 8fe70f7..49aa9f0 100644 --- a/libs/resolvePromises.cjs +++ b/libs/resolvePromises.cjs @@ -3,7 +3,7 @@ /** * Resolves an array of promises or a single promise. * - * @param {Promise | Promise[]} promise - The promise(s) to resolve. + * @param {Promise | Promise[]| any | any[]} promise - The promise(s) to resolve. * @returns {Promise} A promise that resolves when all input promises are resolved. */ const resolvePromises = async (promise) => { diff --git a/package-lock.json b/package-lock.json index ba7a0df..2a1b18a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "requrse", - "version": "0.4.3", + "version": "0.4.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "requrse", - "version": "0.4.3", + "version": "0.4.7", "license": "Apache-2.0", "dependencies": { + "copy-props": "^4.0.0", "deep-equal": "^2.2.2", "graphql-tag": "^2.12.6" }, @@ -157,6 +158,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -361,6 +378,18 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/copy-props": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", + "integrity": "sha512-bVWtw1wQLzzKiYROtvNlbJgxgBYt2bMJpkCbKmXM3xyijvcjjWXEk5nyrrT3bgJ7ODb19ZohE2T0Y3FgNPyoTw==", + "dependencies": { + "each-props": "^3.0.0", + "is-plain-object": "^5.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -508,6 +537,18 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/each-props": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", + "integrity": "sha512-IYf1hpuWrdzse/s/YJOrFmU15lyhSzxelNVAHTEG3DtP4QsLTWZUzcUL3HMXmKQxXpa4EIrBPpwRgj0aehdvAw==", + "dependencies": { + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -636,6 +677,25 @@ "is-callable": "^1.1.3" } }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/foreground-child": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", @@ -679,9 +739,12 @@ "dev": true }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/functions-have-names": { "version": "1.2.3", @@ -1064,6 +1127,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -1177,6 +1248,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -1558,6 +1637,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2372,6 +2465,16 @@ "is-array-buffer": "^3.0.1" } }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==" + }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==" + }, "asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -2540,6 +2643,15 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "copy-props": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", + "integrity": "sha512-bVWtw1wQLzzKiYROtvNlbJgxgBYt2bMJpkCbKmXM3xyijvcjjWXEk5nyrrT3bgJ7ODb19ZohE2T0Y3FgNPyoTw==", + "requires": { + "each-props": "^3.0.0", + "is-plain-object": "^5.0.0" + } + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -2648,6 +2760,15 @@ "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", "dev": true }, + "each-props": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", + "integrity": "sha512-IYf1hpuWrdzse/s/YJOrFmU15lyhSzxelNVAHTEG3DtP4QsLTWZUzcUL3HMXmKQxXpa4EIrBPpwRgj0aehdvAw==", + "requires": { + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0" + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -2740,6 +2861,19 @@ "is-callable": "^1.1.3" } }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==" + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "requires": { + "for-in": "^1.0.1" + } + }, "foreground-child": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", @@ -2774,9 +2908,9 @@ "dev": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "functions-have-names": { "version": "1.2.3", @@ -3039,6 +3173,11 @@ "has-tostringtag": "^1.0.0" } }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -3116,6 +3255,11 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -3383,6 +3527,17 @@ "object-keys": "^1.1.1" } }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/package.json b/package.json index 74ecaee..39b8d0d 100644 --- a/package.json +++ b/package.json @@ -1,57 +1,58 @@ -{ - "name": "requrse", - "version": "0.4.7", - "type": "module", - "description": "Lightweight driven query language", - "main": "libs/executor.cjs", - "scripts": { - "test:starwars": "node samples/starwars/starwars.test.mjs", - "test:mongoose": "node samples/mongoose/mongoose.test.mjs", - "test:mongoose-lookup": "node samples/mongoose/mongoose-lookup.test.mjs", - "test:redis": "node samples/redis/redis.test.mjs", - "test:requrse": "node test/requrse.test.mjs", - "test:inflight-request-cancelation": "node test/inflightRequestCancelation.test.mjs", - "test:fantasy": "node test/fantasy.test.mjs", - "test:basic": "node test/basic.test.mjs", - "test:array": "node test/array.test.mjs", - "test:array-string": "node test/arrayString.test.mjs", - "test:duplicate": "node test/duplicateField.test.mjs", - "test:array-index": "node test/arrayIndex.test.mjs", - "test:waterfall": "node test/waterfall.test.mjs", - "test:class": "node test/class.test.mjs", - "test:get-die": "node test/getDie.test.mjs", - "test": "npm run test:starwars && npm run test:mongoose && npm run test:mongoose-lookup && npm run test:redis && npm run test:requrse && npm run test:inflight-request-cancelation && npm run test:fantasy && npm run test:basic && npm run test:array && npm run test:array-string && npm run test:duplicate && npm run test:array-index && npm run test:waterfall && npm run test:class && npm run test:get-die", - "test:coverage": "c8 --exclude samples --exclude test npm run test", - "test:lcov": "c8 --exclude samples --exclude test --reporter lcov npm run test", - "coverage": "coveralls < coverage/lcov.info" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/syarul/requrse.git" - }, - "keywords": [ - "GraphQL", - "Query Language", - "CRUD", - "api" - ], - "author": "Shahrul Nizam Selamat ", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/syarul/requrse/issues" - }, - "homepage": "https://github.com/syarul/requrse#readme", - "devDependencies": { - "axios": "^1.5.0", - "c8": "^8.0.1", - "coveralls": "^3.1.1", - "dotenv": "^16.3.1", - "graphql": "^16.11.0", - "ioredis": "^5.3.2", - "mongoose": "^7.4.1" - }, - "dependencies": { - "deep-equal": "^2.2.2", - "graphql-tag": "^2.12.6" - } -} +{ + "name": "requrse", + "version": "0.4.8", + "type": "module", + "description": "Lightweight driven query language", + "main": "libs/executor.cjs", + "scripts": { + "test:starwars": "node samples/starwars/starwars.test.mjs", + "test:mongoose": "node samples/mongoose/mongoose.test.mjs", + "test:mongoose-lookup": "node samples/mongoose/mongoose-lookup.test.mjs", + "test:redis": "node samples/redis/redis.test.mjs", + "test:requrse": "node test/requrse.test.mjs", + "test:inflight-request-cancelation": "node test/inflightRequestCancelation.test.mjs", + "test:fantasy": "node test/fantasy.test.mjs", + "test:basic": "node test/basic.test.mjs", + "test:array": "node test/array.test.mjs", + "test:array-string": "node test/arrayString.test.mjs", + "test:duplicate": "node test/duplicateField.test.mjs", + "test:array-index": "node test/arrayIndex.test.mjs", + "test:waterfall": "node test/waterfall.test.mjs", + "test:class": "node test/class.test.mjs", + "test:get-die": "node test/getDie.test.mjs", + "test": "npm run test:starwars && npm run test:mongoose && npm run test:mongoose-lookup && npm run test:redis && npm run test:requrse && npm run test:inflight-request-cancelation && npm run test:fantasy && npm run test:basic && npm run test:array && npm run test:array-string && npm run test:duplicate && npm run test:array-index && npm run test:waterfall && npm run test:class && npm run test:get-die", + "test:coverage": "c8 --exclude samples --exclude test npm run test", + "test:lcov": "c8 --exclude samples --exclude test --reporter lcov npm run test", + "coverage": "coveralls < coverage/lcov.info" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/syarul/requrse.git" + }, + "keywords": [ + "GraphQL", + "Query Language", + "CRUD", + "api" + ], + "author": "Shahrul Nizam Selamat ", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/syarul/requrse/issues" + }, + "homepage": "https://github.com/syarul/requrse#readme", + "devDependencies": { + "axios": "^1.5.0", + "c8": "^8.0.1", + "coveralls": "^3.1.1", + "dotenv": "^16.3.1", + "graphql": "^16.11.0", + "ioredis": "^5.3.2", + "mongoose": "^7.4.1" + }, + "dependencies": { + "copy-props": "4.0.0", + "deep-equal": "2.2.2", + "graphql-tag": "2.12.6" + } +} diff --git a/samples/graphql/random-die.mjs b/samples/graphql/random-die.mjs index cab2eeb..a71ceb6 100644 --- a/samples/graphql/random-die.mjs +++ b/samples/graphql/random-die.mjs @@ -9,28 +9,32 @@ import { const RandomDie = new GraphQLObjectType({ name: "RandomDie", - 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) }, + fields: () => { + const fields = { + numSides: { + type: new GraphQLNonNull(GraphQLInt), + resolve: (die) => die.numSides, }, - resolve: (die, { numRolls }) => { - const output = []; - for (let i = 0; i < numRolls; i++) { - output.push(1 + Math.floor(Math.random() * die.numSides)); - } - return output; + 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; }, }); diff --git a/test/getDie.test.mjs b/test/getDie.test.mjs index a45d598..ca47e34 100644 --- a/test/getDie.test.mjs +++ b/test/getDie.test.mjs @@ -2,6 +2,8 @@ import assert from "assert"; import { RqExtender } from "../libs/executor.cjs"; import { test } from "./fixture/test.mjs"; import gqlToJson from "../libs/gqlToJson.cjs"; +import fs from "node:fs"; +import cache from "../libs/cache.cjs"; class RandomDie extends RqExtender { constructor() { super(); @@ -52,32 +54,6 @@ await test("GetDie", () => { }); }); -await test("GetDie with graphQL query", () => { - const query = ` - { - getDie(numSides: 6) { - numSides - rollOnce - roll(numRolls: 3) - } - } - `; - - // pass the name of the query in options - getDie.compute(query, { rootKey: "Query" }).then(({ Query }) => { - assert.strictEqual(typeof Query.getDie.numSides, "number"); - assert.strictEqual(Query.getDie.numSides, 6); - - assert.strictEqual(typeof Query.getDie.rollOnce, "number"); - assert.ok(Query.getDie.rollOnce >= 1 && Query.getDie.rollOnce <= 6); - - assert.ok(Array.isArray(Query.getDie.roll)); - Query.getDie.roll.forEach((num) => { - assert.ok(num >= 1 && num <= 6); - }); - }); -}); - await test("covers all GraphQL literal cases", () => { const query = ` query TestAllLiterals( @@ -124,3 +100,100 @@ await test("covers all GraphQL literal cases", () => { // ✅ check subField presence assert.ok(result.foo.testField.subField); }); + +await test("GetDie with graphQL query", () => { + const query = ` + { + getDie(numSides: 6) { + numSides + rollOnce + roll(numRolls: 3) + } + } + `; + + getDie.compute(query, { rootKey: "Query" }).then(({ Query }) => { + assert.strictEqual(typeof Query.getDie.numSides, "number"); + assert.strictEqual(Query.getDie.numSides, 6); + + assert.strictEqual(typeof Query.getDie.rollOnce, "number"); + assert.ok(Query.getDie.rollOnce >= 1 && Query.getDie.rollOnce <= 6); + + assert.ok(Array.isArray(Query.getDie.roll)); + Query.getDie.roll.forEach((num) => { + assert.ok(num >= 1 && num <= 6); + }); + }); +}); + +// cleanup cache folder first +if (fs.existsSync(".tmp")) { + fs.rmSync(".tmp", { recursive: true, force: true }); +} + +await test("GetDie cache", () => { + const query = ` + { + getDie(numSides: 6) { + numSides + rollOnce + roll(numRolls: 3) + } + } + `; + + getDie.compute(query, { rootKey: "Query", cache: 5 }).then((res) => { + getDie.compute(query, { rootKey: "Query", cache: 5 }).then((result) => { + assert.deepEqual(res, result); + }); + }); +}); + +await test("GetDie cache expired", async () => { + const query = ` + { + getDie(numSides: 6) { + numSides + rollOnce + roll(numRolls: 3) + } + } + `; + + const res = await getDie.compute(query, { rootKey: "Query", cache: 1 }); + const wait = new Promise((resolve) => setTimeout(resolve, 3000)); + await wait; + const result = await getDie.compute(query, { rootKey: "Query", cache: 1 }); + assert.notDeepEqual(res, result); +}); + +if (fs.existsSync(".tmp")) { + fs.rmSync(".tmp", { recursive: true, force: true }); +} + +await test("cache error create", async () => { + const origWrite = fs.writeFileSync; + fs.writeFileSync = () => { + throw new Error("boom"); + }; + const result = await cache( + "create", + { foo: "bar" }, + { cache: 60 }, + { foo: "bar" }, + ); + assert.deepStrictEqual(result, {}); + fs.writeFileSync = origWrite; +}); + +await test("cache error delete", async () => { + await cache("create", { foo: "bar" }, { cache: 60 }, { foo: "bar" }); + const origWrite = fs.readFileSync; + fs.readFileSync = () => { + throw new Error("boom"); + }; + + const result = await cache("get", { foo: "bar" }, { cache: 60 }); + assert.deepStrictEqual(result, undefined); + fs.readFileSync = origWrite; +});