Object.defineProperty(exports, '__esModule', { value: true }); /** * Extract `StorableObject`s from queries. These will be uploaded separately * before being added back to the queries and then finally stored. * * @param queries - Array of queries that might contain `StorableObject`s. * * @returns Array of `StorableObject`s that will be saved to RONIN. */ const extractStorableObjects = (queries)=>queries.reduce((references, query, queryIndex)=>{ return [ ...references, ...Object.entries(query).reduce((references, [queryType, query])=>{ // Abort if the `queryType` is not one of `set` or `create`. if (![ 'set', 'create' ].includes(queryType)) return references; return [ ...references, ...Object.entries(query).reduce((references, [schema, instructions])=>{ // Access the query instructions according to the query type. const fields = instructions[queryType === 'set' ? 'to' : 'with']; return [ ...references, ...Object.entries(fields).reduce((references, [name, value])=>{ // Values such as `File`, `ReadableStream` and `Buffer` will be // uploaded and stored before being processed further. const isStorableObject = typeof File !== 'undefined' && value instanceof File || typeof ReadableStream !== 'undefined' && value instanceof ReadableStream || typeof Blob !== 'undefined' && value instanceof Blob || typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer || typeof Buffer !== 'undefined' && Buffer.isBuffer(value); if (!isStorableObject) return references; const storarableObject = { query: { index: queryIndex, type: queryType }, schema, field: name, value }; if ('type' in value) { storarableObject.contentType = value.type; } if ('name' in value) { storarableObject.name = value.name; } return [ ...references, storarableObject ]; }, []) ]; }, []) ]; }, []) ]; }, []); /** * Upload `StorableObjectValue`s contained in queries. * * @param storableObjects - Array of `StorableObject`s to upload. * @param options - Optional object containing options for the upload request. * * @returns Array of `StoredObject`s. */ const uploadStorableObjects = async (storableObjects, options = {})=>{ const fetcher = typeof options?.fetch === 'function' ? options.fetch : fetch; const requests = storableObjects.map(async ({ name, value, contentType })=>{ const headers = new Headers(); headers.set('Authorization', `Bearer ${options.token}`); if (contentType) { headers.set('Content-Type', contentType); } if (name) { headers.set('Content-Disposition', `form-data; filename="${name}"`); } const request = new Request('https://storage.ronin.co/', { method: 'PUT', body: value, headers }); const response = await fetcher(request); if (!response.ok) throw new Error(await response.text()); return response.json(); }); return Promise.all(requests).catch((err)=>{ const message = `An error occurred while uploading the binary objects included in the provided queries. ${err}`; throw new Error(message); }); }; /** * ## 🚧 For internal use only! 🚧 * * Process `StorableObjectValue`s contained in queries. * * @param queries - Array of queries that contain `StorableObjectValue`s. * @param upload - A function that receives `StorableObject`s and uploads them. * * @returns Queries with `StorableObjectValue`s replaced by `StoredObject`s. */ const processStorableObjects = async (queries, upload)=>{ const objects = extractStorableObjects(queries); if (objects.length > 0) { const storedObjects = await upload(objects); for(let index = 0; index < objects.length; index++){ const { query, schema, field } = objects[index]; const reference = storedObjects[index]; // @ts-expect-error It is guaranteed that these keys exist. queries[query.index][query.type][schema][query.type === 'set' ? 'to' : 'with'][field] = reference; } } return queries; }; /** * Construct a new object from a "dot string" property accessor. * * @param accessor - Accessor to construct object from. * @param value - Value to set the given accessor to. * * @returns Object constructed from the given accessor and value. */ const objectFromAccessor = (accessor, value)=>setProperty({}, accessor, value); /** * Splits the given path into an array of so-called segments. This is done by * splitting the path on the `.` character, but only if it's not preceded by a * `\` character. This is done to allow for setting values on nested records, * such as `invoice.companyName`. * * @param path - Dot-separated path to split into segments. * * @returns Array of path segments. */ const getPathSegments = (path)=>{ const segments = path// Split path on property and array accessors (`.` and `[]`). By using a // non-printable unicode character (u200B), we can achieve the same result // as when using a negative lookbehind to filter out preceding backslashes, // but with support for Safari, because negative lookbehinds don't work // in Safari at the time of writing. .replace(/\\\./g, '\u200B').split(/[[.]/g).map((s)=>s.replace(/\u200B/g, '.'))// Filter out empty values. (`foo..bar` would otherwise result in // `['foo', '', 'bar']`). .filter((x)=>!!x.trim())// Remove the escaping character (`\`) before escaped segments. // `foo\.bar,baz` will result in `['foo.bar', 'baz']`. .map((x)=>x.replaceAll('\\.', '.')); return segments; }; /** * Set the property at the given path to the given value. * * @param obj - Object to set the property on. * @param pathSegments - An array of property keys leading up to the final * property to set. * @param value - Value to set at the given path. * * @returns Object with the updated property. */ const setPropertyViaPathSegments = (obj, pathSegments, value)=>{ let current = obj; for(let i = 0; i < pathSegments.length; i++){ const key = pathSegments[i]; const isLastKey = i === pathSegments.length - 1; if (isLastKey) { current[key] = typeof value === 'function' ? value(current[key]) : value; } else { // Only create a new object if the current key does not exist, or if it // exists but is not of the correct type. if (!Object.prototype.hasOwnProperty.call(current, key) || typeof current[key] !== 'object') { current[key] = {}; } current = current[key]; } } }; const setProperty = (obj, path, value)=>{ const segments = getPathSegments(path); setPropertyViaPathSegments(obj, segments, value); return obj; }; /** * Gets the property value of an object based on the given path segments * of the property. * * @param obj - The object to get the property value from. * @param pathSegments - An array of property keys leading up to the final * property at the end. * * @returns The property value at the specified path or `undefined` if the path * does not exist. * * @example * const exampleObject = \{ * user: \{ * name: \{ * first: 'John', * last: 'Doe' * \}, * age: 30 * \} * \}; * console.log(getProperty(exampleObject, ['user', 'name', 'first'])); // Output: 'John' * console.log(getProperty(exampleObject, ['user', 'age'])); // Output: 30 * console.log(getProperty(exampleObject, ['user', 'non', 'existing'])); // Output: undefined */ const getPropertyViaPathSegments = (obj, pathSegments)=>{ let current = obj; for (const key of pathSegments){ if (current[key] === null || current[key] === undefined) return undefined; current = current[key]; } return current; }; const getProperty = (obj, path)=>getPropertyViaPathSegments(obj, getPathSegments(path)); /** * Turn the given string into "dash-case", which we use for slugs. * * @param string - String to turn into dash-case. * * @returns String compatible with "dash-case". * * Originally from https://github.com/rayepps/radash/blob/7c6b986d19c68f19ccf5863d518eb19ec9aa4ab8/src/string.ts#L60-L71. */ const toDashCase = (string)=>{ const capitalize = (str)=>{ const lower = str.toLowerCase(); return lower.substring(0, 1).toUpperCase() + lower.substring(1, lower.length); }; const parts = string?.replace(/([A-Z])+/g, capitalize)?.split(/(?=[A-Z])|[.\-\s_]/).map((x)=>x.toLowerCase()) ?? []; if (parts.length === 0) return ''; if (parts.length === 1) return parts[0]; return parts.reduce((acc, part)=>`${acc}-${part.toLowerCase()}`); }; /** * Convert Time fields in a record to JavaScript `Date` objects. * * @param record - A record to format the Time fields of. * @param timeFields - An array of property keys for the time fields. */ const formatTimeFields = (record, timeFields)=>{ timeFields.forEach((field)=>setProperty(record, field, (value)=>value !== null ? new Date(value) : null)); }; const EMPTY = Symbol('empty'); // The order of these types is important, as they determine the order in which // data hooks are run (the "data hook lifecycle"). const HOOK_TYPES = [ 'before', 'during', 'after' ]; const getSchema = (instruction)=>{ const key = Object.keys(instruction)[0]; let schema = String(key); let multipleRecords = false; if (schema.endsWith('s')) { schema = schema.substring(0, schema.length - 1); multipleRecords = true; } return { key, // Convert camel case (e.g. `subscriptionItems`) into slugs // (e.g. `subscription-items`). schema: toDashCase(schema), multipleRecords }; }; /** * Constructs the method name used for a particular type of hook and query. * For example, if `hookType` is "after" and `queryType` is "create", the * resulting method name would be `afterCreate`. * * @param hookType - The type of hook, so "before", "during", or "after". * @param queryType - The type of query. For example: "get", "set", or "drop". * * @returns The method name constructed from the hook and query types. */ const getMethodName = (hookType, queryType)=>{ const capitalizedQueryType = queryType[0].toUpperCase() + queryType.slice(1); return hookType === 'during' ? queryType : hookType + capitalizedQueryType; }; /** * If a query is being run explicitly by importing the client inside a data * hook, this context will contain information about the hook in which the * query is being run. */ const HOOK_CONTEXT = typeof process !== 'undefined' ? import('node:async_hooks').then(({ AsyncLocalStorage })=>{ return new AsyncLocalStorage(); }) : undefined; /** * Based on which type of query is being executed (e.g. "get" or "create"), * this function checks if a hook is defined for the affected schema and runs * said hook with several arguments (such as the query that was run). * For example, if `create.account.with.id('1234');` is run and the `hookType` * is `before`, then the `beforeCreate` hook would be invoked if one is defined * for the "account" schema in the list of hooks. * * @param hooks - A map of all schemas with their respective hooks. * @param hookType - The type of hook, so "before", "during", or "after". * @param query - A deconstructed query for which the hook should be run. * * @returns Information about whether a hook was run, and its potential output. */ const invokeHook = async (hooks, hookType, query)=>{ const hooksForSchema = hooks?.[query.schema]; const hookName = getMethodName(hookType, query.type); // If `oldInstruction` is falsy (e.g. `null`), we want to default to `{}`. // This would happen in cases where all records of a particular schema are // retrieved. For example, the query `get.members();` would trigger this. // It's important to provide an object to hooks, as people might otherwise // try to set properties on a value that's not an object, which would cause // the hook to crash with an exception. // It's also extremely important to clone both of the variables below, as the // hooks will otherwise modify the original that was passed from the outside. const queryInstruction = query.instruction ? structuredClone(query.instruction) : {}; // A list of function arguments that will be passed to every data hook, // regardless of its type or schema. // The first argument is the query instructions, the second argument is a // boolean that indicates whether the query is plural (e.g. `get.members();`), // and the third argument is the result of the query (if any). const hookArguments = [ queryInstruction, query.plural, undefined ]; // For data hooks of type "after" (such as `afterCreate`), we want to pass // a special function argument that contains the result of the query. if (hookType === 'after') { const queryResult = structuredClone(query.result); hookArguments[2] = queryResult; } const parentContext = await HOOK_CONTEXT; // In order to make data hooks as easy as possible to use and prevent any // kind of infinite recursion, we would like to ensure that queries inside // data hooks only run data hooks that come after it in the lifecycle. // // Additionally, if the query being run inside the data hook is for the same // schema as the surrounding data hook, not even the data hooks after it in // the lifecycle should run, meaning no data hooks should run at all. const parentHook = parentContext?.getStore(); const shouldSkip = parentHook && (HOOK_TYPES.indexOf(hookType) <= HOOK_TYPES.indexOf(parentHook.hookType) || query.schema === parentHook.querySchema && HOOK_TYPES.indexOf(hookType) > HOOK_TYPES.indexOf(parentHook.hookType)); if (hooksForSchema && hookName in hooksForSchema && !shouldSkip) { const [instructions, isMultiple, queryResults] = hookArguments; const hook = hooksForSchema[hookName]; const caller = async ()=>{ return hookType === 'after' ? await hook(instructions, isMultiple, queryResults) : await hook(instructions, isMultiple); }; const result = parentContext ? await parentContext.run({ hookType, queryType: query.type, querySchema: query.schema }, caller) : await caller(); return { ran: true, result }; } return { ran: false, result: null }; }; /** * Invokes a particular hook (such as `afterCreate`) and handles its output. * In the case of an "before" hook, a query is returned from the hook, which * must replace the original query in the list of queries. For a "during" hook, * the results of the query are returned and must therefore be merged into the * final list of results. In the case of an "after" hook, nothing must be done * because no output is returned by the hook. * * @param hooks - A map of all schemas with their respective hooks. * @param hookType - The type of hook, so "before", "during", or "after". * @param modifiableQueries - The final list of queries. * @param modifiableResults - The final list of query results. * @param query - The definition and other details of a query that is being run. * * @returns Nothing, because `modifiableQueries` and `modifiableResults` are * directly modified instead. */ const invokeHooks = async (hooks, hookType, modifiableQueries, modifiableResults, query)=>{ const queryType = Object.keys(query.definition)[0]; const queryInstructions = query.definition[queryType]; const { key, schema, multipleRecords } = getSchema(queryInstructions); const oldInstruction = queryInstructions[key]; const executedHookResults = await invokeHook(hooks, hookType, { type: queryType, schema, plural: multipleRecords, instruction: oldInstruction, // For "after" hooks, we want to pass the final result associated with a // particular query, so that the hook can read it. result: hookType === 'after' ? query.result : null }); // We can't assert based on what the hook returned, only based on whether the // hook ran or not. That's because a hook might return any falsy value and // then we would be mislead into thinking that the hook didn't run. const { ran, result: hookResult } = executedHookResults; switch(hookType){ case 'before': if (!ran) break; // If the hook returned a query, we want to replace the original query // with the one returned by the hook. queryInstructions[key] = hookResult; modifiableQueries[query.index] = { [queryType]: queryInstructions }; break; case 'during': if (ran) { // In the case of "during" hooks, we don't want to keep the query // that's responsible for querying a particular schema, because queries // of that schema would be entirely handled by the "during" hooks. // That's why we're deleting the query here. We can't use `splice`, as // that would change the array position of future items that we haven't // yet iterated over, so they would not be able to splice themselves // correctly, as their index in the iterator would not match their // actual index. modifiableQueries[query.index] = EMPTY; // The hook returned a record (or multiple), so we'd like to add those // records to the list of final results. modifiableResults[query.index] = hookResult; } else { // In the case that the hook didn't run and we're dealing with "during" // hooks, we still want to add an empty item to the list, so that the // indexes of results being added afterwards are correct. We have to // use a symbol instead of something like `undefined`, because a hook // might return `undefined` so we wouldn't know whether the array item // is a result or just an empty placeholder. modifiableResults[query.index] = EMPTY; } break; } }; /** * Executes queries and also invokes any potential hooks (such as `afterCreate`) * that might have been provided as part of `options.hooks`. * * @param queries - A list of queries to execute. * @param options - A list of options to change how the queries are executed. To * run hooks, the `options.hooks` property must contain a map of hooks. * * @returns The results of the queries that were passed. */ const runQueriesWithHooks = async (queries, options = {})=>{ let modifiableQueries = Array.from(queries); const modifiableResults = new Array(); const { hooks, waitUntil } = options; // If no hooks were provided, we can just run the queries and return // the results. if (!hooks) return runQueries(modifiableQueries, options); // We're intentionally considering the entire `hooks` option here, instead of // searching for "after" hooks inside of it, because the latter would increase // the error surface and make the logic less reliable. if (typeof process === 'undefined' && hooks && !waitUntil) { let message = 'In the case that the "ronin" package receives a value for'; message += ' its `hooks` option, it must also receive a value for its'; message += ' `waitUntil` option. This requirement only applies when using'; message += ' an edge runtime and ensures that the edge worker continues to'; message += ' execute until all "after" hooks have been executed.'; throw new Error(message); } // Invoke `beforeCreate`, `beforeGet`, `beforeSet`, `beforeDrop`, and // also `beforeCount`. await Promise.all(queries.map((query, index)=>{ return invokeHooks(hooks, 'before', modifiableQueries, modifiableResults, { definition: query, index, result: null }); })); // Invoke `create`, `get`, `set`, `drop`, and `count`. await Promise.all(queries.map((query, index)=>{ return invokeHooks(hooks, 'during', modifiableQueries, modifiableResults, { definition: query, index, result: null }); })); // Filter out queries that were marked as removable by the `invokeHooks` // calls above. We can't just filter by something like `undefined`, because // hooks might be returning those values as successful results. modifiableQueries = modifiableQueries.filter((query)=>query !== EMPTY); // If no queries are remaining, that means all the queries were handled by // "during" hooks above, so there are none remaining to send for execution. if (modifiableQueries.length === 0) return modifiableResults; const results = await runQueries(modifiableQueries, options); for(let index = 0; index < modifiableResults.length; index++){ const existingResult = modifiableResults[index]; // If there isn't an existing result for the current index, that means // `results` will contain the result for that index. However, note that // the indexes in `results` are different because they don't consider the // results that weren't retrieved from RONIN and only retrieved from // "during" hooks. if (existingResult === EMPTY) continue; results.splice(index, 0, existingResult); } // Invoke `afterCreate`, `afterGet`, `afterSet`, `afterDrop` and `afterCount` // (asynchronously, since they shouldn't block). for(let queryIndex = 0; queryIndex < queries.length; queryIndex++){ const query = queries[queryIndex]; const queryType = Object.keys(query)[0]; // "after" hooks should only fire for writes — not reads. if (queryType !== 'set' && queryType !== 'drop' && queryType !== 'create') { continue; } // Select the results that are associated with the current query. Also // ensure an array, to avoid people having to conditionally support both // arrays and objects inside the data hook. const queryResult = Array.isArray(results?.[queryIndex]) ? results?.[queryIndex] : [ results?.[queryIndex] ]; // Run the actual hook functions. const promise = invokeHooks(hooks, 'after', queries, [], { definition: query, index: queryIndex, result: queryResult }); // If the configuration option for extending the lifetime of the edge // worker invocation was passed, provide it with the resulting promise of // the hook invocation above. This will ensure that the worker will // continue to be executed until all of the asynchronous actions // (non-awaited promises) have been resolved. if (waitUntil) waitUntil(promise); } return results; }; class InvalidQueryError extends Error { constructor(details){ super(details.message); this.name = 'InvalidQueryError'; this.message = details.message; this.query = details.query; this.path = details.path; this.details = details.details; } } /** * Utility function to generate a dot-notated string of the given path. * * Example: * * `['queries', 0, 'get', 'account', 'with', 'handle']` * =\> * `queries[0].get.account.with.handle` * * @param path - An array of path segments to combine. * * @returns The dot-notated string. */ const getDotNotatedPath = (segments = [])=>segments.length > 0 ? segments.reduce((path, segment, index)=>{ if (typeof segment === 'number') return `${path}[${segment}]`; if (index === 0) return `${segment}`; return `${path}.${segment}`; }, '') : null; /** * Run a set of given queries. * * @param queries - `Query` array containing the queries to run. These may * contain objects to be stored inside object storage. * @param options - `QueryHandlerOptions` object containing options passed to * the internal `fetch` function. * * @returns Promise resolving the queried data. */ const runQueries = async (queries, options = {})=>{ const hasWriteQuery = queries.some((query)=>[ 'create', 'set', 'drop' ].includes(Object.keys(query)[0])); // Runtimes like Cloudflare Workers don't support `cache` yet. const hasCachingSupport = 'cache' in new Request('https://ronin.co'); const request = new Request('https://data.ronin.co/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${options.token}` }, body: JSON.stringify({ queries }), // Disable cache if write queries are performed, as those must be // guaranteed to reach RONIN. ...hasWriteQuery && hasCachingSupport ? { cache: 'no-store' } : {}, // Allow for passing custom `fetch` options (e.g. in Next.js). ...typeof options?.fetch === 'object' ? options.fetch : {} }); const fetcher = typeof options?.fetch === 'function' ? options.fetch : fetch; const response = await fetcher(request); const { results, error } = await response.json(); // Throw errors that happened during the execution of the queries. if (error) { const exposedError = new Error(error.message); if (error.code) exposedError.code = error.code; throw exposedError; } const startFormatting = performance.now(); for(let i = 0; i < results.length; i++){ const result = results[i]; if ('error' in result && result.error) { const message = result.error.code === 'BAD_REQUEST' ? 'Invalid query provided.' : result.error.message; // Get a dot-notated path to the field that caused the error. const path = result.error.issues?.[0]?.path ? getDotNotatedPath(result.error.issues[0].path) : null; // If a path is given, try resolving the query that caused the error. const query = path && typeof result.error.issues[0].path[1] === 'number' ? queries[result.error.issues[0].path[1]] : null; // Get the last part of the instruction that is invalid. const instruction = query && path ? getProperty(query, path.replace(/queries\[\d+\]\./, '')) : null; // Get potential details about the error. These contain instructions how // the issue might be resolved. const details = result.error.issues?.[0] ? result.error.issues[0].message : null; throw new InvalidQueryError({ message, query: query && path ? `${path.replace(/queries\[\d+\]\./, '')} = ${JSON.stringify(instruction)}` : null, path: path, details }); } // Handle `count` query result. if ('amount' in result && typeof result.amount !== 'undefined' && result.amount !== null) { results[i] = Number(result.amount); continue; } const timeFields = 'schema' in result ? Object.entries(result.schema).filter(([, type])=>type === 'time').map(([name])=>name) : []; // Handle single record result. if ('record' in result) { // This happens if no matching record was found for a singular query, // such as `get.account.with.handle('leo')`. if (result.record === null) { results[i] = null; continue; } formatTimeFields(result.record, timeFields); results[i] = result.record; continue; } // Handle result with multiple records. if ('records' in result) { for (const record of result.records){ formatTimeFields(record, timeFields); } // Expose the pagination cursors in order to allow for retrieving the // previous or next page. // // This value is already available on `result`, but since we're only // returning `result.records`, we want it to be set on that array. if (typeof result.moreBefore !== 'undefined') result.records.moreBefore = result.moreBefore; if (typeof result.moreAfter !== 'undefined') result.records.moreAfter = result.moreAfter; results[i] = result.records; continue; } } const endFormatting = performance.now(); const VERBOSE_LOGGING = typeof process !== 'undefined' && process?.env && process.env.__RENDER_DEBUG_LEVEL === 'verbose' || typeof undefined !== 'undefined' && undefined.__RENDER_DEBUG_LEVEL === 'verbose'; if (VERBOSE_LOGGING) { console.log(`Formatting took ${endFormatting - startFormatting}ms`); } return results; }; /** * Runs a list of `Query`s. * * @param queries - A list of queries to execute. * @param options - A list of options to change how the queries are executed. * * @returns The results of the queries that were passed. */ const runQueriesWithStorageAndHooks = async (queries, options = {})=>{ // Extract and process `StorableObject`s, if any are present. // `queriesPopulatedWithReferences` are the given `queries`, just that any // `StorableObject` they might contain has been processed and the value of the // field has been replaced with the reference to the `StoredObject`. // This way, we only store the `reference` of the `StoredObject` inside the // database for better performance. const queriesPopulatedWithReferences = await processStorableObjects(queries, (objects)=>{ return uploadStorableObjects(objects, options); }); return runQueriesWithHooks(queriesPopulatedWithReferences, options); }; /** * Executes an array of queries and handles their results. It is used to execute * multiple queries at once and return their results in a single promise. * * @param queries - An array of Query objects to be executed. * @param options - An optional object of options for the query execution. * * @returns A Promise that resolves with the query results. * * ### Usage * ```typescript * const results = await queriesHandler([ * { get: { accounts: {} } }, * { get: { account: { with: { email: 'mike@gmail.com' } } } } * ], { token: 'your-ronin-app-token' }); * ``` * * The `RONIN_TOKEN` environment variable will be used (if available) to * authenticate requests if the `token` option is not provided. */ const queriesHandler = async (queries, optionsFactory = {})=>{ const options = typeof optionsFactory === 'function' ? optionsFactory() : optionsFactory; if (!options.token && typeof process !== 'undefined') { const token = typeof process?.env !== 'undefined' ? process.env.RONIN_TOKEN : typeof undefined !== 'undefined' ? undefined.RONIN_TOKEN : undefined; if (!token || token === 'undefined') { const message = 'Please specify the `RONIN_TOKEN` environment variable' + ' or set the `token` option when invoking RONIN.'; throw new Error(message); } options.token = token; } const token = options.token; if (!token) { let message = 'When invoking RONIN from an edge runtime, the'; message += ' `token` option must be set.'; throw new Error(message); } return runQueriesWithStorageAndHooks(queries, options); }; /** * Executes a query and returns the result. * * @param query - A Query object to be executed. * @param options - An optional object of options for the query execution. * * @returns A Promise that resolves with the query result. * * ### Usage * ```typescript * const result = await queryHandler( * { get: { accounts: {} } }, * { token: 'your-token' } * ); * ``` */ const queryHandler = async (query, options)=>{ const results = await queriesHandler([ query ], options); return results[0]; }; let inBatch = false; /** * A utility function that creates a Proxy object to handle dynamic property * access and function calls. It is used to create a syntax that allows for * dynamic query generation. * * @param queryType - The type of the query. This will be used as the key in * the generated query object. * @param queryHandler - A function that handles the execution of the query. * * @returns A Proxy object that intercepts property access and function * calls to generate and execute queries. * * ### Usage * ```typescript * const proxy = getSyntaxProxy('get', async (query) => { * // Execute the query and return the result * }); * * const result = await get.account(); * * const result = await get.account.with.email('mike@gmail.com'); * ``` */ const getSyntaxProxy = (queryType, queryHandler)=>{ return new Proxy({}, { get (_target, prop) { function createProxy(path) { const proxyTargetFunction = ()=>{}; return new Proxy(proxyTargetFunction, { async apply (_target, _thisArg, args) { const value = args[0]; const expanded = objectFromAccessor(path.join('.'), typeof value === 'undefined' ? {} : value); const query = { [queryType]: expanded }; if (inBatch) { return query; } return queryHandler(query); }, get (_target, nextProp) { return createProxy(path.concat([ nextProp ])); } }); } return createProxy([ prop ]); } }); }; /** * Executes a batch of operations and handles their results. It is used to * execute multiple queries at once and return their results at once. * * @param operations - A function that returns an array of Promises. Each * Promise should resolve with a Query object. * @param queriesHandler - A function that handles the execution of the queries. * This function is expected to receive an array of Query objects and return a * Promise that resolves with the results of the queries. * * @returns A Promise that resolves with a tuple of the results of the queries. * * ### Usage * ```typescript * const results = await batch(() => [ * get.accounts(), * get.account.with.email('mike@gmail.com') * ], async (queries) => { * // Execute the queries and return their results * }); * ``` */ const getBatchProxy = async (operations, queriesHandler)=>{ inBatch = true; const queries = await Promise.all(operations()); inBatch = false; return queriesHandler(queries); }; /** * Creates a syntax factory for generating and executing queries. * * @param options - An optional object of options for the query execution. * * Alternatively, a function that returns the object may be provided instead, * which is useful for cases in which the config must be generated dynamically * whenever a query is executed. * * @returns An object with methods for generating and executing different types * of queries. * * ### Usage * ```typescript * const { get, set, create, count, drop } = createSyntaxFactory({ * token: '...' * }); * * await get.accounts(); * * await set.account({ * with: { * email: 'mike@gmail.com', * }, * to: { * status: 'active', * }, * }); * * * await create.account({ with: { email: 'mike@gmail.com' } }); * * await count.accounts(); * * await drop.accounts.with.emailVerified.notBeing(true); * * // Execute a batch of operations * const batchResult = await batch(() => [ * get.accounts(), * get.account.with.email('mike@gmail.com') * ]); * ``` */ const createSyntaxFactory = (options)=>({ create: getSyntaxProxy('create', (query)=>queryHandler(query, options)), get: getSyntaxProxy('get', (query)=>queryHandler(query, options)), set: getSyntaxProxy('set', (query)=>queryHandler(query, options)), drop: getSyntaxProxy('drop', (query)=>queryHandler(query, options)), count: getSyntaxProxy('count', (query)=>queryHandler(query, options)), batch: (operations)=>getBatchProxy(operations, (queries)=>queriesHandler(queries, options)) }); const { create, get, set, drop, count, batch } = createSyntaxFactory({}); exports.batch = batch; exports.count = count; exports.create = create; exports.default = createSyntaxFactory; exports.drop = drop; exports.get = get; exports.set = set;