UNPKG

blade

Version:
490 lines (489 loc) • 12.6 kB
//#region ../blade-compiler/dist/index.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]; /** All write query types. */ const QUERY_TYPES_WRITE = [...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 CURRENT_TIME_EXPRESSION = { [QUERY_SYMBOLS.EXPRESSION]: `strftime('%Y-%m-%dT%H:%M:%f', 'now') || 'Z'` }; 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 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("_"); }; 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; }; /** * 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))); 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 = (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(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; }; /** * This model defines the architecture of the `ronin_schema` table, which is RONIN's * equivalent to the native `sqlite_schema` table provided by SQLite. */ const ROOT_MODEL = { id: "mod_26cedf5fc602c3ba", slug: "roninModel", pluralSlug: "roninModels", name: "Ronin Model", pluralName: "Ronin Models", identifiers: { name: "name", slug: "slug" }, idPrefix: "mod", table: "ronin_schema", system: { model: "root" }, fields: { name: { type: "string" }, pluralName: { type: "string" }, slug: { type: "string" }, pluralSlug: { type: "string" }, idPrefix: { type: "string" }, table: { type: "string" }, "identifiers.name": { type: "string" }, "identifiers.slug": { type: "string" }, fields: { type: "json", defaultValue: {} }, indexes: { type: "json", defaultValue: {} }, presets: { type: "json", defaultValue: {} } } }; /** * We're adding the attributes of the root model at bootup time, so that the performance * of the query compilation is not affected. */ const ROOT_MODEL_WITH_ATTRIBUTES = addDefaultModelAttributes(ROOT_MODEL, true); const PLURAL_MODEL_ENTITIES = { field: "fields", index: "indexes", preset: "presets" }; Object.values(PLURAL_MODEL_ENTITIES); const CLEAN_ROOT_MODEL = omit(ROOT_MODEL, ["system"]); //#endregion export { getQuerySymbol as a, QUERY_TYPES as i, DML_QUERY_TYPES_WRITE as n, getRecordIdentifier as o, QUERY_SYMBOLS as r, DDL_QUERY_TYPES as t };