@ronin/compiler
Advanced tools
Comparing version 0.1.0 to 0.1.1-leo-ron-1083-experimental.41
@@ -1,6 +0,13 @@ | ||
// 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" | ||
@@ -29,3 +36,3 @@ }; | ||
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 +55,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 +100,208 @@ 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 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, | ||
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"; | ||
const isSubQuery = isNested && Object.hasOwn(value, RONIN_SCHEMA_SYMBOLS.QUERY); | ||
if (!(isObject(value) || Array.isArray(value)) || isSubQuery || 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 | ||
}; | ||
}; | ||
// 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 +322,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") { | ||
@@ -133,3 +350,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", | ||
@@ -167,4 +384,3 @@ field: fieldPath, | ||
name: "RONIN - Created By", | ||
type: "reference", | ||
schema: "account", | ||
type: "string", | ||
slug: "ronin.createdBy" | ||
@@ -179,4 +395,3 @@ }, | ||
name: "RONIN - Updated By", | ||
type: "reference", | ||
schema: "account", | ||
type: "string", | ||
slug: "ronin.updatedBy" | ||
@@ -191,2 +406,6 @@ } | ||
pluralSlug: "schemas", | ||
identifiers: { | ||
name: "name", | ||
slug: "slug" | ||
}, | ||
fields: [ | ||
@@ -200,4 +419,7 @@ ...SYSTEM_FIELDS, | ||
{ 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" } | ||
] | ||
@@ -210,17 +432,88 @@ }, | ||
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" } | ||
] | ||
}, | ||
{ | ||
name: "Index", | ||
pluralName: "Indexes", | ||
slug: "index", | ||
pluralSlug: "indexes", | ||
identifiers: { | ||
name: "slug", | ||
slug: "slug" | ||
}, | ||
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", | ||
identifiers: { | ||
name: "slug", | ||
slug: "slug" | ||
}, | ||
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: "effects", 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); | ||
if (!copiedSchema.idPrefix) copiedSchema.idPrefix = copiedSchema.slug.slice(0, 3); | ||
if (!copiedSchema.identifiers) copiedSchema.identifiers = {}; | ||
if (!copiedSchema.identifiers.name) copiedSchema.identifiers.name = "id"; | ||
if (!copiedSchema.identifiers.slug) copiedSchema.identifiers.slug = "id"; | ||
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) { | ||
@@ -230,3 +523,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; | ||
@@ -238,7 +531,11 @@ if (field.kind === "many") { | ||
slug: fieldSlug, | ||
identifiers: { | ||
name: "id", | ||
slug: "id" | ||
}, | ||
fields: [ | ||
{ | ||
slug: "origin", | ||
slug: "source", | ||
type: "reference", | ||
schema: schema.slug | ||
target: schema | ||
}, | ||
@@ -248,3 +545,3 @@ { | ||
type: "reference", | ||
schema: relatedSchema.slug | ||
target: relatedSchema | ||
} | ||
@@ -265,3 +562,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"); | ||
@@ -309,19 +606,33 @@ 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) => { | ||
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"; | ||
@@ -332,3 +643,2 @@ break; | ||
if (kind === "schemas") tableAction = "ALTER"; | ||
schemaPluralSlug = instructionTarget?.pluralSlug?.being || instructionTarget?.pluralSlug; | ||
queryTypeReadable = "updating"; | ||
@@ -338,4 +648,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"; | ||
@@ -345,14 +656,81 @@ 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; | ||
const filterQuery = instructionList?.filter; | ||
const params = []; | ||
let statement2 = `${tableAction}${unique ? " UNIQUE" : ""} INDEX "${indexName}"`; | ||
if (queryType === "create") { | ||
statement2 += ` ON "${tableName}"`; | ||
if (filterQuery) { | ||
const targetSchema = getSchemaBySlug(schemas, schemaSlug); | ||
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 cause = slugToName(instructionList?.cause).toUpperCase(); | ||
const statementParts = [cause, "ON", `"${tableName}"`]; | ||
const effectQueries = instructionList?.effects; | ||
const filterQuery = instructionList?.filter; | ||
if (filterQuery || effectQueries.some((query) => findInObject(query, 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, | ||
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; | ||
} | ||
let statement = `${tableAction} TABLE "${tableName}"`; | ||
if (kind === "schemas") { | ||
const providedFields = instructionList?.fields || []; | ||
const fields = [...SYSTEM_FIELDS, ...providedFields]; | ||
if (queryType === "create" || queryType === "set") { | ||
queryInstructions.to = prepareSchema(queryInstructions.to); | ||
} | ||
if (queryType === "create") { | ||
@@ -368,11 +746,6 @@ const columns = fields.map(getFieldStatement).filter(Boolean); | ||
} | ||
} 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"] | ||
}); | ||
} | ||
dependencyStatements.push({ statement, params: [] }); | ||
return queryInstructions; | ||
} | ||
if (kind === "fields") { | ||
if (queryType === "create") { | ||
@@ -390,220 +763,27 @@ 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}"`; | ||
} | ||
dependencyStatements.push({ statement, 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})`; | ||
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 | ||
}; | ||
}; | ||
@@ -613,3 +793,3 @@ // src/instructions/before-after.ts | ||
var CURSOR_NULL_PLACEHOLDER = "RONIN_NULL"; | ||
var handleBeforeOrAfter = (schema, statementValues, instructions, rootTable) => { | ||
var handleBeforeOrAfter = (schema, statementParams, instructions, rootTable) => { | ||
if (!(instructions.before || instructions.after)) { | ||
@@ -640,6 +820,6 @@ throw new RoninError({ | ||
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)); | ||
} | ||
@@ -649,3 +829,3 @@ if (field.type === "date") { | ||
} | ||
return prepareStatementValue(statementValues, value); | ||
return prepareStatementValue(statementParams, value); | ||
}); | ||
@@ -693,3 +873,3 @@ const compareOperators = [ | ||
// src/instructions/for.ts | ||
var handleFor = (schemas, schema, statementValues, instruction, rootTable) => { | ||
var handleFor = (schemas, schema, statementParams, instruction, rootTable) => { | ||
let statement = ""; | ||
@@ -702,3 +882,3 @@ if (!instruction) return statement; | ||
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" | ||
@@ -708,3 +888,3 @@ }); | ||
const replacedForFilter = structuredClone(forFilter); | ||
replaceInObject( | ||
findInObject( | ||
replacedForFilter, | ||
@@ -717,3 +897,3 @@ RONIN_SCHEMA_SYMBOLS.VALUE, | ||
schema, | ||
statementValues, | ||
statementParams, | ||
"for", | ||
@@ -729,3 +909,3 @@ replacedForFilter, | ||
// src/instructions/including.ts | ||
var handleIncluding = (schemas, statementValues, schema, instruction, rootTable) => { | ||
var handleIncluding = (schemas, statementParams, schema, instruction, rootTable) => { | ||
let statement = ""; | ||
@@ -738,3 +918,3 @@ let rootTableSubQuery; | ||
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" | ||
@@ -765,5 +945,5 @@ }); | ||
schemas, | ||
{ statementValues } | ||
statementParams | ||
); | ||
relatedTableSelector = `(${subSelect.readStatement})`; | ||
relatedTableSelector = `(${subSelect.main.statement})`; | ||
} | ||
@@ -779,3 +959,3 @@ statement += `${joinType} JOIN ${relatedTableSelector} as ${tableAlias}`; | ||
relatedSchema, | ||
statementValues, | ||
statementParams, | ||
"including", | ||
@@ -834,3 +1014,3 @@ queryInstructions?.with, | ||
// src/instructions/selecting.ts | ||
var handleSelecting = (schema, statementValues, instructions) => { | ||
var handleSelecting = (schema, statementParams, instructions) => { | ||
let statement = instructions.selecting ? instructions.selecting.map((slug) => { | ||
@@ -846,3 +1026,3 @@ return getFieldFromSchema(schema, slug, "selecting").fieldSelector; | ||
}).map(([key, value]) => { | ||
return `${prepareStatementValue(statementValues, value)} as "${key}"`; | ||
return `${prepareStatementValue(statementParams, value)} as "${key}"`; | ||
}).join(", "); | ||
@@ -854,3 +1034,3 @@ } | ||
// 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(); | ||
@@ -897,5 +1077,3 @@ const { with: withInstruction, to: toInstruction } = instructions; | ||
} | ||
return compileQueryInput(subQuery, schemas, { | ||
statementValues | ||
}).readStatement; | ||
return compileQueryInput(subQuery, schemas, statementParams).main.statement; | ||
} | ||
@@ -913,6 +1091,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( | ||
{ | ||
@@ -924,17 +1102,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)); | ||
} | ||
@@ -947,3 +1125,3 @@ } | ||
schema, | ||
statementValues, | ||
statementParams, | ||
"to", | ||
@@ -960,3 +1138,3 @@ toInstruction, | ||
schema, | ||
statementValues, | ||
statementParams, | ||
"to", | ||
@@ -976,4 +1154,4 @@ toInstruction, | ||
// src/index.ts | ||
var compileQueryInput = (query, defaultSchemas, options) => { | ||
// src/utils/index.ts | ||
var compileQueryInput = (query, defaultSchemas, statementParams, options) => { | ||
const parsedQuery = splitQuery(query); | ||
@@ -986,6 +1164,10 @@ const { queryType, querySchema, queryInstructions } = parsedQuery; | ||
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 | ||
); | ||
const columns = handleSelecting(schema, statementParams, { | ||
selecting: instructions?.selecting, | ||
@@ -1019,3 +1201,3 @@ including: instructions?.including | ||
rootTableName | ||
} = handleIncluding(schemas, statementValues, schema, instructions?.including, table); | ||
} = handleIncluding(schemas, statementParams, schema, instructions?.including, table); | ||
if (rootTableSubQuery && rootTableName) { | ||
@@ -1043,5 +1225,5 @@ table = rootTableName; | ||
schema, | ||
statementValues, | ||
statementParams, | ||
queryType, | ||
writeStatements, | ||
dependencyStatements, | ||
{ with: instructions.with, to: instructions.to }, | ||
@@ -1057,3 +1239,3 @@ isJoining ? table : void 0 | ||
schema, | ||
statementValues, | ||
statementParams, | ||
instructions?.with, | ||
@@ -1068,3 +1250,3 @@ isJoining ? table : void 0 | ||
schema, | ||
statementValues, | ||
statementParams, | ||
instructions?.for, | ||
@@ -1097,3 +1279,3 @@ isJoining ? table : void 0 | ||
schema, | ||
statementValues, | ||
statementParams, | ||
{ | ||
@@ -1127,14 +1309,29 @@ before: instructions.before, | ||
} | ||
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 dependencyStatements = []; | ||
const mainStatements = []; | ||
for (const query of queries) { | ||
const result = compileQueryInput(query, schemas, 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.41", | ||
"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" ORDER BY "ronin.createdAt" DESC LIMIT 101', | ||
// 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
370019
5
6035
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)