UNPKG

ronin

Version:

Access your RONIN database via TypeScript.

711 lines (702 loc) • 24.1 kB
// src/utils/errors.ts var ClientError = class extends Error { message; code; constructor(details) { super(details.message); this.name = "ClientError"; this.message = details.message; this.code = details.code; } }; var getResponseBody = async (response, options) => { if (response.ok) return response.json(); const text = await response.text(); let json; try { json = JSON.parse(text); } catch (_err) { throw new ClientError({ message: `${options?.errorPrefix ? `${options.errorPrefix} ` : ""}${text}`, code: "JSON_PARSE_ERROR" }); } if (json.error) { json.error.message = `${options?.errorPrefix ? `${options.errorPrefix} ` : ""}${json.error.message}`; throw new ClientError(json.error); } return json; }; // src/storage.ts var isStorableObject = (value) => 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); var extractStorableObjects = (queries) => queries.reduce( (references, query, queryIndex) => { return [ // biome-ignore lint/performance/noAccumulatingSpread: This code is too complex to refactor. ...references, ...Object.entries(query).reduce( (references2, [queryType, query2]) => { if (!["set", "add"].includes(queryType)) return references2; return [ // biome-ignore lint/performance/noAccumulatingSpread: This code is too complex to refactor. ...references2, ...Object.entries(query2).reduce( (references3, [schema, instructions]) => { const fields = instructions[queryType === "set" ? "to" : "with"]; return [ // biome-ignore lint/performance/noAccumulatingSpread: This code is too complex to refactor. ...references3, ...Object.entries(fields || {}).reduce( (references4, [name, value]) => { if (!isStorableObject(value)) return references4; const blobValue = value; const storarableObject = { query: { index: queryIndex, type: queryType }, schema, field: name, value: blobValue }; if ("type" in blobValue) { storarableObject.contentType = blobValue.type; } if ("name" in blobValue) { storarableObject.name = blobValue.name; } return [...references4, storarableObject]; }, [] ) ]; }, [] ) ]; }, [] ) ]; }, [] ); var uploadStorableObjects = (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="${encodeURIComponent(name)}"` ); } const request = new Request("https://storage.ronin.co/", { method: "PUT", body: value, headers }); const response = await fetcher(request); return getResponseBody(response, { errorPrefix: "An error occurred while uploading the binary objects included in the provided queries. Error:" }); } ); return Promise.all(requests); }; var 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]; queries[query.index][query.type][schema][query.type === "set" ? "to" : "with"][field] = reference; } } return queries; }; // src/utils/constants.ts import { DDL_QUERY_TYPES, DML_QUERY_TYPES_WRITE } from "@ronin/compiler"; var WRITE_QUERY_TYPES = [ ...DML_QUERY_TYPES_WRITE, ...DDL_QUERY_TYPES.filter((item) => item !== "list") ]; // src/utils/helpers.ts import { getProperty, setProperty } from "@ronin/syntax/queries"; var 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()}`); }; var formatDateFields = (record, dateFields) => { for (const field of dateFields) { const value = getProperty(record, field); if (typeof value === "undefined" || value === null) continue; setProperty(record, field, new Date(value)); } }; var mergeOptions = (...options) => { return options.reduce((acc, opt) => { const resolvedOpt = typeof opt === "function" ? opt() : opt; Object.assign(acc, resolvedOpt); return acc; }, {}); }; var validateToken = (options = {}) => { if (!options.token && typeof process !== "undefined") { const token = typeof process?.env !== "undefined" ? process.env.RONIN_TOKEN : typeof import.meta?.env !== "undefined" ? import.meta.env.RONIN_TOKEN : void 0; 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; } if (!options.token) { let message = "When invoking RONIN from an edge runtime, the"; message += " `token` option must be set."; throw new Error(message); } }; var omit = (obj, keys) => { if (!obj) return {}; if (!keys || keys.length === 0) return obj; return keys.reduce( (acc, key) => { delete acc[key]; return acc; }, { ...obj } ); }; // src/utils/triggers.ts import { DDL_QUERY_TYPES as DDL_QUERY_TYPES2, QUERY_TYPES, QUERY_TYPES_READ, QUERY_TYPES_WRITE } from "@ronin/compiler"; var EMPTY = Symbol("empty"); var getModel = (instruction) => { const key = Object.keys(instruction)[0]; let model = String(key); let multipleRecords = false; if (model.endsWith("s")) { model = model.substring(0, model.length - 1); multipleRecords = true; } return { key, // Convert camel case (e.g. `subscriptionItems`) into slugs // (e.g. `subscription-items`). model: toDashCase(model), multipleRecords }; }; var getMethodName = (triggerType, queryType) => { const capitalizedQueryType = queryType[0].toUpperCase() + queryType.slice(1); return triggerType === "during" ? queryType : triggerType + capitalizedQueryType; }; var normalizeResults = (result) => { const value = Array.isArray(result) ? result : result === EMPTY ? [] : [result]; return structuredClone(value); }; var invokeTriggers = async (triggerType, definition, options) => { const { triggers, database, client } = options; const { query } = definition; const queryType = Object.keys(query)[0]; let queryModel; let queryModelDashed; let multipleRecords; let oldInstruction; if (DDL_QUERY_TYPES2.includes(queryType)) { queryModel = queryModelDashed = "model"; multipleRecords = false; oldInstruction = query[queryType]; } else { const queryInstructions = query[queryType]; ({ key: queryModel, model: queryModelDashed, multipleRecords } = getModel(queryInstructions)); oldInstruction = queryInstructions[queryModel]; } const triggerFile = database ? "sink" : queryModelDashed; const triggersForModel = triggers[triggerFile]; const triggerName = getMethodName(triggerType, queryType); const queryInstruction = oldInstruction ? structuredClone(oldInstruction) : {}; if (triggersForModel && triggerName in triggersForModel) { const implicit = definition.implicit ?? false; const trigger = triggersForModel[triggerName]; const triggerOptions = { implicit, client, ...triggerFile === "sink" ? { model: queryModel, database } : {} }; const triggerResult = await (triggerType === "following" ? trigger( queryInstruction, multipleRecords, normalizeResults(definition.resultBefore), normalizeResults(definition.resultAfter), triggerOptions ) : trigger( queryInstruction, multipleRecords, triggerOptions )); if (triggerType === "before") { return { queries: triggerResult }; } if (triggerType === "during") { const result = triggerResult; let newQuery = query; if (result && QUERY_TYPES.some((type) => type in result)) { newQuery = result; } else { newQuery = { [queryType]: { [queryModel]: result } }; } return { queries: [newQuery] }; } if (triggerType === "after") { return { queries: triggerResult }; } if (triggerType === "resolving") { const result = triggerResult; return { queries: [], result }; } } return { queries: [], result: EMPTY }; }; var runQueriesWithTriggers = async (queries, options = {}) => { const { triggers, waitUntil, requireTriggers } = options; const triggerErrorType = requireTriggers !== "all" ? ` ${requireTriggers}` : ""; const triggerError = new ClientError({ message: `Please define "during" triggers for the provided${triggerErrorType} queries.`, code: "TRIGGER_REQUIRED" }); if (!triggers) { if (requireTriggers) throw triggerError; return runQueries(queries, options); } const client = createSyntaxFactory(omit(options, ["requireTriggers"])); if (typeof process === "undefined" && !waitUntil) { let message = 'In the case that the "ronin" package receives a value for'; message += " its `triggers` 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 "following" triggers have been executed.'; throw new Error(message); } let queryList = queries.map(({ query, database }) => ({ query, result: EMPTY, database })); await Promise.all( queryList.map(async ({ query, database, implicit }, index) => { const triggerResults = await invokeTriggers( "before", { query, implicit }, { triggers, database, client } ); const queriesToInsert = triggerResults.queries.map((query2) => ({ query: query2, result: EMPTY, database, implicit: true })); queryList.splice(index, 0, ...queriesToInsert); }) ); await Promise.all( queryList.map(async ({ query, database, implicit }, index) => { const triggerResults = await invokeTriggers( "during", { query, implicit }, { triggers, database, client } ); if (triggerResults.queries && triggerResults.queries.length > 0) { queryList[index].query = triggerResults.queries[0]; return; } if (requireTriggers) { const queryType = Object.keys(query)[0]; const requiredTypes = requireTriggers === "read" ? QUERY_TYPES_READ : requireTriggers === "write" ? QUERY_TYPES_WRITE : QUERY_TYPES; if (requiredTypes.includes(queryType)) throw triggerError; } }) ); await Promise.all( queryList.map(async ({ query, database, implicit }, index) => { const triggerResults = await invokeTriggers( "after", { query, implicit }, { triggers, database, client } ); const queriesToInsert = triggerResults.queries.map((query2) => ({ query: query2, result: EMPTY, database, implicit: true })); queryList.splice(index + 1, 0, ...queriesToInsert); }) ); queryList = queryList.flatMap((details, index) => { const { query, database } = details; if (query.set || query.alter) { let newQuery; if (query.set) { const modelSlug = Object.keys(query.set)[0]; newQuery = { get: { [modelSlug]: { with: query.set[modelSlug].with } } }; } else { newQuery = { list: { model: query.alter.model } }; } const diffQuery = { query: newQuery, diffForIndex: index + 1, result: EMPTY, database }; return [diffQuery, details]; } return [details]; }); await Promise.all( queryList.map(async ({ query, database, implicit }, index) => { const triggerResults = await invokeTriggers( "resolving", { query, implicit }, { triggers, database, client } ); queryList[index].result = triggerResults.result; }) ); const queriesWithoutResults = queryList.map((query, index) => ({ ...query, index })).filter((query) => query.result === EMPTY); if (queriesWithoutResults.length > 0) { const resultsFromDatabase = await runQueries(queriesWithoutResults, options); for (let index = 0; index < resultsFromDatabase.length; index++) { const query = queriesWithoutResults[index]; const result = resultsFromDatabase[index].result; queryList[query.index].result = result; } } for (let index = 0; index < queryList.length; index++) { const { query, result, database, implicit } = queryList[index]; const queryType = Object.keys(query)[0]; if (!WRITE_QUERY_TYPES.includes(queryType)) continue; const diffMatch = queryList.find((item) => item.diffForIndex === index); let resultBefore = diffMatch ? diffMatch.result : EMPTY; let resultAfter = result; if (queryType === "remove" || queryType === "drop") { resultBefore = result; resultAfter = EMPTY; } const promise = invokeTriggers( "following", { query, resultBefore, resultAfter, implicit }, { triggers, database, client } ); const clearPromise = promise.then( () => { }, (error) => Promise.reject(error) ); if (waitUntil) waitUntil(clearPromise); } return queryList.filter( (query) => typeof query.diffForIndex === "undefined" && typeof query.implicit === "undefined" ).map(({ result, database }) => ({ result, database })); }; // src/queries.ts var runQueries = async (queries, options = {}) => { validateToken(options); let hasWriteQuery = null; let hasSingleQuery = true; const operations = queries.reduce( (acc, details) => { const { database = "default" } = details; if (!acc[database]) acc[database] = {}; if (database !== "default") hasSingleQuery = false; if ("query" in details) { const { query } = details; if (!acc[database].queries) acc[database].queries = []; acc[database].queries.push(query); const queryType = Object.keys(query)[0]; hasWriteQuery = hasWriteQuery || WRITE_QUERY_TYPES.includes(queryType); return acc; } const { statement } = details; if (!acc[database].nativeQueries) acc[database].nativeQueries = []; acc[database].nativeQueries.push({ query: statement.statement, values: statement.params }); return acc; }, {} ); const requestBody = hasSingleQuery ? operations.default : operations; 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(requestBody), // 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 responseResults = await getResponseBody(response); const startFormatting = performance.now(); const formattedResults = []; if ("results" in responseResults) { const usableResults = responseResults.results; const finalResults = formatResults(usableResults); formattedResults.push(...finalResults.map((result) => ({ result }))); } else { for (const [database, { results }] of Object.entries(responseResults)) { const finalResults = formatResults(results); formattedResults.push(...finalResults.map((result) => ({ result, database }))); } } const endFormatting = performance.now(); const VERBOSE_LOGGING = typeof process !== "undefined" && process?.env && process.env.__RENDER_DEBUG_LEVEL === "verbose" || typeof import.meta?.env !== "undefined" && import.meta.env.__RENDER_DEBUG_LEVEL === "verbose"; if (VERBOSE_LOGGING) { console.log(`Formatting took ${endFormatting - startFormatting}ms`); } return formattedResults; }; async function runQueriesWithStorageAndTriggers(queries, options = {}) { const singleDatabase = Array.isArray(queries); const normalizedQueries = singleDatabase ? { default: queries } : queries; const queriesWithReferences = (await Promise.all( Object.entries(normalizedQueries).map(async ([database, queries2]) => { const populatedQueries = await processStorableObjects(queries2, (objects) => { return uploadStorableObjects(objects, options); }); return populatedQueries.map((query) => ({ query, database: database === "default" ? void 0 : database })); }) )).flat(); const results = await runQueriesWithTriggers(queriesWithReferences, options); if (singleDatabase) return results.filter(({ database }) => !database).map(({ result }) => result); return results.reduce( (acc, { result, database = "default" }) => { if (!acc[database]) acc[database] = []; acc[database].push(result); return acc; }, {} ); } var formatIndividualResult = (result) => { if ("amount" in result && typeof result.amount !== "undefined" && result.amount !== null) { return Number(result.amount); } const dateFields = "modelFields" in result ? Object.entries(result.modelFields).filter(([, type]) => type === "date").map(([slug]) => slug) : []; if ("record" in result) { if (result.record === null) return null; formatDateFields(result.record, dateFields); return result.record; } if ("records" in result) { for (const record of result.records) { formatDateFields(record, dateFields); } const formattedRecords = result.records; if (typeof result.moreBefore !== "undefined") formattedRecords.moreBefore = result.moreBefore; if (typeof result.moreAfter !== "undefined") formattedRecords.moreAfter = result.moreAfter; return formattedRecords; } return result; }; var formatResults = (results) => { const formattedResults = []; for (const result of results) { if ("models" in result) { formattedResults.push( Object.fromEntries( Object.entries(result.models).map(([model, result2]) => { return [model, formatIndividualResult(result2)]; }) ) ); continue; } formattedResults.push(formatIndividualResult(result)); } return formattedResults; }; // src/utils/handlers.ts var queriesHandler = async (queries, options = {}) => { if ("statements" in queries) { const results = await runQueries( queries.statements.map((statement) => ({ statement })), options ); return results.map(({ result }) => result); } if (options.database) { const queryList = { [options.database]: queries }; const result = await runQueriesWithStorageAndTriggers(queryList, options); return result[options.database]; } return runQueriesWithStorageAndTriggers(queries, options); }; var queryHandler = async (query, options) => { const input = "statement" in query ? { statements: [query.statement] } : [query]; const results = await queriesHandler(input, options); return results[0]; }; // src/index.ts import { QUERY_SYMBOLS } from "@ronin/compiler"; import { getBatchProxy, getBatchProxySQL, getSyntaxProxy, getSyntaxProxySQL } from "@ronin/syntax/queries"; var createSyntaxFactory = (options) => { const callback = (defaultQuery, queryOptions) => { const query = defaultQuery; return queryHandler(query[QUERY_SYMBOLS.QUERY], mergeOptions(options, queryOptions)); }; const replacer = (value) => isStorableObject(value) ? value : void 0; return { // Query types for interacting with records. get: getSyntaxProxy({ root: `${QUERY_SYMBOLS.QUERY}.get`, callback, replacer }), set: getSyntaxProxy({ root: `${QUERY_SYMBOLS.QUERY}.set`, callback, replacer }), add: getSyntaxProxy({ root: `${QUERY_SYMBOLS.QUERY}.add`, callback, replacer }), remove: getSyntaxProxy({ root: `${QUERY_SYMBOLS.QUERY}.remove`, callback, replacer }), count: getSyntaxProxy({ root: `${QUERY_SYMBOLS.QUERY}.count`, callback, replacer }), // Query types for interacting with the database schema. list: getSyntaxProxy({ root: `${QUERY_SYMBOLS.QUERY}.list`, callback, replacer }), create: getSyntaxProxy({ root: `${QUERY_SYMBOLS.QUERY}.create`, callback, replacer }), alter: getSyntaxProxy({ root: `${QUERY_SYMBOLS.QUERY}.alter`, callback, replacer }), drop: getSyntaxProxy({ root: `${QUERY_SYMBOLS.QUERY}.drop`, callback, replacer }), // Function for executing a transaction containing multiple queries. batch: (operations, queryOptions) => { const batchOperations = operations; const queries = getBatchProxy(batchOperations).map(({ structure }) => structure); const finalOptions = mergeOptions(options, queryOptions); return queriesHandler(queries, finalOptions); }, sql: getSyntaxProxySQL({ callback: (statement) => queryHandler({ statement }, mergeOptions(options, {})) }), sqlBatch: (operations, queryOptions) => { const batchOperations = operations; const statements = getBatchProxySQL(batchOperations); const finalOptions = mergeOptions(options, queryOptions); return queriesHandler({ statements }, finalOptions); } }; }; var factory = createSyntaxFactory({}); var get = factory.get; var set = factory.set; var add = factory.add; var remove = factory.remove; var count = factory.count; var list = factory.list; var create = factory.create; var alter = factory.alter; var drop = factory.drop; var batch = factory.batch; var sql = factory.sql; var sqlBatch = factory.sqlBatch; var index_default = createSyntaxFactory; export { ClientError, isStorableObject, processStorableObjects, runQueriesWithStorageAndTriggers, createSyntaxFactory, get, set, add, remove, count, list, create, alter, drop, batch, sql, sqlBatch, index_default };