@ronin/compiler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
package/dist/index.js ADDED
@@ -0,0 +1,1093 @@
1
+ // src/utils/index.ts
2
+ import { init as cuid } from "@paralleldrive/cuid2";
3
+ var RONIN_SCHEMA_SYMBOLS = {
4
+ QUERY: "__RONIN_QUERY",
5
+ FIELD: "__RONIN_FIELD_",
6
+ VALUE: "__RONIN_VALUE"
7
+ };
8
+ var RoninError = class extends Error {
9
+ code;
10
+ field;
11
+ fields;
12
+ issues;
13
+ queries;
14
+ constructor(details) {
15
+ super(details.message);
16
+ this.name = "RoninError";
17
+ this.code = details.code;
18
+ this.field = details.field;
19
+ this.fields = details.fields;
20
+ this.issues = details.issues;
21
+ this.queries = details.queries || null;
22
+ }
23
+ };
24
+ var SINGLE_QUOTE_REGEX = /'/g;
25
+ var DOUBLE_QUOTE_REGEX = /"/g;
26
+ var AMPERSAND_REGEX = /\s*&+\s*/g;
27
+ var SPECIAL_CHARACTERS_REGEX = /[^\w\s-]+/g;
28
+ var SPLIT_REGEX = /(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|[\s.\-_]+/;
29
+ var generateRecordId = (prefix) => `${prefix || "rec"}_${cuid({ length: 16 })()}`;
30
+ var capitalize = (str) => {
31
+ if (!str || str.length === 0) return "";
32
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
33
+ };
34
+ var sanitize = (str) => {
35
+ if (!str || str.length === 0) return "";
36
+ return str.replace(SINGLE_QUOTE_REGEX, "").replace(DOUBLE_QUOTE_REGEX, "").replace(AMPERSAND_REGEX, " and ").replace(SPECIAL_CHARACTERS_REGEX, " ").trim();
37
+ };
38
+ var convertToSnakeCase = (str) => {
39
+ if (!str || str.length === 0) return "";
40
+ return sanitize(str).split(SPLIT_REGEX).map((part) => part.toLowerCase()).join("_");
41
+ };
42
+ var convertToCamelCase = (str) => {
43
+ if (!str || str.length === 0) return "";
44
+ return sanitize(str).split(SPLIT_REGEX).map((part, index) => index === 0 ? part.toLowerCase() : capitalize(part)).join("");
45
+ };
46
+ var isObject = (value) => value != null && typeof value === "object" && Array.isArray(value) === false;
47
+ var replaceInObject = (obj, pattern, replacer) => {
48
+ for (const key in obj) {
49
+ const value = obj[key];
50
+ if (isObject(value)) {
51
+ replaceInObject(value, pattern, replacer);
52
+ } else if (typeof value === "string" && value.startsWith(pattern)) {
53
+ obj[key] = value.replace(pattern, replacer);
54
+ }
55
+ }
56
+ };
57
+ var flatten = (obj, prefix = "", res = {}) => {
58
+ for (const key in obj) {
59
+ const path = prefix ? `${prefix}.${key}` : key;
60
+ if (typeof obj[key] === "object" && obj[key] !== null) {
61
+ flatten(obj[key], path, res);
62
+ } else {
63
+ res[path] = obj[key];
64
+ }
65
+ }
66
+ return res;
67
+ };
68
+ var expand = (obj) => {
69
+ return Object.entries(obj).reduce((res, [key, val]) => {
70
+ key.split(".").reduce((acc, part, i, arr) => {
71
+ acc[part] = i === arr.length - 1 ? val : acc[part] || {};
72
+ return acc[part];
73
+ }, res);
74
+ return res;
75
+ }, {});
76
+ };
77
+ var splitQuery = (query) => {
78
+ const queryType = Object.keys(query)[0];
79
+ const querySchema = Object.keys(query[queryType])[0];
80
+ const queryInstructions = query[queryType][querySchema];
81
+ return { queryType, querySchema, queryInstructions };
82
+ };
83
+
84
+ // src/utils/schema.ts
85
+ var getSchemaBySlug = (schemas, slug) => {
86
+ const schema = schemas.find((schema2) => {
87
+ return schema2.slug === slug || schema2.pluralSlug === slug;
88
+ });
89
+ if (!schema) {
90
+ throw new RoninError({
91
+ message: `No matching schema with either Slug or Plural Slug of "${slug}" could be found.`,
92
+ code: "SCHEMA_NOT_FOUND"
93
+ });
94
+ }
95
+ return schema;
96
+ };
97
+ var getTableForSchema = (schema) => {
98
+ return convertToSnakeCase(schema.pluralSlug);
99
+ };
100
+ var getSchemaName = (schema) => {
101
+ return schema.name || schema.slug;
102
+ };
103
+ var composeMetaSchemaSlug = (suffix) => convertToCamelCase(`ronin_${suffix}`);
104
+ var composeAssociationSchemaSlug = (schema, field) => composeMetaSchemaSlug(`${schema.pluralSlug}_${field.slug}`);
105
+ var getFieldSelector = (field, fieldPath, rootTable) => {
106
+ const tablePrefix = rootTable ? `"${rootTable}".` : "";
107
+ if (field.type === "json") {
108
+ const dotParts = fieldPath.split(".");
109
+ const columnName = tablePrefix + dotParts.shift();
110
+ const jsonField = dotParts.join(".");
111
+ return `json_extract(${columnName}, '$.${jsonField}')`;
112
+ }
113
+ return `${tablePrefix}"${fieldPath}"`;
114
+ };
115
+ var getFieldFromSchema = (schema, fieldPath, instructionName, rootTable) => {
116
+ const errorPrefix = `Field "${fieldPath}" defined for \`${instructionName}\``;
117
+ const schemaFields = schema.fields || [];
118
+ let schemaField;
119
+ if (fieldPath.includes(".")) {
120
+ schemaField = schemaFields.find((field) => field.slug === fieldPath.split(".")[0]);
121
+ if (schemaField?.type === "json") {
122
+ const fieldSelector2 = getFieldSelector(schemaField, fieldPath, rootTable);
123
+ return { field: schemaField, fieldSelector: fieldSelector2 };
124
+ }
125
+ }
126
+ schemaField = schemaFields.find((field) => field.slug === fieldPath);
127
+ if (!schemaField) {
128
+ throw new RoninError({
129
+ message: `${errorPrefix} does not exist in schema "${getSchemaName(schema)}".`,
130
+ code: "FIELD_NOT_FOUND",
131
+ field: fieldPath,
132
+ queries: null
133
+ });
134
+ }
135
+ const fieldSelector = getFieldSelector(schemaField, fieldPath, rootTable);
136
+ return { field: schemaField, fieldSelector };
137
+ };
138
+ var SYSTEM_FIELDS = [
139
+ {
140
+ name: "ID",
141
+ type: "string",
142
+ slug: "id",
143
+ displayAs: "single-line"
144
+ },
145
+ {
146
+ name: "RONIN",
147
+ type: "group",
148
+ slug: "ronin"
149
+ },
150
+ {
151
+ name: "RONIN - Locked",
152
+ type: "boolean",
153
+ slug: "ronin.locked"
154
+ },
155
+ {
156
+ name: "RONIN - Created At",
157
+ type: "date",
158
+ slug: "ronin.createdAt"
159
+ },
160
+ {
161
+ name: "RONIN - Created By",
162
+ type: "reference",
163
+ schema: "account",
164
+ slug: "ronin.createdBy"
165
+ },
166
+ {
167
+ name: "RONIN - Updated At",
168
+ type: "date",
169
+ slug: "ronin.updatedAt"
170
+ },
171
+ {
172
+ name: "RONIN - Updated By",
173
+ type: "reference",
174
+ schema: "account",
175
+ slug: "ronin.updatedBy"
176
+ }
177
+ ];
178
+ var SYSTEM_SCHEMAS = [
179
+ {
180
+ name: "Schema",
181
+ pluralName: "Schemas",
182
+ slug: "schema",
183
+ pluralSlug: "schemas",
184
+ fields: [
185
+ ...SYSTEM_FIELDS,
186
+ { slug: "name", type: "string" },
187
+ { slug: "pluralName", type: "string" },
188
+ { slug: "slug", type: "string" },
189
+ { slug: "pluralSlug", type: "string" },
190
+ { slug: "idPrefix", type: "string" },
191
+ { slug: "identifiers", type: "group" },
192
+ { slug: "identifiers.title", type: "string" },
193
+ { slug: "identifiers.slug", type: "string" }
194
+ ]
195
+ },
196
+ {
197
+ name: "Field",
198
+ pluralName: "Fields",
199
+ slug: "field",
200
+ pluralSlug: "fields",
201
+ fields: [
202
+ ...SYSTEM_FIELDS,
203
+ { slug: "name", type: "string" },
204
+ { slug: "slug", type: "string" },
205
+ { slug: "type", type: "string" },
206
+ { slug: "schema", type: "reference", schema: "schema" },
207
+ { slug: "required", type: "boolean" },
208
+ { slug: "defaultValue", type: "string" },
209
+ { slug: "unique", type: "boolean" },
210
+ { slug: "autoIncrement", type: "boolean" }
211
+ ]
212
+ }
213
+ ];
214
+ var addSystemSchemas = (schemas) => {
215
+ const list = [...SYSTEM_SCHEMAS, ...schemas].map((schema) => ({ ...schema }));
216
+ for (const schema of list) {
217
+ const defaultIncluding = {};
218
+ for (const field of schema.fields || []) {
219
+ if (field.type === "reference" && !field.slug.startsWith("ronin.")) {
220
+ const relatedSchema = getSchemaBySlug(list, field.schema);
221
+ let fieldSlug = relatedSchema.slug;
222
+ if (field.kind === "many") {
223
+ fieldSlug = composeAssociationSchemaSlug(schema, field);
224
+ list.push({
225
+ pluralSlug: fieldSlug,
226
+ slug: fieldSlug,
227
+ fields: [
228
+ {
229
+ slug: "origin",
230
+ type: "reference",
231
+ schema: schema.slug
232
+ },
233
+ {
234
+ slug: "target",
235
+ type: "reference",
236
+ schema: relatedSchema.slug
237
+ }
238
+ ]
239
+ });
240
+ }
241
+ defaultIncluding[field.slug] = {
242
+ get: {
243
+ [fieldSlug]: {
244
+ with: {
245
+ // Compare the `id` field of the related schema to the reference field on
246
+ // the root schema (`field.slug`).
247
+ id: `${RONIN_SCHEMA_SYMBOLS.FIELD}${field.slug}`
248
+ }
249
+ }
250
+ }
251
+ };
252
+ const relatedSchemaToModify = list.find((schema2) => schema2.slug === field.schema);
253
+ if (!relatedSchemaToModify) throw new Error("Missing related schema");
254
+ relatedSchemaToModify.including = {
255
+ [schema.pluralSlug]: {
256
+ get: {
257
+ [schema.pluralSlug]: {
258
+ with: {
259
+ [field.slug]: `${RONIN_SCHEMA_SYMBOLS.FIELD}id`
260
+ }
261
+ }
262
+ }
263
+ },
264
+ ...relatedSchemaToModify.including
265
+ };
266
+ }
267
+ }
268
+ schema.fields = [...SYSTEM_FIELDS, ...schema.fields || []];
269
+ schema.including = { ...defaultIncluding, ...schema.including };
270
+ }
271
+ return list;
272
+ };
273
+ var mappedInstructions = {
274
+ create: "to",
275
+ set: "with",
276
+ drop: "with"
277
+ };
278
+ var typesInSQLite = {
279
+ reference: "TEXT",
280
+ string: "TEXT",
281
+ date: "DATETIME",
282
+ blob: "TEXT",
283
+ boolean: "BOOLEAN",
284
+ number: "INTEGER",
285
+ json: "TEXT"
286
+ };
287
+ var getFieldStatement = (field) => {
288
+ if (field.type === "group") return null;
289
+ let statement = `"${field.slug}" ${typesInSQLite[field.type]}`;
290
+ if (field.slug === "id") statement += " PRIMARY KEY";
291
+ if (field.unique === true) statement += " UNIQUE";
292
+ if (field.required === true) statement += " NOT NULL";
293
+ if (typeof field.defaultValue !== "undefined")
294
+ statement += ` DEFAULT ${field.defaultValue}`;
295
+ return statement;
296
+ };
297
+ var addSchemaQueries = (queryDetails, writeStatements) => {
298
+ const { queryType, querySchema, queryInstructions } = queryDetails;
299
+ if (!["create", "set", "drop"].includes(queryType)) return;
300
+ if (!["schema", "schemas", "field", "fields"].includes(querySchema)) return;
301
+ const instructionName = mappedInstructions[queryType];
302
+ const instructionList = queryInstructions[instructionName];
303
+ const kind = ["schema", "schemas"].includes(querySchema) ? "schemas" : "fields";
304
+ const instructionTarget = kind === "schemas" ? instructionList : instructionList?.schema;
305
+ let tableAction = "ALTER";
306
+ let schemaPluralSlug = null;
307
+ let queryTypeReadable = null;
308
+ switch (queryType) {
309
+ case "create": {
310
+ if (kind === "schemas") tableAction = "CREATE";
311
+ schemaPluralSlug = instructionTarget?.pluralSlug;
312
+ queryTypeReadable = "creating";
313
+ break;
314
+ }
315
+ case "set": {
316
+ if (kind === "schemas") tableAction = "ALTER";
317
+ schemaPluralSlug = instructionTarget?.pluralSlug?.being || instructionTarget?.pluralSlug;
318
+ queryTypeReadable = "updating";
319
+ break;
320
+ }
321
+ case "drop": {
322
+ if (kind === "schemas") tableAction = "DROP";
323
+ schemaPluralSlug = instructionTarget?.pluralSlug?.being || instructionTarget?.pluralSlug;
324
+ queryTypeReadable = "deleting";
325
+ break;
326
+ }
327
+ }
328
+ if (!schemaPluralSlug) {
329
+ const field = kind === "schemas" ? "pluralSlug" : "schema.pluralSlug";
330
+ throw new RoninError({
331
+ message: `When ${queryTypeReadable} ${kind}, a \`${field}\` field must be provided in the \`${instructionName}\` instruction.`,
332
+ code: "MISSING_FIELD",
333
+ fields: [field]
334
+ });
335
+ }
336
+ const table = convertToSnakeCase(schemaPluralSlug);
337
+ const fields = [...SYSTEM_FIELDS];
338
+ let statement = `${tableAction} TABLE "${table}"`;
339
+ if (kind === "schemas") {
340
+ if (queryType === "create") {
341
+ const columns = fields.map(getFieldStatement).filter(Boolean);
342
+ statement += ` (${columns.join(", ")})`;
343
+ } else if (queryType === "set") {
344
+ const newSlug = queryInstructions.to?.pluralSlug;
345
+ if (newSlug) {
346
+ const newTable = convertToSnakeCase(newSlug);
347
+ statement += ` RENAME TO "${newTable}"`;
348
+ }
349
+ }
350
+ } else if (kind === "fields") {
351
+ const fieldSlug = instructionTarget?.slug?.being || instructionList?.slug;
352
+ if (!fieldSlug) {
353
+ throw new RoninError({
354
+ message: `When ${queryTypeReadable} fields, a \`slug\` field must be provided in the \`${instructionName}\` instruction.`,
355
+ code: "MISSING_FIELD",
356
+ fields: ["slug"]
357
+ });
358
+ }
359
+ if (queryType === "create") {
360
+ if (!instructionList.type) {
361
+ throw new RoninError({
362
+ message: `When ${queryTypeReadable} fields, a \`type\` field must be provided in the \`to\` instruction.`,
363
+ code: "MISSING_FIELD",
364
+ fields: ["type"]
365
+ });
366
+ }
367
+ statement += ` ADD COLUMN ${getFieldStatement(instructionList)}`;
368
+ } else if (queryType === "set") {
369
+ const newSlug = queryInstructions.to?.slug;
370
+ if (newSlug) {
371
+ statement += ` RENAME COLUMN "${fieldSlug}" TO "${newSlug}"`;
372
+ }
373
+ } else if (queryType === "drop") {
374
+ statement += ` DROP COLUMN "${fieldSlug}"`;
375
+ }
376
+ }
377
+ writeStatements.push(statement);
378
+ };
379
+
380
+ // src/instructions/with.ts
381
+ var WITH_CONDITIONS = [
382
+ "being",
383
+ "notBeing",
384
+ "startingWith",
385
+ "notStartingWith",
386
+ "endingWith",
387
+ "notEndingWith",
388
+ "containing",
389
+ "notContaining",
390
+ "greaterThan",
391
+ "greaterOrEqual",
392
+ "lessThan",
393
+ "lessOrEqual"
394
+ ];
395
+ var handleWith = (schemas, schema, statementValues, instruction, rootTable) => {
396
+ const subStatement = composeConditions(
397
+ schemas,
398
+ schema,
399
+ statementValues,
400
+ "with",
401
+ instruction,
402
+ { rootTable }
403
+ );
404
+ return `(${subStatement})`;
405
+ };
406
+
407
+ // src/utils/statement.ts
408
+ var prepareStatementValue = (statementValues, value, bindNull = false) => {
409
+ if (!bindNull && value === null) return "NULL";
410
+ let formattedValue = value;
411
+ if (Array.isArray(value) || isObject(value)) {
412
+ formattedValue = JSON.stringify(value);
413
+ } else if (typeof value === "boolean") {
414
+ formattedValue = value ? 1 : 0;
415
+ }
416
+ const index = statementValues.push(formattedValue);
417
+ return `?${index}`;
418
+ };
419
+ var composeFieldValues = (schemas, schema, statementValues, instructionName, value, options) => {
420
+ const { field: schemaField, fieldSelector: selector } = getFieldFromSchema(
421
+ schema,
422
+ options.fieldSlug,
423
+ instructionName,
424
+ options.rootTable
425
+ );
426
+ const isSubQuery = isObject(value) && Object.hasOwn(value, RONIN_SCHEMA_SYMBOLS.QUERY);
427
+ const collectStatementValue = options.type !== "fields";
428
+ let conditionSelector = selector;
429
+ let conditionValue = value;
430
+ if (isSubQuery && collectStatementValue) {
431
+ conditionValue = `(${compileQueryInput(
432
+ value[RONIN_SCHEMA_SYMBOLS.QUERY],
433
+ schemas,
434
+ { statementValues }
435
+ ).readStatement})`;
436
+ } else if (typeof value === "string" && value.startsWith(RONIN_SCHEMA_SYMBOLS.FIELD)) {
437
+ conditionSelector = `"${options.customTable}"."${schemaField.slug}"`;
438
+ conditionValue = `"${options.rootTable}"."${value.replace(RONIN_SCHEMA_SYMBOLS.FIELD, "")}"`;
439
+ } else if (schemaField.type === "json" && instructionName === "to") {
440
+ conditionSelector = `"${schemaField.slug}"`;
441
+ if (collectStatementValue) {
442
+ const preparedValue = prepareStatementValue(statementValues, value, false);
443
+ conditionValue = `IIF(${conditionSelector} IS NULL, ${preparedValue}, json_patch(${conditionSelector}, ${preparedValue}))`;
444
+ }
445
+ } else if (collectStatementValue) {
446
+ conditionValue = prepareStatementValue(statementValues, value, false);
447
+ }
448
+ if (options.type === "fields") return conditionSelector;
449
+ if (options.type === "values") return conditionValue;
450
+ const conditionTypes = {
451
+ being: [getMatcher(value, false), conditionValue],
452
+ notBeing: [getMatcher(value, true), conditionValue],
453
+ startingWith: ["LIKE", `${conditionValue}%`],
454
+ notStartingWith: ["NOT LIKE", `${conditionValue}%`],
455
+ endingWith: ["LIKE", `%${conditionValue}`],
456
+ notEndingWith: ["NOT LIKE", `%${conditionValue}`],
457
+ containing: ["LIKE", `%${conditionValue}%`],
458
+ notContaining: ["NOT LIKE", `%${conditionValue}%`],
459
+ greaterThan: [">", conditionValue],
460
+ greaterOrEqual: [">=", conditionValue],
461
+ lessThan: ["<", conditionValue],
462
+ lessOrEqual: ["<=", conditionValue]
463
+ };
464
+ return `${conditionSelector} ${conditionTypes[options.condition || "being"].join(" ")}`;
465
+ };
466
+ var composeConditions = (schemas, schema, statementValues, instructionName, value, options) => {
467
+ const isNested = isObject(value) && Object.keys(value).length > 0;
468
+ if (isNested && Object.keys(value).every(
469
+ (key) => WITH_CONDITIONS.includes(key)
470
+ )) {
471
+ const conditions = Object.entries(value).map(
472
+ ([conditionType, checkValue]) => composeConditions(schemas, schema, statementValues, instructionName, checkValue, {
473
+ ...options,
474
+ condition: conditionType
475
+ })
476
+ );
477
+ return conditions.join(" AND ");
478
+ }
479
+ if (options.fieldSlug) {
480
+ const fieldDetails = getFieldFromSchema(
481
+ schema,
482
+ options.fieldSlug,
483
+ instructionName,
484
+ options.rootTable
485
+ );
486
+ const { field: schemaField } = fieldDetails;
487
+ const consumeJSON = schemaField.type === "json" && instructionName === "to";
488
+ const isSubQuery = isNested && Object.hasOwn(value, RONIN_SCHEMA_SYMBOLS.QUERY);
489
+ if (!(isObject(value) || Array.isArray(value)) || isSubQuery || consumeJSON) {
490
+ return composeFieldValues(
491
+ schemas,
492
+ schema,
493
+ statementValues,
494
+ instructionName,
495
+ value,
496
+ { ...options, fieldSlug: options.fieldSlug }
497
+ );
498
+ }
499
+ if (schemaField.type === "reference" && isNested) {
500
+ const keys = Object.keys(value);
501
+ const values = Object.values(value);
502
+ let recordTarget;
503
+ if (keys.length === 1 && keys[0] === "id") {
504
+ recordTarget = values[0];
505
+ } else {
506
+ const relatedSchema = getSchemaBySlug(schemas, schemaField.schema);
507
+ const subQuery = {
508
+ get: {
509
+ [relatedSchema.slug]: {
510
+ with: value,
511
+ selecting: ["id"]
512
+ }
513
+ }
514
+ };
515
+ recordTarget = {
516
+ [RONIN_SCHEMA_SYMBOLS.QUERY]: subQuery
517
+ };
518
+ }
519
+ return composeConditions(
520
+ schemas,
521
+ schema,
522
+ statementValues,
523
+ instructionName,
524
+ recordTarget,
525
+ options
526
+ );
527
+ }
528
+ }
529
+ if (isNested) {
530
+ const conditions = Object.entries(value).map(([field, value2]) => {
531
+ const nestedFieldSlug = options.fieldSlug ? `${options.fieldSlug}.${field}` : field;
532
+ return composeConditions(schemas, schema, statementValues, instructionName, value2, {
533
+ ...options,
534
+ fieldSlug: nestedFieldSlug
535
+ });
536
+ });
537
+ const joiner = instructionName === "to" ? ", " : " AND ";
538
+ if (instructionName === "to") return `${conditions.join(joiner)}`;
539
+ return conditions.length === 1 ? conditions[0] : options.fieldSlug ? `(${conditions.join(joiner)})` : conditions.join(joiner);
540
+ }
541
+ if (Array.isArray(value)) {
542
+ const conditions = value.map(
543
+ (filter) => composeConditions(
544
+ schemas,
545
+ schema,
546
+ statementValues,
547
+ instructionName,
548
+ filter,
549
+ options
550
+ )
551
+ );
552
+ return conditions.join(" OR ");
553
+ }
554
+ throw new RoninError({
555
+ 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.`,
556
+ code: "INVALID_WITH_VALUE",
557
+ queries: null
558
+ });
559
+ };
560
+ var getMatcher = (value, negative) => {
561
+ if (negative) {
562
+ if (value === null) return "IS NOT";
563
+ return "!=";
564
+ }
565
+ if (value === null) return "IS";
566
+ return "=";
567
+ };
568
+ var formatIdentifiers = ({ identifiers }, queryInstructions) => {
569
+ if (!queryInstructions) return queryInstructions;
570
+ const type = "with" in queryInstructions ? "with" : null;
571
+ if (!type) return queryInstructions;
572
+ const nestedInstructions = queryInstructions[type];
573
+ if (!nestedInstructions || Array.isArray(nestedInstructions))
574
+ return queryInstructions;
575
+ const newNestedInstructions = { ...nestedInstructions };
576
+ for (const oldKey of Object.keys(newNestedInstructions)) {
577
+ if (oldKey !== "titleIdentifier" && oldKey !== "slugIdentifier") continue;
578
+ const identifierName = oldKey === "titleIdentifier" ? "title" : "slug";
579
+ const value = newNestedInstructions[oldKey];
580
+ const newKey = identifiers?.[identifierName] || "id";
581
+ newNestedInstructions[newKey] = value;
582
+ delete newNestedInstructions[oldKey];
583
+ }
584
+ return {
585
+ ...queryInstructions,
586
+ [type]: newNestedInstructions
587
+ };
588
+ };
589
+
590
+ // src/instructions/before-after.ts
591
+ var CURSOR_SEPARATOR = ",";
592
+ var CURSOR_NULL_PLACEHOLDER = "RONIN_NULL";
593
+ var handleBeforeOrAfter = (schema, statementValues, instructions, rootTable) => {
594
+ if (!(instructions.before || instructions.after)) {
595
+ throw new RoninError({
596
+ message: "The `before` or `after` instruction must not be empty.",
597
+ code: "MISSING_INSTRUCTION",
598
+ queries: null
599
+ });
600
+ }
601
+ if (instructions.before && instructions.after) {
602
+ throw new RoninError({
603
+ message: "The `before` and `after` instructions cannot co-exist. Choose one.",
604
+ code: "MUTUALLY_EXCLUSIVE_INSTRUCTIONS",
605
+ queries: null
606
+ });
607
+ }
608
+ const { ascending = [], descending = [] } = instructions.orderedBy || {};
609
+ const clause = instructions.with ? "AND " : "";
610
+ const chunks = (instructions.before || instructions.after).toString().split(CURSOR_SEPARATOR).map(decodeURIComponent);
611
+ const keys = [...ascending, ...descending];
612
+ const values = keys.map((key, index) => {
613
+ const value = chunks[index];
614
+ if (value === CURSOR_NULL_PLACEHOLDER) {
615
+ return "NULL";
616
+ }
617
+ const { field } = getFieldFromSchema(schema, key, "orderedBy");
618
+ if (field.type === "boolean") {
619
+ return prepareStatementValue(statementValues, value === "true");
620
+ }
621
+ if (field.type === "number") {
622
+ return prepareStatementValue(statementValues, Number.parseInt(value));
623
+ }
624
+ if (field.type === "date") {
625
+ return `'${new Date(Number.parseInt(value)).toJSON()}'`;
626
+ }
627
+ return prepareStatementValue(statementValues, value);
628
+ });
629
+ const compareOperators = [
630
+ // Reverse the comparison operators if we're querying for records before.
631
+ ...new Array(ascending.length).fill(instructions.before ? "<" : ">"),
632
+ ...new Array(descending.length).fill(instructions.before ? ">" : "<")
633
+ ];
634
+ const conditions = new Array();
635
+ for (let i = 0; i < keys.length; i++) {
636
+ if (values[i] === "NULL" && compareOperators[i] === "<") {
637
+ continue;
638
+ }
639
+ const condition = new Array();
640
+ for (let j = 0; j <= i; j++) {
641
+ const key = keys[j];
642
+ const value = values[j];
643
+ let { field, fieldSelector } = getFieldFromSchema(
644
+ schema,
645
+ key,
646
+ "orderedBy",
647
+ rootTable
648
+ );
649
+ if (j === i) {
650
+ const closingParentheses = ")".repeat(condition.length);
651
+ const operator = value === "NULL" ? "IS NOT" : compareOperators[j];
652
+ const caseInsensitiveStatement = value !== "NULL" && field.type === "string" ? " COLLATE NOCASE" : "";
653
+ if (value !== "NULL" && operator === "<" && !["ronin.createdAt", "ronin.updatedAt"].includes(key)) {
654
+ fieldSelector = `IFNULL(${fieldSelector}, -1e999)`;
655
+ }
656
+ condition.push(
657
+ `(${fieldSelector} ${operator} ${value}${caseInsensitiveStatement})${closingParentheses}`
658
+ );
659
+ } else {
660
+ const operator = value === "NULL" ? "IS" : "=";
661
+ condition.push(`(${fieldSelector} ${operator} ${value} AND`);
662
+ }
663
+ }
664
+ conditions.push(condition.join(" "));
665
+ }
666
+ return `${clause}(${conditions.join(" OR ")})`;
667
+ };
668
+
669
+ // src/instructions/for.ts
670
+ var handleFor = (schemas, schema, statementValues, instruction, rootTable) => {
671
+ let statement = "";
672
+ if (!instruction) return statement;
673
+ for (const shortcut in instruction) {
674
+ const args = instruction[shortcut];
675
+ const forFilter = schema.for?.[shortcut];
676
+ if (!forFilter) {
677
+ throw new RoninError({
678
+ message: `The provided \`for\` shortcut "${shortcut}" does not exist in schema "${getSchemaName(schema)}".`,
679
+ code: "INVALID_FOR_VALUE"
680
+ });
681
+ }
682
+ const replacedForFilter = structuredClone(forFilter);
683
+ replaceInObject(
684
+ replacedForFilter,
685
+ RONIN_SCHEMA_SYMBOLS.VALUE,
686
+ (match) => match.replace(RONIN_SCHEMA_SYMBOLS.VALUE, args)
687
+ );
688
+ const subStatement = composeConditions(
689
+ schemas,
690
+ schema,
691
+ statementValues,
692
+ "for",
693
+ replacedForFilter,
694
+ { rootTable }
695
+ );
696
+ statement += `(${subStatement})`;
697
+ }
698
+ return statement;
699
+ };
700
+
701
+ // src/instructions/including.ts
702
+ var handleIncluding = (schemas, statementValues, schema, instruction, rootTable) => {
703
+ let statement = "";
704
+ let rootTableSubQuery;
705
+ let rootTableName = rootTable;
706
+ for (const shortcut of instruction || []) {
707
+ const includingQuery = schema.including?.[shortcut];
708
+ if (!includingQuery) {
709
+ throw new RoninError({
710
+ message: `The provided \`including\` shortcut "${shortcut}" does not exist in schema "${getSchemaName(schema)}".`,
711
+ code: "INVALID_INCLUDING_VALUE"
712
+ });
713
+ }
714
+ const { queryType, querySchema, queryInstructions } = splitQuery(includingQuery);
715
+ let modifiableQueryInstructions = queryInstructions;
716
+ const relatedSchema = getSchemaBySlug(schemas, querySchema);
717
+ let joinType = "LEFT";
718
+ let relatedTableSelector = `"${getTableForSchema(relatedSchema)}"`;
719
+ const tableAlias = `including_${shortcut}`;
720
+ const single = querySchema !== relatedSchema.pluralSlug;
721
+ if (!modifiableQueryInstructions?.with) {
722
+ joinType = "CROSS";
723
+ if (single) {
724
+ if (!modifiableQueryInstructions) modifiableQueryInstructions = {};
725
+ modifiableQueryInstructions.limitedTo = 1;
726
+ }
727
+ }
728
+ if (modifiableQueryInstructions?.limitedTo || modifiableQueryInstructions?.orderedBy) {
729
+ const subSelect = compileQueryInput(
730
+ {
731
+ [queryType]: {
732
+ [querySchema]: modifiableQueryInstructions
733
+ }
734
+ },
735
+ schemas,
736
+ { statementValues }
737
+ );
738
+ relatedTableSelector = `(${subSelect.readStatement})`;
739
+ }
740
+ statement += `${joinType} JOIN ${relatedTableSelector} as ${tableAlias}`;
741
+ if (joinType === "LEFT") {
742
+ if (!single) {
743
+ rootTableSubQuery = `SELECT * FROM "${rootTable}" LIMIT 1`;
744
+ rootTableName = `sub_${rootTable}`;
745
+ }
746
+ const subStatement = composeConditions(
747
+ schemas,
748
+ relatedSchema,
749
+ statementValues,
750
+ "including",
751
+ queryInstructions?.with,
752
+ {
753
+ rootTable: rootTableName,
754
+ customTable: tableAlias
755
+ }
756
+ );
757
+ statement += ` ON (${subStatement})`;
758
+ }
759
+ }
760
+ return { statement, rootTableSubQuery, rootTableName };
761
+ };
762
+
763
+ // src/instructions/limited-to.ts
764
+ var handleLimitedTo = (single, instruction) => {
765
+ const pageSize = instruction || 100;
766
+ const finalPageSize = pageSize + 1;
767
+ return `LIMIT ${single ? "1" : finalPageSize} `;
768
+ };
769
+
770
+ // src/instructions/ordered-by.ts
771
+ var handleOrderedBy = (schema, instruction, rootTable) => {
772
+ let statement = "";
773
+ for (const field of instruction.ascending || []) {
774
+ const { field: schemaField, fieldSelector } = getFieldFromSchema(
775
+ schema,
776
+ field,
777
+ "orderedBy.ascending",
778
+ rootTable
779
+ );
780
+ if (statement.length > 0) {
781
+ statement += ", ";
782
+ }
783
+ const caseInsensitiveStatement = schemaField.type === "string" ? " COLLATE NOCASE" : "";
784
+ statement += `${fieldSelector}${caseInsensitiveStatement} ASC`;
785
+ }
786
+ for (const field of instruction.descending || []) {
787
+ const { field: schemaField, fieldSelector } = getFieldFromSchema(
788
+ schema,
789
+ field,
790
+ "orderedBy.descending",
791
+ rootTable
792
+ );
793
+ if (statement.length > 0) {
794
+ statement += ", ";
795
+ }
796
+ const caseInsensitiveStatement = schemaField.type === "string" ? " COLLATE NOCASE" : "";
797
+ statement += `${fieldSelector}${caseInsensitiveStatement} DESC`;
798
+ }
799
+ return `ORDER BY ${statement}`;
800
+ };
801
+
802
+ // src/instructions/selecting.ts
803
+ var handleSelecting = (schema, statementValues, instructions) => {
804
+ let statement = instructions.selecting ? instructions.selecting.map((slug) => {
805
+ return getFieldFromSchema(schema, slug, "selecting").fieldSelector;
806
+ }).join(", ") : "*";
807
+ if (isObject(instructions.including)) {
808
+ statement += ", ";
809
+ statement += Object.entries(
810
+ flatten(instructions.including)
811
+ ).filter(([_, value]) => {
812
+ return !(isObject(value) && Object.hasOwn(value, RONIN_SCHEMA_SYMBOLS.QUERY));
813
+ }).map(([key, value]) => {
814
+ return `${prepareStatementValue(statementValues, value)} as "${key}"`;
815
+ }).join(", ");
816
+ }
817
+ return statement;
818
+ };
819
+
820
+ // src/instructions/to.ts
821
+ var handleTo = (schemas, schema, statementValues, queryType, writeStatements, instructions, rootTable) => {
822
+ const currentTime = (/* @__PURE__ */ new Date()).toISOString();
823
+ const { with: withInstruction, to: toInstruction } = instructions;
824
+ const defaultFields = {};
825
+ if (queryType === "create") {
826
+ defaultFields.id = toInstruction.id || generateRecordId(schema.idPrefix);
827
+ }
828
+ defaultFields.ronin = {
829
+ // If records are being created, set their creation time.
830
+ ...queryType === "create" ? { createdAt: currentTime } : {},
831
+ // If records are being created or updated, set their update time.
832
+ updatedAt: currentTime,
833
+ // Allow for overwriting the default values provided above.
834
+ ...toInstruction.ronin
835
+ };
836
+ const hasSubQuery = Object.hasOwn(toInstruction, RONIN_SCHEMA_SYMBOLS.QUERY);
837
+ if (hasSubQuery) {
838
+ const subQuery = toInstruction[RONIN_SCHEMA_SYMBOLS.QUERY];
839
+ let { querySchema: subQuerySchemaSlug, queryInstructions: subQueryInstructions } = splitQuery(subQuery);
840
+ const subQuerySchema = getSchemaBySlug(schemas, subQuerySchemaSlug);
841
+ const subQuerySelectedFields = subQueryInstructions?.selecting;
842
+ const subQueryIncludedFields = subQueryInstructions?.including;
843
+ const subQueryFields = [
844
+ ...subQuerySelectedFields || (subQuerySchema.fields || []).map((field) => field.slug),
845
+ ...subQueryIncludedFields ? Object.keys(
846
+ flatten(subQueryIncludedFields || {})
847
+ ) : []
848
+ ];
849
+ for (const field of subQueryFields || []) {
850
+ getFieldFromSchema(schema, field, "to");
851
+ }
852
+ const defaultFieldsToAdd = subQuerySelectedFields ? Object.entries(flatten(defaultFields)).filter(([key]) => {
853
+ return !subQuerySelectedFields.includes(key);
854
+ }) : [];
855
+ if (defaultFieldsToAdd.length > 0) {
856
+ const defaultFieldsObject = expand(Object.fromEntries(defaultFieldsToAdd));
857
+ if (!subQueryInstructions) subQueryInstructions = {};
858
+ subQueryInstructions.including = {
859
+ ...defaultFieldsObject,
860
+ ...subQueryInstructions.including
861
+ };
862
+ }
863
+ return compileQueryInput(subQuery, schemas, {
864
+ statementValues
865
+ }).readStatement;
866
+ }
867
+ Object.assign(toInstruction, defaultFields);
868
+ for (const fieldSlug in toInstruction) {
869
+ const fieldValue = toInstruction[fieldSlug];
870
+ const fieldDetails = getFieldFromSchema(schema, fieldSlug, "to");
871
+ if (fieldDetails.field.type === "reference" && fieldDetails.field.kind === "many") {
872
+ delete toInstruction[fieldSlug];
873
+ const associativeSchemaSlug = composeAssociationSchemaSlug(
874
+ schema,
875
+ fieldDetails.field
876
+ );
877
+ const composeStatement = (subQueryType, value) => {
878
+ const origin = queryType === "create" ? { id: toInstruction.id } : withInstruction;
879
+ const recordDetails = { origin };
880
+ if (value) recordDetails.target = value;
881
+ const { readStatement } = compileQueryInput(
882
+ {
883
+ [subQueryType]: {
884
+ [associativeSchemaSlug]: subQueryType === "create" ? { to: recordDetails } : { with: recordDetails }
885
+ }
886
+ },
887
+ schemas,
888
+ { statementValues, disableReturning: true }
889
+ );
890
+ return readStatement;
891
+ };
892
+ if (Array.isArray(fieldValue)) {
893
+ writeStatements.push(composeStatement("drop"));
894
+ for (const record of fieldValue) {
895
+ writeStatements.push(composeStatement("create", record));
896
+ }
897
+ } else if (isObject(fieldValue)) {
898
+ for (const recordToAdd of fieldValue.containing || []) {
899
+ writeStatements.push(composeStatement("create", recordToAdd));
900
+ }
901
+ for (const recordToRemove of fieldValue.notContaining || []) {
902
+ writeStatements.push(composeStatement("drop", recordToRemove));
903
+ }
904
+ }
905
+ }
906
+ }
907
+ let statement = composeConditions(
908
+ schemas,
909
+ schema,
910
+ statementValues,
911
+ "to",
912
+ toInstruction,
913
+ {
914
+ rootTable,
915
+ type: queryType === "create" ? "fields" : void 0
916
+ }
917
+ );
918
+ if (queryType === "create") {
919
+ const deepStatement = composeConditions(
920
+ schemas,
921
+ schema,
922
+ statementValues,
923
+ "to",
924
+ toInstruction,
925
+ {
926
+ rootTable,
927
+ type: "values"
928
+ }
929
+ );
930
+ statement = `(${statement}) VALUES (${deepStatement})`;
931
+ } else if (queryType === "set") {
932
+ statement = `SET ${statement}`;
933
+ }
934
+ return statement;
935
+ };
936
+
937
+ // src/index.ts
938
+ var compileQueryInput = (query, defaultSchemas, options) => {
939
+ const parsedQuery = splitQuery(query);
940
+ const { queryType, querySchema, queryInstructions } = parsedQuery;
941
+ const schemas = addSystemSchemas(defaultSchemas);
942
+ const schema = getSchemaBySlug(schemas, querySchema);
943
+ const single = querySchema !== schema.pluralSlug;
944
+ let instructions = formatIdentifiers(schema, queryInstructions);
945
+ let table = getTableForSchema(schema);
946
+ const statementValues = options?.statementValues || [];
947
+ const writeStatements = [];
948
+ addSchemaQueries(parsedQuery, writeStatements);
949
+ const columns = handleSelecting(schema, statementValues, {
950
+ selecting: instructions?.selecting,
951
+ including: instructions?.including
952
+ });
953
+ let statement = "";
954
+ switch (queryType) {
955
+ case "get":
956
+ statement += `SELECT ${columns} FROM `;
957
+ break;
958
+ case "count":
959
+ statement += `SELECT COUNT(${columns}) FROM `;
960
+ break;
961
+ case "drop":
962
+ statement += "DELETE FROM ";
963
+ break;
964
+ case "create":
965
+ statement += "INSERT INTO ";
966
+ break;
967
+ case "set":
968
+ statement += "UPDATE ";
969
+ break;
970
+ }
971
+ const isJoining = typeof instructions?.including !== "undefined" && !isObject(instructions.including);
972
+ let isJoiningMultipleRows = false;
973
+ if (isJoining) {
974
+ const {
975
+ statement: including,
976
+ rootTableSubQuery,
977
+ rootTableName
978
+ } = handleIncluding(schemas, statementValues, schema, instructions?.including, table);
979
+ if (rootTableSubQuery && rootTableName) {
980
+ table = rootTableName;
981
+ statement += `(${rootTableSubQuery}) as ${rootTableName} `;
982
+ isJoiningMultipleRows = true;
983
+ } else {
984
+ statement += `"${table}" `;
985
+ }
986
+ statement += `${including} `;
987
+ } else {
988
+ statement += `"${table}" `;
989
+ }
990
+ if (queryType === "create" || queryType === "set") {
991
+ if (!isObject(instructions.to) || Object.keys(instructions.to).length === 0) {
992
+ throw new RoninError({
993
+ message: `When using a \`${queryType}\` query, the \`to\` instruction must be a non-empty object.`,
994
+ code: "INVALID_TO_VALUE",
995
+ queries: [query]
996
+ });
997
+ }
998
+ const toStatement = handleTo(
999
+ schemas,
1000
+ schema,
1001
+ statementValues,
1002
+ queryType,
1003
+ writeStatements,
1004
+ { with: instructions.with, to: instructions.to },
1005
+ isJoining ? table : void 0
1006
+ );
1007
+ statement += `${toStatement} `;
1008
+ }
1009
+ const conditions = [];
1010
+ if (queryType !== "create" && instructions && Object.hasOwn(instructions, "with")) {
1011
+ const withStatement = handleWith(
1012
+ schemas,
1013
+ schema,
1014
+ statementValues,
1015
+ instructions?.with,
1016
+ isJoining ? table : void 0
1017
+ );
1018
+ if (withStatement.length > 0) conditions.push(withStatement);
1019
+ }
1020
+ if (instructions && Object.hasOwn(instructions, "for")) {
1021
+ const forStatement = handleFor(
1022
+ schemas,
1023
+ schema,
1024
+ statementValues,
1025
+ instructions?.for,
1026
+ isJoining ? table : void 0
1027
+ );
1028
+ if (forStatement.length > 0) conditions.push(forStatement);
1029
+ }
1030
+ if ((queryType === "get" || queryType === "count") && !single) {
1031
+ instructions = instructions || {};
1032
+ instructions.orderedBy = instructions.orderedBy || {};
1033
+ instructions.orderedBy.ascending = instructions.orderedBy.ascending || [];
1034
+ instructions.orderedBy.descending = instructions.orderedBy.descending || [];
1035
+ if (![
1036
+ ...instructions.orderedBy.ascending,
1037
+ ...instructions.orderedBy.descending
1038
+ ].includes("ronin.createdAt")) {
1039
+ instructions.orderedBy.descending.push("ronin.createdAt");
1040
+ }
1041
+ }
1042
+ if (instructions && (Object.hasOwn(instructions, "before") || Object.hasOwn(instructions, "after"))) {
1043
+ if (single) {
1044
+ throw new RoninError({
1045
+ message: "The `before` and `after` instructions are not supported when querying for a single record.",
1046
+ code: "INVALID_BEFORE_OR_AFTER_INSTRUCTION",
1047
+ queries: [query]
1048
+ });
1049
+ }
1050
+ const beforeAndAfterStatement = handleBeforeOrAfter(
1051
+ schema,
1052
+ statementValues,
1053
+ {
1054
+ before: instructions.before,
1055
+ after: instructions.after,
1056
+ with: instructions.with,
1057
+ orderedBy: instructions.orderedBy
1058
+ },
1059
+ isJoining ? table : void 0
1060
+ );
1061
+ conditions.push(beforeAndAfterStatement);
1062
+ }
1063
+ if (conditions.length > 0) {
1064
+ if (conditions.length === 1) {
1065
+ statement += `WHERE ${conditions[0]} `;
1066
+ } else {
1067
+ statement += `WHERE (${conditions.join(" ")}) `;
1068
+ }
1069
+ }
1070
+ if (instructions?.orderedBy) {
1071
+ const orderedByStatement = handleOrderedBy(
1072
+ schema,
1073
+ instructions.orderedBy,
1074
+ isJoining ? table : void 0
1075
+ );
1076
+ statement += `${orderedByStatement} `;
1077
+ }
1078
+ if (queryType === "get" && !isJoiningMultipleRows) {
1079
+ statement += handleLimitedTo(single, instructions?.limitedTo);
1080
+ }
1081
+ if (["create", "set", "drop"].includes(queryType) && !options?.disableReturning) {
1082
+ statement += "RETURNING * ";
1083
+ }
1084
+ const finalStatement = statement.trimEnd();
1085
+ return {
1086
+ writeStatements,
1087
+ readStatement: finalStatement,
1088
+ values: statementValues
1089
+ };
1090
+ };
1091
+ export {
1092
+ compileQueryInput
1093
+ };