@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 CHANGED
@@ -49,7 +49,6 @@ const query = {
49
49
 
50
50
  const schemas = [
51
51
  {
52
- pluralSlug: 'accounts',
53
52
  slug: 'account',
54
53
  },
55
54
  ];
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>, 'pluralSlug'> & Pick<Schema, 'pluralSlug'>;
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: string;
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 replaceInObject = (obj, pattern, replacer) => {
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
- replaceInObject(value, pattern, replacer);
59
+ found = findInObject(value, pattern, replacer);
52
60
  } else if (typeof value === "string" && value.startsWith(pattern)) {
53
- obj[key] = value.replace(pattern, replacer);
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 tablePrefix = rootTable ? `"${rootTable}".` : "";
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 "${getSchemaName(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
- { slug: "schema", type: "reference", target: { pluralSlug: "schemas" } },
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: { pluralSlug: "schemas" } },
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((schema) => ({ ...schema }));
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.pluralSlug);
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.pluralSlug);
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.pluralSlug);
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 (!["schema", "schemas", "field", "fields"].includes(querySchema)) return;
579
+ if (!SYSTEM_SCHEMA_SLUGS.includes(querySchema)) return;
315
580
  const instructionName = mappedInstructions[queryType];
316
581
  const instructionList = queryInstructions[instructionName];
317
- const kind = ["schema", "schemas"].includes(querySchema) ? "schemas" : "fields";
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") tableAction = "CREATE";
325
- schemaPluralSlug = instructionTarget?.pluralSlug;
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") tableAction = "DROP";
337
- schemaPluralSlug = instructionTarget?.pluralSlug?.being || instructionTarget?.pluralSlug;
599
+ if (kind === "schemas" || kind === "indexes" || kind === "triggers") {
600
+ tableAction = "DROP";
601
+ }
338
602
  queryTypeReadable = "deleting";
339
603
  break;
340
604
  }
341
605
  }
342
- if (!schemaPluralSlug) {
343
- const field = kind === "schemas" ? "pluralSlug" : "schema.pluralSlug";
606
+ const slug = instructionList?.slug?.being || instructionList?.slug;
607
+ if (!slug) {
344
608
  throw new RoninError({
345
- message: `When ${queryTypeReadable} ${kind}, a \`${field}\` field must be provided in the \`${instructionName}\` instruction.`,
609
+ message: `When ${queryTypeReadable} ${kind}, a \`slug\` field must be provided in the \`${instructionName}\` instruction.`,
346
610
  code: "MISSING_FIELD",
347
- fields: [field]
611
+ fields: ["slug"]
348
612
  });
349
613
  }
350
- const table = convertToSnakeCase(schemaPluralSlug);
351
- const fields = [...SYSTEM_FIELDS];
352
- let statement = `${tableAction} TABLE "${table}"`;
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?.pluralSlug;
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
- } else if (kind === "fields") {
365
- const fieldSlug = instructionTarget?.slug?.being || instructionList?.slug;
366
- if (!fieldSlug) {
367
- throw new RoninError({
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 "${fieldSlug}" TO "${newSlug}"`;
711
+ statement += ` RENAME COLUMN "${slug}" TO "${newSlug}"`;
386
712
  }
387
713
  } else if (queryType === "drop") {
388
- statement += ` DROP COLUMN "${fieldSlug}"`;
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 composeFieldValues = (schemas, schema, statementValues, instructionName, value, options) => {
442
- const { field: schemaField, fieldSelector: selector } = getFieldFromSchema(
443
- schema,
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 composeConditions = (schemas, schema, statementValues, instructionName, value, options) => {
475
- const isNested = isObject(value) && Object.keys(value).length > 0;
476
- if (isNested && Object.keys(value).every((key) => key in WITH_CONDITIONS)) {
477
- const conditions = Object.entries(value).map(
478
- ([conditionType, checkValue]) => composeConditions(schemas, schema, statementValues, instructionName, checkValue, {
479
- ...options,
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 (isNested) {
536
- const conditions = Object.entries(value).map(([field, value2]) => {
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
- throw new RoninError({
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 "${getSchemaName(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
- replaceInObject(
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 "${getSchemaName(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.2.1",
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"