@ronin/compiler 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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"
|