@ronin/compiler
Advanced tools
Comparing version 0.9.0 to 0.10.0-leo-ron-1083-experimental-206
@@ -23,2 +23,8 @@ // src/utils/helpers.ts | ||
); | ||
var MODEL_ENTITY_ERROR_CODES = { | ||
field: "FIELD_NOT_FOUND", | ||
index: "INDEX_NOT_FOUND", | ||
trigger: "TRIGGER_NOT_FOUND", | ||
preset: "PRESET_NOT_FOUND" | ||
}; | ||
var RoninError = class extends Error { | ||
@@ -463,63 +469,63 @@ code; | ||
]; | ||
var SYSTEM_MODELS = [ | ||
{ | ||
slug: "model", | ||
identifiers: { | ||
name: "name", | ||
slug: "slug" | ||
}, | ||
// This name mimics the `sqlite_schema` table in SQLite. | ||
table: "ronin_schema", | ||
fields: [ | ||
{ slug: "name", type: "string" }, | ||
{ slug: "pluralName", type: "string" }, | ||
{ slug: "slug", type: "string" }, | ||
{ slug: "pluralSlug", type: "string" }, | ||
{ slug: "idPrefix", type: "string" }, | ||
{ slug: "table", type: "string" }, | ||
{ slug: "identifiers", type: "group" }, | ||
{ slug: "identifiers.name", type: "string" }, | ||
{ slug: "identifiers.slug", type: "string" }, | ||
// Providing an empty object as a default value allows us to use `json_insert` | ||
// without needing to fall back to an empty object in the insertion statement, | ||
// which makes the statement shorter. | ||
{ slug: "fields", type: "json", defaultValue: "{}" }, | ||
{ slug: "indexes", type: "json", defaultValue: "{}" }, | ||
{ slug: "triggers", type: "json", defaultValue: "{}" }, | ||
{ slug: "presets", type: "json", defaultValue: "{}" } | ||
] | ||
} | ||
]; | ||
var addSystemModels = (models) => { | ||
const associativeModels = models.flatMap((model) => { | ||
const addedModels = []; | ||
for (const field of model.fields || []) { | ||
if (field.type === "link" && !field.slug.startsWith("ronin.")) { | ||
const relatedModel = getModelBySlug(models, field.target); | ||
let fieldSlug = relatedModel.slug; | ||
if (field.kind === "many") { | ||
fieldSlug = composeAssociationModelSlug(model, field); | ||
addedModels.push({ | ||
pluralSlug: fieldSlug, | ||
slug: fieldSlug, | ||
associationSlug: field.slug, | ||
fields: [ | ||
{ | ||
slug: "source", | ||
type: "link", | ||
target: model.slug | ||
}, | ||
{ | ||
slug: "target", | ||
type: "link", | ||
target: relatedModel.slug | ||
} | ||
] | ||
}); | ||
} | ||
var ROOT_MODEL = { | ||
slug: "model", | ||
identifiers: { | ||
name: "name", | ||
slug: "slug" | ||
}, | ||
// This name mimics the `sqlite_schema` table in SQLite. | ||
table: "ronin_schema", | ||
// Indicates that the model was automatically generated by RONIN. | ||
system: { model: "root" }, | ||
fields: [ | ||
{ slug: "name", type: "string" }, | ||
{ slug: "pluralName", type: "string" }, | ||
{ slug: "slug", type: "string" }, | ||
{ slug: "pluralSlug", type: "string" }, | ||
{ slug: "idPrefix", type: "string" }, | ||
{ slug: "table", type: "string" }, | ||
{ slug: "identifiers", type: "group" }, | ||
{ slug: "identifiers.name", type: "string" }, | ||
{ slug: "identifiers.slug", type: "string" }, | ||
// Providing an empty object as a default value allows us to use `json_insert` | ||
// without needing to fall back to an empty object in the insertion statement, | ||
// which makes the statement shorter. | ||
{ slug: "fields", type: "json", defaultValue: "{}" }, | ||
{ slug: "indexes", type: "json", defaultValue: "{}" }, | ||
{ slug: "triggers", type: "json", defaultValue: "{}" }, | ||
{ slug: "presets", type: "json", defaultValue: "{}" } | ||
] | ||
}; | ||
var getSystemModels = (models, model) => { | ||
const addedModels = []; | ||
for (const field of model.fields || []) { | ||
if (field.type === "link" && !field.slug.startsWith("ronin.")) { | ||
const relatedModel = getModelBySlug(models, field.target); | ||
let fieldSlug = relatedModel.slug; | ||
if (field.kind === "many") { | ||
fieldSlug = composeAssociationModelSlug(model, field); | ||
addedModels.push({ | ||
pluralSlug: fieldSlug, | ||
slug: fieldSlug, | ||
system: { | ||
model: model.slug, | ||
associationSlug: field.slug | ||
}, | ||
fields: [ | ||
{ | ||
slug: "source", | ||
type: "link", | ||
target: model.slug | ||
}, | ||
{ | ||
slug: "target", | ||
type: "link", | ||
target: relatedModel.slug | ||
} | ||
] | ||
}); | ||
} | ||
} | ||
return addedModels; | ||
}); | ||
return [...SYSTEM_MODELS, ...associativeModels, ...models]; | ||
} | ||
return addedModels; | ||
}; | ||
@@ -566,3 +572,3 @@ var addDefaultModelPresets = (list, model) => { | ||
const pluralSlug = childModel.pluralSlug; | ||
const presetSlug = childModel.associationSlug || pluralSlug; | ||
const presetSlug = childModel.system?.associationSlug || pluralSlug; | ||
defaultPresets.push({ | ||
@@ -623,2 +629,3 @@ instructions: { | ||
if (field.type === "link") { | ||
if (field.kind === "many") return null; | ||
const actions = field.actions || {}; | ||
@@ -648,2 +655,14 @@ const targetTable = getModelBySlug(models, field.target).table; | ||
}; | ||
var handleSystemModel = (models, dependencyStatements, action, systemModel, newModel) => { | ||
const { system: _, ...systemModelClean } = systemModel; | ||
const query = { | ||
[action]: { model: action === "create" ? systemModelClean : systemModelClean.slug } | ||
}; | ||
if (action === "alter" && newModel) { | ||
const { system: _2, ...newModelClean } = newModel; | ||
query.alter.to = newModelClean; | ||
} | ||
const statement = compileQueryInput(query, models, []); | ||
dependencyStatements.push(...statement.dependencies); | ||
}; | ||
var transformMetaQuery = (models, dependencyStatements, statementParams, query) => { | ||
@@ -677,6 +696,3 @@ const { queryType } = splitQuery(query); | ||
if (!(modelSlug && slug)) return query; | ||
const tableAction = ["model", "index", "trigger"].includes(entity) ? action.toUpperCase() : "ALTER"; | ||
const tableName = convertToSnakeCase(pluralize(modelSlug)); | ||
const model = action === "create" && entity === "model" ? null : getModelBySlug(models, modelSlug); | ||
const statement = `${tableAction} TABLE "${tableName}"`; | ||
if (entity === "model") { | ||
@@ -687,3 +703,6 @@ let queryTypeDetails; | ||
const modelWithFields = addDefaultModelFields(newModel, true); | ||
const modelWithPresets = addDefaultModelPresets(models, modelWithFields); | ||
const modelWithPresets = addDefaultModelPresets( | ||
[...models, modelWithFields], | ||
modelWithFields | ||
); | ||
const entities = Object.fromEntries( | ||
@@ -697,3 +716,3 @@ Object.entries(PLURAL_MODEL_ENTITIES).map(([type, pluralType2]) => { | ||
dependencyStatements.push({ | ||
statement: `${statement} (${columns.join(", ")})`, | ||
statement: `CREATE TABLE "${modelWithPresets.table}" (${columns.join(", ")})`, | ||
params: [] | ||
@@ -707,2 +726,5 @@ }); | ||
queryTypeDetails = { to: finalModel }; | ||
getSystemModels(models, modelWithPresets).map((systemModel) => { | ||
return handleSystemModel(models, dependencyStatements, "create", systemModel); | ||
}); | ||
} | ||
@@ -713,7 +735,6 @@ if (action === "alter" && model) { | ||
const modelWithPresets = addDefaultModelPresets(models, modelWithFields); | ||
const newSlug = modelWithPresets.pluralSlug; | ||
if (newSlug) { | ||
const newTable = convertToSnakeCase(newSlug); | ||
const newTableName = modelWithPresets.table; | ||
if (newTableName) { | ||
dependencyStatements.push({ | ||
statement: `${statement} RENAME TO "${newTable}"`, | ||
statement: `ALTER TABLE "${model.table}" RENAME TO "${newTableName}"`, | ||
params: [] | ||
@@ -732,4 +753,7 @@ }); | ||
models.splice(models.indexOf(model), 1); | ||
dependencyStatements.push({ statement, params: [] }); | ||
dependencyStatements.push({ statement: `DROP TABLE "${model.table}"`, params: [] }); | ||
queryTypeDetails = { with: { slug } }; | ||
models.filter(({ system }) => system?.model === model.slug).map((systemModel) => { | ||
return handleSystemModel(models, dependencyStatements, "drop", systemModel); | ||
}); | ||
} | ||
@@ -743,13 +767,32 @@ const queryTypeAction = action === "create" ? "add" : action === "alter" ? "set" : "remove"; | ||
} | ||
if (entity === "field" && model) { | ||
const modelBeforeUpdate = structuredClone(model); | ||
const existingModel = model; | ||
const pluralType = PLURAL_MODEL_ENTITIES[entity]; | ||
const targetEntityIndex = existingModel[pluralType]?.findIndex( | ||
(entity2) => entity2.slug === slug | ||
); | ||
if ((action === "alter" || action === "drop") && (typeof targetEntityIndex === "undefined" || targetEntityIndex === -1)) { | ||
throw new RoninError({ | ||
message: `No ${entity} with slug "${slug}" defined in model "${existingModel.name}".`, | ||
code: MODEL_ENTITY_ERROR_CODES[entity] | ||
}); | ||
} | ||
const existingEntity = existingModel[pluralType]?.[targetEntityIndex]; | ||
if (entity === "field") { | ||
const statement = `ALTER TABLE "${existingModel.table}"`; | ||
const existingField = existingEntity; | ||
const existingLinkField = existingField?.type === "link" && existingField.kind === "many"; | ||
if (action === "create") { | ||
const field2 = jsonValue; | ||
field2.type = field2.type || "string"; | ||
dependencyStatements.push({ | ||
statement: `${statement} ADD COLUMN ${getFieldStatement(models, model, field2)}`, | ||
params: [] | ||
}); | ||
const fieldStatement = getFieldStatement(models, existingModel, field2); | ||
if (fieldStatement) { | ||
dependencyStatements.push({ | ||
statement: `${statement} ADD COLUMN ${fieldStatement}`, | ||
params: [] | ||
}); | ||
} | ||
} else if (action === "alter") { | ||
const newSlug = jsonValue?.slug; | ||
if (newSlug) { | ||
if (newSlug && !existingLinkField) { | ||
dependencyStatements.push({ | ||
@@ -760,3 +803,3 @@ statement: `${statement} RENAME COLUMN "${slug}" TO "${newSlug}"`, | ||
} | ||
} else if (action === "drop") { | ||
} else if (action === "drop" && !existingLinkField) { | ||
dependencyStatements.push({ | ||
@@ -768,7 +811,8 @@ statement: `${statement} DROP COLUMN "${slug}"`, | ||
} | ||
if (entity === "index" && model) { | ||
const statementAction = action.toUpperCase(); | ||
if (entity === "index") { | ||
const index = jsonValue; | ||
const indexName = convertToSnakeCase(slug); | ||
const params = []; | ||
let statement2 = `${tableAction}${index?.unique ? " UNIQUE" : ""} INDEX "${indexName}"`; | ||
let statement = `${statementAction}${index?.unique ? " UNIQUE" : ""} INDEX "${indexName}"`; | ||
if (action === "create") { | ||
@@ -778,5 +822,5 @@ const columns = index.fields.map((field2) => { | ||
if ("slug" in field2) { | ||
({ fieldSelector } = getFieldFromModel(model, field2.slug, "to")); | ||
({ fieldSelector } = getFieldFromModel(existingModel, field2.slug, "to")); | ||
} else if ("expression" in field2) { | ||
fieldSelector = parseFieldExpression(model, "to", field2.expression); | ||
fieldSelector = parseFieldExpression(existingModel, "to", field2.expression); | ||
} | ||
@@ -787,14 +831,14 @@ if (field2.collation) fieldSelector += ` COLLATE ${field2.collation}`; | ||
}); | ||
statement2 += ` ON "${tableName}" (${columns.join(", ")})`; | ||
statement += ` ON "${existingModel.table}" (${columns.join(", ")})`; | ||
if (index.filter) { | ||
const withStatement = handleWith(models, model, params, index.filter); | ||
statement2 += ` WHERE (${withStatement})`; | ||
const withStatement = handleWith(models, existingModel, params, index.filter); | ||
statement += ` WHERE (${withStatement})`; | ||
} | ||
} | ||
dependencyStatements.push({ statement: statement2, params }); | ||
dependencyStatements.push({ statement, params }); | ||
} | ||
if (entity === "trigger" && model) { | ||
if (entity === "trigger") { | ||
const triggerName = convertToSnakeCase(slug); | ||
const params = []; | ||
let statement2 = `${tableAction} TRIGGER "${triggerName}"`; | ||
let statement = `${statementAction} TRIGGER "${triggerName}"`; | ||
if (action === "create") { | ||
@@ -812,7 +856,7 @@ const trigger = jsonValue; | ||
const fieldSelectors = trigger.fields.map((field2) => { | ||
return getFieldFromModel(model, field2.slug, "to").fieldSelector; | ||
return getFieldFromModel(existingModel, field2.slug, "to").fieldSelector; | ||
}); | ||
statementParts.push(`OF (${fieldSelectors.join(", ")})`); | ||
} | ||
statementParts.push("ON", `"${tableName}"`); | ||
statementParts.push("ON", `"${existingModel.table}"`); | ||
if (trigger.filter || trigger.effects.some((query2) => findInObject(query2, RONIN_MODEL_SYMBOLS.FIELD))) { | ||
@@ -825,3 +869,3 @@ statementParts.push("FOR EACH ROW"); | ||
models, | ||
{ ...model, tableAlias }, | ||
{ ...existingModel, tableAlias }, | ||
params, | ||
@@ -835,3 +879,3 @@ trigger.filter | ||
returning: false, | ||
parentModel: model | ||
parentModel: existingModel | ||
}).main.statement; | ||
@@ -842,7 +886,6 @@ }); | ||
if (effectStatements.length > 1) statementParts.push("END"); | ||
statement2 += ` ${statementParts.join(" ")}`; | ||
statement += ` ${statementParts.join(" ")}`; | ||
} | ||
dependencyStatements.push({ statement: statement2, params }); | ||
dependencyStatements.push({ statement, params }); | ||
} | ||
const pluralType = PLURAL_MODEL_ENTITIES[entity]; | ||
const field = `${RONIN_MODEL_SYMBOLS.FIELD}${pluralType}`; | ||
@@ -854,2 +897,6 @@ let json; | ||
json = `json_insert(${field}, '$.${slug}', ${value})`; | ||
existingModel[pluralType] = [ | ||
...existingModel[pluralType] || [], | ||
jsonValue | ||
]; | ||
break; | ||
@@ -860,2 +907,4 @@ } | ||
json = `json_set(${field}, '$.${slug}', json_patch(json_extract(${field}, '$.${slug}'), ${value}))`; | ||
const targetEntity = existingModel[pluralType]; | ||
Object.assign(targetEntity[targetEntityIndex], jsonValue); | ||
break; | ||
@@ -865,4 +914,40 @@ } | ||
json = `json_remove(${field}, '$.${slug}')`; | ||
const targetEntity = existingModel[pluralType]; | ||
targetEntity.splice(targetEntityIndex, 1); | ||
} | ||
} | ||
const currentSystemModels = models.filter(({ system }) => { | ||
return system?.model === existingModel.slug; | ||
}); | ||
const newSystemModels = getSystemModels(models, existingModel); | ||
const matchSystemModels = (oldSystemModel, newSystemModel) => { | ||
const conditions = [ | ||
oldSystemModel.system?.model === newSystemModel.system?.model | ||
]; | ||
if (oldSystemModel.system?.associationSlug) { | ||
const oldFieldIndex = modelBeforeUpdate?.fields.findIndex((item) => { | ||
return item.slug === newSystemModel.system?.associationSlug; | ||
}); | ||
const newFieldIndex = existingModel.fields.findIndex((item) => { | ||
return item.slug === oldSystemModel.system?.associationSlug; | ||
}); | ||
conditions.push(oldFieldIndex === newFieldIndex); | ||
} | ||
return conditions.every((condition) => condition === true); | ||
}; | ||
for (const systemModel of currentSystemModels) { | ||
const exists = newSystemModels.find(matchSystemModels.bind(null, systemModel)); | ||
if (exists) { | ||
if (exists.slug !== systemModel.slug) { | ||
handleSystemModel(models, dependencyStatements, "alter", systemModel, exists); | ||
} | ||
continue; | ||
} | ||
handleSystemModel(models, dependencyStatements, "drop", systemModel); | ||
} | ||
for (const systemModel of newSystemModels) { | ||
const exists = currentSystemModels.find(matchSystemModels.bind(null, systemModel)); | ||
if (exists) continue; | ||
handleSystemModel(models, dependencyStatements, "create", systemModel); | ||
} | ||
return { | ||
@@ -1238,3 +1323,10 @@ set: { | ||
// src/utils/index.ts | ||
var compileQueryInput = (query, models, statementParams, options) => { | ||
var compileQueryInput = (defaultQuery, models, statementParams, options) => { | ||
const dependencyStatements = []; | ||
const query = transformMetaQuery( | ||
models, | ||
dependencyStatements, | ||
statementParams, | ||
defaultQuery | ||
); | ||
const parsedQuery = splitQuery(query); | ||
@@ -1245,3 +1337,2 @@ const { queryType, queryModel, queryInstructions } = parsedQuery; | ||
let instructions = formatIdentifiers(model, queryInstructions); | ||
const dependencyStatements = []; | ||
const returning = options?.returning ?? true; | ||
@@ -1398,3 +1489,7 @@ if (instructions && Object.hasOwn(instructions, "for")) { | ||
compileQueries = (queries, models, options) => { | ||
const modelList = addSystemModels(models).map((model) => { | ||
const modelList = [ | ||
ROOT_MODEL, | ||
...models.flatMap((model) => getSystemModels(models, model)), | ||
...models | ||
].map((model) => { | ||
return addDefaultModelFields(model, true); | ||
@@ -1408,13 +1503,6 @@ }); | ||
for (const query of queries) { | ||
const statementValues = options?.inlineParams ? null : []; | ||
const transformedQuery = transformMetaQuery( | ||
modelListWithPresets, | ||
dependencyStatements, | ||
statementValues, | ||
query | ||
); | ||
const result = compileQueryInput( | ||
transformedQuery, | ||
query, | ||
modelListWithPresets, | ||
statementValues | ||
options?.inlineParams ? null : [] | ||
); | ||
@@ -1421,0 +1509,0 @@ dependencyStatements.push(...result.dependencies); |
{ | ||
"name": "@ronin/compiler", | ||
"version": "0.9.0", | ||
"version": "0.10.0-leo-ron-1083-experimental-206", | ||
"type": "module", | ||
@@ -5,0 +5,0 @@ "description": "Compiles RONIN queries to SQL statements.", |
@@ -55,2 +55,5 @@ # RONIN Compiler | ||
// [{ | ||
// statement: 'CREATE TABLE "accounts" ...', | ||
// params: [] | ||
// }, { | ||
// statement: 'SELECT * FROM "accounts"', | ||
@@ -57,0 +60,0 @@ // params: [], |
Sorry, the diff of this file is too big to display
449927
7528
143