UNPKG

blade

Version:
1,380 lines (1,379 loc) • 137 kB
import { n as Hive, r as Selector, t as RemoteStorage } from "./remote-storage-CThzVgzV.js"; //#region ../blade-client/dist/src-BlzhOSRf.js /** Query types used for reading data. */ const DML_QUERY_TYPES_READ = ["get", "count"]; /** Query types used for writing data. */ const DML_QUERY_TYPES_WRITE = [ "set", "add", "remove" ]; /** Query types used for interacting with data. */ const DML_QUERY_TYPES = [...DML_QUERY_TYPES_READ, ...DML_QUERY_TYPES_WRITE]; /** Query types used for reading the database schema. */ const DDL_QUERY_TYPES_READ = ["list"]; /** Query types used for writing the database schema. */ const DDL_QUERY_TYPES_WRITE = [ "create", "alter", "drop" ]; /** Query types used for interacting with the database schema. */ const DDL_QUERY_TYPES = [...DDL_QUERY_TYPES_READ, ...DDL_QUERY_TYPES_WRITE]; /** All read query types. */ const QUERY_TYPES_READ = [...DML_QUERY_TYPES_READ, ...DDL_QUERY_TYPES_READ]; [...DML_QUERY_TYPES_WRITE, ...DDL_QUERY_TYPES_WRITE]; /** All query types. */ const QUERY_TYPES = [...DML_QUERY_TYPES, ...DDL_QUERY_TYPES]; /** * A list of placeholders that can be located inside queries after those queries were * serialized into JSON objects. * * These placeholders are used to represent special keys and values. For example, if a * query is nested into a query, the nested query will be marked with `__RONIN_QUERY`, * which allows for distinguishing that nested query from an object of instructions. */ const QUERY_SYMBOLS = { QUERY: "__RONIN_QUERY", EXPRESSION: "__RONIN_EXPRESSION", FIELD: "__RONIN_FIELD_", FIELD_PARENT: "__RONIN_FIELD_PARENT_", VALUE: "__RONIN_VALUE" }; /** * A regular expression for matching the symbol that represents a field of a model. */ const RONIN_MODEL_FIELD_REGEX = new RegExp(`${QUERY_SYMBOLS.FIELD}[_a-zA-Z0-9.]+`, "g"); const RAW_FIELD_TYPES = [ "string", "number", "boolean" ]; const CURRENT_TIME_EXPRESSION = { [QUERY_SYMBOLS.EXPRESSION]: `strftime('%Y-%m-%dT%H:%M:%f', 'now') || 'Z'` }; const MOUNTING_PATH_SUFFIX = /(.*?)(\{(\d+)\})?$/; /** * Determines the mounting path and table alias for a sub query. * * @param single - Whether a single or multiple records are being queried. * @param key - The key defined for `including` under which the sub query is mounted. * @param mountingPath - The path of a parent field under which the sub query is mounted. * * @returns A mounting path and a table alias. */ const composeMountingPath = (single, key, mountingPath) => { if (key === "ronin_root") return mountingPath ? mountingPath.replace(MOUNTING_PATH_SUFFIX, (_, p, __, n) => `${p}{${n ? +n + 1 : 1}}`) : key; return `${mountingPath ? `${mountingPath}.` : ""}${single ? key : `${key}[0]`}`; }; const MODEL_ENTITY_ERROR_CODES = { field: "FIELD_NOT_FOUND", index: "INDEX_NOT_FOUND", preset: "PRESET_NOT_FOUND" }; var CompilerError = class extends Error { code; field; fields; issues; queries; constructor(details) { super(details.message); this.name = "CompilerError"; this.code = details.code; this.field = details.field; this.fields = details.fields; this.issues = details.issues; this.queries = details.queries || null; } }; const SINGLE_QUOTE_REGEX = /'/g; const DOUBLE_QUOTE_REGEX = /"/g; const AMPERSAND_REGEX = /\s*&+\s*/g; const SPECIAL_CHARACTERS_REGEX = /[^\w\s-]+/g; const SPLIT_REGEX = /(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|[\s.\-_]+/; /** * Utility function to capitalize the first letter of a string while converting all other * letters to lowercase. * * @param str - The string to capitalize. * * @returns The capitalized string. */ const capitalize = (str) => { if (!str || str.length === 0) return ""; return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); }; /** * Utility function to sanitize a given string. * * - Removes single quotes. * - Removes double quotes. * - Replaces `&` with `and`. * - Replaces special characters with spaces. * - Strips leading and trailing whitespace. * * @param str - The string to sanitize. * * @returns The sanitized string. */ const sanitize = (str) => { if (!str || str.length === 0) return ""; return str.replace(SINGLE_QUOTE_REGEX, "").replace(DOUBLE_QUOTE_REGEX, "").replace(AMPERSAND_REGEX, " and ").replace(SPECIAL_CHARACTERS_REGEX, " ").trim(); }; /** * Utility function to convert a given string to snake-case. * * @param str - The string to convert. * * @returns The converted string. */ const convertToSnakeCase = (str) => { if (!str || str.length === 0) return ""; return sanitize(str).split(SPLIT_REGEX).map((part) => part.toLowerCase()).join("_"); }; /** * Utility function to convert a given string to camel-case. * * @param str - The string to convert. * * @returns The converted string. */ const convertToCamelCase = (str) => { if (!str || str.length === 0) return ""; return sanitize(str).split(SPLIT_REGEX).map((part, index) => index === 0 ? part.toLowerCase() : capitalize(part)).join(""); }; function isObjectObject(value) { return Object.prototype.toString.call(value) === "[object Object]"; } /** * Utility function to check if the given value is an object. * * @param value - Object-like value to check. * * @returns `true` if the provided value is a plain object, otherwise `false`. */ const isObject = (value) => { if (isObjectObject(value) === false) return false; const ctor = value.constructor; if (ctor === void 0) return true; const prot = ctor.prototype; if (isObjectObject(prot) === false) return false; if (prot.hasOwnProperty("isPrototypeOf") === false) return false; return true; }; /** * Checks if the provided value contains a RONIN model symbol (a represenation of a * particular entity inside a query, such as an expression or a sub query) and returns * its type and value. * * @param value - The value that should be checked. * * @returns The type and value of the symbol, if the provided value contains one. */ const getQuerySymbol = (value) => { if (!isObject(value)) return null; const objectValue = value; if (QUERY_SYMBOLS.QUERY in objectValue) return { type: "query", value: objectValue[QUERY_SYMBOLS.QUERY] }; if (QUERY_SYMBOLS.EXPRESSION in objectValue) return { type: "expression", value: objectValue[QUERY_SYMBOLS.EXPRESSION] }; return null; }; /** * Finds all string values that match a given pattern in an object. If needed, it also * replaces them. * * @param obj - The object in which the string values should be found. * @param pattern - The string that values can start with. * @param replacer - A function that returns the replacement value for each match. * * @returns Whether the pattern was found in the object. */ const findInObject = (obj, pattern, replacer) => { let found = false; for (const key in obj) { if (!Object.hasOwn(obj, key)) continue; const value = obj[key]; if (isObject(value)) found = findInObject(value, pattern, replacer); else if (typeof value === "string" && value.startsWith(pattern)) { found = true; if (replacer) obj[key] = value.replace(pattern, replacer); else return found; } } return found; }; /** * Converts an object of nested objects into a flat object, where all keys sit on the * same level (at the root). * * @param obj - The object that should be flattened. * @param prefix - An optional path of a nested field to begin the recursion from. * @param res - The object that the flattened object should be stored in. * * @returns A flattened object. */ const flatten = (obj, prefix = "", res = {}) => { for (const key in obj) { if (!Object.hasOwn(obj, key)) continue; const path = prefix ? `${prefix}.${key}` : key; const value = obj[key]; if (typeof value === "object" && value !== null && !getQuerySymbol(value)) flatten(value, path, res); else res[path] = value; } return res; }; /** * Omits properties from an object. * * @param obj - The object from which properties should be omitted. * @param properties - The properties that should be omitted. * * @returns The object without the omitted properties. */ const omit = (obj, properties) => Object.fromEntries(Object.entries(obj).filter(([key]) => !properties.includes(key))); /** * Picks a property from an object and returns the value of the property. * * @param obj - The object from which the property should be read. * @param path - The path at which the property should be read. * * @returns The value of the property. */ const getProperty$1 = (obj, path) => { return path.split(".").reduce((acc, key) => acc?.[key], obj); }; /** * Splits a path string into an array of path segments. * * @param path - The path string (supports both dot and bracket notation). * * @returns An array of path segments. */ const getPathSegments$1 = (path) => { return path.split(/[.[\]]/g).filter((segment) => segment.trim().length > 0); }; /** * Sets a property on an object by mutating the object in place. * * @param obj - The object on which the property should be set. * @param path - The path at which the property should be set. * @param value - The value of the property. * * @returns Nothing. */ const setProperty$1 = (obj, path, value) => { const segments = getPathSegments$1(path); const _set = (node) => { if (segments.length > 1) { const key = segments.shift(); const nextIsNum = !Number.isNaN(Number.parseInt(segments[0])); if (typeof node[key] !== "object" || node[key] === null) node[key] = nextIsNum ? [] : {}; _set(node[key]); } else node[segments[0]] = value; }; _set(obj); }; /** * Deletes a property from an object by mutating the object in place. Additionally, if * after deletion, any parent objects become empty, those empty objects are also deleted. * * @param obj - The object from which the property should be deleted. * @param path - The path at which the property should be deleted. * * @returns Nothing. */ const deleteProperty = (obj, path) => { const segments = getPathSegments$1(path); const _delete = (node, segs) => { const key = segs[0]; if (segs.length === 1) delete node[key]; else if (node[key] && typeof node[key] === "object" && node[key] !== null) { if (_delete(node[key], segs.slice(1))) delete node[key]; } return Object.keys(node).length === 0; }; _delete(obj, segments); }; /** * Splits a query into its type, model, and instructions. * * @param query - The query to split. * * @returns The type, model, and instructions of the provided query. */ const splitQuery = (query) => { const queryType = Object.keys(query)[0]; const queryModel = Object.keys(query[queryType])[0]; return { queryType, queryModel, queryInstructions: query[queryType][queryModel] }; }; const CURSOR_SEPARATOR = ","; const CURSOR_NULL_PLACEHOLDER = "RONIN_NULL"; /** * Generates a pagination cursor for the provided record. * * @param model - The schema that defined the structure of the record. * @param orderedBy - An object specifying the sorting order for the record fields. * @param record - The record for which the pagination cursor should be generated. * * @returns The generated pagination cursor. */ const generatePaginationCursor = (model, orderedBy, record) => { const { ascending = [], descending = [] } = orderedBy || {}; const keys = [...ascending, ...descending]; if (keys.length === 0) keys.push("ronin.createdAt"); return keys.map((fieldSlug) => { const property = getProperty$1(record, fieldSlug); if (property === null || property === void 0) return CURSOR_NULL_PLACEHOLDER; const { field } = getFieldFromModel(model, fieldSlug, { instructionName: "orderedBy" }); if (field.type === "date") return new Date(property).getTime(); return property; }).map((cursor) => encodeURIComponent(String(cursor))).join(CURSOR_SEPARATOR); }; /** * Generates SQL syntax for the `before` or `after` query instructions, which are used * for paginating a list of records. * * Specifically, the values of `before` and `after` should be derived from the * `moreBefore` and `moreAfter` properties available on a list of records * retrieved from RONIN. * * @param model - The model associated with the current query. * @param statementParams - A collection of values that will automatically be * inserted into the query by SQLite. * @param queryType - The type of query that is being executed. * @param instructions - The instructions associated with the current query. * * @returns The SQL syntax for the provided `before` or `after` instruction. */ const handleBeforeOrAfter = (model, statementParams, queryType, instructions) => { if (!(instructions.before || instructions.after)) throw new CompilerError({ message: "The `before` or `after` instruction must not be empty.", code: "MISSING_INSTRUCTION" }); if (instructions.before && instructions.after) throw new CompilerError({ message: "The `before` and `after` instructions cannot co-exist. Choose one.", code: "MUTUALLY_EXCLUSIVE_INSTRUCTIONS" }); if (!instructions.limitedTo && queryType !== "count") { let message = "When providing a pagination cursor in the `before` or `after`"; message += " instruction, a `limitedTo` instruction must be provided as well, to"; message += " define the page size."; throw new CompilerError({ message, code: "MISSING_INSTRUCTION" }); } const { ascending = [], descending = [] } = instructions.orderedBy || {}; const clause = instructions.with ? "AND " : ""; const chunks = (instructions.before || instructions.after).toString().split(CURSOR_SEPARATOR).map(decodeURIComponent); const keys = [...ascending, ...descending]; const values = keys.map((key, index) => { const value = chunks[index]; if (value === CURSOR_NULL_PLACEHOLDER) return "NULL"; const { field } = getFieldFromModel(model, key, { instructionName: "orderedBy" }); if (field.type === "boolean") return prepareStatementValue(statementParams, value === "true"); if (field.type === "number") return prepareStatementValue(statementParams, Number.parseInt(value)); if (field.type === "date") return `'${new Date(Number.parseInt(value)).toJSON()}'`; return prepareStatementValue(statementParams, value); }); const compareOperators = [...new Array(ascending.length).fill(instructions.before ? "<" : ">"), ...new Array(descending.length).fill(instructions.before ? ">" : "<")]; const conditions = new Array(); for (let i = 0; i < keys.length; i++) { if (values[i] === "NULL" && compareOperators[i] === "<") continue; const condition = new Array(); for (let j = 0; j <= i; j++) { const key = keys[j]; const value = values[j]; let { field, fieldSelector } = getFieldFromModel(model, key, { instructionName: "orderedBy" }); if (j === i) { const closingParentheses = ")".repeat(condition.length); const operator = value === "NULL" ? "IS NOT" : compareOperators[j]; const caseInsensitiveStatement = value !== "NULL" && field.type === "string" ? " COLLATE NOCASE" : ""; if (value !== "NULL" && operator === "<" && !["ronin.createdAt", "ronin.updatedAt"].includes(key)) fieldSelector = `IFNULL(${fieldSelector}, -1e999)`; condition.push(`(${fieldSelector} ${operator} ${value}${caseInsensitiveStatement})${closingParentheses}`); } else { const operator = value === "NULL" ? "IS" : "="; condition.push(`(${fieldSelector} ${operator} ${value} AND`); } } conditions.push(condition.join(" ")); } return `${clause}(${conditions.join(" OR ")})`; }; /** * Generates the SQL syntax for the `including` query instruction, which allows for * joining records from other models. * * @param models - A list of models. * @param model - The model associated with the current query. * @param statementParams - A collection of values that will automatically be * inserted into the query by SQLite. * @param single - Whether a single or multiple records are being queried. * @param instruction - The `including` instruction provided in the current query. * @param options - Additional options for customizing the behavior of the function. * * @returns The SQL syntax for the provided `including` instruction. */ const handleIncluding = (models, model, statementParams, single, instructions, options = { inlineDefaults: false }) => { let statement = ""; let tableSubQuery; for (const ephemeralFieldSlug in instructions.including) { if (!Object.hasOwn(instructions.including, ephemeralFieldSlug)) continue; const symbol = getQuerySymbol(instructions.including[ephemeralFieldSlug]); if (symbol?.type !== "query") continue; const { queryType, queryModel, queryInstructions } = splitQuery(symbol.value); let modifiableQueryInstructions = queryInstructions; if (queryType === "count") continue; const relatedModel = getModelBySlug(models, queryModel); let joinType = "LEFT"; let relatedTableSelector = `"${relatedModel.table}"`; const subSingle = queryModel !== relatedModel.pluralSlug; const subMountingPath = composeMountingPath(subSingle, ephemeralFieldSlug, options.mountingPath); const tableAlias = `including_${subMountingPath}`; if (!modifiableQueryInstructions?.with) { joinType = "CROSS"; if (subSingle) { if (!modifiableQueryInstructions) modifiableQueryInstructions = {}; modifiableQueryInstructions.limitedTo = 1; } } if (modifiableQueryInstructions?.limitedTo || modifiableQueryInstructions?.orderedBy) relatedTableSelector = `(${compileQueryInput({ [queryType]: { [queryModel]: modifiableQueryInstructions } }, models, statementParams, { parentModel: model, inlineDefaults: options.inlineDefaults }).main.sql})`; statement += `${joinType} JOIN ${relatedTableSelector} as "${tableAlias}"`; model.tableAlias = model.tableAlias || model.table; if (joinType === "LEFT") { const subStatement = composeConditions(models, { ...relatedModel, tableAlias }, statementParams, "including", queryInstructions?.with, { parentModel: model }); statement += ` ON (${subStatement})`; } if (single && !subSingle) tableSubQuery = compileQueryInput({ get: { [model.slug]: { ...instructions.with ? { with: instructions.with } : {}, ...instructions.orderedBy ? { orderedBy: instructions.orderedBy } : {} } } }, [{ ...model, tableAlias: void 0 }], statementParams, { inlineDefaults: options.inlineDefaults, explicitColumns: false }).main.sql; if (modifiableQueryInstructions?.including) { const subIncluding = handleIncluding(models, { ...relatedModel, tableAlias }, statementParams, subSingle, { including: modifiableQueryInstructions.including }, { mountingPath: subMountingPath, inlineDefaults: options.inlineDefaults }); statement += ` ${subIncluding.statement}`; } } return { statement, tableSubQuery }; }; /** * Generates the SQL syntax for the `limitedTo` query instruction, which allows for * limiting the amount of records that are returned. * * @param single - Whether a single or multiple records are being queried. * @param instruction - The `limitedTo` instruction provided in the current query. * * @returns The SQL syntax for the provided `limitedTo` instruction. */ const handleLimitedTo = (single, instruction) => { let amount; if (instruction) amount = instruction + 1; if (single) amount = 1; return `LIMIT ${amount} `; }; /** * Generates the SQL syntax for the `orderedBy` query instruction, which allows for * ordering the list of records that are returned. * * @param model - The model associated with the current query. * @param instruction - The `orderedBy` instruction provided in the current query. * * @returns The SQL syntax for the provided `orderedBy` instruction. */ const handleOrderedBy = (model, instruction) => { let statement = ""; const items = [...(instruction.ascending || []).map((value) => ({ value, order: "ASC" })), ...(instruction.descending || []).map((value) => ({ value, order: "DESC" }))]; for (const item of items) { if (statement.length > 0) statement += ", "; const symbol = getQuerySymbol(item.value); const instructionName = item.order === "ASC" ? "orderedBy.ascending" : "orderedBy.descending"; if (symbol?.type === "expression") { statement += `(${parseFieldExpression(model, instructionName, symbol.value)}) ${item.order}`; continue; } const { field: modelField, fieldSelector } = getFieldFromModel(model, item.value, { instructionName }); const caseInsensitiveStatement = modelField.type === "string" ? " COLLATE NOCASE" : ""; statement += `${fieldSelector}${caseInsensitiveStatement} ${item.order}`; } return `ORDER BY ${statement}`; }; /** * Generates the SQL syntax for the `selecting` query instruction, which allows for * selecting a list of columns from rows. * * @param models - A list of models. * @param model - The model associated with the current query. * @param single - Whether a single or multiple records are being queried. * @param statementParams - A collection of values that will automatically be * inserted into the query by SQLite. * @param queryType - The type of query that is being executed. * @param instructions - The instructions associated with the current query. * @param options - Additional options for customizing the behavior of the function. * * @returns An SQL string containing the columns that should be selected. */ const handleSelecting = (models, model, statementParams, queryType, single, instructions, options) => { let isJoining = false; const selectedFields = filterSelectedFields(model, instructions.selecting).filter((field) => !(field.type === "link" && field.kind === "many")).map((field) => { const newField = { ...field, mountingPath: field.slug }; if (options.mountingPath && options.mountingPath !== "ronin_root") newField.mountingPath = `${options.mountingPath.replace(/\{\d+\}/g, "")}.${field.slug}`; return newField; }); const joinedSelectedFields = []; const joinedColumns = []; if (instructions.including) { if (getQuerySymbol(instructions.including)?.type === "query") { instructions.including.ronin_root = { ...instructions.including }; delete instructions.including[QUERY_SYMBOLS.QUERY]; } const flatObject = flatten(instructions.including); for (const [key, value] of Object.entries(flatObject)) { const symbol = getQuerySymbol(value); if (symbol?.type === "query") { const { queryType: queryType$1, queryModel, queryInstructions } = splitQuery(symbol.value); const subQueryModel = getModelBySlug(models, queryModel); if (queryType$1 === "count") { const subSelect = compileQueryInput(symbol.value, models, statementParams, { parentModel: { ...model, tableAlias: model.table }, inlineDefaults: options.inlineDefaults }); selectedFields.push({ slug: key, mountingPath: key, type: "number", mountedValue: `(${subSelect.main.sql})` }); continue; } isJoining = true; const subSingle = queryModel !== subQueryModel.pluralSlug; if (!model.tableAlias) model.tableAlias = single && !subSingle ? `sub_${model.table}` : model.table; const subMountingPath = composeMountingPath(subSingle, key, options.mountingPath); const { columns: nestedColumns, selectedFields: nestedSelectedFields } = handleSelecting(models, { ...subQueryModel, tableAlias: `including_${subMountingPath}` }, statementParams, queryType$1, subSingle, { selecting: queryInstructions?.selecting, including: queryInstructions?.including, orderedBy: queryInstructions?.orderedBy, limitedTo: queryInstructions?.limitedTo }, { ...options, mountingPath: subMountingPath }); if (nestedColumns !== "*") joinedColumns.push(nestedColumns); joinedSelectedFields.push(...nestedSelectedFields); continue; } let mountedValue = value; if (symbol?.type === "expression") mountedValue = `(${parseFieldExpression(model, "including", symbol.value)})`; else mountedValue = prepareStatementValue(statementParams, value); const existingField = selectedFields.findIndex((field) => field.slug === key); if (existingField > -1) selectedFields.splice(existingField, 1); selectedFields.push({ slug: key, mountingPath: key, type: RAW_FIELD_TYPES.includes(typeof value) ? typeof value : "string", mountedValue }); } } if (queryType === "get" && !single && typeof instructions.limitedTo !== "undefined") { const orderedFields = Object.values(instructions.orderedBy || {}).flat().map((fieldSlug) => { return getFieldFromModel(model, fieldSlug, { instructionName: "orderedBy" }); }); for (const orderedField of orderedFields) { const { field } = orderedField; if (selectedFields.some(({ slug }) => slug === field.slug)) continue; selectedFields.push({ slug: field.slug, mountingPath: field.slug, excluded: true }); } } let columnList = "*"; if (options.explicitColumns) columnList = [...selectedFields.map((selectedField) => { if (selectedField.mountedValue) return `${selectedField.mountedValue} as "${selectedField.slug}"`; const { fieldSelector } = getFieldFromModel(model, selectedField.slug, { instructionName: "selecting" }); if (options.mountingPath) return `${fieldSelector} as "${options.mountingPath}.${selectedField.slug}"`; return fieldSelector; }), ...joinedColumns].join(", "); return { columns: columnList, isJoining, selectedFields: [...selectedFields, ...joinedSelectedFields] }; }; const conjunctions = [ "for", "and", "nor", "but", "or", "yet", "so" ]; const articles = [ "a", "an", "the" ]; const prepositions = [ "aboard", "about", "above", "across", "after", "against", "along", "amid", "among", "anti", "around", "as", "at", "before", "behind", "below", "beneath", "beside", "besides", "between", "beyond", "but", "by", "concerning", "considering", "despite", "down", "during", "except", "excepting", "excluding", "following", "for", "from", "in", "inside", "into", "like", "minus", "near", "of", "off", "on", "onto", "opposite", "over", "past", "per", "plus", "regarding", "round", "save", "since", "than", "through", "to", "toward", "towards", "under", "underneath", "unlike", "until", "up", "upon", "versus", "via", "with", "within", "without" ]; const lowerCase = /* @__PURE__ */ new Set([ ...conjunctions, ...articles, ...prepositions ]); const specials = [ "ZEIT", "ZEIT Inc.", "Vercel", "Vercel Inc.", "CLI", "API", "HTTP", "HTTPS", "JSX", "DNS", "URL", "now.sh", "now.json", "vercel.app", "vercel.json", "CI", "CD", "CDN", "package.json", "package.lock", "yarn.lock", "GitHub", "GitLab", "CSS", "Sass", "JS", "JavaScript", "TypeScript", "HTML", "WordPress", "Next.js", "Node.js", "Webpack", "Docker", "Bash", "Kubernetes", "SWR", "TinaCMS", "UI", "UX", "TS", "TSX", "iPhone", "iPad", "watchOS", "iOS", "iPadOS", "macOS", "PHP", "composer.json", "composer.lock", "CMS", "SQL", "C", "C#", "GraphQL", "GraphiQL", "JWT", "JWTs" ]; const word = `[^\\s'\u2019\\(\\)!?;:"-]`; const regex = new RegExp(`(?:(?:(\\s?(?:^|[.\\(\\)!?;:"-])\\s*)(${word}))|(${word}))(${word}*[\u2019']*${word}*)`, "g"); const convertToRegExp = (specials2) => specials2.map((s) => [new RegExp(`\\b${s}\\b`, "gi"), s]); function parseMatch(match) { const firstCharacter = match[0]; if (/\s/.test(firstCharacter)) return match.slice(1); if (/[\(\)]/.test(firstCharacter)) return null; return match; } var src_default$1 = (str, options = {}) => { str = str.toLowerCase().replace(regex, (m, lead = "", forced, lower, rest, offset, string) => { const isLastWord = m.length + offset >= string.length; const parsedMatch = parseMatch(m); if (!parsedMatch) return m; if (!forced) { const fullLower = lower + rest; if (lowerCase.has(fullLower) && !isLastWord) return parsedMatch; } return lead + (lower || forced).toUpperCase() + rest; }); const customSpecials = options.special || []; convertToRegExp([...specials, ...customSpecials]).forEach(([pattern, s]) => { str = str.replace(pattern, s); }); return str; }; /** * Converts a slug to a readable name by splitting it on uppercase characters * and returning it formatted as title case. * * @example * ```ts * slugToName('activeAt'); // 'Active At' * ``` * * @param slug - The slug string to convert. * * @returns The formatted name in title case. */ const slugToName = (slug) => { return src_default$1(slug.replace(/([a-z])([A-Z])/g, "$1 $2")); }; const VOWELS = [ "a", "e", "i", "o", "u" ]; /** * Pluralizes a singular English noun according to basic English pluralization rules. * * This function handles the following cases: * - **Words ending with a consonant followed by 'y'**: Replaces the 'y' with 'ies'. * - **Words ending with 's', 'ch', 'sh', or 'ex'**: Adds 'es' to the end of the word. * - **All other words**: Adds 's' to the end of the word. * * @example * ```ts * pluralize('baby'); // 'babies' * pluralize('key'); // 'keys' * pluralize('bus'); // 'buses' * pluralize('church'); // 'churches' * pluralize('cat'); // 'cats' * ``` * * @param word - The singular noun to pluralize. * * @returns The plural form of the input word. */ const pluralize = (word$1) => { const lastLetter = word$1.slice(-1).toLowerCase(); const secondLastLetter = word$1.slice(-2, -1).toLowerCase(); if (lastLetter === "y" && !VOWELS.includes(secondLastLetter)) return `${word$1.slice(0, -1)}ies`; if (lastLetter === "s" || word$1.slice(-2).toLowerCase() === "ch" || word$1.slice(-2).toLowerCase() === "sh" || word$1.slice(-2).toLowerCase() === "ex") return `${word$1}es`; return `${word$1}s`; }; /** * A list of settings that can be automatically generated based on other settings. * * The first item in each tuple is the setting that should be generated, the second item * is the setting that should be used as a base, and the third item is the function that * should be used to generate the new setting. */ const modelAttributes = [ [ "pluralSlug", "slug", pluralize, true ], [ "name", "slug", slugToName, false ], [ "pluralName", "pluralSlug", slugToName, false ], [ "idPrefix", "slug", (slug) => slug.slice(0, 3).toLowerCase(), false ], [ "table", "pluralSlug", convertToSnakeCase, true ] ]; /** * Generates a unique identifier for a newly created record. * * @returns A string containing the ID. */ const getRecordIdentifier = (prefix) => { return `${prefix}_${Array.from(crypto.getRandomValues(new Uint8Array(12))).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16).toLowerCase()}`; }; /** * Sets default values for the attributes of a model (such as `name`, `pluralName`, etc). * * @param model - The model that should receive defaults. * @param isNew - Whether the model is being newly created. * * @returns The updated model. */ const addDefaultModelAttributes = (model, isNew) => { const copiedModel = { ...model }; if (isNew && !copiedModel.id) copiedModel.id = getRecordIdentifier("mod"); for (const [setting, base, generator, mustRegenerate] of modelAttributes) { if (!(isNew || mustRegenerate)) continue; if (copiedModel[setting] || !copiedModel[base]) continue; copiedModel[setting] = generator(copiedModel[base]); } const newFields = copiedModel.fields || []; if (isNew || Object.keys(newFields).length > 0) { if (!copiedModel.identifiers) copiedModel.identifiers = {}; if (!copiedModel.identifiers.name) { const suitableField = Object.entries(newFields).find(([fieldSlug, field]) => field.type === "string" && field.required === true && ["name"].includes(fieldSlug)); copiedModel.identifiers.name = suitableField?.[0] || "id"; } if (!copiedModel.identifiers.slug) { const suitableField = Object.entries(newFields).find(([fieldSlug, field]) => field.type === "string" && field.unique === true && field.required === true && ["slug", "handle"].includes(fieldSlug)); copiedModel.identifiers.slug = suitableField?.[0] || "id"; } } return copiedModel; }; /** * Provides default system fields for a given model. * * @param model - The model that should receive defaults. * @param isNew - Whether the model is being newly created. * * @returns The updated model. */ const addDefaultModelFields = (model, isNew) => { const copiedModel = { ...model }; const existingFields = copiedModel.fields || []; if (isNew || Object.keys(existingFields).length > 0) copiedModel.fields = { ...Object.fromEntries(Object.entries(getSystemFields(copiedModel.idPrefix)).filter(([newFieldSlug]) => { return !Object.hasOwn(existingFields, newFieldSlug); })), ...existingFields }; return copiedModel; }; /** * Provides default system presets for a given model. * * @param list - The list of all models. * @param model - The model for which default presets should be added. * * @returns The model with default presets added. */ const addDefaultModelPresets = (list$1, model) => { const defaultPresets = {}; for (const [fieldSlug, rest] of Object.entries(model.fields || {})) { const field = { slug: fieldSlug, ...rest }; if (field.type === "link" && !fieldSlug.startsWith("ronin.")) { const targetModel = getModelBySlug(list$1, field.target); if (field.kind === "many") { const systemModel = list$1.find(({ system }) => { return system?.model === model.id && system?.associationSlug === field.slug; }); if (!systemModel) continue; defaultPresets[fieldSlug] = { instructions: { including: { [fieldSlug]: { [QUERY_SYMBOLS.QUERY]: { get: { [systemModel.pluralSlug]: { with: { source: { [QUERY_SYMBOLS.EXPRESSION]: `${QUERY_SYMBOLS.FIELD_PARENT}id` } }, including: { [QUERY_SYMBOLS.QUERY]: { get: { [targetModel.slug]: { with: { id: { [QUERY_SYMBOLS.EXPRESSION]: `${QUERY_SYMBOLS.FIELD_PARENT}target` } } } } } }, selecting: [ "**", "!source", "!target" ] } } } } } }, name: slugToName(fieldSlug), system: true }; continue; } defaultPresets[fieldSlug] = { instructions: { including: { [fieldSlug]: { [QUERY_SYMBOLS.QUERY]: { get: { [targetModel.slug]: { with: { id: { [QUERY_SYMBOLS.EXPRESSION]: `${QUERY_SYMBOLS.FIELD_PARENT}${field.slug}` } } } } } } } }, name: slugToName(fieldSlug), system: true }; } } const childModels = list$1.map((subModel) => { if (subModel.system?.associationSlug) return null; const field = Object.entries(subModel.fields).find(([fieldSlug, rest]) => { const field$1 = { slug: fieldSlug, ...rest }; return field$1.type === "link" && field$1.target === model.slug; }); if (!field) return null; return { model: subModel, field: { slug: field[0], ...field[1] } }; }).filter((match) => match !== null); for (const childMatch of childModels) { const { model: childModel, field: childField } = childMatch; const pluralSlug = childModel.pluralSlug; const presetSlug = childModel.system?.associationSlug || pluralSlug; defaultPresets[presetSlug] = { instructions: { including: { [presetSlug]: { [QUERY_SYMBOLS.QUERY]: { get: { [pluralSlug]: { with: { [childField.slug]: { [QUERY_SYMBOLS.EXPRESSION]: `${QUERY_SYMBOLS.FIELD_PARENT}id` } } } } } } } }, name: slugToName(presetSlug), system: true }; } if (Object.keys(defaultPresets).length > 0) { const existingPresets = model.presets; model.presets = { ...Object.fromEntries(Object.entries(defaultPresets).filter(([newPresetSlug]) => { return !existingPresets?.[newPresetSlug]; })), ...existingPresets }; } return model; }; /** * Generates the SQL syntax for the `to` query instruction, which allows for providing * values that should be stored in the records that are being addressed. * * @param models - A list of models. * @param model - The model associated with the current query. * @param statementParams - A collection of values that will automatically be * inserted into the query by SQLite. * @param queryType - The type of query that is being executed. * @param dependencyStatements - A list of SQL statements to be executed before the main * SQL statement, in order to prepare for it. * @param instructions - The `to` and `with` instruction included in the query. * @param options - Additional options to adjust the behavior of the statement generation. * * @returns The SQL syntax for the provided `to` instruction. */ const handleTo = (models, model, statementParams, queryType, dependencyStatements, instructions, options) => { const { with: withInstruction, to: toInstruction } = instructions; const defaultFields = {}; const currentTime = /* @__PURE__ */ new Date(); if (queryType === "add" && options?.inlineDefaults) defaultFields.id = toInstruction.id || getRecordIdentifier(model.idPrefix); if (queryType === "add" || queryType === "set" || toInstruction.ronin) { const defaults = options?.inlineDefaults ? { ...queryType === "add" && { createdAt: currentTime }, updatedAt: currentTime, ...toInstruction.ronin } : { ...queryType === "set" ? { updatedAt: CURRENT_TIME_EXPRESSION } : {}, ...toInstruction.ronin }; if (Object.keys(defaults).length > 0) defaultFields.ronin = defaults; } const symbol = getQuerySymbol(toInstruction); if (symbol?.type === "query") { const { queryModel: subQueryModelSlug, queryInstructions: subQueryInstructions } = splitQuery(symbol.value); const subQueryModel = getModelBySlug(models, subQueryModelSlug); const subQuerySelectedFields = subQueryInstructions?.selecting; const subQueryIncludedFields = subQueryInstructions?.including; const subQueryFields = [...filterSelectedFields(subQueryModel, subQuerySelectedFields).map((field) => field.slug), ...subQueryIncludedFields ? Object.keys(flatten(subQueryIncludedFields || {})) : []]; for (const field of subQueryFields || []) getFieldFromModel(model, field, { instructionName: "to" }); let statement$1 = ""; if (subQuerySelectedFields) statement$1 = `(${subQueryFields.map((field) => { return getFieldFromModel(model, field, { instructionName: "to" }).fieldSelector; }).join(", ")}) `; statement$1 += compileQueryInput(symbol.value, models, statementParams, { inlineDefaults: options?.inlineDefaults || false }).main.sql; return statement$1; } Object.assign(toInstruction, defaultFields); for (const fieldSlug in toInstruction) { if (!Object.hasOwn(toInstruction, fieldSlug)) continue; const fieldValue = toInstruction[fieldSlug]; const fieldDetails = getFieldFromModel(model, fieldSlug, { instructionName: "to" }, false); if (fieldDetails?.field.type === "link" && fieldDetails.field.kind === "many") { delete toInstruction[fieldSlug]; const associativeModelSlug = composeAssociationModelSlug(model, fieldDetails.field); const composeStatement = (subQueryType, value) => { const recordDetails = { source: queryType === "add" ? toInstruction : withInstruction }; if (value) recordDetails.target = value; const query = compileQueryInput({ [subQueryType]: { [associativeModelSlug]: { with: recordDetails } } }, models, [], { returning: false, inlineDefaults: options?.inlineDefaults || false }).main; dependencyStatements.push({ ...query, after: true }); }; if (Array.isArray(fieldValue)) { if (queryType === "set") composeStatement("remove"); for (const record of fieldValue) composeStatement("add", record); } else if (isObject(fieldValue)) { const value = fieldValue; for (const recordToAdd of value.containing || []) composeStatement("add", recordToAdd); for (const recordToRemove of value.notContaining || []) composeStatement("remove", recordToRemove); } } } let statement = composeConditions(models, model, statementParams, "to", toInstruction, { parentModel: options?.parentModel, type: queryType === "add" ? "fields" : void 0 }); if (queryType === "add") { const deepStatement = composeConditions(models, model, statementParams, "to", toInstruction, { parentModel: options?.parentModel, type: "values" }); statement = `(${statement}) VALUES (${deepStatement})`; } else if (queryType === "set") statement = `SET ${statement}`; return statement; }; /** * Generates the SQL syntax for the `using` query instruction, which allows for quickly * adding a list of pre-defined instructions to a query. * * @param model - The model associated with the current query. * @param instructions - The instructions of the current query. * * @returns The SQL syntax for the provided `using` instruction. */ const handleUsing = (model, instructions) => { const normalizedUsing = Array.isArray(instructions.using) ? Object.fromEntries(instructions.using.map((presetSlug) => [presetSlug, null])) : instructions.using; if ("links" in normalizedUsing) for (const [fieldSlug, rest] of Object.entries(model.fields)) { const field = { slug: fieldSlug, ...rest }; if (field.type !== "link" || field.kind === "many") continue; normalizedUsing[fieldSlug] = null; } for (const presetSlug in normalizedUsing) { if (!Object.hasOwn(normalizedUsing, presetSlug) || presetSlug === "links") continue; const arg = normalizedUsing[presetSlug]; const preset = model.presets?.[presetSlug]; if (!preset) throw new CompilerError({ message: `Preset "${presetSlug}" does not exist in model "${model.name}".`, code: "PRESET_NOT_FOUND" }); const replacedUsingFilter = structuredClone(preset.instructions); if (arg !== null) findInObject(replacedUsingFilter, QUERY_SYMBOLS.VALUE, (match) => match.replace(QUERY_SYMBOLS.VALUE, arg)); for (const subInstruction in replacedUsingFilter) { if (!Object.hasOwn(replacedUsingFilter, subInstruction)) continue; const instructionName = subInstruction; const currentValue = instructions[instructionName]; if (currentValue) { let newValue; if (Array.isArray(currentValue)) newValue = Array.from(new Set([...replacedUsingFilter[instructionName], ...currentValue])); else if (isObject(currentValue)) newValue = { ...replacedUsingFilter[instructionName], ...currentValue }; Object.assign(instructions, { [instructionName]: newValue }); continue; } Object.assign(instructions, { [instructionName]: replacedUsingFilter[instructionName] }); } } return instructions; }; /** * Composes an SQL statement for a provided RONIN query. * * @param query - The RONIN query for which an SQL statement should be composed. * @param models - A list of models. * @param statementParams - A collection of values that will automatically be * inserted into the query by SQLite. * @param options - Additional options to adjust the behavior of the statement generation. * * @returns The composed SQL statement. */ const compileQueryInput = (defaultQuery, models, statementParams, options) => { const dependencyStatements = []; const query = transformMetaQuery(models, dependencyStatements, statementParams, defaultQuery, { inlineDefaults: options?.inlineDefaults || false }); if (query === null) return { dependencies: [], main: dependencyStatements[0], selectedFields: [], model: ROOT_MODEL_WITH_ATTRIBUTES, updatedQuery: defaultQuery }; const { queryType, queryModel, queryInstructions } = splitQuery(query); const model = getModelBySlug(models, queryModel); const single = queryModel !== model.pluralSlug; let instructions = formatIdentifiers(model, queryInstructions); const returning = options?.returning ?? true; if (instructions && typeof instructions.using !== "undefined") instructions = handleUsing(model, instructions); if (queryType === "count") { if (!instructions) instructions = {}; instructions.selecting = ["amount"]; instructions.including = Object.assign(instructions?.including || {}, { amount: { [QUERY_SYMBOLS.EXPRESSION]: "COUNT(*)" } }); } if (options?.defaultRecordLimit && !single && queryType === "get" && !instructions?.limitedTo) { if (!instructions) instructions = {}; instructions.limitedTo = options.defaultRecordLimit; } if (!single && (queryType === "get" && instructions?.limitedTo || queryType === "count" && (instructions?.before || instructions?.after))) { instructions = instructions || {}; instructions.orderedBy = instructions.orderedBy || {}; instructions.orderedBy.ascending = instructions.orderedBy.ascending || []; instructions.orderedBy.descending = instructions.orderedBy.descending || []; if (![...instructions.orderedBy.ascending, ...instructions.orderedBy.descending].includes("ronin.createdAt")) instructions.orderedBy.descending.push("ronin.createdAt"); } const { columns, isJoining, selectedFields } = handleSelecting(models, model, statementParams, queryType, single, { selecting: instructions?.selecting, including: instructions?.including, orderedBy: instructions?.orderedBy, limitedTo: instructions?.limitedTo }, { inlineDefaults: options?.inlineDefaults ?? false, explicitColumns: options?.explicitColumns ?? true }); let statement = ""; switch (queryType) { case "get": case "count": statement += `SELECT ${columns} FROM `; break; case "set": statement += "UPDATE "; break; case "add": statement += "INSERT INTO "; break; case "remove": statement += "DELETE FROM "; break; } let isJoiningMultipleRows = false; if (isJoining) { const { statement: including, tableSubQuery } = handleIncluding(models, model, statementParams, single, { with: instructions?.with, orderedBy: instructions?.orderedBy, including: instructions?.including }); if (tableSubQuery) { statement += `(${tableSubQuery}) as ${model.tableAlias} `; isJoiningMultipleRows = true; if (instructions?.["with"]) delete instructions["with"]; if (instructions?.["orderedBy"]) delete instructions["orderedBy"]; } else statement += `"${model.table}" `; statement += `${including} `; } else statement += `"${model.table}" `; if (queryType === "add" || queryType === "set") { const instructionName = queryType === "add" ? "with" : "to"; const instructionValue = instructions[instructionName]; if (!(instructionValue && isObject(instructionValue)) || Object.keys(instructionValue).length === 0) throw new CompilerError({ message: `When using a \`${queryType}\` query, the \`${instructionName}\` instruction must be a non-empty object.`, code: instructionName === "to" ? "INVALID_TO_VALUE" : "INVALID_WITH_VALUE", queries: [query] }); const toStatement = handleTo(models, model, statementParams, queryType, dependencyStatements, { with: instructions.with, to: instructionValue }, options); statement += `${toStatement} `; } const conditions = []; if (queryType !== "add" && instructions && Object.hasOwn(instructions, "with")) { const withStatement = handleWith(models, model, statementParams, instructions.with, options?.parentModel); if (withStatement.length > 0) conditions.push(withStatement); } if (instructions && (typeof instructions.before !== "undefined" || typeof instructions.after !== "undefined")) { if (single) throw new CompilerError({ message: "The `before` and `after` instructions are not supported when querying for a single record.", code: "INVALID_BEFORE_OR_AFTER_INSTRUCTION", queries: [query] }); const beforeAndAfterStatement = handleBeforeOrAfter(model, statementParams, queryType, { before: instructions.before, after: instructions.after, with: instructions.with, orderedBy: instructions.orderedBy, limitedTo: instructions.limitedTo }); conditions.push(beforeAndAfterStatement); } if (conditions.length > 0) if (conditions.length === 1) statement += `WHERE ${conditions[0]} `; else statement += `WHERE (${conditions.join(" ")}) `; if (instructions?.orderedBy) { const orderedByStatement = handleOrderedBy(model, instructions.orderedBy); statement += `${orderedByStatement} `; } if (queryType === "get" && !isJoiningMultipleRows && (single || instructions?.limitedTo)) statement += handleLimitedTo(single, instructions?.limitedTo); if (DML_QUERY_TYPES_WRITE.includes(queryType) && returning) statement += `RETURNING ${columns}`; const mainStatement = { sql: statement.trimEnd(), params: statementParams || [] }; if (returning) mainStatement.returning = true; return { dependencies: dependencyStatements, main: mainStatement, selectedFields, model, updatedQuery: query }; }; /** * Serializes individual keys and values within a JSON object and escapes query symbols. * * @param key - The key of the JSON property. * @param value - The value of the JSON property. * * @returns The serialized value of the JSON property. */ const replaceJSON = (key, value) => { if (key === QUERY_SYMBOLS.EXPRESSION) return value.replaceAll(`'`, `''`); return value; }; /** * Determines which of the provided model fields match a given pattern. * * @param fields - The fields of a particular model. * @param pattern - The pattern to match against the fields. * * @returns The fields that match the provided pattern. */ const matchSelectedFields = (fields, pattern) => { let regexStr = pattern.replace(/\./g, "\\."); regexStr = regexStr.replace(/\*\*/g, "<<DOUBLESTAR>>"); regexStr = regexStr.replace(/\*/g, "[^.]*"); regexStr = regexStr.replace(/<<DOUBLESTAR>>/g, ".*"); const regex$1 = /* @__PURE__ */ new RegExp(`^${regexStr}$`); return fields.filter((field) => regex$1.test(field.slug)); }; /** * Determines which fields of a model should be selected by a query, based on the value * of a provided `selecting` instruction. * * @param model - The model associated with the current query. * @param instruction - The `selecting` instruction provided in the current query. * * @returns The list of fields that should be selected. */ const filterSelectedFields = (model, instruction) => { const mappedFields = Object.entries(model.fields).map(([fieldSlug, field]) => ({ slug: fieldSlug, ...field })); if (!instruction) return mappedFields; let selectedFields = []; for (const pattern of instruction) { const isNegative = pattern.startsWith("!"); const cleanPattern = isNegative ? pattern.slice(1) : pattern; const matchedFields = matchSelectedFields(isNegative ? selectedFields : mappedFields, cleanPattern); if (