@ronin/compiler
Advanced tools
Comparing version 0.1.0 to 0.1.1-leo-ron-1083-experimental.70
1177
dist/index.js
@@ -1,8 +0,19 @@ | ||
// src/utils/index.ts | ||
// src/utils/helpers.ts | ||
import { init as cuid } from "@paralleldrive/cuid2"; | ||
var RONIN_SCHEMA_SYMBOLS = { | ||
// Represents a sub query. | ||
QUERY: "__RONIN_QUERY", | ||
// Represents the value of a field in a schema. | ||
FIELD: "__RONIN_FIELD_", | ||
// Represents the old value of a field in a schema. Used for triggers. | ||
FIELD_OLD: "__RONIN_FIELD_OLD_", | ||
// Represents the new value of a field in a schema. Used for triggers. | ||
FIELD_NEW: "__RONIN_FIELD_NEW_", | ||
// Represents a value provided to a query preset. | ||
VALUE: "__RONIN_VALUE" | ||
}; | ||
var RONIN_SCHEMA_FIELD_REGEX = new RegExp( | ||
`${RONIN_SCHEMA_SYMBOLS.FIELD}[a-zA-Z0-9]+`, | ||
"g" | ||
); | ||
var RoninError = class extends Error { | ||
@@ -29,3 +40,3 @@ code; | ||
var SPLIT_REGEX = /(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|[\s.\-_]+/; | ||
var generateRecordId = (prefix) => `${prefix || "rec"}_${cuid({ length: 16 })()}`; | ||
var generateRecordId = (prefix) => `${prefix}_${cuid({ length: 16 })()}`; | ||
var capitalize = (str) => { | ||
@@ -48,11 +59,18 @@ if (!str || str.length === 0) return ""; | ||
var isObject = (value) => value != null && typeof value === "object" && Array.isArray(value) === false; | ||
var replaceInObject = (obj, pattern, replacer) => { | ||
var findInObject = (obj, pattern, replacer) => { | ||
let found = false; | ||
for (const key in obj) { | ||
const value = obj[key]; | ||
if (isObject(value)) { | ||
replaceInObject(value, pattern, replacer); | ||
found = findInObject(value, pattern, replacer); | ||
} else if (typeof value === "string" && value.startsWith(pattern)) { | ||
obj[key] = value.replace(pattern, replacer); | ||
found = true; | ||
if (replacer) { | ||
obj[key] = value.replace(pattern, replacer); | ||
} else { | ||
return found; | ||
} | ||
} | ||
} | ||
return found; | ||
}; | ||
@@ -86,3 +104,209 @@ var flatten = (obj, prefix = "", res = {}) => { | ||
// src/utils/statement.ts | ||
var prepareStatementValue = (statementParams, value) => { | ||
if (value === null) return "NULL"; | ||
if (!statementParams) return JSON.stringify(value); | ||
let formattedValue = value; | ||
if (Array.isArray(value) || isObject(value)) { | ||
formattedValue = JSON.stringify(value); | ||
} else if (typeof value === "boolean") { | ||
formattedValue = value ? 1 : 0; | ||
} | ||
const index = statementParams.push(formattedValue); | ||
return `?${index}`; | ||
}; | ||
var composeFieldValues = (schemas, schema, statementParams, instructionName, value, options) => { | ||
const { field: schemaField, fieldSelector: selector } = getFieldFromSchema( | ||
schema, | ||
options.fieldSlug, | ||
instructionName, | ||
options.rootTable | ||
); | ||
const collectStatementValue = options.type !== "fields"; | ||
let conditionSelector = selector; | ||
let conditionValue = value; | ||
if (getSubQuery(value) && collectStatementValue) { | ||
conditionValue = `(${compileQueryInput( | ||
value[RONIN_SCHEMA_SYMBOLS.QUERY], | ||
schemas, | ||
statementParams | ||
).main.statement})`; | ||
} else if (typeof value === "string" && value.startsWith(RONIN_SCHEMA_SYMBOLS.FIELD)) { | ||
let targetTable = `"${options.rootTable}"`; | ||
let toReplace = RONIN_SCHEMA_SYMBOLS.FIELD; | ||
if (value.startsWith(RONIN_SCHEMA_SYMBOLS.FIELD_OLD)) { | ||
targetTable = "OLD"; | ||
toReplace = RONIN_SCHEMA_SYMBOLS.FIELD_OLD; | ||
} else if (value.startsWith(RONIN_SCHEMA_SYMBOLS.FIELD_NEW)) { | ||
targetTable = "NEW"; | ||
toReplace = RONIN_SCHEMA_SYMBOLS.FIELD_NEW; | ||
} | ||
conditionSelector = `${options.customTable ? `"${options.customTable}".` : ""}"${schemaField.slug}"`; | ||
conditionValue = `${targetTable}."${value.replace(toReplace, "")}"`; | ||
} else if (schemaField.type === "json" && instructionName === "to") { | ||
conditionSelector = `"${schemaField.slug}"`; | ||
if (collectStatementValue) { | ||
const preparedValue = prepareStatementValue(statementParams, value); | ||
conditionValue = `IIF(${conditionSelector} IS NULL, ${preparedValue}, json_patch(${conditionSelector}, ${preparedValue}))`; | ||
} | ||
} else if (collectStatementValue) { | ||
conditionValue = prepareStatementValue(statementParams, value); | ||
} | ||
if (options.type === "fields") return conditionSelector; | ||
if (options.type === "values") return conditionValue; | ||
return `${conditionSelector} ${WITH_CONDITIONS[options.condition || "being"](conditionValue, value)}`; | ||
}; | ||
var composeConditions = (schemas, schema, statementParams, instructionName, value, options) => { | ||
const isNested = isObject(value) && Object.keys(value).length > 0; | ||
if (isNested && Object.keys(value).every((key) => key in WITH_CONDITIONS)) { | ||
const conditions = Object.entries(value).map( | ||
([conditionType, checkValue]) => composeConditions(schemas, schema, statementParams, instructionName, checkValue, { | ||
...options, | ||
condition: conditionType | ||
}) | ||
); | ||
return conditions.join(" AND "); | ||
} | ||
if (options.fieldSlug) { | ||
const fieldDetails = getFieldFromSchema( | ||
schema, | ||
options.fieldSlug, | ||
instructionName, | ||
options.rootTable | ||
); | ||
const { field: schemaField } = fieldDetails; | ||
const consumeJSON = schemaField.type === "json" && instructionName === "to"; | ||
if (!(isObject(value) || Array.isArray(value)) || getSubQuery(value) || consumeJSON) { | ||
return composeFieldValues( | ||
schemas, | ||
schema, | ||
statementParams, | ||
instructionName, | ||
value, | ||
{ ...options, fieldSlug: options.fieldSlug } | ||
); | ||
} | ||
if (schemaField.type === "reference" && isNested) { | ||
const keys = Object.keys(value); | ||
const values = Object.values(value); | ||
let recordTarget; | ||
if (keys.length === 1 && keys[0] === "id") { | ||
recordTarget = values[0]; | ||
} else { | ||
const relatedSchema = getSchemaBySlug(schemas, schemaField.target.slug); | ||
const subQuery = { | ||
get: { | ||
[relatedSchema.slug]: { | ||
with: value, | ||
selecting: ["id"] | ||
} | ||
} | ||
}; | ||
recordTarget = { | ||
[RONIN_SCHEMA_SYMBOLS.QUERY]: subQuery | ||
}; | ||
} | ||
return composeConditions( | ||
schemas, | ||
schema, | ||
statementParams, | ||
instructionName, | ||
recordTarget, | ||
options | ||
); | ||
} | ||
} | ||
if (isNested) { | ||
const conditions = Object.entries(value).map(([field, value2]) => { | ||
const nestedFieldSlug = options.fieldSlug ? `${options.fieldSlug}.${field}` : field; | ||
return composeConditions(schemas, schema, statementParams, instructionName, value2, { | ||
...options, | ||
fieldSlug: nestedFieldSlug | ||
}); | ||
}); | ||
const joiner = instructionName === "to" ? ", " : " AND "; | ||
if (instructionName === "to") return `${conditions.join(joiner)}`; | ||
return conditions.length === 1 ? conditions[0] : options.fieldSlug ? `(${conditions.join(joiner)})` : conditions.join(joiner); | ||
} | ||
if (Array.isArray(value)) { | ||
const conditions = value.map( | ||
(filter) => composeConditions( | ||
schemas, | ||
schema, | ||
statementParams, | ||
instructionName, | ||
filter, | ||
options | ||
) | ||
); | ||
return conditions.join(" OR "); | ||
} | ||
throw new RoninError({ | ||
message: `The \`with\` instruction must not contain an empty field. The following fields are empty: \`${options.fieldSlug}\`. If you meant to query by an empty field, try using \`null\` instead.`, | ||
code: "INVALID_WITH_VALUE", | ||
queries: null | ||
}); | ||
}; | ||
var formatIdentifiers = ({ identifiers }, queryInstructions) => { | ||
if (!queryInstructions) return queryInstructions; | ||
const type = "with" in queryInstructions ? "with" : null; | ||
if (!type) return queryInstructions; | ||
const nestedInstructions = queryInstructions[type]; | ||
if (!nestedInstructions || Array.isArray(nestedInstructions)) | ||
return queryInstructions; | ||
const newNestedInstructions = { ...nestedInstructions }; | ||
for (const oldKey of Object.keys(newNestedInstructions)) { | ||
if (oldKey !== "nameIdentifier" && oldKey !== "slugIdentifier") continue; | ||
const identifierName = oldKey === "nameIdentifier" ? "name" : "slug"; | ||
const value = newNestedInstructions[oldKey]; | ||
const newKey = identifiers[identifierName]; | ||
newNestedInstructions[newKey] = value; | ||
delete newNestedInstructions[oldKey]; | ||
} | ||
return { | ||
...queryInstructions, | ||
[type]: newNestedInstructions | ||
}; | ||
}; | ||
var getSubQuery = (value) => { | ||
return isObject(value) ? value[RONIN_SCHEMA_SYMBOLS.QUERY] || null : null; | ||
}; | ||
// src/instructions/with.ts | ||
var getMatcher = (value, negative) => { | ||
if (negative) { | ||
if (value === null) return "IS NOT"; | ||
return "!="; | ||
} | ||
if (value === null) return "IS"; | ||
return "="; | ||
}; | ||
var WITH_CONDITIONS = { | ||
being: (value, baseValue) => `${getMatcher(baseValue, false)} ${value}`, | ||
notBeing: (value, baseValue) => `${getMatcher(baseValue, true)} ${value}`, | ||
startingWith: (value) => `LIKE ${value}%`, | ||
notStartingWith: (value) => `NOT LIKE ${value}%`, | ||
endingWith: (value) => `LIKE %${value}`, | ||
notEndingWith: (value) => `NOT LIKE %${value}`, | ||
containing: (value) => `LIKE %${value}%`, | ||
notContaining: (value) => `NOT LIKE %${value}%`, | ||
greaterThan: (value) => `> ${value}`, | ||
greaterOrEqual: (value) => `>= ${value}`, | ||
lessThan: (value) => `< ${value}`, | ||
lessOrEqual: (value) => `<= ${value}` | ||
}; | ||
var handleWith = (schemas, schema, statementParams, instruction, rootTable) => { | ||
const subStatement = composeConditions( | ||
schemas, | ||
schema, | ||
statementParams, | ||
"with", | ||
instruction, | ||
{ rootTable } | ||
); | ||
return `(${subStatement})`; | ||
}; | ||
// src/utils/schema.ts | ||
import title from "title"; | ||
var getSchemaBySlug = (schemas, slug) => { | ||
@@ -103,9 +327,7 @@ const schema = schemas.find((schema2) => { | ||
}; | ||
var getSchemaName = (schema) => { | ||
return schema.name || schema.slug; | ||
}; | ||
var composeMetaSchemaSlug = (suffix) => convertToCamelCase(`ronin_${suffix}`); | ||
var composeAssociationSchemaSlug = (schema, field) => composeMetaSchemaSlug(`${schema.pluralSlug}_${field.slug}`); | ||
var composeAssociationSchemaSlug = (schema, field) => composeMetaSchemaSlug(`${schema.slug}_${field.slug}`); | ||
var getFieldSelector = (field, fieldPath, rootTable) => { | ||
const tablePrefix = rootTable ? `"${rootTable}".` : ""; | ||
const symbol = rootTable?.startsWith(RONIN_SCHEMA_SYMBOLS.FIELD) ? `${rootTable.replace(RONIN_SCHEMA_SYMBOLS.FIELD, "").slice(0, -1)}.` : ""; | ||
const tablePrefix = symbol || (rootTable ? `"${rootTable}".` : ""); | ||
if (field.type === "json") { | ||
@@ -133,3 +355,3 @@ const dotParts = fieldPath.split("."); | ||
throw new RoninError({ | ||
message: `${errorPrefix} does not exist in schema "${getSchemaName(schema)}".`, | ||
message: `${errorPrefix} does not exist in schema "${schema.name}".`, | ||
code: "FIELD_NOT_FOUND", | ||
@@ -143,2 +365,49 @@ field: fieldPath, | ||
}; | ||
var slugToName = (slug) => { | ||
const name = slug.replace(/([a-z])([A-Z])/g, "$1 $2"); | ||
return title(name); | ||
}; | ||
var VOWELS = ["a", "e", "i", "o", "u"]; | ||
var pluralize = (word) => { | ||
const lastLetter = word.slice(-1).toLowerCase(); | ||
const secondLastLetter = word.slice(-2, -1).toLowerCase(); | ||
if (lastLetter === "y" && !VOWELS.includes(secondLastLetter)) { | ||
return `${word.slice(0, -1)}ies`; | ||
} | ||
if (lastLetter === "s" || word.slice(-2).toLowerCase() === "ch" || word.slice(-2).toLowerCase() === "sh" || word.slice(-2).toLowerCase() === "ex") { | ||
return `${word}es`; | ||
} | ||
return `${word}s`; | ||
}; | ||
var schemaSettings = [ | ||
["pluralSlug", "slug", pluralize], | ||
["name", "slug", slugToName], | ||
["pluralName", "pluralSlug", slugToName], | ||
["idPrefix", "slug", (slug) => slug.slice(0, 3)] | ||
]; | ||
var addDefaultSchemaFields = (schema, isNew) => { | ||
const copiedSchema = { ...schema }; | ||
for (const [setting, base, generator] of schemaSettings) { | ||
if (copiedSchema[setting] || !copiedSchema[base]) continue; | ||
copiedSchema[setting] = generator(copiedSchema[base]); | ||
} | ||
const newFields = copiedSchema.fields || []; | ||
if (isNew || newFields.length > 0) { | ||
if (!copiedSchema.identifiers) copiedSchema.identifiers = {}; | ||
if (!copiedSchema.identifiers.name) { | ||
const suitableField = newFields.find( | ||
(field) => field.type === "string" && field.required === true && ["name"].includes(field.slug) | ||
); | ||
copiedSchema.identifiers.name = suitableField?.slug || "id"; | ||
} | ||
if (!copiedSchema.identifiers.slug) { | ||
const suitableField = newFields.find( | ||
(field) => field.type === "string" && field.unique === true && field.required === true && ["slug", "handle"].includes(field.slug) | ||
); | ||
copiedSchema.identifiers.slug = suitableField?.slug || "id"; | ||
} | ||
copiedSchema.fields = [...SYSTEM_FIELDS, ...newFields]; | ||
} | ||
return copiedSchema; | ||
}; | ||
var SYSTEM_FIELDS = [ | ||
@@ -168,4 +437,3 @@ { | ||
name: "RONIN - Created By", | ||
type: "reference", | ||
schema: "account", | ||
type: "string", | ||
slug: "ronin.createdBy" | ||
@@ -180,4 +448,3 @@ }, | ||
name: "RONIN - Updated By", | ||
type: "reference", | ||
schema: "account", | ||
type: "string", | ||
slug: "ronin.updatedBy" | ||
@@ -188,8 +455,8 @@ } | ||
{ | ||
name: "Schema", | ||
pluralName: "Schemas", | ||
slug: "schema", | ||
pluralSlug: "schemas", | ||
identifiers: { | ||
name: "name", | ||
slug: "slug" | ||
}, | ||
fields: [ | ||
...SYSTEM_FIELDS, | ||
{ slug: "name", type: "string" }, | ||
@@ -201,35 +468,106 @@ { slug: "pluralName", type: "string" }, | ||
{ slug: "identifiers", type: "group" }, | ||
{ slug: "identifiers.title", type: "string" }, | ||
{ slug: "identifiers.slug", type: "string" } | ||
{ slug: "identifiers.name", type: "string" }, | ||
{ slug: "identifiers.slug", type: "string" }, | ||
{ slug: "fields", type: "json" }, | ||
{ slug: "indexes", type: "json" }, | ||
{ slug: "triggers", type: "json" }, | ||
{ slug: "presets", type: "json" } | ||
] | ||
}, | ||
{ | ||
name: "Field", | ||
pluralName: "Fields", | ||
slug: "field", | ||
pluralSlug: "fields", | ||
identifiers: { | ||
name: "name", | ||
slug: "slug" | ||
}, | ||
fields: [ | ||
...SYSTEM_FIELDS, | ||
{ slug: "name", type: "string" }, | ||
{ slug: "slug", type: "string" }, | ||
{ slug: "type", type: "string" }, | ||
{ slug: "schema", type: "reference", schema: "schema" }, | ||
{ slug: "slug", type: "string", required: true }, | ||
{ slug: "type", type: "string", required: true }, | ||
{ | ||
slug: "schema", | ||
type: "reference", | ||
target: { slug: "schema" }, | ||
required: true | ||
}, | ||
{ slug: "required", type: "boolean" }, | ||
{ slug: "defaultValue", type: "string" }, | ||
{ slug: "unique", type: "boolean" }, | ||
{ slug: "autoIncrement", type: "boolean" } | ||
{ slug: "autoIncrement", type: "boolean" }, | ||
// Only allowed for fields of type "reference". | ||
{ slug: "target", type: "reference", target: { slug: "schema" } }, | ||
{ slug: "kind", type: "string" }, | ||
{ slug: "actions", type: "group" }, | ||
{ slug: "actions.onDelete", type: "string" }, | ||
{ slug: "actions.onUpdate", type: "string" } | ||
] | ||
}, | ||
{ | ||
slug: "index", | ||
identifiers: { | ||
name: "slug", | ||
slug: "slug" | ||
}, | ||
fields: [ | ||
{ slug: "slug", type: "string", required: true }, | ||
{ | ||
slug: "schema", | ||
type: "reference", | ||
target: { slug: "schema" }, | ||
required: true | ||
}, | ||
{ slug: "unique", type: "boolean" }, | ||
{ slug: "filter", type: "json" }, | ||
{ slug: "fields", type: "json", required: true } | ||
] | ||
}, | ||
{ | ||
slug: "trigger", | ||
identifiers: { | ||
name: "slug", | ||
slug: "slug" | ||
}, | ||
fields: [ | ||
{ slug: "slug", type: "string", required: true }, | ||
{ | ||
slug: "schema", | ||
type: "reference", | ||
target: { slug: "schema" }, | ||
required: true | ||
}, | ||
{ slug: "when", type: "string", required: true }, | ||
{ slug: "action", type: "string", required: true }, | ||
{ slug: "filter", type: "json" }, | ||
{ slug: "effects", type: "json", required: true }, | ||
{ slug: "fields", type: "json" } | ||
] | ||
}, | ||
{ | ||
slug: "preset", | ||
fields: [ | ||
{ slug: "slug", type: "string", required: true }, | ||
{ | ||
slug: "schema", | ||
type: "reference", | ||
target: { slug: "schema" }, | ||
required: true | ||
}, | ||
{ slug: "instructions", type: "json", required: true } | ||
] | ||
} | ||
]; | ||
].map((schema) => addDefaultSchemaFields(schema, true)); | ||
var SYSTEM_SCHEMA_SLUGS = SYSTEM_SCHEMAS.flatMap(({ slug, pluralSlug }) => [ | ||
slug, | ||
pluralSlug | ||
]); | ||
var addSystemSchemas = (schemas) => { | ||
const list = [...SYSTEM_SCHEMAS, ...schemas].map((schema) => ({ ...schema })); | ||
for (const schema of list) { | ||
const defaultIncluding = {}; | ||
const associativeSchemas = schemas.flatMap((schema) => { | ||
const addedSchemas = []; | ||
for (const field of schema.fields || []) { | ||
if (field.type === "reference" && !field.slug.startsWith("ronin.")) { | ||
const relatedSchema = getSchemaBySlug(list, field.schema); | ||
const relatedSchema = getSchemaBySlug(schemas, field.target.slug); | ||
let fieldSlug = relatedSchema.slug; | ||
if (field.kind === "many") { | ||
fieldSlug = composeAssociationSchemaSlug(schema, field); | ||
list.push({ | ||
addedSchemas.push({ | ||
pluralSlug: fieldSlug, | ||
@@ -239,5 +577,5 @@ slug: fieldSlug, | ||
{ | ||
slug: "origin", | ||
slug: "source", | ||
type: "reference", | ||
schema: schema.slug | ||
target: { slug: schema.slug } | ||
}, | ||
@@ -247,3 +585,3 @@ { | ||
type: "reference", | ||
schema: relatedSchema.slug | ||
target: { slug: relatedSchema.slug } | ||
} | ||
@@ -253,33 +591,72 @@ ] | ||
} | ||
defaultIncluding[field.slug] = { | ||
get: { | ||
[fieldSlug]: { | ||
with: { | ||
// Compare the `id` field of the related schema to the reference field on | ||
// the root schema (`field.slug`). | ||
id: `${RONIN_SCHEMA_SYMBOLS.FIELD}${field.slug}` | ||
} | ||
} | ||
return addedSchemas; | ||
}); | ||
return [...SYSTEM_SCHEMAS, ...associativeSchemas, ...schemas]; | ||
}; | ||
var addDefaultSchemaPresets = (list, schema) => { | ||
const defaultPresets = []; | ||
for (const field of schema.fields || []) { | ||
if (field.type === "reference" && !field.slug.startsWith("ronin.")) { | ||
const relatedSchema = getSchemaBySlug(list, field.target.slug); | ||
let fieldSlug = relatedSchema.slug; | ||
if (field.kind === "many") { | ||
fieldSlug = composeAssociationSchemaSlug(schema, field); | ||
} | ||
defaultPresets.push({ | ||
instructions: { | ||
including: { | ||
[field.slug]: { | ||
[RONIN_SCHEMA_SYMBOLS.QUERY]: { | ||
get: { | ||
[fieldSlug]: { | ||
with: { | ||
// Compare the `id` field of the related schema to the reference field on | ||
// the root schema (`field.slug`). | ||
id: `${RONIN_SCHEMA_SYMBOLS.FIELD}${field.slug}` | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
const relatedSchemaToModify = list.find((schema2) => schema2.slug === field.schema); | ||
if (!relatedSchemaToModify) throw new Error("Missing related schema"); | ||
relatedSchemaToModify.including = { | ||
[schema.pluralSlug]: { | ||
get: { | ||
[schema.pluralSlug]: { | ||
with: { | ||
[field.slug]: `${RONIN_SCHEMA_SYMBOLS.FIELD}id` | ||
}, | ||
slug: field.slug | ||
}); | ||
} | ||
} | ||
const childSchemas = list.map((subSchema) => { | ||
const field = subSchema.fields?.find((field2) => { | ||
return field2.type === "reference" && field2.target.slug === schema.slug; | ||
}); | ||
if (!field) return null; | ||
return { schema: subSchema, field }; | ||
}).filter((match) => match !== null); | ||
for (const childMatch of childSchemas) { | ||
const { schema: childSchema, field: childField } = childMatch; | ||
const pluralSlug = childSchema.pluralSlug; | ||
defaultPresets.push({ | ||
instructions: { | ||
including: { | ||
[pluralSlug]: { | ||
[RONIN_SCHEMA_SYMBOLS.QUERY]: { | ||
get: { | ||
[pluralSlug]: { | ||
with: { | ||
[childField.slug]: `${RONIN_SCHEMA_SYMBOLS.FIELD}id` | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
...relatedSchemaToModify.including | ||
}; | ||
} | ||
} | ||
schema.fields = [...SYSTEM_FIELDS, ...schema.fields || []]; | ||
schema.including = { ...defaultIncluding, ...schema.including }; | ||
} | ||
} | ||
}, | ||
slug: pluralSlug | ||
}); | ||
} | ||
return list; | ||
if (Object.keys(defaultPresets).length > 0) { | ||
schema.presets = [...defaultPresets, ...schema.presets || []]; | ||
} | ||
return schema; | ||
}; | ||
@@ -308,19 +685,33 @@ var mappedInstructions = { | ||
statement += ` DEFAULT ${field.defaultValue}`; | ||
if (field.type === "reference") { | ||
const actions = field.actions || {}; | ||
const targetTable = convertToSnakeCase(pluralize(field.target.slug)); | ||
statement += ` REFERENCES ${targetTable}("id")`; | ||
for (const trigger in actions) { | ||
const triggerName = trigger.toUpperCase().slice(2); | ||
const action = actions[trigger]; | ||
statement += ` ON ${triggerName} ${action}`; | ||
} | ||
} | ||
return statement; | ||
}; | ||
var addSchemaQueries = (queryDetails, writeStatements) => { | ||
const { queryType, querySchema, queryInstructions } = queryDetails; | ||
if (!["create", "set", "drop"].includes(queryType)) return; | ||
if (!["schema", "schemas", "field", "fields"].includes(querySchema)) return; | ||
var addSchemaQueries = (schemas, queryDetails, dependencyStatements) => { | ||
const { | ||
queryType, | ||
querySchema, | ||
queryInstructions: queryInstructionsRaw | ||
} = queryDetails; | ||
const queryInstructions = queryInstructionsRaw; | ||
if (!["create", "set", "drop"].includes(queryType)) return queryInstructions; | ||
if (!SYSTEM_SCHEMA_SLUGS.includes(querySchema)) return queryInstructions; | ||
const instructionName = mappedInstructions[queryType]; | ||
const instructionList = queryInstructions[instructionName]; | ||
const kind = ["schema", "schemas"].includes(querySchema) ? "schemas" : "fields"; | ||
const instructionTarget = kind === "schemas" ? instructionList : instructionList?.schema; | ||
const kind = getSchemaBySlug(SYSTEM_SCHEMAS, querySchema).pluralSlug; | ||
let tableAction = "ALTER"; | ||
let schemaPluralSlug = null; | ||
let queryTypeReadable = null; | ||
switch (queryType) { | ||
case "create": { | ||
if (kind === "schemas") tableAction = "CREATE"; | ||
schemaPluralSlug = instructionTarget?.pluralSlug; | ||
if (kind === "schemas" || kind === "indexes" || kind === "triggers") { | ||
tableAction = "CREATE"; | ||
} | ||
queryTypeReadable = "creating"; | ||
@@ -331,3 +722,2 @@ break; | ||
if (kind === "schemas") tableAction = "ALTER"; | ||
schemaPluralSlug = instructionTarget?.pluralSlug?.being || instructionTarget?.pluralSlug; | ||
queryTypeReadable = "updating"; | ||
@@ -337,4 +727,5 @@ break; | ||
case "drop": { | ||
if (kind === "schemas") tableAction = "DROP"; | ||
schemaPluralSlug = instructionTarget?.pluralSlug?.being || instructionTarget?.pluralSlug; | ||
if (kind === "schemas" || kind === "indexes" || kind === "triggers") { | ||
tableAction = "DROP"; | ||
} | ||
queryTypeReadable = "deleting"; | ||
@@ -344,17 +735,128 @@ break; | ||
} | ||
if (!schemaPluralSlug) { | ||
const field = kind === "schemas" ? "pluralSlug" : "schema.pluralSlug"; | ||
const slug = instructionList?.slug?.being || instructionList?.slug; | ||
if (!slug) { | ||
throw new RoninError({ | ||
message: `When ${queryTypeReadable} ${kind}, a \`${field}\` field must be provided in the \`${instructionName}\` instruction.`, | ||
message: `When ${queryTypeReadable} ${kind}, a \`slug\` field must be provided in the \`${instructionName}\` instruction.`, | ||
code: "MISSING_FIELD", | ||
fields: [field] | ||
fields: ["slug"] | ||
}); | ||
} | ||
const table = convertToSnakeCase(schemaPluralSlug); | ||
const fields = [...SYSTEM_FIELDS]; | ||
let statement = `${tableAction} TABLE "${table}"`; | ||
const schemaInstruction = instructionList?.schema; | ||
const schemaSlug = schemaInstruction?.slug?.being || schemaInstruction?.slug; | ||
if (kind !== "schemas" && !schemaSlug) { | ||
throw new RoninError({ | ||
message: `When ${queryTypeReadable} ${kind}, a \`schema.slug\` field must be provided in the \`${instructionName}\` instruction.`, | ||
code: "MISSING_FIELD", | ||
fields: ["schema.slug"] | ||
}); | ||
} | ||
const usableSlug = kind === "schemas" ? slug : schemaSlug; | ||
const tableName = convertToSnakeCase(pluralize(usableSlug)); | ||
const targetSchema = kind === "schemas" && queryType === "create" ? null : getSchemaBySlug(schemas, usableSlug); | ||
if (kind === "indexes") { | ||
const indexName = convertToSnakeCase(slug); | ||
const unique = instructionList?.unique; | ||
const filterQuery = instructionList?.filter; | ||
const fields = instructionList?.fields; | ||
const params = []; | ||
let statement2 = `${tableAction}${unique ? " UNIQUE" : ""} INDEX "${indexName}"`; | ||
if (queryType === "create") { | ||
const schema = targetSchema; | ||
const columns = fields.map((field) => { | ||
let fieldSelector = ""; | ||
if ("slug" in field) { | ||
({ fieldSelector } = getFieldFromSchema(schema, field.slug, "to")); | ||
} else if ("expression" in field) { | ||
fieldSelector = field.expression.replace(RONIN_SCHEMA_FIELD_REGEX, (match) => { | ||
const fieldSlug = match.replace(RONIN_SCHEMA_SYMBOLS.FIELD, ""); | ||
return getFieldFromSchema(schema, fieldSlug, "to").fieldSelector; | ||
}); | ||
} | ||
if (field.collation) fieldSelector += ` COLLATE ${field.collation}`; | ||
if (field.order) fieldSelector += ` ${field.order}`; | ||
return fieldSelector; | ||
}); | ||
statement2 += ` ON "${tableName}" (${columns.join(", ")})`; | ||
if (filterQuery) { | ||
const withStatement = handleWith( | ||
schemas, | ||
targetSchema, | ||
params, | ||
filterQuery | ||
); | ||
statement2 += ` WHERE (${withStatement})`; | ||
} | ||
} | ||
dependencyStatements.push({ statement: statement2, params }); | ||
return queryInstructions; | ||
} | ||
if (kind === "triggers") { | ||
const triggerName = convertToSnakeCase(slug); | ||
const params = []; | ||
let statement2 = `${tableAction} TRIGGER "${triggerName}"`; | ||
if (queryType === "create") { | ||
const { when, action } = instructionList; | ||
const statementParts = [`${when} ${action}`]; | ||
const effectQueries = instructionList?.effects; | ||
const filterQuery = instructionList?.filter; | ||
const fields = instructionList?.fields; | ||
if (fields) { | ||
if (action !== "UPDATE") { | ||
throw new RoninError({ | ||
message: `When ${queryTypeReadable} ${kind}, targeting specific fields requires the \`UPDATE\` action.`, | ||
code: "INVALID_SCHEMA_VALUE", | ||
fields: ["action"] | ||
}); | ||
} | ||
const fieldSelectors = fields.map((field) => { | ||
return getFieldFromSchema(targetSchema, field.slug, "to").fieldSelector; | ||
}); | ||
statementParts.push(`OF (${fieldSelectors.join(", ")})`); | ||
} | ||
statementParts.push("ON", `"${tableName}"`); | ||
if (filterQuery || effectQueries.some((query) => findInObject(query, RONIN_SCHEMA_SYMBOLS.FIELD))) { | ||
statementParts.push("FOR EACH ROW"); | ||
} | ||
if (filterQuery) { | ||
const tablePlaceholder = action === "DELETE" ? RONIN_SCHEMA_SYMBOLS.FIELD_OLD : RONIN_SCHEMA_SYMBOLS.FIELD_NEW; | ||
const withStatement = handleWith( | ||
schemas, | ||
targetSchema, | ||
params, | ||
filterQuery, | ||
tablePlaceholder | ||
); | ||
statementParts.push("WHEN", `(${withStatement})`); | ||
} | ||
const effectStatements = effectQueries.map((effectQuery) => { | ||
return compileQueryInput(effectQuery, schemas, params, { | ||
returning: false | ||
}).main.statement; | ||
}); | ||
if (effectStatements.length > 1) statementParts.push("BEGIN"); | ||
statementParts.push(effectStatements.join("; ")); | ||
if (effectStatements.length > 1) statementParts.push("END"); | ||
statement2 += ` ${statementParts.join(" ")}`; | ||
} | ||
dependencyStatements.push({ statement: statement2, params }); | ||
return queryInstructions; | ||
} | ||
const statement = `${tableAction} TABLE "${tableName}"`; | ||
if (kind === "schemas") { | ||
if (queryType === "create" || queryType === "set") { | ||
const schemaWithFields = addDefaultSchemaFields( | ||
queryInstructions.to, | ||
queryType === "create" | ||
); | ||
const schemaWithPresets = addDefaultSchemaPresets(schemas, schemaWithFields); | ||
queryInstructions.to = schemaWithPresets; | ||
} | ||
if (queryType === "create") { | ||
const { fields } = queryInstructions.to; | ||
const columns = fields.map(getFieldStatement).filter(Boolean); | ||
statement += ` (${columns.join(", ")})`; | ||
dependencyStatements.push({ | ||
statement: `${statement} (${columns.join(", ")})`, | ||
params: [] | ||
}); | ||
schemas.push(queryInstructions.to); | ||
} else if (queryType === "set") { | ||
@@ -364,14 +866,15 @@ const newSlug = queryInstructions.to?.pluralSlug; | ||
const newTable = convertToSnakeCase(newSlug); | ||
statement += ` RENAME TO "${newTable}"`; | ||
dependencyStatements.push({ | ||
statement: `${statement} RENAME TO "${newTable}"`, | ||
params: [] | ||
}); | ||
} | ||
Object.assign(targetSchema, queryInstructions.to); | ||
} else if (queryType === "drop") { | ||
schemas.splice(schemas.indexOf(targetSchema), 1); | ||
dependencyStatements.push({ statement, params: [] }); | ||
} | ||
} else if (kind === "fields") { | ||
const fieldSlug = instructionTarget?.slug?.being || instructionList?.slug; | ||
if (!fieldSlug) { | ||
throw new RoninError({ | ||
message: `When ${queryTypeReadable} fields, a \`slug\` field must be provided in the \`${instructionName}\` instruction.`, | ||
code: "MISSING_FIELD", | ||
fields: ["slug"] | ||
}); | ||
} | ||
return queryInstructions; | ||
} | ||
if (kind === "fields") { | ||
if (queryType === "create") { | ||
@@ -385,234 +888,32 @@ if (!instructionList.type) { | ||
} | ||
statement += ` ADD COLUMN ${getFieldStatement(instructionList)}`; | ||
dependencyStatements.push({ | ||
statement: `${statement} ADD COLUMN ${getFieldStatement(instructionList)}`, | ||
params: [] | ||
}); | ||
} else if (queryType === "set") { | ||
const newSlug = queryInstructions.to?.slug; | ||
if (newSlug) { | ||
statement += ` RENAME COLUMN "${fieldSlug}" TO "${newSlug}"`; | ||
dependencyStatements.push({ | ||
statement: `${statement} RENAME COLUMN "${slug}" TO "${newSlug}"`, | ||
params: [] | ||
}); | ||
} | ||
} else if (queryType === "drop") { | ||
statement += ` DROP COLUMN "${fieldSlug}"`; | ||
dependencyStatements.push({ | ||
statement: `${statement} DROP COLUMN "${slug}"`, | ||
params: [] | ||
}); | ||
} | ||
} | ||
writeStatements.push(statement); | ||
return queryInstructions; | ||
}; | ||
// src/instructions/with.ts | ||
var WITH_CONDITIONS = [ | ||
"being", | ||
"notBeing", | ||
"startingWith", | ||
"notStartingWith", | ||
"endingWith", | ||
"notEndingWith", | ||
"containing", | ||
"notContaining", | ||
"greaterThan", | ||
"greaterOrEqual", | ||
"lessThan", | ||
"lessOrEqual" | ||
]; | ||
var handleWith = (schemas, schema, statementValues, instruction, rootTable) => { | ||
const subStatement = composeConditions( | ||
schemas, | ||
schema, | ||
statementValues, | ||
"with", | ||
instruction, | ||
{ rootTable } | ||
); | ||
return `(${subStatement})`; | ||
}; | ||
// src/utils/statement.ts | ||
var prepareStatementValue = (statementValues, value, bindNull = false) => { | ||
if (!bindNull && value === null) return "NULL"; | ||
let formattedValue = value; | ||
if (Array.isArray(value) || isObject(value)) { | ||
formattedValue = JSON.stringify(value); | ||
} else if (typeof value === "boolean") { | ||
formattedValue = value ? 1 : 0; | ||
} | ||
const index = statementValues.push(formattedValue); | ||
return `?${index}`; | ||
}; | ||
var composeFieldValues = (schemas, schema, statementValues, instructionName, value, options) => { | ||
const { field: schemaField, fieldSelector: selector } = getFieldFromSchema( | ||
schema, | ||
options.fieldSlug, | ||
instructionName, | ||
options.rootTable | ||
); | ||
const isSubQuery = isObject(value) && Object.hasOwn(value, RONIN_SCHEMA_SYMBOLS.QUERY); | ||
const collectStatementValue = options.type !== "fields"; | ||
let conditionSelector = selector; | ||
let conditionValue = value; | ||
if (isSubQuery && collectStatementValue) { | ||
conditionValue = `(${compileQueryInput( | ||
value[RONIN_SCHEMA_SYMBOLS.QUERY], | ||
schemas, | ||
{ statementValues } | ||
).readStatement})`; | ||
} else if (typeof value === "string" && value.startsWith(RONIN_SCHEMA_SYMBOLS.FIELD)) { | ||
conditionSelector = `"${options.customTable}"."${schemaField.slug}"`; | ||
conditionValue = `"${options.rootTable}"."${value.replace(RONIN_SCHEMA_SYMBOLS.FIELD, "")}"`; | ||
} else if (schemaField.type === "json" && instructionName === "to") { | ||
conditionSelector = `"${schemaField.slug}"`; | ||
if (collectStatementValue) { | ||
const preparedValue = prepareStatementValue(statementValues, value, false); | ||
conditionValue = `IIF(${conditionSelector} IS NULL, ${preparedValue}, json_patch(${conditionSelector}, ${preparedValue}))`; | ||
} | ||
} else if (collectStatementValue) { | ||
conditionValue = prepareStatementValue(statementValues, value, false); | ||
} | ||
if (options.type === "fields") return conditionSelector; | ||
if (options.type === "values") return conditionValue; | ||
const conditionTypes = { | ||
being: [getMatcher(value, false), conditionValue], | ||
notBeing: [getMatcher(value, true), conditionValue], | ||
startingWith: ["LIKE", `${conditionValue}%`], | ||
notStartingWith: ["NOT LIKE", `${conditionValue}%`], | ||
endingWith: ["LIKE", `%${conditionValue}`], | ||
notEndingWith: ["NOT LIKE", `%${conditionValue}`], | ||
containing: ["LIKE", `%${conditionValue}%`], | ||
notContaining: ["NOT LIKE", `%${conditionValue}%`], | ||
greaterThan: [">", conditionValue], | ||
greaterOrEqual: [">=", conditionValue], | ||
lessThan: ["<", conditionValue], | ||
lessOrEqual: ["<=", conditionValue] | ||
}; | ||
return `${conditionSelector} ${conditionTypes[options.condition || "being"].join(" ")}`; | ||
}; | ||
var composeConditions = (schemas, schema, statementValues, instructionName, value, options) => { | ||
const isNested = isObject(value) && Object.keys(value).length > 0; | ||
if (isNested && Object.keys(value).every( | ||
(key) => WITH_CONDITIONS.includes(key) | ||
)) { | ||
const conditions = Object.entries(value).map( | ||
([conditionType, checkValue]) => composeConditions(schemas, schema, statementValues, instructionName, checkValue, { | ||
...options, | ||
condition: conditionType | ||
}) | ||
); | ||
return conditions.join(" AND "); | ||
} | ||
if (options.fieldSlug) { | ||
const fieldDetails = getFieldFromSchema( | ||
schema, | ||
options.fieldSlug, | ||
instructionName, | ||
options.rootTable | ||
); | ||
const { field: schemaField } = fieldDetails; | ||
const consumeJSON = schemaField.type === "json" && instructionName === "to"; | ||
const isSubQuery = isNested && Object.hasOwn(value, RONIN_SCHEMA_SYMBOLS.QUERY); | ||
if (!(isObject(value) || Array.isArray(value)) || isSubQuery || consumeJSON) { | ||
return composeFieldValues( | ||
schemas, | ||
schema, | ||
statementValues, | ||
instructionName, | ||
value, | ||
{ ...options, fieldSlug: options.fieldSlug } | ||
); | ||
} | ||
if (schemaField.type === "reference" && isNested) { | ||
const keys = Object.keys(value); | ||
const values = Object.values(value); | ||
let recordTarget; | ||
if (keys.length === 1 && keys[0] === "id") { | ||
recordTarget = values[0]; | ||
} else { | ||
const relatedSchema = getSchemaBySlug(schemas, schemaField.schema); | ||
const subQuery = { | ||
get: { | ||
[relatedSchema.slug]: { | ||
with: value, | ||
selecting: ["id"] | ||
} | ||
} | ||
}; | ||
recordTarget = { | ||
[RONIN_SCHEMA_SYMBOLS.QUERY]: subQuery | ||
}; | ||
} | ||
return composeConditions( | ||
schemas, | ||
schema, | ||
statementValues, | ||
instructionName, | ||
recordTarget, | ||
options | ||
); | ||
} | ||
} | ||
if (isNested) { | ||
const conditions = Object.entries(value).map(([field, value2]) => { | ||
const nestedFieldSlug = options.fieldSlug ? `${options.fieldSlug}.${field}` : field; | ||
return composeConditions(schemas, schema, statementValues, instructionName, value2, { | ||
...options, | ||
fieldSlug: nestedFieldSlug | ||
}); | ||
}); | ||
const joiner = instructionName === "to" ? ", " : " AND "; | ||
if (instructionName === "to") return `${conditions.join(joiner)}`; | ||
return conditions.length === 1 ? conditions[0] : options.fieldSlug ? `(${conditions.join(joiner)})` : conditions.join(joiner); | ||
} | ||
if (Array.isArray(value)) { | ||
const conditions = value.map( | ||
(filter) => composeConditions( | ||
schemas, | ||
schema, | ||
statementValues, | ||
instructionName, | ||
filter, | ||
options | ||
) | ||
); | ||
return conditions.join(" OR "); | ||
} | ||
throw new RoninError({ | ||
message: `The \`with\` instruction must not contain an empty field. The following fields are empty: \`${options.fieldSlug}\`. If you meant to query by an empty field, try using \`null\` instead.`, | ||
code: "INVALID_WITH_VALUE", | ||
queries: null | ||
}); | ||
}; | ||
var getMatcher = (value, negative) => { | ||
if (negative) { | ||
if (value === null) return "IS NOT"; | ||
return "!="; | ||
} | ||
if (value === null) return "IS"; | ||
return "="; | ||
}; | ||
var formatIdentifiers = ({ identifiers }, queryInstructions) => { | ||
if (!queryInstructions) return queryInstructions; | ||
const type = "with" in queryInstructions ? "with" : null; | ||
if (!type) return queryInstructions; | ||
const nestedInstructions = queryInstructions[type]; | ||
if (!nestedInstructions || Array.isArray(nestedInstructions)) | ||
return queryInstructions; | ||
const newNestedInstructions = { ...nestedInstructions }; | ||
for (const oldKey of Object.keys(newNestedInstructions)) { | ||
if (oldKey !== "titleIdentifier" && oldKey !== "slugIdentifier") continue; | ||
const identifierName = oldKey === "titleIdentifier" ? "title" : "slug"; | ||
const value = newNestedInstructions[oldKey]; | ||
const newKey = identifiers?.[identifierName] || "id"; | ||
newNestedInstructions[newKey] = value; | ||
delete newNestedInstructions[oldKey]; | ||
} | ||
return { | ||
...queryInstructions, | ||
[type]: newNestedInstructions | ||
}; | ||
}; | ||
// src/instructions/before-after.ts | ||
var CURSOR_SEPARATOR = ","; | ||
var CURSOR_NULL_PLACEHOLDER = "RONIN_NULL"; | ||
var handleBeforeOrAfter = (schema, statementValues, instructions, rootTable) => { | ||
var handleBeforeOrAfter = (schema, statementParams, instructions, rootTable) => { | ||
if (!(instructions.before || instructions.after)) { | ||
throw new RoninError({ | ||
message: "The `before` or `after` instruction must not be empty.", | ||
code: "MISSING_INSTRUCTION", | ||
queries: null | ||
code: "MISSING_INSTRUCTION" | ||
}); | ||
@@ -623,6 +924,14 @@ } | ||
message: "The `before` and `after` instructions cannot co-exist. Choose one.", | ||
code: "MUTUALLY_EXCLUSIVE_INSTRUCTIONS", | ||
queries: null | ||
code: "MUTUALLY_EXCLUSIVE_INSTRUCTIONS" | ||
}); | ||
} | ||
if (!instructions.limitedTo) { | ||
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 RoninError({ | ||
message, | ||
code: "MISSING_INSTRUCTION" | ||
}); | ||
} | ||
const { ascending = [], descending = [] } = instructions.orderedBy || {}; | ||
@@ -639,6 +948,6 @@ const clause = instructions.with ? "AND " : ""; | ||
if (field.type === "boolean") { | ||
return prepareStatementValue(statementValues, value === "true"); | ||
return prepareStatementValue(statementParams, value === "true"); | ||
} | ||
if (field.type === "number") { | ||
return prepareStatementValue(statementValues, Number.parseInt(value)); | ||
return prepareStatementValue(statementParams, Number.parseInt(value)); | ||
} | ||
@@ -648,3 +957,3 @@ if (field.type === "date") { | ||
} | ||
return prepareStatementValue(statementValues, value); | ||
return prepareStatementValue(statementParams, value); | ||
}); | ||
@@ -692,46 +1001,56 @@ const compareOperators = [ | ||
// src/instructions/for.ts | ||
var handleFor = (schemas, schema, statementValues, instruction, rootTable) => { | ||
let statement = ""; | ||
if (!instruction) return statement; | ||
for (const shortcut in instruction) { | ||
const args = instruction[shortcut]; | ||
const forFilter = schema.for?.[shortcut]; | ||
if (!forFilter) { | ||
var handleFor = (schema, instructions) => { | ||
const normalizedFor = Array.isArray(instructions.for) ? Object.fromEntries(instructions.for.map((presetSlug) => [presetSlug, null])) : instructions.for; | ||
for (const presetSlug in normalizedFor) { | ||
const arg = normalizedFor[presetSlug]; | ||
const preset = schema.presets?.find((preset2) => preset2.slug === presetSlug); | ||
if (!preset) { | ||
throw new RoninError({ | ||
message: `The provided \`for\` shortcut "${shortcut}" does not exist in schema "${getSchemaName(schema)}".`, | ||
code: "INVALID_FOR_VALUE" | ||
message: `Preset "${presetSlug}" does not exist in schema "${schema.name}".`, | ||
code: "PRESET_NOT_FOUND" | ||
}); | ||
} | ||
const replacedForFilter = structuredClone(forFilter); | ||
replaceInObject( | ||
replacedForFilter, | ||
RONIN_SCHEMA_SYMBOLS.VALUE, | ||
(match) => match.replace(RONIN_SCHEMA_SYMBOLS.VALUE, args) | ||
); | ||
const subStatement = composeConditions( | ||
schemas, | ||
schema, | ||
statementValues, | ||
"for", | ||
replacedForFilter, | ||
{ rootTable } | ||
); | ||
statement += `(${subStatement})`; | ||
const replacedForFilter = structuredClone(preset.instructions); | ||
if (arg !== null) { | ||
findInObject( | ||
replacedForFilter, | ||
RONIN_SCHEMA_SYMBOLS.VALUE, | ||
(match) => match.replace(RONIN_SCHEMA_SYMBOLS.VALUE, arg) | ||
); | ||
} | ||
for (const subInstruction in replacedForFilter) { | ||
const instructionName = subInstruction; | ||
const currentValue = instructions[instructionName]; | ||
if (currentValue) { | ||
let newValue; | ||
if (Array.isArray(currentValue)) { | ||
newValue = [ | ||
...replacedForFilter[instructionName], | ||
...currentValue | ||
]; | ||
} else if (isObject(currentValue)) { | ||
newValue = { | ||
...replacedForFilter[instructionName], | ||
...currentValue | ||
}; | ||
} | ||
Object.assign(instructions, { [instructionName]: newValue }); | ||
continue; | ||
} | ||
Object.assign(instructions, { | ||
[instructionName]: replacedForFilter[instructionName] | ||
}); | ||
} | ||
} | ||
return statement; | ||
return instructions; | ||
}; | ||
// src/instructions/including.ts | ||
var handleIncluding = (schemas, statementValues, schema, instruction, rootTable) => { | ||
var handleIncluding = (schemas, statementParams, instruction, rootTable) => { | ||
let statement = ""; | ||
let rootTableSubQuery; | ||
let rootTableName = rootTable; | ||
for (const shortcut of instruction || []) { | ||
const includingQuery = schema.including?.[shortcut]; | ||
if (!includingQuery) { | ||
throw new RoninError({ | ||
message: `The provided \`including\` shortcut "${shortcut}" does not exist in schema "${getSchemaName(schema)}".`, | ||
code: "INVALID_INCLUDING_VALUE" | ||
}); | ||
} | ||
for (const ephemeralFieldSlug in instruction) { | ||
const includingQuery = getSubQuery(instruction[ephemeralFieldSlug]); | ||
if (!includingQuery) continue; | ||
const { queryType, querySchema, queryInstructions } = splitQuery(includingQuery); | ||
@@ -742,3 +1061,3 @@ let modifiableQueryInstructions = queryInstructions; | ||
let relatedTableSelector = `"${getTableForSchema(relatedSchema)}"`; | ||
const tableAlias = `including_${shortcut}`; | ||
const tableAlias = `including_${ephemeralFieldSlug}`; | ||
const single = querySchema !== relatedSchema.pluralSlug; | ||
@@ -760,5 +1079,5 @@ if (!modifiableQueryInstructions?.with) { | ||
schemas, | ||
{ statementValues } | ||
statementParams | ||
); | ||
relatedTableSelector = `(${subSelect.readStatement})`; | ||
relatedTableSelector = `(${subSelect.main.statement})`; | ||
} | ||
@@ -774,3 +1093,3 @@ statement += `${joinType} JOIN ${relatedTableSelector} as ${tableAlias}`; | ||
relatedSchema, | ||
statementValues, | ||
statementParams, | ||
"including", | ||
@@ -791,5 +1110,6 @@ queryInstructions?.with, | ||
var handleLimitedTo = (single, instruction) => { | ||
const pageSize = instruction || 100; | ||
const finalPageSize = pageSize + 1; | ||
return `LIMIT ${single ? "1" : finalPageSize} `; | ||
let amount; | ||
if (instruction) amount = instruction + 1; | ||
if (single) amount = 1; | ||
return `LIMIT ${amount} `; | ||
}; | ||
@@ -830,21 +1150,26 @@ | ||
// src/instructions/selecting.ts | ||
var handleSelecting = (schema, statementValues, instructions) => { | ||
var handleSelecting = (schema, statementParams, instructions) => { | ||
let isJoining = false; | ||
let statement = instructions.selecting ? instructions.selecting.map((slug) => { | ||
return getFieldFromSchema(schema, slug, "selecting").fieldSelector; | ||
}).join(", ") : "*"; | ||
if (isObject(instructions.including)) { | ||
statement += ", "; | ||
statement += Object.entries( | ||
flatten(instructions.including) | ||
).filter(([_, value]) => { | ||
return !(isObject(value) && Object.hasOwn(value, RONIN_SCHEMA_SYMBOLS.QUERY)); | ||
}).map(([key, value]) => { | ||
return `${prepareStatementValue(statementValues, value)} as "${key}"`; | ||
}).join(", "); | ||
if (instructions.including) { | ||
const filteredObject = Object.entries(instructions.including).filter(([_, value]) => { | ||
const hasQuery = getSubQuery(value); | ||
if (hasQuery) isJoining = true; | ||
return !hasQuery; | ||
}); | ||
const newObjectEntries = Object.entries(flatten(Object.fromEntries(filteredObject))); | ||
if (newObjectEntries.length > 0) { | ||
statement += ", "; | ||
statement += newObjectEntries.map(([key, value]) => { | ||
return `${prepareStatementValue(statementParams, value)} as "${key}"`; | ||
}).join(", "); | ||
} | ||
} | ||
return statement; | ||
return { columns: statement, isJoining }; | ||
}; | ||
// src/instructions/to.ts | ||
var handleTo = (schemas, schema, statementValues, queryType, writeStatements, instructions, rootTable) => { | ||
var handleTo = (schemas, schema, statementParams, queryType, dependencyStatements, instructions, rootTable) => { | ||
const currentTime = (/* @__PURE__ */ new Date()).toISOString(); | ||
@@ -864,5 +1189,4 @@ const { with: withInstruction, to: toInstruction } = instructions; | ||
}; | ||
const hasSubQuery = Object.hasOwn(toInstruction, RONIN_SCHEMA_SYMBOLS.QUERY); | ||
if (hasSubQuery) { | ||
const subQuery = toInstruction[RONIN_SCHEMA_SYMBOLS.QUERY]; | ||
const subQuery = getSubQuery(toInstruction); | ||
if (subQuery) { | ||
let { querySchema: subQuerySchemaSlug, queryInstructions: subQueryInstructions } = splitQuery(subQuery); | ||
@@ -892,5 +1216,3 @@ const subQuerySchema = getSchemaBySlug(schemas, subQuerySchemaSlug); | ||
} | ||
return compileQueryInput(subQuery, schemas, { | ||
statementValues | ||
}).readStatement; | ||
return compileQueryInput(subQuery, schemas, statementParams).main.statement; | ||
} | ||
@@ -908,6 +1230,6 @@ Object.assign(toInstruction, defaultFields); | ||
const composeStatement = (subQueryType, value) => { | ||
const origin = queryType === "create" ? { id: toInstruction.id } : withInstruction; | ||
const recordDetails = { origin }; | ||
const source = queryType === "create" ? { id: toInstruction.id } : withInstruction; | ||
const recordDetails = { source }; | ||
if (value) recordDetails.target = value; | ||
const { readStatement } = compileQueryInput( | ||
return compileQueryInput( | ||
{ | ||
@@ -919,17 +1241,17 @@ [subQueryType]: { | ||
schemas, | ||
{ statementValues, disableReturning: true } | ||
); | ||
return readStatement; | ||
[], | ||
{ returning: false } | ||
).main; | ||
}; | ||
if (Array.isArray(fieldValue)) { | ||
writeStatements.push(composeStatement("drop")); | ||
dependencyStatements.push(composeStatement("drop")); | ||
for (const record of fieldValue) { | ||
writeStatements.push(composeStatement("create", record)); | ||
dependencyStatements.push(composeStatement("create", record)); | ||
} | ||
} else if (isObject(fieldValue)) { | ||
for (const recordToAdd of fieldValue.containing || []) { | ||
writeStatements.push(composeStatement("create", recordToAdd)); | ||
dependencyStatements.push(composeStatement("create", recordToAdd)); | ||
} | ||
for (const recordToRemove of fieldValue.notContaining || []) { | ||
writeStatements.push(composeStatement("drop", recordToRemove)); | ||
dependencyStatements.push(composeStatement("drop", recordToRemove)); | ||
} | ||
@@ -942,3 +1264,3 @@ } | ||
schema, | ||
statementValues, | ||
statementParams, | ||
"to", | ||
@@ -955,3 +1277,3 @@ toInstruction, | ||
schema, | ||
statementValues, | ||
statementParams, | ||
"to", | ||
@@ -971,7 +1293,6 @@ toInstruction, | ||
// src/index.ts | ||
var compileQueryInput = (query, defaultSchemas, options) => { | ||
// src/utils/index.ts | ||
var compileQueryInput = (query, schemas, statementParams, options) => { | ||
const parsedQuery = splitQuery(query); | ||
const { queryType, querySchema, queryInstructions } = parsedQuery; | ||
const schemas = addSystemSchemas(defaultSchemas); | ||
const schema = getSchemaBySlug(schemas, querySchema); | ||
@@ -981,6 +1302,13 @@ const single = querySchema !== schema.pluralSlug; | ||
let table = getTableForSchema(schema); | ||
const statementValues = options?.statementValues || []; | ||
const writeStatements = []; | ||
addSchemaQueries(parsedQuery, writeStatements); | ||
const columns = handleSelecting(schema, statementValues, { | ||
const dependencyStatements = []; | ||
const returning = options?.returning ?? true; | ||
instructions = addSchemaQueries( | ||
schemas, | ||
{ queryType, querySchema, queryInstructions: instructions }, | ||
dependencyStatements | ||
); | ||
if (instructions && Object.hasOwn(instructions, "for")) { | ||
instructions = handleFor(schema, instructions); | ||
} | ||
const { columns, isJoining } = handleSelecting(schema, statementParams, { | ||
selecting: instructions?.selecting, | ||
@@ -1007,3 +1335,2 @@ including: instructions?.including | ||
} | ||
const isJoining = typeof instructions?.including !== "undefined" && !isObject(instructions.including); | ||
let isJoiningMultipleRows = false; | ||
@@ -1015,3 +1342,3 @@ if (isJoining) { | ||
rootTableName | ||
} = handleIncluding(schemas, statementValues, schema, instructions?.including, table); | ||
} = handleIncluding(schemas, statementParams, instructions?.including, table); | ||
if (rootTableSubQuery && rootTableName) { | ||
@@ -1039,5 +1366,5 @@ table = rootTableName; | ||
schema, | ||
statementValues, | ||
statementParams, | ||
queryType, | ||
writeStatements, | ||
dependencyStatements, | ||
{ with: instructions.with, to: instructions.to }, | ||
@@ -1053,3 +1380,3 @@ isJoining ? table : void 0 | ||
schema, | ||
statementValues, | ||
statementParams, | ||
instructions?.with, | ||
@@ -1060,13 +1387,3 @@ isJoining ? table : void 0 | ||
} | ||
if (instructions && Object.hasOwn(instructions, "for")) { | ||
const forStatement = handleFor( | ||
schemas, | ||
schema, | ||
statementValues, | ||
instructions?.for, | ||
isJoining ? table : void 0 | ||
); | ||
if (forStatement.length > 0) conditions.push(forStatement); | ||
} | ||
if ((queryType === "get" || queryType === "count") && !single) { | ||
if ((queryType === "get" || queryType === "count") && !single && instructions?.limitedTo) { | ||
instructions = instructions || {}; | ||
@@ -1093,3 +1410,3 @@ instructions.orderedBy = instructions.orderedBy || {}; | ||
schema, | ||
statementValues, | ||
statementParams, | ||
{ | ||
@@ -1099,3 +1416,4 @@ before: instructions.before, | ||
with: instructions.with, | ||
orderedBy: instructions.orderedBy | ||
orderedBy: instructions.orderedBy, | ||
limitedTo: instructions.limitedTo | ||
}, | ||
@@ -1121,17 +1439,42 @@ isJoining ? table : void 0 | ||
} | ||
if (queryType === "get" && !isJoiningMultipleRows) { | ||
if (queryType === "get" && !isJoiningMultipleRows && (single || instructions?.limitedTo)) { | ||
statement += handleLimitedTo(single, instructions?.limitedTo); | ||
} | ||
if (["create", "set", "drop"].includes(queryType) && !options?.disableReturning) { | ||
if (["create", "set", "drop"].includes(queryType) && returning) { | ||
statement += "RETURNING * "; | ||
} | ||
const finalStatement = statement.trimEnd(); | ||
const mainStatement = { | ||
statement: statement.trimEnd(), | ||
params: statementParams || [] | ||
}; | ||
if (returning) mainStatement.returning = true; | ||
return { | ||
writeStatements, | ||
readStatement: finalStatement, | ||
values: statementValues | ||
dependencies: dependencyStatements, | ||
main: mainStatement | ||
}; | ||
}; | ||
// src/index.ts | ||
var compileQueries = (queries, schemas, options) => { | ||
const schemaList = addSystemSchemas(schemas).map((schema) => { | ||
return addDefaultSchemaFields(schema, true); | ||
}); | ||
const schemaListWithPresets = schemaList.map((schema) => { | ||
return addDefaultSchemaPresets(schemaList, schema); | ||
}); | ||
const dependencyStatements = []; | ||
const mainStatements = []; | ||
for (const query of queries) { | ||
const result = compileQueryInput( | ||
query, | ||
schemaListWithPresets, | ||
options?.inlineValues ? null : [] | ||
); | ||
dependencyStatements.push(...result.dependencies); | ||
mainStatements.push(result.main); | ||
} | ||
return [...dependencyStatements, ...mainStatements]; | ||
}; | ||
export { | ||
compileQueryInput | ||
compileQueries | ||
}; |
{ | ||
"name": "@ronin/compiler", | ||
"version": "0.1.0", | ||
"version": "0.1.1-leo-ron-1083-experimental.70", | ||
"type": "module", | ||
@@ -11,3 +11,5 @@ "description": "Compiles RONIN queries to SQL statements.", | ||
"types": "./dist/index.d.ts", | ||
"files": ["dist"], | ||
"files": [ | ||
"dist" | ||
], | ||
"scripts": { | ||
@@ -22,7 +24,12 @@ "lint": "bun run --bun lint:tsc && bun run --bun lint:biome", | ||
}, | ||
"keywords": ["query", "compiler", "sql"], | ||
"keywords": [ | ||
"query", | ||
"compiler", | ||
"sql" | ||
], | ||
"author": "ronin", | ||
"license": "MIT", | ||
"license": "Apache-2.0", | ||
"dependencies": { | ||
"@paralleldrive/cuid2": "2.2.2" | ||
"@paralleldrive/cuid2": "2.2.2", | ||
"title": "3.5.3" | ||
}, | ||
@@ -32,2 +39,3 @@ "devDependencies": { | ||
"@types/bun": "1.1.10", | ||
"@types/title": "3.4.3", | ||
"tsup": "8.3.0", | ||
@@ -34,0 +42,0 @@ "typescript": "5.6.2", |
104
README.md
# RONIN Compiler | ||
This package compiles RONIN queries to SQL statements. | ||
This package compiles [RONIN queries](https://ronin.co/docs/queries) to SQL statements. | ||
## Usage | ||
## Setup | ||
You don't need to install this package explicitly, as it is already included in the [RONIN client](https://github.com/ronin-co/client). | ||
However, we would be excited to welcome your feature suggestions or bug fixes for the RONIN compiler. Read on to learn more about how to suggest changes. | ||
## Contributing | ||
To start contributing code, first make sure you have [Bun](https://bun.sh) installed, which is a JavaScript runtime. | ||
Next, [clone the repo](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) and install its dependencies: | ||
```bash | ||
bun install | ||
``` | ||
Once that's done, link the package to make it available to all of your local projects: | ||
```bash | ||
bun link | ||
``` | ||
Inside your project, you can then run the following command, which is similar to `bun add @ronin/compiler` or `npm install @ronin/compiler`, except that it doesn't install `@ronin/compiler` from npm, but instead uses your local clone of the package: | ||
```bash | ||
bun link @ronin/compiler | ||
``` | ||
If your project is not yet compatible with [Bun](https://bun.sh), feel free to replace all of the occurrences of the word `bun` in the commands above with `npm` instead. | ||
You will just need to make sure that, once you [create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request#creating-the-pull-request) on the current repo, it will not contain a `package-lock.json` file, which is usually generated by npm. Instead, we're using the `bun.lockb` file for this purpose (locking sub dependencies to a certain version). | ||
### Developing | ||
The programmatic API of the RONIN compiler looks like this: | ||
```typescript | ||
import { compileQueryInput } from '@ronin/compiler'; | ||
import { | ||
compileQueries, | ||
const query = { | ||
type Query, | ||
type Schema, | ||
type Statement | ||
} from '@ronin/compiler'; | ||
const queries: Array<Query> = [{ | ||
get: { | ||
accounts: null, | ||
}, | ||
}; | ||
accounts: null | ||
} | ||
}]; | ||
const schemas = [ | ||
{ | ||
pluralSlug: 'accounts', | ||
slug: 'account', | ||
}, | ||
]; | ||
const schemas: Array<Schema> = [{ | ||
slug: 'account' | ||
}]; | ||
const { writeStatements, readStatement } = compileQueryInput(query, schemas); | ||
const statements: Array<Statements> = compileQueries(queries, schemas); | ||
// [{ | ||
// statement: 'SELECT * FROM "accounts"', | ||
// params: [], | ||
// returning: true, | ||
// }] | ||
``` | ||
console.log(readStatement); | ||
// SELECT * FROM "accounts" ORDER BY "ronin.createdAt" DESC LIMIT 101 | ||
#### Options | ||
To fine-tune the behavior of the compiler, you can pass the following options: | ||
```typescript | ||
compileQueries(queries, schemas, { | ||
// Instead of returning an array of values for every statement (which allows for | ||
// preventing SQL injections), all values are inlined directly into the SQL strings. | ||
// This option should only be used if the generated SQL will be manually verified. | ||
inlineValues: true | ||
}); | ||
``` | ||
## Testing | ||
#### Transpilation | ||
Use the following command to run the test suite: | ||
In order to be compatible with a wide range of projects, the source code of the `compiler` repo needs to be compiled (transpiled) whenever you make changes to it. To automate this, you can keep this command running in your terminal: | ||
```bash | ||
bun run dev | ||
``` | ||
Whenever you make a change to the source code, it will then automatically be transpiled again. | ||
### Running Tests | ||
The RONIN compiler has 100% test coverage, which means that every single line of code is tested automatically, to ensure that any change to the source code doesn't cause a regression. | ||
Before you create a pull request on the `compiler` repo, it is therefore advised to run those tests in order to ensure everything works as expected: | ||
```bash | ||
# Run all tests | ||
bun test | ||
# Alternatively, run a single test | ||
bun test -t 'your test name' | ||
``` |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Unpublished package
Supply chain riskPackage version was not found on the registry. It may exist on a different registry and need to be configured to pull from that registry.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Unpopular package
QualityThis package is not very popular.
Found 1 instance in 1 package
427921
5
7021
0
104
2
6
1
+ Addedtitle@3.5.3
+ Addedansi-styles@3.2.1(transitive)
+ Addedarch@2.2.0(transitive)
+ Addedarg@1.0.0(transitive)
+ Addedchalk@2.3.0(transitive)
+ Addedclipboardy@1.2.2(transitive)
+ Addedcolor-convert@1.9.3(transitive)
+ Addedcolor-name@1.1.3(transitive)
+ Addedcross-spawn@5.1.0(transitive)
+ Addedescape-string-regexp@1.0.5(transitive)
+ Addedexeca@0.8.0(transitive)
+ Addedget-stream@3.0.0(transitive)
+ Addedhas-flag@2.0.0(transitive)
+ Addedis-stream@1.1.0(transitive)
+ Addedisexe@2.0.0(transitive)
+ Addedlru-cache@4.1.5(transitive)
+ Addednpm-run-path@2.0.2(transitive)
+ Addedp-finally@1.0.0(transitive)
+ Addedpath-key@2.0.1(transitive)
+ Addedpseudomap@1.0.2(transitive)
+ Addedshebang-command@1.2.0(transitive)
+ Addedshebang-regex@1.0.0(transitive)
+ Addedsignal-exit@3.0.7(transitive)
+ Addedstrip-eof@1.0.0(transitive)
+ Addedsupports-color@4.5.0(transitive)
+ Addedtitle@3.5.3(transitive)
+ Addedtitleize@1.0.0(transitive)
+ Addedwhich@1.3.1(transitive)
+ Addedyallist@2.1.2(transitive)