@ronin/compiler
Advanced tools
Comparing version 0.1.0 to 0.1.1-leo-ron-1083-experimental.13
// src/utils/index.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" | ||
@@ -47,11 +54,18 @@ }; | ||
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; | ||
}; | ||
@@ -85,3 +99,207 @@ var flatten = (obj, prefix = "", res = {}) => { | ||
// 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)) { | ||
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(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; | ||
return `${conditionSelector} ${WITH_CONDITIONS[options.condition || "being"](conditionValue, value)}`; | ||
}; | ||
var composeConditions = (schemas, schema, statementValues, 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, 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.target.slug); | ||
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 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/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, statementValues, instruction, rootTable) => { | ||
const subStatement = composeConditions( | ||
schemas, | ||
schema, | ||
statementValues, | ||
"with", | ||
instruction, | ||
{ rootTable } | ||
); | ||
return `(${subStatement})`; | ||
}; | ||
// src/utils/schema.ts | ||
import title from "title"; | ||
var getSchemaBySlug = (schemas, slug) => { | ||
@@ -102,9 +320,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 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") { | ||
@@ -132,3 +348,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", | ||
@@ -166,4 +382,3 @@ field: fieldPath, | ||
name: "RONIN - Created By", | ||
type: "reference", | ||
schema: "account", | ||
type: "string", | ||
slug: "ronin.createdBy" | ||
@@ -178,4 +393,3 @@ }, | ||
name: "RONIN - Updated By", | ||
type: "reference", | ||
schema: "account", | ||
type: "string", | ||
slug: "ronin.updatedBy" | ||
@@ -210,14 +424,69 @@ } | ||
{ 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" } | ||
] | ||
}, | ||
{ | ||
name: "Index", | ||
pluralName: "Indexes", | ||
slug: "index", | ||
pluralSlug: "indexes", | ||
fields: [ | ||
...SYSTEM_FIELDS, | ||
{ slug: "slug", type: "string", required: true }, | ||
{ | ||
slug: "schema", | ||
type: "reference", | ||
target: { slug: "schema" }, | ||
required: true | ||
}, | ||
{ slug: "unique", type: "boolean" }, | ||
{ slug: "filter", type: "json" } | ||
] | ||
}, | ||
{ | ||
name: "Trigger", | ||
pluralName: "Triggers", | ||
slug: "trigger", | ||
pluralSlug: "triggers", | ||
fields: [ | ||
...SYSTEM_FIELDS, | ||
{ slug: "slug", type: "string", required: true }, | ||
{ slug: "schema", type: "reference", target: { slug: "schema" }, required: true }, | ||
{ slug: "cause", type: "string", required: true }, | ||
{ slug: "filter", type: "json" }, | ||
{ slug: "effect", type: "json", required: true } | ||
] | ||
} | ||
]; | ||
var SYSTEM_SCHEMA_SLUGS = SYSTEM_SCHEMAS.flatMap(({ slug, pluralSlug }) => [ | ||
slug, | ||
pluralSlug | ||
]); | ||
var prepareSchema = (schema) => { | ||
const copiedSchema = { ...schema }; | ||
if (!copiedSchema.pluralSlug) copiedSchema.pluralSlug = pluralize(copiedSchema.slug); | ||
if (!copiedSchema.name) copiedSchema.name = slugToName(copiedSchema.slug); | ||
if (!copiedSchema.pluralName) | ||
copiedSchema.pluralName = slugToName(copiedSchema.pluralSlug); | ||
return copiedSchema; | ||
}; | ||
var addSystemSchemas = (schemas) => { | ||
const list = [...SYSTEM_SCHEMAS, ...schemas].map((schema) => ({ ...schema })); | ||
const list = [...SYSTEM_SCHEMAS, ...schemas].map(prepareSchema); | ||
for (const schema of list) { | ||
@@ -227,3 +496,3 @@ const defaultIncluding = {}; | ||
if (field.type === "reference" && !field.slug.startsWith("ronin.")) { | ||
const relatedSchema = getSchemaBySlug(list, field.schema); | ||
const relatedSchema = getSchemaBySlug(list, field.target.slug); | ||
let fieldSlug = relatedSchema.slug; | ||
@@ -237,5 +506,5 @@ if (field.kind === "many") { | ||
{ | ||
slug: "origin", | ||
slug: "source", | ||
type: "reference", | ||
schema: schema.slug | ||
target: schema | ||
}, | ||
@@ -245,3 +514,3 @@ { | ||
type: "reference", | ||
schema: relatedSchema.slug | ||
target: relatedSchema | ||
} | ||
@@ -262,3 +531,3 @@ ] | ||
}; | ||
const relatedSchemaToModify = list.find((schema2) => schema2.slug === field.schema); | ||
const relatedSchemaToModify = getSchemaBySlug(list, field.target.slug); | ||
if (!relatedSchemaToModify) throw new Error("Missing related schema"); | ||
@@ -306,19 +575,28 @@ relatedSchemaToModify.including = { | ||
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) => { | ||
var addSchemaQueries = (schemas, statementValues, queryDetails, writeStatements) => { | ||
const { queryType, querySchema, queryInstructions } = queryDetails; | ||
if (!["create", "set", "drop"].includes(queryType)) return; | ||
if (!["schema", "schemas", "field", "fields"].includes(querySchema)) return; | ||
if (!SYSTEM_SCHEMA_SLUGS.includes(querySchema)) return; | ||
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"; | ||
@@ -329,3 +607,2 @@ break; | ||
if (kind === "schemas") tableAction = "ALTER"; | ||
schemaPluralSlug = instructionTarget?.pluralSlug?.being || instructionTarget?.pluralSlug; | ||
queryTypeReadable = "updating"; | ||
@@ -335,4 +612,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"; | ||
@@ -342,14 +620,64 @@ 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 tableName = convertToSnakeCase(pluralize(kind === "schemas" ? slug : schemaSlug)); | ||
if (kind === "indexes") { | ||
const indexName = convertToSnakeCase(slug); | ||
const unique = instructionList?.unique; | ||
let statement2 = `${tableAction}${unique ? " UNIQUE" : ""} INDEX "${indexName}"`; | ||
if (queryType === "create") statement2 += ` ON "${tableName}"`; | ||
writeStatements.push(statement2); | ||
return; | ||
} | ||
if (kind === "triggers") { | ||
const triggerName = convertToSnakeCase(slug); | ||
let statement2 = `${tableAction} TRIGGER "${triggerName}"`; | ||
if (queryType === "create") { | ||
const cause = slugToName(instructionList?.cause).toUpperCase(); | ||
const statementParts = [cause, "ON", `"${tableName}"`]; | ||
const effectQuery = instructionList?.effect; | ||
const filterQuery = instructionList?.filter; | ||
if (filterQuery || findInObject(effectQuery, RONIN_SCHEMA_SYMBOLS.FIELD)) { | ||
statementParts.push("FOR EACH ROW"); | ||
} | ||
if (filterQuery) { | ||
const targetSchema = getSchemaBySlug(schemas, schemaSlug); | ||
const tablePlaceholder = cause.endsWith("DELETE") ? RONIN_SCHEMA_SYMBOLS.FIELD_OLD : RONIN_SCHEMA_SYMBOLS.FIELD_NEW; | ||
const withStatement = handleWith( | ||
schemas, | ||
targetSchema, | ||
statementValues, | ||
filterQuery, | ||
tablePlaceholder | ||
); | ||
statementParts.push("WHEN", `(${withStatement})`); | ||
} | ||
const { readStatement: effectStatement } = compileQueryInput(effectQuery, schemas, { | ||
statementValues, | ||
disableReturning: true | ||
}); | ||
statementParts.push("BEGIN", effectStatement); | ||
statement2 += ` ${statementParts.join(" ")}`; | ||
} | ||
writeStatements.push(statement2); | ||
return; | ||
} | ||
let statement = `${tableAction} TABLE "${tableName}"`; | ||
if (kind === "schemas") { | ||
const fields = [...SYSTEM_FIELDS]; | ||
if (queryType === "create") { | ||
@@ -359,17 +687,12 @@ const columns = fields.map(getFieldStatement).filter(Boolean); | ||
} else if (queryType === "set") { | ||
const newSlug = queryInstructions.to?.pluralSlug; | ||
const newSlug = queryInstructions.to?.slug; | ||
if (newSlug) { | ||
const newTable = convertToSnakeCase(newSlug); | ||
const newTable = convertToSnakeCase(pluralize(newSlug)); | ||
statement += ` RENAME TO "${newTable}"`; | ||
} | ||
} | ||
} 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"] | ||
}); | ||
} | ||
writeStatements.push(statement); | ||
return; | ||
} | ||
if (kind === "fields") { | ||
if (queryType === "create") { | ||
@@ -387,220 +710,26 @@ if (!instructionList.type) { | ||
if (newSlug) { | ||
statement += ` RENAME COLUMN "${fieldSlug}" TO "${newSlug}"`; | ||
statement += ` RENAME COLUMN "${slug}" TO "${newSlug}"`; | ||
} | ||
} else if (queryType === "drop") { | ||
statement += ` DROP COLUMN "${fieldSlug}"`; | ||
statement += ` DROP COLUMN "${slug}"`; | ||
} | ||
writeStatements.push(statement); | ||
} | ||
writeStatements.push(statement); | ||
}; | ||
// 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})`; | ||
var slugToName = (slug) => { | ||
const name = slug.replace(/([a-z])([A-Z])/g, "$1 $2"); | ||
return title(name); | ||
}; | ||
// 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; | ||
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`; | ||
} | ||
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 (lastLetter === "s" || word.slice(-2).toLowerCase() === "ch" || word.slice(-2).toLowerCase() === "sh" || word.slice(-2).toLowerCase() === "ex") { | ||
return `${word}es`; | ||
} | ||
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(" ")}`; | ||
return `${word}s`; | ||
}; | ||
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 | ||
}; | ||
}; | ||
@@ -695,3 +824,3 @@ // src/instructions/before-after.ts | ||
throw new RoninError({ | ||
message: `The provided \`for\` shortcut "${shortcut}" does not exist in schema "${getSchemaName(schema)}".`, | ||
message: `The provided \`for\` shortcut "${shortcut}" does not exist in schema "${schema.name}".`, | ||
code: "INVALID_FOR_VALUE" | ||
@@ -701,3 +830,3 @@ }); | ||
const replacedForFilter = structuredClone(forFilter); | ||
replaceInObject( | ||
findInObject( | ||
replacedForFilter, | ||
@@ -729,3 +858,3 @@ RONIN_SCHEMA_SYMBOLS.VALUE, | ||
throw new RoninError({ | ||
message: `The provided \`including\` shortcut "${shortcut}" does not exist in schema "${getSchemaName(schema)}".`, | ||
message: `The provided \`including\` shortcut "${shortcut}" does not exist in schema "${schema.name}".`, | ||
code: "INVALID_INCLUDING_VALUE" | ||
@@ -898,4 +1027,4 @@ }); | ||
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; | ||
@@ -969,3 +1098,3 @@ const { readStatement } = compileQueryInput( | ||
const writeStatements = []; | ||
addSchemaQueries(parsedQuery, writeStatements); | ||
addSchemaQueries(schemas, statementValues, parsedQuery, writeStatements); | ||
const columns = handleSelecting(schema, statementValues, { | ||
@@ -972,0 +1101,0 @@ selecting: instructions?.selecting, |
{ | ||
"name": "@ronin/compiler", | ||
"version": "0.1.0", | ||
"version": "0.1.1-leo-ron-1083-experimental.13", | ||
"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", |
# 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 | ||
@@ -18,3 +52,2 @@ import { compileQueryInput } from '@ronin/compiler'; | ||
{ | ||
pluralSlug: 'accounts', | ||
slug: 'account', | ||
@@ -30,8 +63,22 @@ }, | ||
## Testing | ||
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: | ||
Use the following command to run the test suite: | ||
```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
365251
5
5922
83
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)