@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 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"