@ronin/compiler 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +387 -239
- package/package.json +4 -2
package/README.md
CHANGED
package/dist/index.d.ts
CHANGED
@@ -4667,7 +4667,7 @@ type SchemaFieldNormal = SchemaFieldBasics & {
|
|
4667
4667
|
type SchemaFieldReferenceAction = 'CASCADE' | 'RESTRICT' | 'SET NULL' | 'SET DEFAULT' | 'NO ACTION';
|
4668
4668
|
type SchemaFieldReference = SchemaFieldBasics & {
|
4669
4669
|
type: 'reference';
|
4670
|
-
target: Omit<Partial<Schema>, '
|
4670
|
+
target: Omit<Partial<Schema>, 'slug'> & Pick<Schema, 'slug'>;
|
4671
4671
|
kind?: 'one' | 'many';
|
4672
4672
|
actions?: {
|
4673
4673
|
onDelete?: SchemaFieldReferenceAction;
|
@@ -4679,7 +4679,7 @@ interface Schema {
|
|
4679
4679
|
name?: string;
|
4680
4680
|
pluralName?: string;
|
4681
4681
|
slug: string;
|
4682
|
-
pluralSlug
|
4682
|
+
pluralSlug?: string;
|
4683
4683
|
identifiers?: {
|
4684
4684
|
title?: string;
|
4685
4685
|
slug?: string;
|
package/dist/index.js
CHANGED
@@ -1,8 +1,15 @@
|
|
1
1
|
// src/utils/index.ts
|
2
2
|
import { init as cuid } from "@paralleldrive/cuid2";
|
3
3
|
var RONIN_SCHEMA_SYMBOLS = {
|
4
|
+
// Represents a sub query.
|
4
5
|
QUERY: "__RONIN_QUERY",
|
6
|
+
// Represents the value of a field in a schema.
|
5
7
|
FIELD: "__RONIN_FIELD_",
|
8
|
+
// Represents the old value of a field in a schema. Used for triggers.
|
9
|
+
FIELD_OLD: "__RONIN_FIELD_OLD_",
|
10
|
+
// Represents the new value of a field in a schema. Used for triggers.
|
11
|
+
FIELD_NEW: "__RONIN_FIELD_NEW_",
|
12
|
+
// Represents a value provided to a query preset.
|
6
13
|
VALUE: "__RONIN_VALUE"
|
7
14
|
};
|
8
15
|
var RoninError = class extends Error {
|
@@ -44,15 +51,22 @@ var convertToCamelCase = (str) => {
|
|
44
51
|
return sanitize(str).split(SPLIT_REGEX).map((part, index) => index === 0 ? part.toLowerCase() : capitalize(part)).join("");
|
45
52
|
};
|
46
53
|
var isObject = (value) => value != null && typeof value === "object" && Array.isArray(value) === false;
|
47
|
-
var
|
54
|
+
var findInObject = (obj, pattern, replacer) => {
|
55
|
+
let found = false;
|
48
56
|
for (const key in obj) {
|
49
57
|
const value = obj[key];
|
50
58
|
if (isObject(value)) {
|
51
|
-
|
59
|
+
found = findInObject(value, pattern, replacer);
|
52
60
|
} else if (typeof value === "string" && value.startsWith(pattern)) {
|
53
|
-
|
61
|
+
found = true;
|
62
|
+
if (replacer) {
|
63
|
+
obj[key] = value.replace(pattern, replacer);
|
64
|
+
} else {
|
65
|
+
return found;
|
66
|
+
}
|
54
67
|
}
|
55
68
|
}
|
69
|
+
return found;
|
56
70
|
};
|
57
71
|
var flatten = (obj, prefix = "", res = {}) => {
|
58
72
|
for (const key in obj) {
|
@@ -81,7 +95,211 @@ var splitQuery = (query) => {
|
|
81
95
|
return { queryType, querySchema, queryInstructions };
|
82
96
|
};
|
83
97
|
|
98
|
+
// src/utils/statement.ts
|
99
|
+
var prepareStatementValue = (statementValues, value, bindNull = false) => {
|
100
|
+
if (!bindNull && value === null) return "NULL";
|
101
|
+
let formattedValue = value;
|
102
|
+
if (Array.isArray(value) || isObject(value)) {
|
103
|
+
formattedValue = JSON.stringify(value);
|
104
|
+
} else if (typeof value === "boolean") {
|
105
|
+
formattedValue = value ? 1 : 0;
|
106
|
+
}
|
107
|
+
const index = statementValues.push(formattedValue);
|
108
|
+
return `?${index}`;
|
109
|
+
};
|
110
|
+
var composeFieldValues = (schemas, schema, statementValues, instructionName, value, options) => {
|
111
|
+
const { field: schemaField, fieldSelector: selector } = getFieldFromSchema(
|
112
|
+
schema,
|
113
|
+
options.fieldSlug,
|
114
|
+
instructionName,
|
115
|
+
options.rootTable
|
116
|
+
);
|
117
|
+
const isSubQuery = isObject(value) && Object.hasOwn(value, RONIN_SCHEMA_SYMBOLS.QUERY);
|
118
|
+
const collectStatementValue = options.type !== "fields";
|
119
|
+
let conditionSelector = selector;
|
120
|
+
let conditionValue = value;
|
121
|
+
if (isSubQuery && collectStatementValue) {
|
122
|
+
conditionValue = `(${compileQueryInput(
|
123
|
+
value[RONIN_SCHEMA_SYMBOLS.QUERY],
|
124
|
+
schemas,
|
125
|
+
{ statementValues }
|
126
|
+
).readStatement})`;
|
127
|
+
} else if (typeof value === "string" && value.startsWith(RONIN_SCHEMA_SYMBOLS.FIELD)) {
|
128
|
+
let targetTable = `"${options.rootTable}"`;
|
129
|
+
let toReplace = RONIN_SCHEMA_SYMBOLS.FIELD;
|
130
|
+
if (value.startsWith(RONIN_SCHEMA_SYMBOLS.FIELD_OLD)) {
|
131
|
+
targetTable = "OLD";
|
132
|
+
toReplace = RONIN_SCHEMA_SYMBOLS.FIELD_OLD;
|
133
|
+
} else if (value.startsWith(RONIN_SCHEMA_SYMBOLS.FIELD_NEW)) {
|
134
|
+
targetTable = "NEW";
|
135
|
+
toReplace = RONIN_SCHEMA_SYMBOLS.FIELD_NEW;
|
136
|
+
}
|
137
|
+
conditionSelector = `${options.customTable ? `"${options.customTable}".` : ""}"${schemaField.slug}"`;
|
138
|
+
conditionValue = `${targetTable}."${value.replace(toReplace, "")}"`;
|
139
|
+
} else if (schemaField.type === "json" && instructionName === "to") {
|
140
|
+
conditionSelector = `"${schemaField.slug}"`;
|
141
|
+
if (collectStatementValue) {
|
142
|
+
const preparedValue = prepareStatementValue(statementValues, value, false);
|
143
|
+
conditionValue = `IIF(${conditionSelector} IS NULL, ${preparedValue}, json_patch(${conditionSelector}, ${preparedValue}))`;
|
144
|
+
}
|
145
|
+
} else if (collectStatementValue) {
|
146
|
+
conditionValue = prepareStatementValue(statementValues, value, false);
|
147
|
+
}
|
148
|
+
if (options.type === "fields") return conditionSelector;
|
149
|
+
if (options.type === "values") return conditionValue;
|
150
|
+
return `${conditionSelector} ${WITH_CONDITIONS[options.condition || "being"](conditionValue, value)}`;
|
151
|
+
};
|
152
|
+
var composeConditions = (schemas, schema, statementValues, instructionName, value, options) => {
|
153
|
+
const isNested = isObject(value) && Object.keys(value).length > 0;
|
154
|
+
if (isNested && Object.keys(value).every((key) => key in WITH_CONDITIONS)) {
|
155
|
+
const conditions = Object.entries(value).map(
|
156
|
+
([conditionType, checkValue]) => composeConditions(schemas, schema, statementValues, instructionName, checkValue, {
|
157
|
+
...options,
|
158
|
+
condition: conditionType
|
159
|
+
})
|
160
|
+
);
|
161
|
+
return conditions.join(" AND ");
|
162
|
+
}
|
163
|
+
if (options.fieldSlug) {
|
164
|
+
const fieldDetails = getFieldFromSchema(
|
165
|
+
schema,
|
166
|
+
options.fieldSlug,
|
167
|
+
instructionName,
|
168
|
+
options.rootTable
|
169
|
+
);
|
170
|
+
const { field: schemaField } = fieldDetails;
|
171
|
+
const consumeJSON = schemaField.type === "json" && instructionName === "to";
|
172
|
+
const isSubQuery = isNested && Object.hasOwn(value, RONIN_SCHEMA_SYMBOLS.QUERY);
|
173
|
+
if (!(isObject(value) || Array.isArray(value)) || isSubQuery || consumeJSON) {
|
174
|
+
return composeFieldValues(
|
175
|
+
schemas,
|
176
|
+
schema,
|
177
|
+
statementValues,
|
178
|
+
instructionName,
|
179
|
+
value,
|
180
|
+
{ ...options, fieldSlug: options.fieldSlug }
|
181
|
+
);
|
182
|
+
}
|
183
|
+
if (schemaField.type === "reference" && isNested) {
|
184
|
+
const keys = Object.keys(value);
|
185
|
+
const values = Object.values(value);
|
186
|
+
let recordTarget;
|
187
|
+
if (keys.length === 1 && keys[0] === "id") {
|
188
|
+
recordTarget = values[0];
|
189
|
+
} else {
|
190
|
+
const relatedSchema = getSchemaBySlug(schemas, schemaField.target.slug);
|
191
|
+
const subQuery = {
|
192
|
+
get: {
|
193
|
+
[relatedSchema.slug]: {
|
194
|
+
with: value,
|
195
|
+
selecting: ["id"]
|
196
|
+
}
|
197
|
+
}
|
198
|
+
};
|
199
|
+
recordTarget = {
|
200
|
+
[RONIN_SCHEMA_SYMBOLS.QUERY]: subQuery
|
201
|
+
};
|
202
|
+
}
|
203
|
+
return composeConditions(
|
204
|
+
schemas,
|
205
|
+
schema,
|
206
|
+
statementValues,
|
207
|
+
instructionName,
|
208
|
+
recordTarget,
|
209
|
+
options
|
210
|
+
);
|
211
|
+
}
|
212
|
+
}
|
213
|
+
if (isNested) {
|
214
|
+
const conditions = Object.entries(value).map(([field, value2]) => {
|
215
|
+
const nestedFieldSlug = options.fieldSlug ? `${options.fieldSlug}.${field}` : field;
|
216
|
+
return composeConditions(schemas, schema, statementValues, instructionName, value2, {
|
217
|
+
...options,
|
218
|
+
fieldSlug: nestedFieldSlug
|
219
|
+
});
|
220
|
+
});
|
221
|
+
const joiner = instructionName === "to" ? ", " : " AND ";
|
222
|
+
if (instructionName === "to") return `${conditions.join(joiner)}`;
|
223
|
+
return conditions.length === 1 ? conditions[0] : options.fieldSlug ? `(${conditions.join(joiner)})` : conditions.join(joiner);
|
224
|
+
}
|
225
|
+
if (Array.isArray(value)) {
|
226
|
+
const conditions = value.map(
|
227
|
+
(filter) => composeConditions(
|
228
|
+
schemas,
|
229
|
+
schema,
|
230
|
+
statementValues,
|
231
|
+
instructionName,
|
232
|
+
filter,
|
233
|
+
options
|
234
|
+
)
|
235
|
+
);
|
236
|
+
return conditions.join(" OR ");
|
237
|
+
}
|
238
|
+
throw new RoninError({
|
239
|
+
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.`,
|
240
|
+
code: "INVALID_WITH_VALUE",
|
241
|
+
queries: null
|
242
|
+
});
|
243
|
+
};
|
244
|
+
var formatIdentifiers = ({ identifiers }, queryInstructions) => {
|
245
|
+
if (!queryInstructions) return queryInstructions;
|
246
|
+
const type = "with" in queryInstructions ? "with" : null;
|
247
|
+
if (!type) return queryInstructions;
|
248
|
+
const nestedInstructions = queryInstructions[type];
|
249
|
+
if (!nestedInstructions || Array.isArray(nestedInstructions))
|
250
|
+
return queryInstructions;
|
251
|
+
const newNestedInstructions = { ...nestedInstructions };
|
252
|
+
for (const oldKey of Object.keys(newNestedInstructions)) {
|
253
|
+
if (oldKey !== "titleIdentifier" && oldKey !== "slugIdentifier") continue;
|
254
|
+
const identifierName = oldKey === "titleIdentifier" ? "title" : "slug";
|
255
|
+
const value = newNestedInstructions[oldKey];
|
256
|
+
const newKey = identifiers?.[identifierName] || "id";
|
257
|
+
newNestedInstructions[newKey] = value;
|
258
|
+
delete newNestedInstructions[oldKey];
|
259
|
+
}
|
260
|
+
return {
|
261
|
+
...queryInstructions,
|
262
|
+
[type]: newNestedInstructions
|
263
|
+
};
|
264
|
+
};
|
265
|
+
|
266
|
+
// src/instructions/with.ts
|
267
|
+
var getMatcher = (value, negative) => {
|
268
|
+
if (negative) {
|
269
|
+
if (value === null) return "IS NOT";
|
270
|
+
return "!=";
|
271
|
+
}
|
272
|
+
if (value === null) return "IS";
|
273
|
+
return "=";
|
274
|
+
};
|
275
|
+
var WITH_CONDITIONS = {
|
276
|
+
being: (value, baseValue) => `${getMatcher(baseValue, false)} ${value}`,
|
277
|
+
notBeing: (value, baseValue) => `${getMatcher(baseValue, true)} ${value}`,
|
278
|
+
startingWith: (value) => `LIKE ${value}%`,
|
279
|
+
notStartingWith: (value) => `NOT LIKE ${value}%`,
|
280
|
+
endingWith: (value) => `LIKE %${value}`,
|
281
|
+
notEndingWith: (value) => `NOT LIKE %${value}`,
|
282
|
+
containing: (value) => `LIKE %${value}%`,
|
283
|
+
notContaining: (value) => `NOT LIKE %${value}%`,
|
284
|
+
greaterThan: (value) => `> ${value}`,
|
285
|
+
greaterOrEqual: (value) => `>= ${value}`,
|
286
|
+
lessThan: (value) => `< ${value}`,
|
287
|
+
lessOrEqual: (value) => `<= ${value}`
|
288
|
+
};
|
289
|
+
var handleWith = (schemas, schema, statementValues, instruction, rootTable) => {
|
290
|
+
const subStatement = composeConditions(
|
291
|
+
schemas,
|
292
|
+
schema,
|
293
|
+
statementValues,
|
294
|
+
"with",
|
295
|
+
instruction,
|
296
|
+
{ rootTable }
|
297
|
+
);
|
298
|
+
return `(${subStatement})`;
|
299
|
+
};
|
300
|
+
|
84
301
|
// src/utils/schema.ts
|
302
|
+
import title from "title";
|
85
303
|
var getSchemaBySlug = (schemas, slug) => {
|
86
304
|
const schema = schemas.find((schema2) => {
|
87
305
|
return schema2.slug === slug || schema2.pluralSlug === slug;
|
@@ -97,13 +315,11 @@ var getSchemaBySlug = (schemas, slug) => {
|
|
97
315
|
var getTableForSchema = (schema) => {
|
98
316
|
return convertToSnakeCase(schema.pluralSlug);
|
99
317
|
};
|
100
|
-
var getSchemaName = (schema) => {
|
101
|
-
return schema.name || schema.slug;
|
102
|
-
};
|
103
318
|
var composeMetaSchemaSlug = (suffix) => convertToCamelCase(`ronin_${suffix}`);
|
104
319
|
var composeAssociationSchemaSlug = (schema, field) => composeMetaSchemaSlug(`${schema.pluralSlug}_${field.slug}`);
|
105
320
|
var getFieldSelector = (field, fieldPath, rootTable) => {
|
106
|
-
const
|
321
|
+
const symbol = rootTable?.startsWith(RONIN_SCHEMA_SYMBOLS.FIELD) ? `${rootTable.replace(RONIN_SCHEMA_SYMBOLS.FIELD, "").slice(0, -1)}.` : "";
|
322
|
+
const tablePrefix = symbol || (rootTable ? `"${rootTable}".` : "");
|
107
323
|
if (field.type === "json") {
|
108
324
|
const dotParts = fieldPath.split(".");
|
109
325
|
const columnName = tablePrefix + dotParts.shift();
|
@@ -126,7 +342,7 @@ var getFieldFromSchema = (schema, fieldPath, instructionName, rootTable) => {
|
|
126
342
|
schemaField = schemaFields.find((field) => field.slug === fieldPath);
|
127
343
|
if (!schemaField) {
|
128
344
|
throw new RoninError({
|
129
|
-
message: `${errorPrefix} does not exist in schema "${
|
345
|
+
message: `${errorPrefix} does not exist in schema "${schema.name}".`,
|
130
346
|
code: "FIELD_NOT_FOUND",
|
131
347
|
field: fieldPath,
|
132
348
|
queries: null
|
@@ -201,27 +417,76 @@ var SYSTEM_SCHEMAS = [
|
|
201
417
|
{ slug: "name", type: "string" },
|
202
418
|
{ slug: "slug", type: "string", required: true },
|
203
419
|
{ slug: "type", type: "string", required: true },
|
204
|
-
{
|
420
|
+
{
|
421
|
+
slug: "schema",
|
422
|
+
type: "reference",
|
423
|
+
target: { slug: "schema" },
|
424
|
+
required: true
|
425
|
+
},
|
205
426
|
{ slug: "required", type: "boolean" },
|
206
427
|
{ slug: "defaultValue", type: "string" },
|
207
428
|
{ slug: "unique", type: "boolean" },
|
208
429
|
{ slug: "autoIncrement", type: "boolean" },
|
209
430
|
// Only allowed for fields of type "reference".
|
210
|
-
{ slug: "target", type: "reference", target: {
|
431
|
+
{ slug: "target", type: "reference", target: { slug: "schema" } },
|
211
432
|
{ slug: "kind", type: "string" },
|
212
433
|
{ slug: "actions", type: "group" },
|
213
434
|
{ slug: "actions.onDelete", type: "string" },
|
214
435
|
{ slug: "actions.onUpdate", type: "string" }
|
215
436
|
]
|
437
|
+
},
|
438
|
+
{
|
439
|
+
name: "Index",
|
440
|
+
pluralName: "Indexes",
|
441
|
+
slug: "index",
|
442
|
+
pluralSlug: "indexes",
|
443
|
+
fields: [
|
444
|
+
...SYSTEM_FIELDS,
|
445
|
+
{ slug: "slug", type: "string", required: true },
|
446
|
+
{
|
447
|
+
slug: "schema",
|
448
|
+
type: "reference",
|
449
|
+
target: { slug: "schema" },
|
450
|
+
required: true
|
451
|
+
},
|
452
|
+
{ slug: "unique", type: "boolean" },
|
453
|
+
{ slug: "filter", type: "json" }
|
454
|
+
]
|
455
|
+
},
|
456
|
+
{
|
457
|
+
name: "Trigger",
|
458
|
+
pluralName: "Triggers",
|
459
|
+
slug: "trigger",
|
460
|
+
pluralSlug: "triggers",
|
461
|
+
fields: [
|
462
|
+
...SYSTEM_FIELDS,
|
463
|
+
{ slug: "slug", type: "string", required: true },
|
464
|
+
{ slug: "schema", type: "reference", target: { slug: "schema" }, required: true },
|
465
|
+
{ slug: "cause", type: "string", required: true },
|
466
|
+
{ slug: "filter", type: "json" },
|
467
|
+
{ slug: "effects", type: "json", required: true }
|
468
|
+
]
|
216
469
|
}
|
217
470
|
];
|
471
|
+
var SYSTEM_SCHEMA_SLUGS = SYSTEM_SCHEMAS.flatMap(({ slug, pluralSlug }) => [
|
472
|
+
slug,
|
473
|
+
pluralSlug
|
474
|
+
]);
|
475
|
+
var prepareSchema = (schema) => {
|
476
|
+
const copiedSchema = { ...schema };
|
477
|
+
if (!copiedSchema.pluralSlug) copiedSchema.pluralSlug = pluralize(copiedSchema.slug);
|
478
|
+
if (!copiedSchema.name) copiedSchema.name = slugToName(copiedSchema.slug);
|
479
|
+
if (!copiedSchema.pluralName)
|
480
|
+
copiedSchema.pluralName = slugToName(copiedSchema.pluralSlug);
|
481
|
+
return copiedSchema;
|
482
|
+
};
|
218
483
|
var addSystemSchemas = (schemas) => {
|
219
|
-
const list = [...SYSTEM_SCHEMAS, ...schemas].map(
|
484
|
+
const list = [...SYSTEM_SCHEMAS, ...schemas].map(prepareSchema);
|
220
485
|
for (const schema of list) {
|
221
486
|
const defaultIncluding = {};
|
222
487
|
for (const field of schema.fields || []) {
|
223
488
|
if (field.type === "reference" && !field.slug.startsWith("ronin.")) {
|
224
|
-
const relatedSchema = getSchemaBySlug(list, field.target.
|
489
|
+
const relatedSchema = getSchemaBySlug(list, field.target.slug);
|
225
490
|
let fieldSlug = relatedSchema.slug;
|
226
491
|
if (field.kind === "many") {
|
227
492
|
fieldSlug = composeAssociationSchemaSlug(schema, field);
|
@@ -253,7 +518,7 @@ var addSystemSchemas = (schemas) => {
|
|
253
518
|
}
|
254
519
|
}
|
255
520
|
};
|
256
|
-
const relatedSchemaToModify = getSchemaBySlug(list, field.target.
|
521
|
+
const relatedSchemaToModify = getSchemaBySlug(list, field.target.slug);
|
257
522
|
if (!relatedSchemaToModify) throw new Error("Missing related schema");
|
258
523
|
relatedSchemaToModify.including = {
|
259
524
|
[schema.pluralSlug]: {
|
@@ -298,7 +563,7 @@ var getFieldStatement = (field) => {
|
|
298
563
|
statement += ` DEFAULT ${field.defaultValue}`;
|
299
564
|
if (field.type === "reference") {
|
300
565
|
const actions = field.actions || {};
|
301
|
-
const targetTable = convertToSnakeCase(field.target.
|
566
|
+
const targetTable = convertToSnakeCase(pluralize(field.target.slug));
|
302
567
|
statement += ` REFERENCES ${targetTable}("id")`;
|
303
568
|
for (const trigger in actions) {
|
304
569
|
const triggerName = trigger.toUpperCase().slice(2);
|
@@ -308,68 +573,129 @@ var getFieldStatement = (field) => {
|
|
308
573
|
}
|
309
574
|
return statement;
|
310
575
|
};
|
311
|
-
var addSchemaQueries = (queryDetails, writeStatements) => {
|
576
|
+
var addSchemaQueries = (schemas, statementValues, queryDetails, writeStatements) => {
|
312
577
|
const { queryType, querySchema, queryInstructions } = queryDetails;
|
313
578
|
if (!["create", "set", "drop"].includes(queryType)) return;
|
314
|
-
if (!
|
579
|
+
if (!SYSTEM_SCHEMA_SLUGS.includes(querySchema)) return;
|
315
580
|
const instructionName = mappedInstructions[queryType];
|
316
581
|
const instructionList = queryInstructions[instructionName];
|
317
|
-
const kind =
|
318
|
-
const instructionTarget = kind === "schemas" ? instructionList : instructionList?.schema;
|
582
|
+
const kind = getSchemaBySlug(SYSTEM_SCHEMAS, querySchema).pluralSlug;
|
319
583
|
let tableAction = "ALTER";
|
320
|
-
let schemaPluralSlug = null;
|
321
584
|
let queryTypeReadable = null;
|
322
585
|
switch (queryType) {
|
323
586
|
case "create": {
|
324
|
-
if (kind === "schemas"
|
325
|
-
|
587
|
+
if (kind === "schemas" || kind === "indexes" || kind === "triggers") {
|
588
|
+
tableAction = "CREATE";
|
589
|
+
}
|
326
590
|
queryTypeReadable = "creating";
|
327
591
|
break;
|
328
592
|
}
|
329
593
|
case "set": {
|
330
594
|
if (kind === "schemas") tableAction = "ALTER";
|
331
|
-
schemaPluralSlug = instructionTarget?.pluralSlug?.being || instructionTarget?.pluralSlug;
|
332
595
|
queryTypeReadable = "updating";
|
333
596
|
break;
|
334
597
|
}
|
335
598
|
case "drop": {
|
336
|
-
if (kind === "schemas"
|
337
|
-
|
599
|
+
if (kind === "schemas" || kind === "indexes" || kind === "triggers") {
|
600
|
+
tableAction = "DROP";
|
601
|
+
}
|
338
602
|
queryTypeReadable = "deleting";
|
339
603
|
break;
|
340
604
|
}
|
341
605
|
}
|
342
|
-
|
343
|
-
|
606
|
+
const slug = instructionList?.slug?.being || instructionList?.slug;
|
607
|
+
if (!slug) {
|
344
608
|
throw new RoninError({
|
345
|
-
message: `When ${queryTypeReadable} ${kind}, a
|
609
|
+
message: `When ${queryTypeReadable} ${kind}, a \`slug\` field must be provided in the \`${instructionName}\` instruction.`,
|
346
610
|
code: "MISSING_FIELD",
|
347
|
-
fields: [
|
611
|
+
fields: ["slug"]
|
348
612
|
});
|
349
613
|
}
|
350
|
-
const
|
351
|
-
const
|
352
|
-
|
614
|
+
const schemaInstruction = instructionList?.schema;
|
615
|
+
const schemaSlug = schemaInstruction?.slug?.being || schemaInstruction?.slug;
|
616
|
+
if (kind !== "schemas" && !schemaSlug) {
|
617
|
+
throw new RoninError({
|
618
|
+
message: `When ${queryTypeReadable} ${kind}, a \`schema.slug\` field must be provided in the \`${instructionName}\` instruction.`,
|
619
|
+
code: "MISSING_FIELD",
|
620
|
+
fields: ["schema.slug"]
|
621
|
+
});
|
622
|
+
}
|
623
|
+
const tableName = convertToSnakeCase(pluralize(kind === "schemas" ? slug : schemaSlug));
|
624
|
+
if (kind === "indexes") {
|
625
|
+
const indexName = convertToSnakeCase(slug);
|
626
|
+
const unique = instructionList?.unique;
|
627
|
+
const filterQuery = instructionList?.filter;
|
628
|
+
let statement2 = `${tableAction}${unique ? " UNIQUE" : ""} INDEX "${indexName}"`;
|
629
|
+
if (queryType === "create") {
|
630
|
+
statement2 += ` ON "${tableName}"`;
|
631
|
+
if (filterQuery) {
|
632
|
+
const targetSchema = getSchemaBySlug(schemas, schemaSlug);
|
633
|
+
const withStatement = handleWith(
|
634
|
+
schemas,
|
635
|
+
targetSchema,
|
636
|
+
statementValues,
|
637
|
+
filterQuery
|
638
|
+
);
|
639
|
+
statement2 += ` WHERE (${withStatement})`;
|
640
|
+
}
|
641
|
+
}
|
642
|
+
writeStatements.push(statement2);
|
643
|
+
return;
|
644
|
+
}
|
645
|
+
if (kind === "triggers") {
|
646
|
+
const triggerName = convertToSnakeCase(slug);
|
647
|
+
let statement2 = `${tableAction} TRIGGER "${triggerName}"`;
|
648
|
+
if (queryType === "create") {
|
649
|
+
const cause = slugToName(instructionList?.cause).toUpperCase();
|
650
|
+
const statementParts = [cause, "ON", `"${tableName}"`];
|
651
|
+
const effectQueries = instructionList?.effects;
|
652
|
+
const filterQuery = instructionList?.filter;
|
653
|
+
if (filterQuery || effectQueries.some((query) => findInObject(query, RONIN_SCHEMA_SYMBOLS.FIELD))) {
|
654
|
+
statementParts.push("FOR EACH ROW");
|
655
|
+
}
|
656
|
+
if (filterQuery) {
|
657
|
+
const targetSchema = getSchemaBySlug(schemas, schemaSlug);
|
658
|
+
const tablePlaceholder = cause.endsWith("DELETE") ? RONIN_SCHEMA_SYMBOLS.FIELD_OLD : RONIN_SCHEMA_SYMBOLS.FIELD_NEW;
|
659
|
+
const withStatement = handleWith(
|
660
|
+
schemas,
|
661
|
+
targetSchema,
|
662
|
+
statementValues,
|
663
|
+
filterQuery,
|
664
|
+
tablePlaceholder
|
665
|
+
);
|
666
|
+
statementParts.push("WHEN", `(${withStatement})`);
|
667
|
+
}
|
668
|
+
const effectStatements = effectQueries.map((effectQuery) => {
|
669
|
+
return compileQueryInput(effectQuery, schemas, {
|
670
|
+
statementValues,
|
671
|
+
disableReturning: true
|
672
|
+
}).readStatement;
|
673
|
+
});
|
674
|
+
if (effectStatements.length > 1) statementParts.push("BEGIN");
|
675
|
+
statementParts.push(effectStatements.join("; "));
|
676
|
+
if (effectStatements.length > 1) statementParts.push("END");
|
677
|
+
statement2 += ` ${statementParts.join(" ")}`;
|
678
|
+
}
|
679
|
+
writeStatements.push(statement2);
|
680
|
+
return;
|
681
|
+
}
|
682
|
+
let statement = `${tableAction} TABLE "${tableName}"`;
|
353
683
|
if (kind === "schemas") {
|
684
|
+
const fields = [...SYSTEM_FIELDS];
|
354
685
|
if (queryType === "create") {
|
355
686
|
const columns = fields.map(getFieldStatement).filter(Boolean);
|
356
687
|
statement += ` (${columns.join(", ")})`;
|
357
688
|
} else if (queryType === "set") {
|
358
|
-
const newSlug = queryInstructions.to?.
|
689
|
+
const newSlug = queryInstructions.to?.slug;
|
359
690
|
if (newSlug) {
|
360
|
-
const newTable = convertToSnakeCase(newSlug);
|
691
|
+
const newTable = convertToSnakeCase(pluralize(newSlug));
|
361
692
|
statement += ` RENAME TO "${newTable}"`;
|
362
693
|
}
|
363
694
|
}
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
message: `When ${queryTypeReadable} fields, a \`slug\` field must be provided in the \`${instructionName}\` instruction.`,
|
369
|
-
code: "MISSING_FIELD",
|
370
|
-
fields: ["slug"]
|
371
|
-
});
|
372
|
-
}
|
695
|
+
writeStatements.push(statement);
|
696
|
+
return;
|
697
|
+
}
|
698
|
+
if (kind === "fields") {
|
373
699
|
if (queryType === "create") {
|
374
700
|
if (!instructionList.type) {
|
375
701
|
throw new RoninError({
|
@@ -382,207 +708,29 @@ var addSchemaQueries = (queryDetails, writeStatements) => {
|
|
382
708
|
} else if (queryType === "set") {
|
383
709
|
const newSlug = queryInstructions.to?.slug;
|
384
710
|
if (newSlug) {
|
385
|
-
statement += ` RENAME COLUMN "${
|
711
|
+
statement += ` RENAME COLUMN "${slug}" TO "${newSlug}"`;
|
386
712
|
}
|
387
713
|
} else if (queryType === "drop") {
|
388
|
-
statement += ` DROP COLUMN "${
|
714
|
+
statement += ` DROP COLUMN "${slug}"`;
|
389
715
|
}
|
716
|
+
writeStatements.push(statement);
|
390
717
|
}
|
391
|
-
writeStatements.push(statement);
|
392
|
-
};
|
393
|
-
|
394
|
-
// src/instructions/with.ts
|
395
|
-
var getMatcher = (value, negative) => {
|
396
|
-
if (negative) {
|
397
|
-
if (value === null) return "IS NOT";
|
398
|
-
return "!=";
|
399
|
-
}
|
400
|
-
if (value === null) return "IS";
|
401
|
-
return "=";
|
402
|
-
};
|
403
|
-
var WITH_CONDITIONS = {
|
404
|
-
being: (value, baseValue) => `${getMatcher(baseValue, false)} ${value}`,
|
405
|
-
notBeing: (value, baseValue) => `${getMatcher(baseValue, true)} ${value}`,
|
406
|
-
startingWith: (value) => `LIKE ${value}%`,
|
407
|
-
notStartingWith: (value) => `NOT LIKE ${value}%`,
|
408
|
-
endingWith: (value) => `LIKE %${value}`,
|
409
|
-
notEndingWith: (value) => `NOT LIKE %${value}`,
|
410
|
-
containing: (value) => `LIKE %${value}%`,
|
411
|
-
notContaining: (value) => `NOT LIKE %${value}%`,
|
412
|
-
greaterThan: (value) => `> ${value}`,
|
413
|
-
greaterOrEqual: (value) => `>= ${value}`,
|
414
|
-
lessThan: (value) => `< ${value}`,
|
415
|
-
lessOrEqual: (value) => `<= ${value}`
|
416
|
-
};
|
417
|
-
var handleWith = (schemas, schema, statementValues, instruction, rootTable) => {
|
418
|
-
const subStatement = composeConditions(
|
419
|
-
schemas,
|
420
|
-
schema,
|
421
|
-
statementValues,
|
422
|
-
"with",
|
423
|
-
instruction,
|
424
|
-
{ rootTable }
|
425
|
-
);
|
426
|
-
return `(${subStatement})`;
|
427
|
-
};
|
428
|
-
|
429
|
-
// src/utils/statement.ts
|
430
|
-
var prepareStatementValue = (statementValues, value, bindNull = false) => {
|
431
|
-
if (!bindNull && value === null) return "NULL";
|
432
|
-
let formattedValue = value;
|
433
|
-
if (Array.isArray(value) || isObject(value)) {
|
434
|
-
formattedValue = JSON.stringify(value);
|
435
|
-
} else if (typeof value === "boolean") {
|
436
|
-
formattedValue = value ? 1 : 0;
|
437
|
-
}
|
438
|
-
const index = statementValues.push(formattedValue);
|
439
|
-
return `?${index}`;
|
440
718
|
};
|
441
|
-
var
|
442
|
-
const
|
443
|
-
|
444
|
-
options.fieldSlug,
|
445
|
-
instructionName,
|
446
|
-
options.rootTable
|
447
|
-
);
|
448
|
-
const isSubQuery = isObject(value) && Object.hasOwn(value, RONIN_SCHEMA_SYMBOLS.QUERY);
|
449
|
-
const collectStatementValue = options.type !== "fields";
|
450
|
-
let conditionSelector = selector;
|
451
|
-
let conditionValue = value;
|
452
|
-
if (isSubQuery && collectStatementValue) {
|
453
|
-
conditionValue = `(${compileQueryInput(
|
454
|
-
value[RONIN_SCHEMA_SYMBOLS.QUERY],
|
455
|
-
schemas,
|
456
|
-
{ statementValues }
|
457
|
-
).readStatement})`;
|
458
|
-
} else if (typeof value === "string" && value.startsWith(RONIN_SCHEMA_SYMBOLS.FIELD)) {
|
459
|
-
conditionSelector = `"${options.customTable}"."${schemaField.slug}"`;
|
460
|
-
conditionValue = `"${options.rootTable}"."${value.replace(RONIN_SCHEMA_SYMBOLS.FIELD, "")}"`;
|
461
|
-
} else if (schemaField.type === "json" && instructionName === "to") {
|
462
|
-
conditionSelector = `"${schemaField.slug}"`;
|
463
|
-
if (collectStatementValue) {
|
464
|
-
const preparedValue = prepareStatementValue(statementValues, value, false);
|
465
|
-
conditionValue = `IIF(${conditionSelector} IS NULL, ${preparedValue}, json_patch(${conditionSelector}, ${preparedValue}))`;
|
466
|
-
}
|
467
|
-
} else if (collectStatementValue) {
|
468
|
-
conditionValue = prepareStatementValue(statementValues, value, false);
|
469
|
-
}
|
470
|
-
if (options.type === "fields") return conditionSelector;
|
471
|
-
if (options.type === "values") return conditionValue;
|
472
|
-
return `${conditionSelector} ${WITH_CONDITIONS[options.condition || "being"](conditionValue, value)}`;
|
719
|
+
var slugToName = (slug) => {
|
720
|
+
const name = slug.replace(/([a-z])([A-Z])/g, "$1 $2");
|
721
|
+
return title(name);
|
473
722
|
};
|
474
|
-
var
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
condition: conditionType
|
481
|
-
})
|
482
|
-
);
|
483
|
-
return conditions.join(" AND ");
|
484
|
-
}
|
485
|
-
if (options.fieldSlug) {
|
486
|
-
const fieldDetails = getFieldFromSchema(
|
487
|
-
schema,
|
488
|
-
options.fieldSlug,
|
489
|
-
instructionName,
|
490
|
-
options.rootTable
|
491
|
-
);
|
492
|
-
const { field: schemaField } = fieldDetails;
|
493
|
-
const consumeJSON = schemaField.type === "json" && instructionName === "to";
|
494
|
-
const isSubQuery = isNested && Object.hasOwn(value, RONIN_SCHEMA_SYMBOLS.QUERY);
|
495
|
-
if (!(isObject(value) || Array.isArray(value)) || isSubQuery || consumeJSON) {
|
496
|
-
return composeFieldValues(
|
497
|
-
schemas,
|
498
|
-
schema,
|
499
|
-
statementValues,
|
500
|
-
instructionName,
|
501
|
-
value,
|
502
|
-
{ ...options, fieldSlug: options.fieldSlug }
|
503
|
-
);
|
504
|
-
}
|
505
|
-
if (schemaField.type === "reference" && isNested) {
|
506
|
-
const keys = Object.keys(value);
|
507
|
-
const values = Object.values(value);
|
508
|
-
let recordTarget;
|
509
|
-
if (keys.length === 1 && keys[0] === "id") {
|
510
|
-
recordTarget = values[0];
|
511
|
-
} else {
|
512
|
-
const relatedSchema = getSchemaBySlug(schemas, schemaField.target.pluralSlug);
|
513
|
-
const subQuery = {
|
514
|
-
get: {
|
515
|
-
[relatedSchema.slug]: {
|
516
|
-
with: value,
|
517
|
-
selecting: ["id"]
|
518
|
-
}
|
519
|
-
}
|
520
|
-
};
|
521
|
-
recordTarget = {
|
522
|
-
[RONIN_SCHEMA_SYMBOLS.QUERY]: subQuery
|
523
|
-
};
|
524
|
-
}
|
525
|
-
return composeConditions(
|
526
|
-
schemas,
|
527
|
-
schema,
|
528
|
-
statementValues,
|
529
|
-
instructionName,
|
530
|
-
recordTarget,
|
531
|
-
options
|
532
|
-
);
|
533
|
-
}
|
723
|
+
var VOWELS = ["a", "e", "i", "o", "u"];
|
724
|
+
var pluralize = (word) => {
|
725
|
+
const lastLetter = word.slice(-1).toLowerCase();
|
726
|
+
const secondLastLetter = word.slice(-2, -1).toLowerCase();
|
727
|
+
if (lastLetter === "y" && !VOWELS.includes(secondLastLetter)) {
|
728
|
+
return `${word.slice(0, -1)}ies`;
|
534
729
|
}
|
535
|
-
if (
|
536
|
-
|
537
|
-
const nestedFieldSlug = options.fieldSlug ? `${options.fieldSlug}.${field}` : field;
|
538
|
-
return composeConditions(schemas, schema, statementValues, instructionName, value2, {
|
539
|
-
...options,
|
540
|
-
fieldSlug: nestedFieldSlug
|
541
|
-
});
|
542
|
-
});
|
543
|
-
const joiner = instructionName === "to" ? ", " : " AND ";
|
544
|
-
if (instructionName === "to") return `${conditions.join(joiner)}`;
|
545
|
-
return conditions.length === 1 ? conditions[0] : options.fieldSlug ? `(${conditions.join(joiner)})` : conditions.join(joiner);
|
546
|
-
}
|
547
|
-
if (Array.isArray(value)) {
|
548
|
-
const conditions = value.map(
|
549
|
-
(filter) => composeConditions(
|
550
|
-
schemas,
|
551
|
-
schema,
|
552
|
-
statementValues,
|
553
|
-
instructionName,
|
554
|
-
filter,
|
555
|
-
options
|
556
|
-
)
|
557
|
-
);
|
558
|
-
return conditions.join(" OR ");
|
730
|
+
if (lastLetter === "s" || word.slice(-2).toLowerCase() === "ch" || word.slice(-2).toLowerCase() === "sh" || word.slice(-2).toLowerCase() === "ex") {
|
731
|
+
return `${word}es`;
|
559
732
|
}
|
560
|
-
|
561
|
-
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.`,
|
562
|
-
code: "INVALID_WITH_VALUE",
|
563
|
-
queries: null
|
564
|
-
});
|
565
|
-
};
|
566
|
-
var formatIdentifiers = ({ identifiers }, queryInstructions) => {
|
567
|
-
if (!queryInstructions) return queryInstructions;
|
568
|
-
const type = "with" in queryInstructions ? "with" : null;
|
569
|
-
if (!type) return queryInstructions;
|
570
|
-
const nestedInstructions = queryInstructions[type];
|
571
|
-
if (!nestedInstructions || Array.isArray(nestedInstructions))
|
572
|
-
return queryInstructions;
|
573
|
-
const newNestedInstructions = { ...nestedInstructions };
|
574
|
-
for (const oldKey of Object.keys(newNestedInstructions)) {
|
575
|
-
if (oldKey !== "titleIdentifier" && oldKey !== "slugIdentifier") continue;
|
576
|
-
const identifierName = oldKey === "titleIdentifier" ? "title" : "slug";
|
577
|
-
const value = newNestedInstructions[oldKey];
|
578
|
-
const newKey = identifiers?.[identifierName] || "id";
|
579
|
-
newNestedInstructions[newKey] = value;
|
580
|
-
delete newNestedInstructions[oldKey];
|
581
|
-
}
|
582
|
-
return {
|
583
|
-
...queryInstructions,
|
584
|
-
[type]: newNestedInstructions
|
585
|
-
};
|
733
|
+
return `${word}s`;
|
586
734
|
};
|
587
735
|
|
588
736
|
// src/instructions/before-after.ts
|
@@ -673,12 +821,12 @@ var handleFor = (schemas, schema, statementValues, instruction, rootTable) => {
|
|
673
821
|
const forFilter = schema.for?.[shortcut];
|
674
822
|
if (!forFilter) {
|
675
823
|
throw new RoninError({
|
676
|
-
message: `The provided \`for\` shortcut "${shortcut}" does not exist in schema "${
|
824
|
+
message: `The provided \`for\` shortcut "${shortcut}" does not exist in schema "${schema.name}".`,
|
677
825
|
code: "INVALID_FOR_VALUE"
|
678
826
|
});
|
679
827
|
}
|
680
828
|
const replacedForFilter = structuredClone(forFilter);
|
681
|
-
|
829
|
+
findInObject(
|
682
830
|
replacedForFilter,
|
683
831
|
RONIN_SCHEMA_SYMBOLS.VALUE,
|
684
832
|
(match) => match.replace(RONIN_SCHEMA_SYMBOLS.VALUE, args)
|
@@ -705,7 +853,7 @@ var handleIncluding = (schemas, statementValues, schema, instruction, rootTable)
|
|
705
853
|
const includingQuery = schema.including?.[shortcut];
|
706
854
|
if (!includingQuery) {
|
707
855
|
throw new RoninError({
|
708
|
-
message: `The provided \`including\` shortcut "${shortcut}" does not exist in schema "${
|
856
|
+
message: `The provided \`including\` shortcut "${shortcut}" does not exist in schema "${schema.name}".`,
|
709
857
|
code: "INVALID_INCLUDING_VALUE"
|
710
858
|
});
|
711
859
|
}
|
@@ -943,7 +1091,7 @@ var compileQueryInput = (query, defaultSchemas, options) => {
|
|
943
1091
|
let table = getTableForSchema(schema);
|
944
1092
|
const statementValues = options?.statementValues || [];
|
945
1093
|
const writeStatements = [];
|
946
|
-
addSchemaQueries(parsedQuery, writeStatements);
|
1094
|
+
addSchemaQueries(schemas, statementValues, parsedQuery, writeStatements);
|
947
1095
|
const columns = handleSelecting(schema, statementValues, {
|
948
1096
|
selecting: instructions?.selecting,
|
949
1097
|
including: instructions?.including
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@ronin/compiler",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.3.0",
|
4
4
|
"type": "module",
|
5
5
|
"description": "Compiles RONIN queries to SQL statements.",
|
6
6
|
"publishConfig": {
|
@@ -28,11 +28,13 @@
|
|
28
28
|
"author": "ronin",
|
29
29
|
"license": "Apache-2.0",
|
30
30
|
"dependencies": {
|
31
|
-
"@paralleldrive/cuid2": "2.2.2"
|
31
|
+
"@paralleldrive/cuid2": "2.2.2",
|
32
|
+
"title": "3.5.3"
|
32
33
|
},
|
33
34
|
"devDependencies": {
|
34
35
|
"@biomejs/biome": "1.9.2",
|
35
36
|
"@types/bun": "1.1.10",
|
37
|
+
"@types/title": "3.4.3",
|
36
38
|
"tsup": "8.3.0",
|
37
39
|
"typescript": "5.6.2",
|
38
40
|
"zod": "3.23.8"
|