@ronin/compiler 0.13.10 → 0.13.11

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +1560 -1536
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -150,1659 +150,1679 @@ var splitQuery = (query) => {
150
150
  return { queryType, queryModel, queryInstructions };
151
151
  };
152
152
 
153
- // src/utils/statement.ts
154
- var replaceJSON = (key, value) => {
155
- if (key === QUERY_SYMBOLS.EXPRESSION) return value.replaceAll(`'`, `''`);
156
- return value;
153
+ // src/utils/pagination.ts
154
+ var CURSOR_SEPARATOR = ",";
155
+ var CURSOR_NULL_PLACEHOLDER = "RONIN_NULL";
156
+ var generatePaginationCursor = (model, orderedBy, record) => {
157
+ const { ascending = [], descending = [] } = orderedBy || {};
158
+ const keys = [...ascending, ...descending];
159
+ if (keys.length === 0) keys.push("ronin.createdAt");
160
+ const cursors = keys.map((fieldSlug) => {
161
+ const property = getProperty(record, fieldSlug);
162
+ if (property === null || property === void 0) return CURSOR_NULL_PLACEHOLDER;
163
+ const { field } = getFieldFromModel(model, fieldSlug, {
164
+ instructionName: "orderedBy"
165
+ });
166
+ if (field.type === "date") return new Date(property).getTime();
167
+ return property;
168
+ });
169
+ return cursors.map((cursor) => encodeURIComponent(String(cursor))).join(CURSOR_SEPARATOR);
157
170
  };
158
- var prepareStatementValue = (statementParams, value) => {
159
- if (value === null) return "NULL";
160
- if (!statementParams) {
161
- const valueString = typeof value === "object" ? `json('${JSON.stringify(value, replaceJSON)}')` : `'${value.toString()}'`;
162
- return valueString;
171
+
172
+ // src/instructions/before-after.ts
173
+ var handleBeforeOrAfter = (model, statementParams, instructions) => {
174
+ if (!(instructions.before || instructions.after)) {
175
+ throw new RoninError({
176
+ message: "The `before` or `after` instruction must not be empty.",
177
+ code: "MISSING_INSTRUCTION"
178
+ });
163
179
  }
164
- let formattedValue = value;
165
- if (Array.isArray(value) || isObject(value)) {
166
- formattedValue = JSON.stringify(value);
167
- } else if (typeof value === "boolean") {
168
- formattedValue = value ? 1 : 0;
180
+ if (instructions.before && instructions.after) {
181
+ throw new RoninError({
182
+ message: "The `before` and `after` instructions cannot co-exist. Choose one.",
183
+ code: "MUTUALLY_EXCLUSIVE_INSTRUCTIONS"
184
+ });
169
185
  }
170
- const index = statementParams.push(formattedValue);
171
- return `?${index}`;
172
- };
173
- var parseFieldExpression = (model, instructionName, expression, parentModel) => {
174
- return expression.replace(RONIN_MODEL_FIELD_REGEX, (match) => {
175
- let toReplace = QUERY_SYMBOLS.FIELD;
176
- let rootModel = model;
177
- if (match.startsWith(QUERY_SYMBOLS.FIELD_PARENT)) {
178
- rootModel = parentModel;
179
- toReplace = QUERY_SYMBOLS.FIELD_PARENT;
180
- if (match.startsWith(QUERY_SYMBOLS.FIELD_PARENT_OLD)) {
181
- rootModel.tableAlias = toReplace = QUERY_SYMBOLS.FIELD_PARENT_OLD;
182
- } else if (match.startsWith(QUERY_SYMBOLS.FIELD_PARENT_NEW)) {
183
- rootModel.tableAlias = toReplace = QUERY_SYMBOLS.FIELD_PARENT_NEW;
184
- }
186
+ if (!instructions.limitedTo) {
187
+ let message = "When providing a pagination cursor in the `before` or `after`";
188
+ message += " instruction, a `limitedTo` instruction must be provided as well, to";
189
+ message += " define the page size.";
190
+ throw new RoninError({
191
+ message,
192
+ code: "MISSING_INSTRUCTION"
193
+ });
194
+ }
195
+ const { ascending = [], descending = [] } = instructions.orderedBy || {};
196
+ const clause = instructions.with ? "AND " : "";
197
+ const chunks = (instructions.before || instructions.after).toString().split(CURSOR_SEPARATOR).map(decodeURIComponent);
198
+ const keys = [...ascending, ...descending];
199
+ const values = keys.map((key, index) => {
200
+ const value = chunks[index];
201
+ if (value === CURSOR_NULL_PLACEHOLDER) {
202
+ return "NULL";
185
203
  }
186
- const fieldSlug = match.replace(toReplace, "");
187
- const field = getFieldFromModel(rootModel, fieldSlug, { instructionName });
188
- return field.fieldSelector;
204
+ const { field } = getFieldFromModel(model, key, {
205
+ instructionName: "orderedBy"
206
+ });
207
+ if (field.type === "boolean") {
208
+ return prepareStatementValue(statementParams, value === "true");
209
+ }
210
+ if (field.type === "number") {
211
+ return prepareStatementValue(statementParams, Number.parseInt(value));
212
+ }
213
+ if (field.type === "date") {
214
+ return `'${new Date(Number.parseInt(value)).toJSON()}'`;
215
+ }
216
+ return prepareStatementValue(statementParams, value);
189
217
  });
190
- };
191
- var composeFieldValues = (models, model, statementParams, instructionName, value, options) => {
192
- const { fieldSelector: conditionSelector } = getFieldFromModel(
193
- model,
194
- options.fieldSlug,
195
- { instructionName }
196
- );
197
- const collectStatementValue = options.type !== "fields";
198
- const symbol = getSymbol(value);
199
- const syntax = WITH_CONDITIONS[options.condition || "being"](value);
200
- let conditionValue = syntax[1];
201
- if (symbol) {
202
- if (symbol?.type === "expression") {
203
- conditionValue = parseFieldExpression(
204
- model,
205
- instructionName,
206
- symbol.value,
207
- options.parentModel
208
- );
218
+ const compareOperators = [
219
+ // Reverse the comparison operators if we're querying for records before.
220
+ ...new Array(ascending.length).fill(instructions.before ? "<" : ">"),
221
+ ...new Array(descending.length).fill(instructions.before ? ">" : "<")
222
+ ];
223
+ const conditions = new Array();
224
+ for (let i = 0; i < keys.length; i++) {
225
+ if (values[i] === "NULL" && compareOperators[i] === "<") {
226
+ continue;
209
227
  }
210
- if (symbol.type === "query" && collectStatementValue) {
211
- conditionValue = `(${compileQueryInput(symbol.value, models, statementParams).main.statement})`;
228
+ const condition = new Array();
229
+ for (let j = 0; j <= i; j++) {
230
+ const key = keys[j];
231
+ const value = values[j];
232
+ let { field, fieldSelector } = getFieldFromModel(model, key, {
233
+ instructionName: "orderedBy"
234
+ });
235
+ if (j === i) {
236
+ const closingParentheses = ")".repeat(condition.length);
237
+ const operator = value === "NULL" ? "IS NOT" : compareOperators[j];
238
+ const caseInsensitiveStatement = value !== "NULL" && field.type === "string" ? " COLLATE NOCASE" : "";
239
+ if (value !== "NULL" && operator === "<" && !["ronin.createdAt", "ronin.updatedAt"].includes(key)) {
240
+ fieldSelector = `IFNULL(${fieldSelector}, -1e999)`;
241
+ }
242
+ condition.push(
243
+ `(${fieldSelector} ${operator} ${value}${caseInsensitiveStatement})${closingParentheses}`
244
+ );
245
+ } else {
246
+ const operator = value === "NULL" ? "IS" : "=";
247
+ condition.push(`(${fieldSelector} ${operator} ${value} AND`);
248
+ }
212
249
  }
213
- } else if (collectStatementValue) {
214
- conditionValue = prepareStatementValue(statementParams, conditionValue);
250
+ conditions.push(condition.join(" "));
215
251
  }
216
- if (options.type === "fields") return conditionSelector;
217
- if (options.type === "values") return conditionValue;
218
- return `${conditionSelector} ${syntax[0]} ${conditionValue}`;
252
+ return `${clause}(${conditions.join(" OR ")})`;
219
253
  };
220
- var composeConditions = (models, model, statementParams, instructionName, value, options) => {
221
- const isNested = isObject(value) && Object.keys(value).length > 0;
222
- if (isNested && Object.keys(value).every((key) => key in WITH_CONDITIONS)) {
223
- const conditions = Object.entries(value).map(
224
- ([conditionType, checkValue]) => composeConditions(models, model, statementParams, instructionName, checkValue, {
225
- ...options,
226
- condition: conditionType
227
- })
228
- );
229
- return conditions.join(" AND ");
230
- }
231
- if (options.fieldSlug) {
232
- const childField = model.fields.some(({ slug }) => {
233
- return slug.includes(".") && slug.split(".")[0] === options.fieldSlug;
234
- });
235
- if (!childField) {
236
- const fieldDetails = getFieldFromModel(model, options.fieldSlug, {
237
- instructionName
254
+
255
+ // src/instructions/for.ts
256
+ var handleFor = (model, instructions) => {
257
+ const normalizedFor = Array.isArray(instructions.for) ? Object.fromEntries(instructions.for.map((presetSlug) => [presetSlug, null])) : instructions.for;
258
+ for (const presetSlug in normalizedFor) {
259
+ if (!Object.hasOwn(normalizedFor, presetSlug)) continue;
260
+ const arg = normalizedFor[presetSlug];
261
+ const preset = model.presets?.find((preset2) => preset2.slug === presetSlug);
262
+ if (!preset) {
263
+ throw new RoninError({
264
+ message: `Preset "${presetSlug}" does not exist in model "${model.name}".`,
265
+ code: "PRESET_NOT_FOUND"
238
266
  });
239
- const { field: modelField } = fieldDetails || {};
240
- const consumeJSON = modelField?.type === "json" && instructionName === "to";
241
- if (modelField && !(isObject(value) || Array.isArray(value)) || getSymbol(value) || consumeJSON) {
242
- return composeFieldValues(
243
- models,
244
- model,
245
- statementParams,
246
- instructionName,
247
- value,
248
- { ...options, fieldSlug: options.fieldSlug }
249
- );
250
- }
251
- if (modelField?.type === "link" && isNested) {
252
- const keys = Object.keys(value);
253
- const values = Object.values(value);
254
- let recordTarget;
255
- if (keys.length === 1 && keys[0] === "id") {
256
- recordTarget = values[0];
257
- } else {
258
- const relatedModel = getModelBySlug(models, modelField.target);
259
- const subQuery = {
260
- get: {
261
- [relatedModel.slug]: {
262
- with: value,
263
- selecting: ["id"]
264
- }
265
- }
266
- };
267
- recordTarget = {
268
- [QUERY_SYMBOLS.QUERY]: subQuery
267
+ }
268
+ const replacedForFilter = structuredClone(preset.instructions);
269
+ if (arg !== null) {
270
+ findInObject(
271
+ replacedForFilter,
272
+ QUERY_SYMBOLS.VALUE,
273
+ (match) => match.replace(QUERY_SYMBOLS.VALUE, arg)
274
+ );
275
+ }
276
+ for (const subInstruction in replacedForFilter) {
277
+ if (!Object.hasOwn(replacedForFilter, subInstruction)) continue;
278
+ const instructionName = subInstruction;
279
+ const currentValue = instructions[instructionName];
280
+ if (currentValue) {
281
+ let newValue;
282
+ if (Array.isArray(currentValue)) {
283
+ newValue = [
284
+ ...replacedForFilter[instructionName],
285
+ ...currentValue
286
+ ];
287
+ } else if (isObject(currentValue)) {
288
+ newValue = {
289
+ ...replacedForFilter[instructionName],
290
+ ...currentValue
269
291
  };
270
292
  }
271
- return composeConditions(
272
- models,
273
- model,
274
- statementParams,
275
- instructionName,
276
- recordTarget,
277
- options
278
- );
293
+ Object.assign(instructions, { [instructionName]: newValue });
294
+ continue;
279
295
  }
296
+ Object.assign(instructions, {
297
+ [instructionName]: replacedForFilter[instructionName]
298
+ });
280
299
  }
281
300
  }
282
- if (isNested) {
283
- const conditions = Object.entries(value).map(([field, value2]) => {
284
- const nestedFieldSlug = options.fieldSlug ? `${options.fieldSlug}.${field}` : field;
285
- return composeConditions(models, model, statementParams, instructionName, value2, {
286
- ...options,
287
- fieldSlug: nestedFieldSlug
288
- });
289
- });
290
- const joiner = instructionName === "to" ? ", " : " AND ";
291
- if (instructionName === "to") return `${conditions.join(joiner)}`;
292
- return conditions.length === 1 ? conditions[0] : options.fieldSlug ? `(${conditions.join(joiner)})` : conditions.join(joiner);
293
- }
294
- if (Array.isArray(value)) {
295
- const conditions = value.map(
296
- (filter) => composeConditions(models, model, statementParams, instructionName, filter, options)
297
- );
298
- return conditions.join(" OR ");
299
- }
300
- throw new RoninError({
301
- 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.`,
302
- code: "INVALID_WITH_VALUE",
303
- queries: null
304
- });
301
+ return instructions;
305
302
  };
306
- var formatIdentifiers = ({ identifiers }, queryInstructions) => {
307
- if (!queryInstructions) return queryInstructions;
308
- const type = "with" in queryInstructions ? "with" : null;
309
- if (!type) return queryInstructions;
310
- const nestedInstructions = queryInstructions[type];
311
- if (!nestedInstructions || Array.isArray(nestedInstructions))
312
- return queryInstructions;
313
- const newNestedInstructions = { ...nestedInstructions };
314
- for (const oldKey of Object.keys(newNestedInstructions)) {
315
- if (oldKey !== "nameIdentifier" && oldKey !== "slugIdentifier") continue;
316
- const identifierName = oldKey === "nameIdentifier" ? "name" : "slug";
317
- const value = newNestedInstructions[oldKey];
318
- const newKey = identifiers[identifierName];
319
- newNestedInstructions[newKey] = value;
320
- delete newNestedInstructions[oldKey];
303
+
304
+ // src/instructions/including.ts
305
+ var handleIncluding = (models, model, statementParams, instruction) => {
306
+ let statement = "";
307
+ let tableSubQuery;
308
+ for (const ephemeralFieldSlug in instruction) {
309
+ if (!Object.hasOwn(instruction, ephemeralFieldSlug)) continue;
310
+ const symbol = getSymbol(instruction[ephemeralFieldSlug]);
311
+ if (symbol?.type !== "query") continue;
312
+ const { queryType, queryModel, queryInstructions } = splitQuery(symbol.value);
313
+ let modifiableQueryInstructions = queryInstructions;
314
+ const relatedModel = getModelBySlug(models, queryModel);
315
+ let joinType = "LEFT";
316
+ let relatedTableSelector = `"${relatedModel.table}"`;
317
+ const tableAlias = composeIncludedTableAlias(ephemeralFieldSlug);
318
+ const single = queryModel !== relatedModel.pluralSlug;
319
+ if (!modifiableQueryInstructions?.with) {
320
+ joinType = "CROSS";
321
+ if (single) {
322
+ if (!modifiableQueryInstructions) modifiableQueryInstructions = {};
323
+ modifiableQueryInstructions.limitedTo = 1;
324
+ }
325
+ }
326
+ if (modifiableQueryInstructions?.limitedTo || modifiableQueryInstructions?.orderedBy) {
327
+ const subSelect = compileQueryInput(
328
+ {
329
+ [queryType]: {
330
+ [queryModel]: modifiableQueryInstructions
331
+ }
332
+ },
333
+ models,
334
+ statementParams
335
+ );
336
+ relatedTableSelector = `(${subSelect.main.statement})`;
337
+ }
338
+ statement += `${joinType} JOIN ${relatedTableSelector} as ${tableAlias}`;
339
+ model.tableAlias = model.tableAlias || model.table;
340
+ if (joinType === "LEFT") {
341
+ const subStatement = composeConditions(
342
+ models,
343
+ { ...relatedModel, tableAlias },
344
+ statementParams,
345
+ "including",
346
+ queryInstructions?.with,
347
+ {
348
+ parentModel: model
349
+ }
350
+ );
351
+ statement += ` ON (${subStatement})`;
352
+ }
353
+ if (!single) tableSubQuery = `SELECT * FROM "${model.table}" LIMIT 1`;
321
354
  }
322
- return {
323
- ...queryInstructions,
324
- [type]: newNestedInstructions
325
- };
355
+ return { statement, tableSubQuery };
326
356
  };
327
357
 
328
- // src/instructions/with.ts
329
- var getMatcher = (value, negative) => {
330
- if (negative) {
331
- if (value === null) return "IS NOT";
332
- return "!=";
333
- }
334
- if (value === null) return "IS";
335
- return "=";
358
+ // src/instructions/limited-to.ts
359
+ var handleLimitedTo = (single, instruction) => {
360
+ let amount;
361
+ if (instruction) amount = instruction + 1;
362
+ if (single) amount = 1;
363
+ return `LIMIT ${amount} `;
336
364
  };
337
- var WITH_CONDITIONS = {
338
- being: (value) => [getMatcher(value, false), value],
339
- notBeing: (value) => [getMatcher(value, true), value],
340
- startingWith: (value) => ["LIKE", `${value}%`],
341
- notStartingWith: (value) => ["NOT LIKE", `${value}%`],
342
- endingWith: (value) => ["LIKE", `%${value}`],
343
- notEndingWith: (value) => ["NOT LIKE", `%${value}`],
344
- containing: (value) => ["LIKE", `%${value}%`],
345
- notContaining: (value) => ["NOT LIKE", `%${value}%`],
346
- greaterThan: (value) => [">", value],
347
- greaterOrEqual: (value) => [">=", value],
348
- lessThan: (value) => ["<", value],
349
- lessOrEqual: (value) => ["<=", value]
365
+
366
+ // src/instructions/ordered-by.ts
367
+ var handleOrderedBy = (model, instruction) => {
368
+ let statement = "";
369
+ const items = [
370
+ ...(instruction.ascending || []).map((value) => ({ value, order: "ASC" })),
371
+ ...(instruction.descending || []).map((value) => ({ value, order: "DESC" }))
372
+ ];
373
+ for (const item of items) {
374
+ if (statement.length > 0) {
375
+ statement += ", ";
376
+ }
377
+ const symbol = getSymbol(item.value);
378
+ const instructionName = item.order === "ASC" ? "orderedBy.ascending" : "orderedBy.descending";
379
+ if (symbol?.type === "expression") {
380
+ statement += `(${parseFieldExpression(model, instructionName, symbol.value)}) ${item.order}`;
381
+ continue;
382
+ }
383
+ const { field: modelField, fieldSelector } = getFieldFromModel(
384
+ model,
385
+ item.value,
386
+ { instructionName }
387
+ );
388
+ const caseInsensitiveStatement = modelField.type === "string" ? " COLLATE NOCASE" : "";
389
+ statement += `${fieldSelector}${caseInsensitiveStatement} ${item.order}`;
390
+ }
391
+ return `ORDER BY ${statement}`;
350
392
  };
351
- var handleWith = (models, model, statementParams, instruction, parentModel) => {
352
- const subStatement = composeConditions(
353
- models,
354
- model,
355
- statementParams,
356
- "with",
357
- instruction,
358
- { parentModel }
359
- );
360
- return `(${subStatement})`;
393
+
394
+ // src/instructions/selecting.ts
395
+ var handleSelecting = (models, model, statementParams, instructions, options) => {
396
+ let loadedFields = [];
397
+ let expandColumns = false;
398
+ let statement = "*";
399
+ let isJoining = false;
400
+ if (instructions.including) {
401
+ const flatObject = flatten(instructions.including);
402
+ instructions.including = {};
403
+ for (const [key, value] of Object.entries(flatObject)) {
404
+ const symbol = getSymbol(value);
405
+ if (symbol?.type === "query") {
406
+ const { queryModel, queryInstructions } = splitQuery(symbol.value);
407
+ const subQueryModel = getModelBySlug(models, queryModel);
408
+ isJoining = true;
409
+ expandColumns = Boolean(options?.expandColumns || queryInstructions?.selecting);
410
+ const tableAlias = composeIncludedTableAlias(key);
411
+ const single = queryModel !== subQueryModel.pluralSlug;
412
+ if (!single) {
413
+ model.tableAlias = `sub_${model.table}`;
414
+ }
415
+ const queryModelFields = queryInstructions?.selecting ? subQueryModel.fields.filter((field) => {
416
+ return queryInstructions.selecting?.includes(field.slug);
417
+ }) : (
418
+ // Exclude link fields with cardinality "many", since those don't exist as columns.
419
+ subQueryModel.fields.filter((field) => {
420
+ return !(field.type === "link" && field.kind === "many");
421
+ })
422
+ );
423
+ for (const field of queryModelFields) {
424
+ loadedFields.push({ ...field, parentField: key });
425
+ if (expandColumns) {
426
+ const newValue2 = parseFieldExpression(
427
+ { ...subQueryModel, tableAlias },
428
+ "including",
429
+ `${QUERY_SYMBOLS.FIELD}${field.slug}`
430
+ );
431
+ instructions.including[`${tableAlias}.${field.slug}`] = newValue2;
432
+ }
433
+ }
434
+ continue;
435
+ }
436
+ let newValue = value;
437
+ if (symbol?.type === "expression") {
438
+ newValue = `(${parseFieldExpression(model, "including", symbol.value)})`;
439
+ } else {
440
+ newValue = prepareStatementValue(statementParams, value);
441
+ }
442
+ instructions.including[key] = newValue;
443
+ loadedFields.push({
444
+ slug: key,
445
+ type: RAW_FIELD_TYPES.includes(typeof value) ? typeof value : "string"
446
+ });
447
+ }
448
+ }
449
+ if (expandColumns) {
450
+ instructions.selecting = model.fields.filter((field) => !(field.type === "link" && field.kind === "many")).map((field) => field.slug);
451
+ }
452
+ if (instructions.selecting) {
453
+ const usableModel = expandColumns ? { ...model, tableAlias: model.tableAlias || model.table } : model;
454
+ const selectedFields = [];
455
+ statement = instructions.selecting.map((slug) => {
456
+ const { field, fieldSelector } = getFieldFromModel(usableModel, slug, {
457
+ instructionName: "selecting"
458
+ });
459
+ selectedFields.push(field);
460
+ return fieldSelector;
461
+ }).join(", ");
462
+ loadedFields = [...selectedFields, ...loadedFields];
463
+ } else {
464
+ loadedFields = [
465
+ ...model.fields.filter(
466
+ (field) => !(field.type === "link" && field.kind === "many")
467
+ ),
468
+ ...loadedFields
469
+ ];
470
+ }
471
+ if (instructions.including && Object.keys(instructions.including).length > 0) {
472
+ statement += ", ";
473
+ statement += Object.entries(instructions.including).map(([key, value]) => `${value} as "${key}"`).join(", ");
474
+ }
475
+ return { columns: statement, isJoining, loadedFields };
361
476
  };
362
477
 
363
- // node_modules/title/dist/esm/lower-case.js
364
- var conjunctions = [
365
- "for",
366
- "and",
367
- "nor",
368
- "but",
369
- "or",
370
- "yet",
371
- "so"
372
- ];
373
- var articles = [
374
- "a",
375
- "an",
376
- "the"
377
- ];
378
- var prepositions = [
379
- "aboard",
380
- "about",
381
- "above",
382
- "across",
383
- "after",
384
- "against",
385
- "along",
386
- "amid",
387
- "among",
388
- "anti",
389
- "around",
390
- "as",
391
- "at",
392
- "before",
393
- "behind",
394
- "below",
395
- "beneath",
396
- "beside",
397
- "besides",
398
- "between",
399
- "beyond",
400
- "but",
401
- "by",
402
- "concerning",
403
- "considering",
404
- "despite",
405
- "down",
406
- "during",
407
- "except",
408
- "excepting",
409
- "excluding",
410
- "following",
411
- "for",
412
- "from",
413
- "in",
414
- "inside",
415
- "into",
416
- "like",
417
- "minus",
418
- "near",
419
- "of",
420
- "off",
421
- "on",
422
- "onto",
423
- "opposite",
424
- "over",
425
- "past",
426
- "per",
427
- "plus",
428
- "regarding",
429
- "round",
430
- "save",
431
- "since",
432
- "than",
433
- "through",
434
- "to",
435
- "toward",
436
- "towards",
437
- "under",
438
- "underneath",
439
- "unlike",
440
- "until",
441
- "up",
442
- "upon",
443
- "versus",
444
- "via",
445
- "with",
446
- "within",
447
- "without"
448
- ];
449
- var lowerCase = /* @__PURE__ */ new Set([
450
- ...conjunctions,
451
- ...articles,
452
- ...prepositions
453
- ]);
454
-
455
- // node_modules/title/dist/esm/specials.js
456
- var specials = [
457
- "ZEIT",
458
- "ZEIT Inc.",
459
- "Vercel",
460
- "Vercel Inc.",
461
- "CLI",
462
- "API",
463
- "HTTP",
464
- "HTTPS",
465
- "JSX",
466
- "DNS",
467
- "URL",
468
- "now.sh",
469
- "now.json",
470
- "vercel.app",
471
- "vercel.json",
472
- "CI",
473
- "CD",
474
- "CDN",
475
- "package.json",
476
- "package.lock",
477
- "yarn.lock",
478
- "GitHub",
479
- "GitLab",
480
- "CSS",
481
- "Sass",
482
- "JS",
483
- "JavaScript",
484
- "TypeScript",
485
- "HTML",
486
- "WordPress",
487
- "Next.js",
488
- "Node.js",
489
- "Webpack",
490
- "Docker",
491
- "Bash",
492
- "Kubernetes",
493
- "SWR",
494
- "TinaCMS",
495
- "UI",
496
- "UX",
497
- "TS",
498
- "TSX",
499
- "iPhone",
500
- "iPad",
501
- "watchOS",
502
- "iOS",
503
- "iPadOS",
504
- "macOS",
505
- "PHP",
506
- "composer.json",
507
- "composer.lock",
508
- "CMS",
509
- "SQL",
510
- "C",
511
- "C#",
512
- "GraphQL",
513
- "GraphiQL",
514
- "JWT",
515
- "JWTs"
516
- ];
517
-
518
- // node_modules/title/dist/esm/index.js
519
- var word = `[^\\s'\u2019\\(\\)!?;:"-]`;
520
- var regex = new RegExp(`(?:(?:(\\s?(?:^|[.\\(\\)!?;:"-])\\s*)(${word}))|(${word}))(${word}*[\u2019']*${word}*)`, "g");
521
- var convertToRegExp = (specials2) => specials2.map((s) => [new RegExp(`\\b${s}\\b`, "gi"), s]);
522
- function parseMatch(match) {
523
- const firstCharacter = match[0];
524
- if (/\s/.test(firstCharacter)) {
525
- return match.slice(1);
526
- }
527
- if (/[\(\)]/.test(firstCharacter)) {
528
- return null;
478
+ // src/instructions/to.ts
479
+ var handleTo = (models, model, statementParams, queryType, dependencyStatements, instructions, parentModel) => {
480
+ const { with: withInstruction, to: toInstruction } = instructions;
481
+ const defaultFields = {};
482
+ if (queryType === "set" || toInstruction.ronin) {
483
+ defaultFields.ronin = {
484
+ // If records are being updated, bump their update time.
485
+ ...queryType === "set" ? { updatedAt: CURRENT_TIME_EXPRESSION } : {},
486
+ // Allow for overwriting the default values provided above.
487
+ ...toInstruction.ronin
488
+ };
529
489
  }
530
- return match;
531
- }
532
- var src_default = (str, options = {}) => {
533
- str = str.toLowerCase().replace(regex, (m, lead = "", forced, lower, rest, offset, string) => {
534
- const isLastWord = m.length + offset >= string.length;
535
- const parsedMatch = parseMatch(m);
536
- if (!parsedMatch) {
537
- return m;
538
- }
539
- if (!forced) {
540
- const fullLower = lower + rest;
541
- if (lowerCase.has(fullLower) && !isLastWord) {
542
- return parsedMatch;
543
- }
490
+ const symbol = getSymbol(toInstruction);
491
+ if (symbol?.type === "query") {
492
+ const { queryModel: subQueryModelSlug, queryInstructions: subQueryInstructions } = splitQuery(symbol.value);
493
+ const subQueryModel = getModelBySlug(models, subQueryModelSlug);
494
+ if (subQueryInstructions?.selecting) {
495
+ const currentFields = new Set(subQueryInstructions.selecting);
496
+ currentFields.add("id");
497
+ subQueryInstructions.selecting = Array.from(currentFields);
544
498
  }
545
- return lead + (lower || forced).toUpperCase() + rest;
546
- });
547
- const customSpecials = options.special || [];
548
- const replace = [...specials, ...customSpecials];
549
- const replaceRegExp = convertToRegExp(replace);
550
- replaceRegExp.forEach(([pattern, s]) => {
551
- str = str.replace(pattern, s);
552
- });
553
- return str;
554
- };
555
-
556
- // src/utils/model.ts
557
- var getModelBySlug = (models, slug) => {
558
- const model = models.find((model2) => {
559
- return model2.slug === slug || model2.pluralSlug === slug;
560
- });
561
- if (!model) {
562
- throw new RoninError({
563
- message: `No matching model with either Slug or Plural Slug of "${slug}" could be found.`,
564
- code: "MODEL_NOT_FOUND"
565
- });
566
- }
567
- return model;
568
- };
569
- var composeAssociationModelSlug = (model, field) => convertToCamelCase(`ronin_link_${model.slug}_${field.slug}`);
570
- var getFieldSelector = (model, field, fieldPath, writing) => {
571
- const symbol = model.tableAlias?.startsWith(QUERY_SYMBOLS.FIELD_PARENT) ? `${model.tableAlias.replace(QUERY_SYMBOLS.FIELD_PARENT, "").slice(0, -1)}.` : "";
572
- const tablePrefix = symbol || (model.tableAlias ? `"${model.tableAlias}".` : "");
573
- if (field.type === "json" && !writing) {
574
- const dotParts = fieldPath.split(".");
575
- const columnName = tablePrefix + dotParts.shift();
576
- const jsonField = dotParts.join(".");
577
- return `json_extract(${columnName}, '$.${jsonField}')`;
578
- }
579
- return `${tablePrefix}"${fieldPath}"`;
580
- };
581
- function getFieldFromModel(model, fieldPath, source, shouldThrow = true) {
582
- const writingField = "instructionName" in source ? source.instructionName === "to" : true;
583
- const errorTarget = "instructionName" in source ? `\`${source.instructionName}\`` : `${source.modelEntityType} "${source.modelEntityName}"`;
584
- const errorPrefix = `Field "${fieldPath}" defined for ${errorTarget}`;
585
- const modelFields = model.fields || [];
586
- let modelField;
587
- if (fieldPath.includes(".")) {
588
- modelField = modelFields.find((field) => field.slug === fieldPath.split(".")[0]);
589
- if (modelField?.type === "json") {
590
- const fieldSelector2 = getFieldSelector(model, modelField, fieldPath, writingField);
591
- return { field: modelField, fieldSelector: fieldSelector2 };
499
+ const subQuerySelectedFields = subQueryInstructions?.selecting;
500
+ const subQueryIncludedFields = subQueryInstructions?.including;
501
+ const subQueryFields = [
502
+ ...subQuerySelectedFields || (subQueryModel.fields || []).map((field) => field.slug),
503
+ ...subQueryIncludedFields ? Object.keys(
504
+ flatten(subQueryIncludedFields || {})
505
+ ) : []
506
+ ];
507
+ for (const field of subQueryFields || []) {
508
+ getFieldFromModel(model, field, { instructionName: "to" });
592
509
  }
593
- }
594
- modelField = modelFields.find((field) => field.slug === fieldPath);
595
- if (!modelField) {
596
- if (shouldThrow) {
597
- throw new RoninError({
598
- message: `${errorPrefix} does not exist in model "${model.name}".`,
599
- code: "FIELD_NOT_FOUND",
600
- field: fieldPath,
601
- queries: null
510
+ let statement2 = "";
511
+ if (subQuerySelectedFields) {
512
+ const columns = subQueryFields.map((field) => {
513
+ return getFieldFromModel(model, field, { instructionName: "to" }).fieldSelector;
602
514
  });
515
+ statement2 = `(${columns.join(", ")}) `;
603
516
  }
604
- return null;
517
+ statement2 += compileQueryInput(symbol.value, models, statementParams).main.statement;
518
+ return statement2;
605
519
  }
606
- const fieldSelector = getFieldSelector(model, modelField, fieldPath, writingField);
607
- return { field: modelField, fieldSelector };
608
- }
609
- var slugToName = (slug) => {
610
- const name = slug.replace(/([a-z])([A-Z])/g, "$1 $2");
611
- return src_default(name);
612
- };
613
- var VOWELS = ["a", "e", "i", "o", "u"];
614
- var pluralize = (word2) => {
615
- const lastLetter = word2.slice(-1).toLowerCase();
616
- const secondLastLetter = word2.slice(-2, -1).toLowerCase();
617
- if (lastLetter === "y" && !VOWELS.includes(secondLastLetter)) {
618
- return `${word2.slice(0, -1)}ies`;
520
+ Object.assign(toInstruction, defaultFields);
521
+ for (const fieldSlug in toInstruction) {
522
+ if (!Object.hasOwn(toInstruction, fieldSlug)) continue;
523
+ const fieldValue = toInstruction[fieldSlug];
524
+ const fieldDetails = getFieldFromModel(
525
+ model,
526
+ fieldSlug,
527
+ { instructionName: "to" },
528
+ false
529
+ );
530
+ if (fieldDetails?.field.type === "link" && fieldDetails.field.kind === "many") {
531
+ delete toInstruction[fieldSlug];
532
+ const associativeModelSlug = composeAssociationModelSlug(model, fieldDetails.field);
533
+ const composeStatement = (subQueryType, value) => {
534
+ const source = queryType === "add" ? { id: toInstruction.id } : withInstruction;
535
+ const recordDetails = { source };
536
+ if (value) recordDetails.target = value;
537
+ return compileQueryInput(
538
+ {
539
+ [subQueryType]: {
540
+ [associativeModelSlug]: subQueryType === "add" ? { to: recordDetails } : { with: recordDetails }
541
+ }
542
+ },
543
+ models,
544
+ [],
545
+ { returning: false }
546
+ ).main;
547
+ };
548
+ if (Array.isArray(fieldValue)) {
549
+ dependencyStatements.push(composeStatement("remove"));
550
+ for (const record of fieldValue) {
551
+ dependencyStatements.push(composeStatement("add", record));
552
+ }
553
+ } else if (isObject(fieldValue)) {
554
+ const value = fieldValue;
555
+ for (const recordToAdd of value.containing || []) {
556
+ dependencyStatements.push(composeStatement("add", recordToAdd));
557
+ }
558
+ for (const recordToRemove of value.notContaining || []) {
559
+ dependencyStatements.push(composeStatement("remove", recordToRemove));
560
+ }
561
+ }
562
+ }
619
563
  }
620
- if (lastLetter === "s" || word2.slice(-2).toLowerCase() === "ch" || word2.slice(-2).toLowerCase() === "sh" || word2.slice(-2).toLowerCase() === "ex") {
621
- return `${word2}es`;
564
+ let statement = composeConditions(models, model, statementParams, "to", toInstruction, {
565
+ parentModel,
566
+ type: queryType === "add" ? "fields" : void 0
567
+ });
568
+ if (queryType === "add") {
569
+ const deepStatement = composeConditions(
570
+ models,
571
+ model,
572
+ statementParams,
573
+ "to",
574
+ toInstruction,
575
+ {
576
+ parentModel,
577
+ type: "values"
578
+ }
579
+ );
580
+ statement = `(${statement}) VALUES (${deepStatement})`;
581
+ } else if (queryType === "set") {
582
+ statement = `SET ${statement}`;
622
583
  }
623
- return `${word2}s`;
584
+ return statement;
624
585
  };
625
- var modelAttributes = [
626
- ["pluralSlug", "slug", pluralize],
627
- ["name", "slug", slugToName],
628
- ["pluralName", "pluralSlug", slugToName],
629
- ["idPrefix", "slug", (slug) => slug.slice(0, 3)],
630
- ["table", "pluralSlug", convertToSnakeCase]
631
- ];
632
- var addDefaultModelFields = (model, isNew) => {
633
- const copiedModel = { ...model };
634
- for (const [setting, base, generator] of modelAttributes) {
635
- if (copiedModel[setting] || !copiedModel[base]) continue;
636
- copiedModel[setting] = generator(copiedModel[base]);
586
+
587
+ // src/utils/index.ts
588
+ var compileQueryInput = (defaultQuery, models, statementParams, options) => {
589
+ const dependencyStatements = [];
590
+ const query = transformMetaQuery(
591
+ models,
592
+ dependencyStatements,
593
+ statementParams,
594
+ defaultQuery
595
+ );
596
+ if (query === null)
597
+ return { dependencies: [], main: dependencyStatements[0], loadedFields: [] };
598
+ const parsedQuery = splitQuery(query);
599
+ const { queryType, queryModel, queryInstructions } = parsedQuery;
600
+ const model = getModelBySlug(models, queryModel);
601
+ const single = queryModel !== model.pluralSlug;
602
+ let instructions = formatIdentifiers(model, queryInstructions);
603
+ const returning = options?.returning ?? true;
604
+ if (instructions && Object.hasOwn(instructions, "for")) {
605
+ instructions = handleFor(model, instructions);
637
606
  }
638
- const newFields = copiedModel.fields || [];
639
- if (isNew || newFields.length > 0) {
640
- if (!copiedModel.identifiers) copiedModel.identifiers = {};
641
- if (!copiedModel.identifiers.name) {
642
- const suitableField = newFields.find(
643
- (field) => field.type === "string" && field.required === true && ["name"].includes(field.slug)
644
- );
645
- copiedModel.identifiers.name = suitableField?.slug || "id";
607
+ const { columns, isJoining, loadedFields } = handleSelecting(
608
+ models,
609
+ model,
610
+ statementParams,
611
+ {
612
+ selecting: instructions?.selecting,
613
+ including: instructions?.including
614
+ },
615
+ options
616
+ );
617
+ let statement = "";
618
+ switch (queryType) {
619
+ case "get":
620
+ statement += `SELECT ${columns} FROM `;
621
+ break;
622
+ case "set":
623
+ statement += "UPDATE ";
624
+ break;
625
+ case "add":
626
+ statement += "INSERT INTO ";
627
+ break;
628
+ case "remove":
629
+ statement += "DELETE FROM ";
630
+ break;
631
+ case "count":
632
+ statement += `SELECT COUNT(${columns}) FROM `;
633
+ break;
634
+ }
635
+ let isJoiningMultipleRows = false;
636
+ if (isJoining) {
637
+ const { statement: including, tableSubQuery } = handleIncluding(
638
+ models,
639
+ model,
640
+ statementParams,
641
+ instructions?.including
642
+ );
643
+ if (tableSubQuery) {
644
+ statement += `(${tableSubQuery}) as ${model.tableAlias} `;
645
+ isJoiningMultipleRows = true;
646
+ } else {
647
+ statement += `"${model.table}" `;
646
648
  }
647
- if (!copiedModel.identifiers.slug) {
648
- const suitableField = newFields.find(
649
- (field) => field.type === "string" && field.unique === true && field.required === true && ["slug", "handle"].includes(field.slug)
650
- );
651
- copiedModel.identifiers.slug = suitableField?.slug || "id";
649
+ statement += `${including} `;
650
+ } else {
651
+ statement += `"${model.table}" `;
652
+ }
653
+ if (queryType === "add" || queryType === "set") {
654
+ if (!isObject(instructions.to) || Object.keys(instructions.to).length === 0) {
655
+ throw new RoninError({
656
+ message: `When using a \`${queryType}\` query, the \`to\` instruction must be a non-empty object.`,
657
+ code: "INVALID_TO_VALUE",
658
+ queries: [query]
659
+ });
652
660
  }
653
- copiedModel.fields = [...getSystemFields(copiedModel.idPrefix), ...newFields];
661
+ const toStatement = handleTo(
662
+ models,
663
+ model,
664
+ statementParams,
665
+ queryType,
666
+ dependencyStatements,
667
+ { with: instructions.with, to: instructions.to },
668
+ options?.parentModel
669
+ );
670
+ statement += `${toStatement} `;
654
671
  }
655
- return copiedModel;
656
- };
657
- var getSystemFields = (idPrefix = "rec") => [
658
- {
659
- name: "ID",
660
- type: "string",
661
- slug: "id",
662
- defaultValue: {
663
- // Since default values in SQLite cannot rely on other columns, we unfortunately
664
- // cannot rely on the `idPrefix` column here. Instead, we need to inject it directly
665
- // into the expression as a static string.
666
- [QUERY_SYMBOLS.EXPRESSION]: `'${idPrefix}_' || lower(substr(hex(randomblob(12)), 1, 16))`
672
+ const conditions = [];
673
+ if (queryType !== "add" && instructions && Object.hasOwn(instructions, "with")) {
674
+ const withStatement = handleWith(
675
+ models,
676
+ model,
677
+ statementParams,
678
+ instructions.with,
679
+ options?.parentModel
680
+ );
681
+ if (withStatement.length > 0) conditions.push(withStatement);
682
+ }
683
+ if ((queryType === "get" || queryType === "count") && !single && instructions?.limitedTo) {
684
+ instructions = instructions || {};
685
+ instructions.orderedBy = instructions.orderedBy || {};
686
+ instructions.orderedBy.ascending = instructions.orderedBy.ascending || [];
687
+ instructions.orderedBy.descending = instructions.orderedBy.descending || [];
688
+ if (![
689
+ ...instructions.orderedBy.ascending,
690
+ ...instructions.orderedBy.descending
691
+ ].includes("ronin.createdAt")) {
692
+ instructions.orderedBy.descending.push("ronin.createdAt");
667
693
  }
668
- },
669
- {
670
- name: "RONIN - Locked",
671
- type: "boolean",
672
- slug: "ronin.locked"
673
- },
674
- {
675
- name: "RONIN - Created At",
676
- type: "date",
677
- slug: "ronin.createdAt",
678
- defaultValue: CURRENT_TIME_EXPRESSION
679
- },
680
- {
681
- name: "RONIN - Created By",
682
- type: "string",
683
- slug: "ronin.createdBy"
684
- },
685
- {
686
- name: "RONIN - Updated At",
687
- type: "date",
688
- slug: "ronin.updatedAt",
689
- defaultValue: CURRENT_TIME_EXPRESSION
690
- },
691
- {
692
- name: "RONIN - Updated By",
693
- type: "string",
694
- slug: "ronin.updatedBy"
695
694
  }
696
- ];
697
- var ROOT_MODEL = {
698
- slug: "model",
699
- identifiers: {
700
- name: "name",
701
- slug: "slug"
702
- },
703
- // This name mimics the `sqlite_schema` table in SQLite.
704
- table: "ronin_schema",
705
- // Indicates that the model was automatically generated by RONIN.
706
- system: { model: "root" },
707
- fields: [
708
- { slug: "name", type: "string" },
709
- { slug: "pluralName", type: "string" },
710
- { slug: "slug", type: "string" },
711
- { slug: "pluralSlug", type: "string" },
712
- { slug: "idPrefix", type: "string" },
713
- { slug: "table", type: "string" },
714
- { slug: "identifiers.name", type: "string" },
715
- { slug: "identifiers.slug", type: "string" },
716
- // Providing an empty object as a default value allows us to use `json_insert`
717
- // without needing to fall back to an empty object in the insertion statement,
718
- // which makes the statement shorter.
719
- { slug: "fields", type: "json", defaultValue: "{}" },
720
- { slug: "indexes", type: "json", defaultValue: "{}" },
721
- { slug: "triggers", type: "json", defaultValue: "{}" },
722
- { slug: "presets", type: "json", defaultValue: "{}" }
723
- ]
724
- };
725
- var getSystemModels = (models, model) => {
726
- const addedModels = [];
727
- for (const field of model.fields || []) {
728
- if (field.type === "link" && !field.slug.startsWith("ronin.")) {
729
- const relatedModel = getModelBySlug(models, field.target);
730
- let fieldSlug = relatedModel.slug;
731
- if (field.kind === "many") {
732
- fieldSlug = composeAssociationModelSlug(model, field);
733
- addedModels.push({
734
- pluralSlug: fieldSlug,
735
- slug: fieldSlug,
736
- system: {
737
- model: model.slug,
738
- associationSlug: field.slug
739
- },
740
- fields: [
741
- {
742
- slug: "source",
743
- type: "link",
744
- target: model.slug
745
- },
746
- {
747
- slug: "target",
748
- type: "link",
749
- target: relatedModel.slug
750
- }
751
- ]
752
- });
753
- }
754
- }
755
- }
756
- return addedModels;
757
- };
758
- var addDefaultModelPresets = (list, model) => {
759
- const defaultPresets = [];
760
- for (const field of model.fields || []) {
761
- if (field.type === "link" && !field.slug.startsWith("ronin.")) {
762
- const relatedModel = getModelBySlug(list, field.target);
763
- if (field.kind === "many") continue;
764
- defaultPresets.push({
765
- instructions: {
766
- including: {
767
- [field.slug]: {
768
- [QUERY_SYMBOLS.QUERY]: {
769
- get: {
770
- [relatedModel.slug]: {
771
- with: {
772
- // Compare the `id` field of the related model to the link field on
773
- // the root model (`field.slug`).
774
- id: {
775
- [QUERY_SYMBOLS.EXPRESSION]: `${QUERY_SYMBOLS.FIELD_PARENT}${field.slug}`
776
- }
777
- }
778
- }
779
- }
780
- }
781
- }
782
- }
783
- },
784
- slug: field.slug
695
+ if (instructions && (Object.hasOwn(instructions, "before") || Object.hasOwn(instructions, "after"))) {
696
+ if (single) {
697
+ throw new RoninError({
698
+ message: "The `before` and `after` instructions are not supported when querying for a single record.",
699
+ code: "INVALID_BEFORE_OR_AFTER_INSTRUCTION",
700
+ queries: [query]
785
701
  });
786
702
  }
787
- }
788
- const childModels = list.map((subModel) => {
789
- const field = subModel.fields?.find((field2) => {
790
- return field2.type === "link" && field2.target === model.slug;
791
- });
792
- if (!field) return null;
793
- return { model: subModel, field };
794
- }).filter((match) => match !== null);
795
- for (const childMatch of childModels) {
796
- const { model: childModel, field: childField } = childMatch;
797
- const pluralSlug = childModel.pluralSlug;
798
- const presetSlug = childModel.system?.associationSlug || pluralSlug;
799
- defaultPresets.push({
800
- instructions: {
801
- including: {
802
- [presetSlug]: {
803
- [QUERY_SYMBOLS.QUERY]: {
804
- get: {
805
- [pluralSlug]: {
806
- with: {
807
- [childField.slug]: {
808
- [QUERY_SYMBOLS.EXPRESSION]: `${QUERY_SYMBOLS.FIELD_PARENT}id`
809
- }
810
- }
811
- }
812
- }
813
- }
814
- }
815
- }
816
- },
817
- slug: presetSlug
703
+ const beforeAndAfterStatement = handleBeforeOrAfter(model, statementParams, {
704
+ before: instructions.before,
705
+ after: instructions.after,
706
+ with: instructions.with,
707
+ orderedBy: instructions.orderedBy,
708
+ limitedTo: instructions.limitedTo
818
709
  });
710
+ conditions.push(beforeAndAfterStatement);
819
711
  }
820
- if (Object.keys(defaultPresets).length > 0) {
821
- model.presets = [...defaultPresets, ...model.presets || []];
822
- }
823
- return model;
824
- };
825
- var typesInSQLite = {
826
- link: "TEXT",
827
- string: "TEXT",
828
- date: "DATETIME",
829
- blob: "TEXT",
830
- boolean: "BOOLEAN",
831
- number: "INTEGER",
832
- json: "TEXT"
833
- };
834
- var getFieldStatement = (models, model, field) => {
835
- let statement = `"${field.slug}" ${typesInSQLite[field.type || "string"]}`;
836
- if (field.slug === "id") statement += " PRIMARY KEY";
837
- if (field.unique === true) statement += " UNIQUE";
838
- if (field.required === true) statement += " NOT NULL";
839
- if (typeof field.defaultValue !== "undefined") {
840
- const symbol = getSymbol(field.defaultValue);
841
- let value = typeof field.defaultValue === "string" ? `'${field.defaultValue}'` : field.defaultValue;
842
- if (symbol) value = `(${parseFieldExpression(model, "to", symbol.value)})`;
843
- statement += ` DEFAULT ${value}`;
712
+ if (conditions.length > 0) {
713
+ if (conditions.length === 1) {
714
+ statement += `WHERE ${conditions[0]} `;
715
+ } else {
716
+ statement += `WHERE (${conditions.join(" ")}) `;
717
+ }
844
718
  }
845
- if (field.type === "string" && field.collation) {
846
- statement += ` COLLATE ${field.collation}`;
719
+ if (instructions?.orderedBy) {
720
+ const orderedByStatement = handleOrderedBy(model, instructions.orderedBy);
721
+ statement += `${orderedByStatement} `;
847
722
  }
848
- if (field.type === "number" && field.increment === true) {
849
- statement += " AUTOINCREMENT";
723
+ if (queryType === "get" && !isJoiningMultipleRows && (single || instructions?.limitedTo)) {
724
+ statement += handleLimitedTo(single, instructions?.limitedTo);
850
725
  }
851
- if (typeof field.check !== "undefined") {
852
- const symbol = getSymbol(field.check);
853
- statement += ` CHECK (${parseFieldExpression(model, "to", symbol?.value)})`;
726
+ if (["add", "set", "remove"].includes(queryType) && returning) {
727
+ statement += "RETURNING * ";
854
728
  }
855
- if (typeof field.computedAs !== "undefined") {
856
- const { kind, value } = field.computedAs;
857
- const symbol = getSymbol(value);
858
- statement += ` GENERATED ALWAYS AS (${parseFieldExpression(model, "to", symbol?.value)}) ${kind}`;
729
+ const mainStatement = {
730
+ statement: statement.trimEnd(),
731
+ params: statementParams || []
732
+ };
733
+ if (returning) mainStatement.returning = true;
734
+ return {
735
+ dependencies: dependencyStatements,
736
+ main: mainStatement,
737
+ loadedFields
738
+ };
739
+ };
740
+
741
+ // src/utils/statement.ts
742
+ var replaceJSON = (key, value) => {
743
+ if (key === QUERY_SYMBOLS.EXPRESSION) return value.replaceAll(`'`, `''`);
744
+ return value;
745
+ };
746
+ var prepareStatementValue = (statementParams, value) => {
747
+ if (value === null) return "NULL";
748
+ if (!statementParams) {
749
+ const valueString = typeof value === "object" ? `json('${JSON.stringify(value, replaceJSON)}')` : `'${value.toString()}'`;
750
+ return valueString;
859
751
  }
860
- if (field.type === "link") {
861
- if (field.kind === "many") return null;
862
- const actions = field.actions || {};
863
- const modelList = models.some((item) => item.slug === model.slug) ? models : [...models, model];
864
- const targetTable = getModelBySlug(modelList, field.target).table;
865
- statement += ` REFERENCES ${targetTable}("id")`;
866
- for (const trigger in actions) {
867
- if (!Object.hasOwn(actions, trigger)) continue;
868
- const triggerName = trigger.toUpperCase().slice(2);
869
- const action = actions[trigger];
870
- statement += ` ON ${triggerName} ${action}`;
871
- }
752
+ let formattedValue = value;
753
+ if (Array.isArray(value) || isObject(value)) {
754
+ formattedValue = JSON.stringify(value);
755
+ } else if (typeof value === "boolean") {
756
+ formattedValue = value ? 1 : 0;
872
757
  }
873
- return statement;
874
- };
875
- var PLURAL_MODEL_ENTITIES = {
876
- field: "fields",
877
- index: "indexes",
878
- trigger: "triggers",
879
- preset: "presets"
758
+ const index = statementParams.push(formattedValue);
759
+ return `?${index}`;
880
760
  };
881
- var PLURAL_MODEL_ENTITIES_VALUES = Object.values(PLURAL_MODEL_ENTITIES);
882
- var formatModelEntity = (type, entities) => {
883
- const entries = entities?.map((entity) => {
884
- const { slug, ...rest } = "slug" in entity ? entity : { slug: `${type}Slug`, ...entity };
885
- return [slug, rest];
761
+ var parseFieldExpression = (model, instructionName, expression, parentModel) => {
762
+ return expression.replace(RONIN_MODEL_FIELD_REGEX, (match) => {
763
+ let toReplace = QUERY_SYMBOLS.FIELD;
764
+ let rootModel = model;
765
+ if (match.startsWith(QUERY_SYMBOLS.FIELD_PARENT)) {
766
+ rootModel = parentModel;
767
+ toReplace = QUERY_SYMBOLS.FIELD_PARENT;
768
+ if (match.startsWith(QUERY_SYMBOLS.FIELD_PARENT_OLD)) {
769
+ rootModel.tableAlias = toReplace = QUERY_SYMBOLS.FIELD_PARENT_OLD;
770
+ } else if (match.startsWith(QUERY_SYMBOLS.FIELD_PARENT_NEW)) {
771
+ rootModel.tableAlias = toReplace = QUERY_SYMBOLS.FIELD_PARENT_NEW;
772
+ }
773
+ }
774
+ const fieldSlug = match.replace(toReplace, "");
775
+ const field = getFieldFromModel(rootModel, fieldSlug, { instructionName });
776
+ return field.fieldSelector;
886
777
  });
887
- return entries ? Object.fromEntries(entries) : void 0;
888
- };
889
- var handleSystemModel = (models, dependencyStatements, action, systemModel, newModel) => {
890
- const { system: _, ...systemModelClean } = systemModel;
891
- const query = {
892
- [action]: { model: action === "create" ? systemModelClean : systemModelClean.slug }
893
- };
894
- if (action === "alter" && newModel && "alter" in query && query.alter) {
895
- const { system: _2, ...newModelClean } = newModel;
896
- query.alter.to = newModelClean;
897
- }
898
- const statement = compileQueryInput(query, models, []);
899
- dependencyStatements.push(...statement.dependencies);
900
778
  };
901
- var transformMetaQuery = (models, dependencyStatements, statementParams, query) => {
902
- const { queryType } = splitQuery(query);
903
- const subAltering = "alter" in query && query.alter && !("to" in query.alter);
904
- const action = subAltering && query.alter ? Object.keys(query.alter).filter((key) => key !== "model")[0] : queryType;
905
- const actionReadable = action === "create" ? "creating" : action === "alter" ? "altering" : "dropping";
906
- const entity = subAltering && query.alter ? Object.keys(query.alter[action])[0] : "model";
907
- let slug = entity === "model" && action === "create" ? null : query[queryType].model;
908
- let modelSlug = slug;
909
- let jsonValue;
910
- if ("create" in query && query.create) {
911
- const init = query.create.model;
912
- jsonValue = "to" in query.create ? { slug: init, ...query.create.to } : init;
913
- slug = modelSlug = jsonValue.slug;
914
- }
915
- if ("alter" in query && query.alter) {
916
- if ("to" in query.alter) {
917
- jsonValue = query.alter.to;
918
- } else {
919
- slug = query.alter[action][entity];
920
- if ("create" in query.alter) {
921
- const item = query.alter.create[entity];
922
- slug = item.slug || `${entity}Slug`;
923
- jsonValue = { slug, ...item };
924
- }
925
- if ("alter" in query.alter && query.alter.alter) jsonValue = query.alter.alter.to;
926
- }
927
- }
928
- if (!(modelSlug && slug)) return query;
929
- const model = action === "create" && entity === "model" ? null : getModelBySlug(models, modelSlug);
930
- if (entity === "model") {
931
- let queryTypeDetails = {};
932
- if (action === "create") {
933
- const newModel = jsonValue;
934
- const modelWithFields = addDefaultModelFields(newModel, true);
935
- const modelWithPresets = addDefaultModelPresets(
936
- [...models, modelWithFields],
937
- modelWithFields
938
- );
939
- modelWithPresets.fields = modelWithPresets.fields.map((field2) => ({
940
- ...field2,
941
- // Default field type.
942
- type: field2.type || "string",
943
- // Default field name.
944
- name: field2.name || slugToName(field2.slug)
945
- }));
946
- const columns = modelWithPresets.fields.map((field2) => getFieldStatement(models, modelWithPresets, field2)).filter(Boolean);
947
- const entities = Object.fromEntries(
948
- Object.entries(PLURAL_MODEL_ENTITIES).map(([type, pluralType2]) => {
949
- const list = modelWithPresets[pluralType2];
950
- return [pluralType2, formatModelEntity(type, list)];
951
- })
779
+ var composeFieldValues = (models, model, statementParams, instructionName, value, options) => {
780
+ const { fieldSelector: conditionSelector } = getFieldFromModel(
781
+ model,
782
+ options.fieldSlug,
783
+ { instructionName }
784
+ );
785
+ const collectStatementValue = options.type !== "fields";
786
+ const symbol = getSymbol(value);
787
+ const syntax = WITH_CONDITIONS[options.condition || "being"](value);
788
+ let conditionValue = syntax[1];
789
+ if (symbol) {
790
+ if (symbol?.type === "expression") {
791
+ conditionValue = parseFieldExpression(
792
+ model,
793
+ instructionName,
794
+ symbol.value,
795
+ options.parentModel
952
796
  );
953
- dependencyStatements.push({
954
- statement: `CREATE TABLE "${modelWithPresets.table}" (${columns.join(", ")})`,
955
- params: []
956
- });
957
- models.push(modelWithPresets);
958
- const modelWithObjects = Object.assign({}, modelWithPresets);
959
- for (const entity2 in entities) {
960
- if (!Object.hasOwn(entities, entity2)) continue;
961
- Object.defineProperty(modelWithObjects, entity2, { value: entities[entity2] });
962
- }
963
- queryTypeDetails = { to: modelWithObjects };
964
- getSystemModels(models, modelWithPresets).map((systemModel) => {
965
- return handleSystemModel(models, dependencyStatements, "create", systemModel);
966
- });
967
797
  }
968
- if (action === "alter" && model) {
969
- const newModel = jsonValue;
970
- const modelWithFields = addDefaultModelFields(newModel, false);
971
- const modelWithPresets = addDefaultModelPresets(models, modelWithFields);
972
- const newTableName = modelWithPresets.table;
973
- if (newTableName) {
974
- dependencyStatements.push({
975
- statement: `ALTER TABLE "${model.table}" RENAME TO "${newTableName}"`,
976
- params: []
977
- });
978
- }
979
- Object.assign(model, modelWithPresets);
980
- queryTypeDetails = {
981
- with: {
982
- slug
983
- },
984
- to: modelWithPresets
985
- };
986
- }
987
- if (action === "drop" && model) {
988
- models.splice(models.indexOf(model), 1);
989
- dependencyStatements.push({ statement: `DROP TABLE "${model.table}"`, params: [] });
990
- queryTypeDetails = { with: { slug } };
991
- models.filter(({ system }) => system?.model === model.slug).map((systemModel) => {
992
- return handleSystemModel(models, dependencyStatements, "drop", systemModel);
993
- });
798
+ if (symbol.type === "query" && collectStatementValue) {
799
+ conditionValue = `(${compileQueryInput(symbol.value, models, statementParams).main.statement})`;
994
800
  }
995
- const modelSlug2 = "to" in queryTypeDetails ? queryTypeDetails?.to?.slug : "with" in queryTypeDetails ? queryTypeDetails?.with?.slug : void 0;
996
- if (modelSlug2 === "model") return null;
997
- const queryTypeAction = action === "create" ? "add" : action === "alter" ? "set" : "remove";
998
- return {
999
- [queryTypeAction]: {
1000
- model: queryTypeDetails
1001
- }
1002
- };
801
+ } else if (collectStatementValue) {
802
+ conditionValue = prepareStatementValue(statementParams, conditionValue);
1003
803
  }
1004
- const modelBeforeUpdate = structuredClone(model);
1005
- const existingModel = model;
1006
- const pluralType = PLURAL_MODEL_ENTITIES[entity];
1007
- const targetEntityIndex = existingModel[pluralType]?.findIndex(
1008
- (entity2) => entity2.slug === slug
1009
- );
1010
- if ((action === "alter" || action === "drop") && (typeof targetEntityIndex === "undefined" || targetEntityIndex === -1)) {
1011
- throw new RoninError({
1012
- message: `No ${entity} with slug "${slug}" defined in model "${existingModel.name}".`,
1013
- code: MODEL_ENTITY_ERROR_CODES[entity]
1014
- });
804
+ if (options.type === "fields") return conditionSelector;
805
+ if (options.type === "values") return conditionValue;
806
+ return `${conditionSelector} ${syntax[0]} ${conditionValue}`;
807
+ };
808
+ var composeConditions = (models, model, statementParams, instructionName, value, options) => {
809
+ const isNested = isObject(value) && Object.keys(value).length > 0;
810
+ if (isNested && Object.keys(value).every((key) => key in WITH_CONDITIONS)) {
811
+ const conditions = Object.entries(value).map(
812
+ ([conditionType, checkValue]) => composeConditions(models, model, statementParams, instructionName, checkValue, {
813
+ ...options,
814
+ condition: conditionType
815
+ })
816
+ );
817
+ return conditions.join(" AND ");
1015
818
  }
1016
- const existingEntity = existingModel[pluralType]?.[targetEntityIndex];
1017
- if (action === "create" && existingEntity) {
1018
- throw new RoninError({
1019
- message: `A ${entity} with the slug "${slug}" already exists.`,
1020
- code: "EXISTING_MODEL_ENTITY",
1021
- fields: ["slug"]
819
+ if (options.fieldSlug) {
820
+ const childField = model.fields.some(({ slug }) => {
821
+ return slug.includes(".") && slug.split(".")[0] === options.fieldSlug;
1022
822
  });
1023
- }
1024
- if (entity === "field") {
1025
- const statement = `ALTER TABLE "${existingModel.table}"`;
1026
- const existingField = existingEntity;
1027
- const existingLinkField = existingField?.type === "link" && existingField.kind === "many";
1028
- if (action === "create") {
1029
- const field2 = jsonValue;
1030
- field2.type = field2.type || "string";
1031
- field2.name = field2.name || slugToName(field2.slug);
1032
- const fieldStatement = getFieldStatement(models, existingModel, field2);
1033
- if (fieldStatement) {
1034
- dependencyStatements.push({
1035
- statement: `${statement} ADD COLUMN ${fieldStatement}`,
1036
- params: []
1037
- });
1038
- }
1039
- } else if (action === "alter") {
1040
- const field2 = jsonValue;
1041
- const newSlug = field2.slug;
1042
- if (newSlug) {
1043
- field2.name = field2.name || slugToName(field2.slug);
1044
- if (!existingLinkField) {
1045
- dependencyStatements.push({
1046
- statement: `${statement} RENAME COLUMN "${slug}" TO "${newSlug}"`,
1047
- params: []
1048
- });
1049
- }
1050
- }
1051
- } else if (action === "drop" && !existingLinkField) {
1052
- const systemFields = getSystemFields(existingModel.idPrefix);
1053
- const isSystemField = systemFields.some((field2) => field2.slug === slug);
1054
- if (isSystemField) {
1055
- throw new RoninError({
1056
- message: `The ${entity} "${slug}" is a system ${entity} and cannot be removed.`,
1057
- code: "REQUIRED_MODEL_ENTITY"
1058
- });
1059
- }
1060
- dependencyStatements.push({
1061
- statement: `${statement} DROP COLUMN "${slug}"`,
1062
- params: []
1063
- });
1064
- }
1065
- }
1066
- const statementAction = action.toUpperCase();
1067
- if (entity === "index") {
1068
- const index = jsonValue;
1069
- const indexName = convertToSnakeCase(slug);
1070
- let statement = `${statementAction}${index?.unique ? " UNIQUE" : ""} INDEX "${indexName}"`;
1071
- if (action === "create") {
1072
- if (!Array.isArray(index.fields) || index.fields.length === 0) {
1073
- throw new RoninError({
1074
- message: `When ${actionReadable} ${PLURAL_MODEL_ENTITIES[entity]}, at least one field must be provided.`,
1075
- code: "INVALID_MODEL_VALUE",
1076
- fields: ["fields"]
1077
- });
1078
- }
1079
- const columns = index.fields.map((field2) => {
1080
- let fieldSelector = "";
1081
- if ("slug" in field2) {
1082
- ({ fieldSelector } = getFieldFromModel(existingModel, field2.slug, {
1083
- modelEntityType: "index",
1084
- modelEntityName: indexName
1085
- }));
1086
- } else if ("expression" in field2) {
1087
- fieldSelector = parseFieldExpression(existingModel, "to", field2.expression);
1088
- }
1089
- if (field2.collation) fieldSelector += ` COLLATE ${field2.collation}`;
1090
- if (field2.order) fieldSelector += ` ${field2.order}`;
1091
- return fieldSelector;
823
+ if (!childField) {
824
+ const fieldDetails = getFieldFromModel(model, options.fieldSlug, {
825
+ instructionName
1092
826
  });
1093
- statement += ` ON "${existingModel.table}" (${columns.join(", ")})`;
1094
- if (index.filter) {
1095
- const withStatement = handleWith(models, existingModel, null, index.filter);
1096
- statement += ` WHERE (${withStatement})`;
827
+ const { field: modelField } = fieldDetails || {};
828
+ const consumeJSON = modelField?.type === "json" && instructionName === "to";
829
+ if (modelField && !(isObject(value) || Array.isArray(value)) || getSymbol(value) || consumeJSON) {
830
+ return composeFieldValues(
831
+ models,
832
+ model,
833
+ statementParams,
834
+ instructionName,
835
+ value,
836
+ { ...options, fieldSlug: options.fieldSlug }
837
+ );
1097
838
  }
1098
- }
1099
- dependencyStatements.push({ statement, params: [] });
1100
- }
1101
- if (entity === "trigger") {
1102
- const triggerName = convertToSnakeCase(slug);
1103
- let statement = `${statementAction} TRIGGER "${triggerName}"`;
1104
- if (action === "create") {
1105
- const trigger = jsonValue;
1106
- const statementParts = [`${trigger.when} ${trigger.action}`];
1107
- if (trigger.fields) {
1108
- if (trigger.action !== "UPDATE") {
1109
- throw new RoninError({
1110
- message: `When ${actionReadable} ${PLURAL_MODEL_ENTITIES[entity]}, targeting specific fields requires the \`UPDATE\` action.`,
1111
- code: "INVALID_MODEL_VALUE",
1112
- fields: ["action"]
1113
- });
839
+ if (modelField?.type === "link" && isNested) {
840
+ const keys = Object.keys(value);
841
+ const values = Object.values(value);
842
+ let recordTarget;
843
+ if (keys.length === 1 && keys[0] === "id") {
844
+ recordTarget = values[0];
845
+ } else {
846
+ const relatedModel = getModelBySlug(models, modelField.target);
847
+ const subQuery = {
848
+ get: {
849
+ [relatedModel.slug]: {
850
+ with: value,
851
+ selecting: ["id"]
852
+ }
853
+ }
854
+ };
855
+ recordTarget = {
856
+ [QUERY_SYMBOLS.QUERY]: subQuery
857
+ };
1114
858
  }
1115
- const fieldSelectors = trigger.fields.map((field2) => {
1116
- return getFieldFromModel(existingModel, field2.slug, {
1117
- modelEntityType: "trigger",
1118
- modelEntityName: triggerName
1119
- }).fieldSelector;
1120
- });
1121
- statementParts.push(`OF (${fieldSelectors.join(", ")})`);
1122
- }
1123
- statementParts.push("ON", `"${existingModel.table}"`);
1124
- if (trigger.filter || trigger.effects.some((query2) => findInObject(query2, QUERY_SYMBOLS.FIELD))) {
1125
- statementParts.push("FOR EACH ROW");
1126
- }
1127
- if (trigger.filter) {
1128
- const tableAlias = trigger.action === "DELETE" ? QUERY_SYMBOLS.FIELD_PARENT_OLD : QUERY_SYMBOLS.FIELD_PARENT_NEW;
1129
- const withStatement = handleWith(
859
+ return composeConditions(
1130
860
  models,
1131
- { ...existingModel, tableAlias },
1132
- null,
1133
- trigger.filter
861
+ model,
862
+ statementParams,
863
+ instructionName,
864
+ recordTarget,
865
+ options
1134
866
  );
1135
- statementParts.push("WHEN", `(${withStatement})`);
1136
867
  }
1137
- const effectStatements = trigger.effects.map((effectQuery) => {
1138
- return compileQueryInput(effectQuery, models, null, {
1139
- returning: false,
1140
- parentModel: existingModel
1141
- }).main.statement;
1142
- });
1143
- statementParts.push("BEGIN");
1144
- statementParts.push(`${effectStatements.join("; ")};`);
1145
- statementParts.push("END");
1146
- statement += ` ${statementParts.join(" ")}`;
1147
868
  }
1148
- dependencyStatements.push({ statement, params: [] });
1149
869
  }
1150
- const field = `${QUERY_SYMBOLS.FIELD}${pluralType}`;
1151
- let json;
1152
- switch (action) {
1153
- case "create": {
1154
- const value = prepareStatementValue(statementParams, jsonValue);
1155
- json = `json_insert(${field}, '$.${slug}', ${value})`;
1156
- existingModel[pluralType] = [
1157
- ...existingModel[pluralType] || [],
1158
- jsonValue
1159
- ];
1160
- break;
1161
- }
1162
- case "alter": {
1163
- const value = prepareStatementValue(statementParams, jsonValue);
1164
- json = `json_set(${field}, '$.${slug}', json_patch(json_extract(${field}, '$.${slug}'), ${value}))`;
1165
- const targetEntity = existingModel[pluralType];
1166
- Object.assign(targetEntity[targetEntityIndex], jsonValue);
1167
- break;
1168
- }
1169
- case "drop": {
1170
- json = `json_remove(${field}, '$.${slug}')`;
1171
- const targetEntity = existingModel[pluralType];
1172
- targetEntity.splice(targetEntityIndex, 1);
1173
- }
870
+ if (isNested) {
871
+ const conditions = Object.entries(value).map(([field, value2]) => {
872
+ const nestedFieldSlug = options.fieldSlug ? `${options.fieldSlug}.${field}` : field;
873
+ return composeConditions(models, model, statementParams, instructionName, value2, {
874
+ ...options,
875
+ fieldSlug: nestedFieldSlug
876
+ });
877
+ });
878
+ const joiner = instructionName === "to" ? ", " : " AND ";
879
+ if (instructionName === "to") return `${conditions.join(joiner)}`;
880
+ return conditions.length === 1 ? conditions[0] : options.fieldSlug ? `(${conditions.join(joiner)})` : conditions.join(joiner);
881
+ }
882
+ if (Array.isArray(value)) {
883
+ const conditions = value.map(
884
+ (filter) => composeConditions(models, model, statementParams, instructionName, filter, options)
885
+ );
886
+ return conditions.join(" OR ");
887
+ }
888
+ throw new RoninError({
889
+ 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.`,
890
+ code: "INVALID_WITH_VALUE",
891
+ queries: null
892
+ });
893
+ };
894
+ var formatIdentifiers = ({ identifiers }, queryInstructions) => {
895
+ if (!queryInstructions) return queryInstructions;
896
+ const type = "with" in queryInstructions ? "with" : null;
897
+ if (!type) return queryInstructions;
898
+ const nestedInstructions = queryInstructions[type];
899
+ if (!nestedInstructions || Array.isArray(nestedInstructions))
900
+ return queryInstructions;
901
+ const newNestedInstructions = { ...nestedInstructions };
902
+ for (const oldKey of Object.keys(newNestedInstructions)) {
903
+ if (oldKey !== "nameIdentifier" && oldKey !== "slugIdentifier") continue;
904
+ const identifierName = oldKey === "nameIdentifier" ? "name" : "slug";
905
+ const value = newNestedInstructions[oldKey];
906
+ const newKey = identifiers[identifierName];
907
+ newNestedInstructions[newKey] = value;
908
+ delete newNestedInstructions[oldKey];
909
+ }
910
+ return {
911
+ ...queryInstructions,
912
+ [type]: newNestedInstructions
913
+ };
914
+ };
915
+
916
+ // src/instructions/with.ts
917
+ var getMatcher = (value, negative) => {
918
+ if (negative) {
919
+ if (value === null) return "IS NOT";
920
+ return "!=";
921
+ }
922
+ if (value === null) return "IS";
923
+ return "=";
924
+ };
925
+ var WITH_CONDITIONS = {
926
+ being: (value) => [getMatcher(value, false), value],
927
+ notBeing: (value) => [getMatcher(value, true), value],
928
+ startingWith: (value) => ["LIKE", `${value}%`],
929
+ notStartingWith: (value) => ["NOT LIKE", `${value}%`],
930
+ endingWith: (value) => ["LIKE", `%${value}`],
931
+ notEndingWith: (value) => ["NOT LIKE", `%${value}`],
932
+ containing: (value) => ["LIKE", `%${value}%`],
933
+ notContaining: (value) => ["NOT LIKE", `%${value}%`],
934
+ greaterThan: (value) => [">", value],
935
+ greaterOrEqual: (value) => [">=", value],
936
+ lessThan: (value) => ["<", value],
937
+ lessOrEqual: (value) => ["<=", value]
938
+ };
939
+ var handleWith = (models, model, statementParams, instruction, parentModel) => {
940
+ const subStatement = composeConditions(
941
+ models,
942
+ model,
943
+ statementParams,
944
+ "with",
945
+ instruction,
946
+ { parentModel }
947
+ );
948
+ return `(${subStatement})`;
949
+ };
950
+
951
+ // node_modules/title/dist/esm/lower-case.js
952
+ var conjunctions = [
953
+ "for",
954
+ "and",
955
+ "nor",
956
+ "but",
957
+ "or",
958
+ "yet",
959
+ "so"
960
+ ];
961
+ var articles = [
962
+ "a",
963
+ "an",
964
+ "the"
965
+ ];
966
+ var prepositions = [
967
+ "aboard",
968
+ "about",
969
+ "above",
970
+ "across",
971
+ "after",
972
+ "against",
973
+ "along",
974
+ "amid",
975
+ "among",
976
+ "anti",
977
+ "around",
978
+ "as",
979
+ "at",
980
+ "before",
981
+ "behind",
982
+ "below",
983
+ "beneath",
984
+ "beside",
985
+ "besides",
986
+ "between",
987
+ "beyond",
988
+ "but",
989
+ "by",
990
+ "concerning",
991
+ "considering",
992
+ "despite",
993
+ "down",
994
+ "during",
995
+ "except",
996
+ "excepting",
997
+ "excluding",
998
+ "following",
999
+ "for",
1000
+ "from",
1001
+ "in",
1002
+ "inside",
1003
+ "into",
1004
+ "like",
1005
+ "minus",
1006
+ "near",
1007
+ "of",
1008
+ "off",
1009
+ "on",
1010
+ "onto",
1011
+ "opposite",
1012
+ "over",
1013
+ "past",
1014
+ "per",
1015
+ "plus",
1016
+ "regarding",
1017
+ "round",
1018
+ "save",
1019
+ "since",
1020
+ "than",
1021
+ "through",
1022
+ "to",
1023
+ "toward",
1024
+ "towards",
1025
+ "under",
1026
+ "underneath",
1027
+ "unlike",
1028
+ "until",
1029
+ "up",
1030
+ "upon",
1031
+ "versus",
1032
+ "via",
1033
+ "with",
1034
+ "within",
1035
+ "without"
1036
+ ];
1037
+ var lowerCase = /* @__PURE__ */ new Set([
1038
+ ...conjunctions,
1039
+ ...articles,
1040
+ ...prepositions
1041
+ ]);
1042
+
1043
+ // node_modules/title/dist/esm/specials.js
1044
+ var specials = [
1045
+ "ZEIT",
1046
+ "ZEIT Inc.",
1047
+ "Vercel",
1048
+ "Vercel Inc.",
1049
+ "CLI",
1050
+ "API",
1051
+ "HTTP",
1052
+ "HTTPS",
1053
+ "JSX",
1054
+ "DNS",
1055
+ "URL",
1056
+ "now.sh",
1057
+ "now.json",
1058
+ "vercel.app",
1059
+ "vercel.json",
1060
+ "CI",
1061
+ "CD",
1062
+ "CDN",
1063
+ "package.json",
1064
+ "package.lock",
1065
+ "yarn.lock",
1066
+ "GitHub",
1067
+ "GitLab",
1068
+ "CSS",
1069
+ "Sass",
1070
+ "JS",
1071
+ "JavaScript",
1072
+ "TypeScript",
1073
+ "HTML",
1074
+ "WordPress",
1075
+ "Next.js",
1076
+ "Node.js",
1077
+ "Webpack",
1078
+ "Docker",
1079
+ "Bash",
1080
+ "Kubernetes",
1081
+ "SWR",
1082
+ "TinaCMS",
1083
+ "UI",
1084
+ "UX",
1085
+ "TS",
1086
+ "TSX",
1087
+ "iPhone",
1088
+ "iPad",
1089
+ "watchOS",
1090
+ "iOS",
1091
+ "iPadOS",
1092
+ "macOS",
1093
+ "PHP",
1094
+ "composer.json",
1095
+ "composer.lock",
1096
+ "CMS",
1097
+ "SQL",
1098
+ "C",
1099
+ "C#",
1100
+ "GraphQL",
1101
+ "GraphiQL",
1102
+ "JWT",
1103
+ "JWTs"
1104
+ ];
1105
+
1106
+ // node_modules/title/dist/esm/index.js
1107
+ var word = `[^\\s'\u2019\\(\\)!?;:"-]`;
1108
+ var regex = new RegExp(`(?:(?:(\\s?(?:^|[.\\(\\)!?;:"-])\\s*)(${word}))|(${word}))(${word}*[\u2019']*${word}*)`, "g");
1109
+ var convertToRegExp = (specials2) => specials2.map((s) => [new RegExp(`\\b${s}\\b`, "gi"), s]);
1110
+ function parseMatch(match) {
1111
+ const firstCharacter = match[0];
1112
+ if (/\s/.test(firstCharacter)) {
1113
+ return match.slice(1);
1114
+ }
1115
+ if (/[\(\)]/.test(firstCharacter)) {
1116
+ return null;
1174
1117
  }
1175
- const currentSystemModels = models.filter(({ system }) => {
1176
- return system?.model === existingModel.slug;
1177
- });
1178
- const newSystemModels = getSystemModels(models, existingModel);
1179
- const matchSystemModels = (oldSystemModel, newSystemModel) => {
1180
- const conditions = [
1181
- oldSystemModel.system?.model === newSystemModel.system?.model
1182
- ];
1183
- if (oldSystemModel.system?.associationSlug) {
1184
- const oldFieldIndex = modelBeforeUpdate?.fields.findIndex((item) => {
1185
- return item.slug === newSystemModel.system?.associationSlug;
1186
- });
1187
- const newFieldIndex = existingModel.fields.findIndex((item) => {
1188
- return item.slug === oldSystemModel.system?.associationSlug;
1189
- });
1190
- conditions.push(oldFieldIndex === newFieldIndex);
1191
- }
1192
- return conditions.every((condition) => condition === true);
1193
- };
1194
- for (const systemModel of currentSystemModels) {
1195
- const exists = newSystemModels.find(matchSystemModels.bind(null, systemModel));
1196
- if (exists) {
1197
- if (exists.slug !== systemModel.slug) {
1198
- handleSystemModel(models, dependencyStatements, "alter", systemModel, exists);
1199
- }
1200
- continue;
1118
+ return match;
1119
+ }
1120
+ var src_default = (str, options = {}) => {
1121
+ str = str.toLowerCase().replace(regex, (m, lead = "", forced, lower, rest, offset, string) => {
1122
+ const isLastWord = m.length + offset >= string.length;
1123
+ const parsedMatch = parseMatch(m);
1124
+ if (!parsedMatch) {
1125
+ return m;
1201
1126
  }
1202
- handleSystemModel(models, dependencyStatements, "drop", systemModel);
1203
- }
1204
- for (const systemModel of newSystemModels) {
1205
- const exists = currentSystemModels.find(matchSystemModels.bind(null, systemModel));
1206
- if (exists) continue;
1207
- handleSystemModel(models, dependencyStatements, "create", systemModel);
1208
- }
1209
- return {
1210
- set: {
1211
- model: {
1212
- with: { slug: modelSlug },
1213
- to: {
1214
- [pluralType]: { [QUERY_SYMBOLS.EXPRESSION]: json }
1215
- }
1127
+ if (!forced) {
1128
+ const fullLower = lower + rest;
1129
+ if (lowerCase.has(fullLower) && !isLastWord) {
1130
+ return parsedMatch;
1216
1131
  }
1217
1132
  }
1218
- };
1219
- };
1220
-
1221
- // src/utils/pagination.ts
1222
- var CURSOR_SEPARATOR = ",";
1223
- var CURSOR_NULL_PLACEHOLDER = "RONIN_NULL";
1224
- var generatePaginationCursor = (model, orderedBy, record) => {
1225
- const { ascending = [], descending = [] } = orderedBy || {};
1226
- const keys = [...ascending, ...descending];
1227
- if (keys.length === 0) keys.push("ronin.createdAt");
1228
- const cursors = keys.map((fieldSlug) => {
1229
- const property = getProperty(record, fieldSlug);
1230
- if (property === null || property === void 0) return CURSOR_NULL_PLACEHOLDER;
1231
- const { field } = getFieldFromModel(model, fieldSlug, {
1232
- instructionName: "orderedBy"
1233
- });
1234
- if (field.type === "date") return new Date(property).getTime();
1235
- return property;
1133
+ return lead + (lower || forced).toUpperCase() + rest;
1236
1134
  });
1237
- return cursors.map((cursor) => encodeURIComponent(String(cursor))).join(CURSOR_SEPARATOR);
1135
+ const customSpecials = options.special || [];
1136
+ const replace = [...specials, ...customSpecials];
1137
+ const replaceRegExp = convertToRegExp(replace);
1138
+ replaceRegExp.forEach(([pattern, s]) => {
1139
+ str = str.replace(pattern, s);
1140
+ });
1141
+ return str;
1238
1142
  };
1239
1143
 
1240
- // src/instructions/before-after.ts
1241
- var handleBeforeOrAfter = (model, statementParams, instructions) => {
1242
- if (!(instructions.before || instructions.after)) {
1243
- throw new RoninError({
1244
- message: "The `before` or `after` instruction must not be empty.",
1245
- code: "MISSING_INSTRUCTION"
1246
- });
1247
- }
1248
- if (instructions.before && instructions.after) {
1249
- throw new RoninError({
1250
- message: "The `before` and `after` instructions cannot co-exist. Choose one.",
1251
- code: "MUTUALLY_EXCLUSIVE_INSTRUCTIONS"
1252
- });
1253
- }
1254
- if (!instructions.limitedTo) {
1255
- let message = "When providing a pagination cursor in the `before` or `after`";
1256
- message += " instruction, a `limitedTo` instruction must be provided as well, to";
1257
- message += " define the page size.";
1258
- throw new RoninError({
1259
- message,
1260
- code: "MISSING_INSTRUCTION"
1261
- });
1144
+ // src/model/defaults.ts
1145
+ var slugToName = (slug) => {
1146
+ const name = slug.replace(/([a-z])([A-Z])/g, "$1 $2");
1147
+ return src_default(name);
1148
+ };
1149
+ var VOWELS = ["a", "e", "i", "o", "u"];
1150
+ var pluralize = (word2) => {
1151
+ const lastLetter = word2.slice(-1).toLowerCase();
1152
+ const secondLastLetter = word2.slice(-2, -1).toLowerCase();
1153
+ if (lastLetter === "y" && !VOWELS.includes(secondLastLetter)) {
1154
+ return `${word2.slice(0, -1)}ies`;
1262
1155
  }
1263
- const { ascending = [], descending = [] } = instructions.orderedBy || {};
1264
- const clause = instructions.with ? "AND " : "";
1265
- const chunks = (instructions.before || instructions.after).toString().split(CURSOR_SEPARATOR).map(decodeURIComponent);
1266
- const keys = [...ascending, ...descending];
1267
- const values = keys.map((key, index) => {
1268
- const value = chunks[index];
1269
- if (value === CURSOR_NULL_PLACEHOLDER) {
1270
- return "NULL";
1271
- }
1272
- const { field } = getFieldFromModel(model, key, {
1273
- instructionName: "orderedBy"
1274
- });
1275
- if (field.type === "boolean") {
1276
- return prepareStatementValue(statementParams, value === "true");
1277
- }
1278
- if (field.type === "number") {
1279
- return prepareStatementValue(statementParams, Number.parseInt(value));
1280
- }
1281
- if (field.type === "date") {
1282
- return `'${new Date(Number.parseInt(value)).toJSON()}'`;
1283
- }
1284
- return prepareStatementValue(statementParams, value);
1285
- });
1286
- const compareOperators = [
1287
- // Reverse the comparison operators if we're querying for records before.
1288
- ...new Array(ascending.length).fill(instructions.before ? "<" : ">"),
1289
- ...new Array(descending.length).fill(instructions.before ? ">" : "<")
1290
- ];
1291
- const conditions = new Array();
1292
- for (let i = 0; i < keys.length; i++) {
1293
- if (values[i] === "NULL" && compareOperators[i] === "<") {
1294
- continue;
1295
- }
1296
- const condition = new Array();
1297
- for (let j = 0; j <= i; j++) {
1298
- const key = keys[j];
1299
- const value = values[j];
1300
- let { field, fieldSelector } = getFieldFromModel(model, key, {
1301
- instructionName: "orderedBy"
1302
- });
1303
- if (j === i) {
1304
- const closingParentheses = ")".repeat(condition.length);
1305
- const operator = value === "NULL" ? "IS NOT" : compareOperators[j];
1306
- const caseInsensitiveStatement = value !== "NULL" && field.type === "string" ? " COLLATE NOCASE" : "";
1307
- if (value !== "NULL" && operator === "<" && !["ronin.createdAt", "ronin.updatedAt"].includes(key)) {
1308
- fieldSelector = `IFNULL(${fieldSelector}, -1e999)`;
1309
- }
1310
- condition.push(
1311
- `(${fieldSelector} ${operator} ${value}${caseInsensitiveStatement})${closingParentheses}`
1312
- );
1313
- } else {
1314
- const operator = value === "NULL" ? "IS" : "=";
1315
- condition.push(`(${fieldSelector} ${operator} ${value} AND`);
1316
- }
1317
- }
1318
- conditions.push(condition.join(" "));
1156
+ if (lastLetter === "s" || word2.slice(-2).toLowerCase() === "ch" || word2.slice(-2).toLowerCase() === "sh" || word2.slice(-2).toLowerCase() === "ex") {
1157
+ return `${word2}es`;
1319
1158
  }
1320
- return `${clause}(${conditions.join(" OR ")})`;
1159
+ return `${word2}s`;
1160
+ };
1161
+ var modelAttributes = [
1162
+ ["pluralSlug", "slug", pluralize],
1163
+ ["name", "slug", slugToName],
1164
+ ["pluralName", "pluralSlug", slugToName],
1165
+ ["idPrefix", "slug", (slug) => slug.slice(0, 3)],
1166
+ ["table", "pluralSlug", convertToSnakeCase]
1167
+ ];
1168
+ var getModelIdentifier = () => {
1169
+ return `mod_${Array.from(crypto.getRandomValues(new Uint8Array(12))).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16).toLowerCase()}`;
1321
1170
  };
1322
-
1323
- // src/instructions/for.ts
1324
- var handleFor = (model, instructions) => {
1325
- const normalizedFor = Array.isArray(instructions.for) ? Object.fromEntries(instructions.for.map((presetSlug) => [presetSlug, null])) : instructions.for;
1326
- for (const presetSlug in normalizedFor) {
1327
- if (!Object.hasOwn(normalizedFor, presetSlug)) continue;
1328
- const arg = normalizedFor[presetSlug];
1329
- const preset = model.presets?.find((preset2) => preset2.slug === presetSlug);
1330
- if (!preset) {
1331
- throw new RoninError({
1332
- message: `Preset "${presetSlug}" does not exist in model "${model.name}".`,
1333
- code: "PRESET_NOT_FOUND"
1334
- });
1335
- }
1336
- const replacedForFilter = structuredClone(preset.instructions);
1337
- if (arg !== null) {
1338
- findInObject(
1339
- replacedForFilter,
1340
- QUERY_SYMBOLS.VALUE,
1341
- (match) => match.replace(QUERY_SYMBOLS.VALUE, arg)
1171
+ var addDefaultModelAttributes = (model, isNew) => {
1172
+ const copiedModel = { ...model };
1173
+ if (isNew && !copiedModel.id) copiedModel.id = getModelIdentifier();
1174
+ for (const [setting, base, generator] of modelAttributes) {
1175
+ if (copiedModel[setting] || !copiedModel[base]) continue;
1176
+ copiedModel[setting] = generator(copiedModel[base]);
1177
+ }
1178
+ const newFields = copiedModel.fields || [];
1179
+ if (isNew || newFields.length > 0) {
1180
+ if (!copiedModel.identifiers) copiedModel.identifiers = {};
1181
+ if (!copiedModel.identifiers.name) {
1182
+ const suitableField = newFields.find(
1183
+ (field) => field.type === "string" && field.required === true && ["name"].includes(field.slug)
1342
1184
  );
1185
+ copiedModel.identifiers.name = suitableField?.slug || "id";
1343
1186
  }
1344
- for (const subInstruction in replacedForFilter) {
1345
- if (!Object.hasOwn(replacedForFilter, subInstruction)) continue;
1346
- const instructionName = subInstruction;
1347
- const currentValue = instructions[instructionName];
1348
- if (currentValue) {
1349
- let newValue;
1350
- if (Array.isArray(currentValue)) {
1351
- newValue = [
1352
- ...replacedForFilter[instructionName],
1353
- ...currentValue
1354
- ];
1355
- } else if (isObject(currentValue)) {
1356
- newValue = {
1357
- ...replacedForFilter[instructionName],
1358
- ...currentValue
1359
- };
1360
- }
1361
- Object.assign(instructions, { [instructionName]: newValue });
1362
- continue;
1363
- }
1364
- Object.assign(instructions, {
1365
- [instructionName]: replacedForFilter[instructionName]
1366
- });
1187
+ if (!copiedModel.identifiers.slug) {
1188
+ const suitableField = newFields.find(
1189
+ (field) => field.type === "string" && field.unique === true && field.required === true && ["slug", "handle"].includes(field.slug)
1190
+ );
1191
+ copiedModel.identifiers.slug = suitableField?.slug || "id";
1367
1192
  }
1368
1193
  }
1369
- return instructions;
1194
+ return copiedModel;
1370
1195
  };
1371
-
1372
- // src/instructions/including.ts
1373
- var handleIncluding = (models, model, statementParams, instruction) => {
1374
- let statement = "";
1375
- let tableSubQuery;
1376
- for (const ephemeralFieldSlug in instruction) {
1377
- if (!Object.hasOwn(instruction, ephemeralFieldSlug)) continue;
1378
- const symbol = getSymbol(instruction[ephemeralFieldSlug]);
1379
- if (symbol?.type !== "query") continue;
1380
- const { queryType, queryModel, queryInstructions } = splitQuery(symbol.value);
1381
- let modifiableQueryInstructions = queryInstructions;
1382
- const relatedModel = getModelBySlug(models, queryModel);
1383
- let joinType = "LEFT";
1384
- let relatedTableSelector = `"${relatedModel.table}"`;
1385
- const tableAlias = composeIncludedTableAlias(ephemeralFieldSlug);
1386
- const single = queryModel !== relatedModel.pluralSlug;
1387
- if (!modifiableQueryInstructions?.with) {
1388
- joinType = "CROSS";
1389
- if (single) {
1390
- if (!modifiableQueryInstructions) modifiableQueryInstructions = {};
1391
- modifiableQueryInstructions.limitedTo = 1;
1392
- }
1393
- }
1394
- if (modifiableQueryInstructions?.limitedTo || modifiableQueryInstructions?.orderedBy) {
1395
- const subSelect = compileQueryInput(
1396
- {
1397
- [queryType]: {
1398
- [queryModel]: modifiableQueryInstructions
1196
+ var addDefaultModelFields = (model, isNew) => {
1197
+ const copiedModel = { ...model };
1198
+ const newFields = copiedModel.fields || [];
1199
+ if (isNew || newFields.length > 0) {
1200
+ copiedModel.fields = [...getSystemFields(copiedModel.idPrefix), ...newFields];
1201
+ }
1202
+ return copiedModel;
1203
+ };
1204
+ var addDefaultModelPresets = (list, model) => {
1205
+ const defaultPresets = [];
1206
+ for (const field of model.fields || []) {
1207
+ if (field.type === "link" && !field.slug.startsWith("ronin.")) {
1208
+ const relatedModel = getModelBySlug(list, field.target);
1209
+ if (field.kind === "many") continue;
1210
+ defaultPresets.push({
1211
+ instructions: {
1212
+ including: {
1213
+ [field.slug]: {
1214
+ [QUERY_SYMBOLS.QUERY]: {
1215
+ get: {
1216
+ [relatedModel.slug]: {
1217
+ with: {
1218
+ // Compare the `id` field of the related model to the link field on
1219
+ // the root model (`field.slug`).
1220
+ id: {
1221
+ [QUERY_SYMBOLS.EXPRESSION]: `${QUERY_SYMBOLS.FIELD_PARENT}${field.slug}`
1222
+ }
1223
+ }
1224
+ }
1225
+ }
1226
+ }
1227
+ }
1399
1228
  }
1400
1229
  },
1401
- models,
1402
- statementParams
1403
- );
1404
- relatedTableSelector = `(${subSelect.main.statement})`;
1230
+ slug: field.slug
1231
+ });
1405
1232
  }
1406
- statement += `${joinType} JOIN ${relatedTableSelector} as ${tableAlias}`;
1407
- model.tableAlias = model.tableAlias || model.table;
1408
- if (joinType === "LEFT") {
1409
- const subStatement = composeConditions(
1410
- models,
1411
- { ...relatedModel, tableAlias },
1412
- statementParams,
1413
- "including",
1414
- queryInstructions?.with,
1415
- {
1416
- parentModel: model
1233
+ }
1234
+ const childModels = list.map((subModel) => {
1235
+ const field = subModel.fields?.find((field2) => {
1236
+ return field2.type === "link" && field2.target === model.slug;
1237
+ });
1238
+ if (!field) return null;
1239
+ return { model: subModel, field };
1240
+ }).filter((match) => match !== null);
1241
+ for (const childMatch of childModels) {
1242
+ const { model: childModel, field: childField } = childMatch;
1243
+ const pluralSlug = childModel.pluralSlug;
1244
+ const presetSlug = childModel.system?.associationSlug || pluralSlug;
1245
+ defaultPresets.push({
1246
+ instructions: {
1247
+ including: {
1248
+ [presetSlug]: {
1249
+ [QUERY_SYMBOLS.QUERY]: {
1250
+ get: {
1251
+ [pluralSlug]: {
1252
+ with: {
1253
+ [childField.slug]: {
1254
+ [QUERY_SYMBOLS.EXPRESSION]: `${QUERY_SYMBOLS.FIELD_PARENT}id`
1255
+ }
1256
+ }
1257
+ }
1258
+ }
1259
+ }
1260
+ }
1417
1261
  }
1418
- );
1419
- statement += ` ON (${subStatement})`;
1420
- }
1421
- if (!single) tableSubQuery = `SELECT * FROM "${model.table}" LIMIT 1`;
1262
+ },
1263
+ slug: presetSlug
1264
+ });
1422
1265
  }
1423
- return { statement, tableSubQuery };
1266
+ if (Object.keys(defaultPresets).length > 0) {
1267
+ model.presets = [...defaultPresets, ...model.presets || []];
1268
+ }
1269
+ return model;
1424
1270
  };
1425
1271
 
1426
- // src/instructions/limited-to.ts
1427
- var handleLimitedTo = (single, instruction) => {
1428
- let amount;
1429
- if (instruction) amount = instruction + 1;
1430
- if (single) amount = 1;
1431
- return `LIMIT ${amount} `;
1272
+ // src/model/index.ts
1273
+ var getModelBySlug = (models, slug) => {
1274
+ const model = models.find((model2) => {
1275
+ return model2.slug === slug || model2.pluralSlug === slug;
1276
+ });
1277
+ if (!model) {
1278
+ throw new RoninError({
1279
+ message: `No matching model with either Slug or Plural Slug of "${slug}" could be found.`,
1280
+ code: "MODEL_NOT_FOUND"
1281
+ });
1282
+ }
1283
+ return model;
1432
1284
  };
1433
-
1434
- // src/instructions/ordered-by.ts
1435
- var handleOrderedBy = (model, instruction) => {
1436
- let statement = "";
1437
- const items = [
1438
- ...(instruction.ascending || []).map((value) => ({ value, order: "ASC" })),
1439
- ...(instruction.descending || []).map((value) => ({ value, order: "DESC" }))
1440
- ];
1441
- for (const item of items) {
1442
- if (statement.length > 0) {
1443
- statement += ", ";
1285
+ var composeAssociationModelSlug = (model, field) => convertToCamelCase(`ronin_link_${model.slug}_${field.slug}`);
1286
+ var getFieldSelector = (model, field, fieldPath, writing) => {
1287
+ const symbol = model.tableAlias?.startsWith(QUERY_SYMBOLS.FIELD_PARENT) ? `${model.tableAlias.replace(QUERY_SYMBOLS.FIELD_PARENT, "").slice(0, -1)}.` : "";
1288
+ const tablePrefix = symbol || (model.tableAlias ? `"${model.tableAlias}".` : "");
1289
+ if (field.type === "json" && !writing) {
1290
+ const dotParts = fieldPath.split(".");
1291
+ const columnName = tablePrefix + dotParts.shift();
1292
+ const jsonField = dotParts.join(".");
1293
+ return `json_extract(${columnName}, '$.${jsonField}')`;
1294
+ }
1295
+ return `${tablePrefix}"${fieldPath}"`;
1296
+ };
1297
+ function getFieldFromModel(model, fieldPath, source, shouldThrow = true) {
1298
+ const writingField = "instructionName" in source ? source.instructionName === "to" : true;
1299
+ const errorTarget = "instructionName" in source ? `\`${source.instructionName}\`` : `${source.modelEntityType} "${source.modelEntityName}"`;
1300
+ const errorPrefix = `Field "${fieldPath}" defined for ${errorTarget}`;
1301
+ const modelFields = model.fields || [];
1302
+ let modelField;
1303
+ if (fieldPath.includes(".")) {
1304
+ modelField = modelFields.find((field) => field.slug === fieldPath.split(".")[0]);
1305
+ if (modelField?.type === "json") {
1306
+ const fieldSelector2 = getFieldSelector(model, modelField, fieldPath, writingField);
1307
+ return { field: modelField, fieldSelector: fieldSelector2 };
1444
1308
  }
1445
- const symbol = getSymbol(item.value);
1446
- const instructionName = item.order === "ASC" ? "orderedBy.ascending" : "orderedBy.descending";
1447
- if (symbol?.type === "expression") {
1448
- statement += `(${parseFieldExpression(model, instructionName, symbol.value)}) ${item.order}`;
1449
- continue;
1309
+ }
1310
+ modelField = modelFields.find((field) => field.slug === fieldPath);
1311
+ if (!modelField) {
1312
+ if (shouldThrow) {
1313
+ throw new RoninError({
1314
+ message: `${errorPrefix} does not exist in model "${model.name}".`,
1315
+ code: "FIELD_NOT_FOUND",
1316
+ field: fieldPath,
1317
+ queries: null
1318
+ });
1319
+ }
1320
+ return null;
1321
+ }
1322
+ const fieldSelector = getFieldSelector(model, modelField, fieldPath, writingField);
1323
+ return { field: modelField, fieldSelector };
1324
+ }
1325
+ var getSystemFields = (idPrefix = "rec") => [
1326
+ {
1327
+ name: "ID",
1328
+ type: "string",
1329
+ slug: "id",
1330
+ defaultValue: {
1331
+ // Since default values in SQLite cannot rely on other columns, we unfortunately
1332
+ // cannot rely on the `idPrefix` column here. Instead, we need to inject it directly
1333
+ // into the expression as a static string.
1334
+ [QUERY_SYMBOLS.EXPRESSION]: `'${idPrefix}_' || lower(substr(hex(randomblob(12)), 1, 16))`
1450
1335
  }
1451
- const { field: modelField, fieldSelector } = getFieldFromModel(
1452
- model,
1453
- item.value,
1454
- { instructionName }
1455
- );
1456
- const caseInsensitiveStatement = modelField.type === "string" ? " COLLATE NOCASE" : "";
1457
- statement += `${fieldSelector}${caseInsensitiveStatement} ${item.order}`;
1336
+ },
1337
+ {
1338
+ name: "RONIN - Locked",
1339
+ type: "boolean",
1340
+ slug: "ronin.locked"
1341
+ },
1342
+ {
1343
+ name: "RONIN - Created At",
1344
+ type: "date",
1345
+ slug: "ronin.createdAt",
1346
+ defaultValue: CURRENT_TIME_EXPRESSION
1347
+ },
1348
+ {
1349
+ name: "RONIN - Created By",
1350
+ type: "string",
1351
+ slug: "ronin.createdBy"
1352
+ },
1353
+ {
1354
+ name: "RONIN - Updated At",
1355
+ type: "date",
1356
+ slug: "ronin.updatedAt",
1357
+ defaultValue: CURRENT_TIME_EXPRESSION
1358
+ },
1359
+ {
1360
+ name: "RONIN - Updated By",
1361
+ type: "string",
1362
+ slug: "ronin.updatedBy"
1458
1363
  }
1459
- return `ORDER BY ${statement}`;
1364
+ ];
1365
+ var ROOT_MODEL = {
1366
+ slug: "model",
1367
+ identifiers: {
1368
+ name: "name",
1369
+ slug: "slug"
1370
+ },
1371
+ // This name mimics the `sqlite_schema` table in SQLite.
1372
+ table: "ronin_schema",
1373
+ // Indicates that the model was automatically generated by RONIN.
1374
+ system: { model: "root" },
1375
+ fields: [
1376
+ { slug: "name", type: "string" },
1377
+ { slug: "pluralName", type: "string" },
1378
+ { slug: "slug", type: "string" },
1379
+ { slug: "pluralSlug", type: "string" },
1380
+ { slug: "idPrefix", type: "string" },
1381
+ { slug: "table", type: "string" },
1382
+ { slug: "identifiers.name", type: "string" },
1383
+ { slug: "identifiers.slug", type: "string" },
1384
+ // Providing an empty object as a default value allows us to use `json_insert`
1385
+ // without needing to fall back to an empty object in the insertion statement,
1386
+ // which makes the statement shorter.
1387
+ { slug: "fields", type: "json", defaultValue: "{}" },
1388
+ { slug: "indexes", type: "json", defaultValue: "{}" },
1389
+ { slug: "triggers", type: "json", defaultValue: "{}" },
1390
+ { slug: "presets", type: "json", defaultValue: "{}" }
1391
+ ]
1460
1392
  };
1461
-
1462
- // src/instructions/selecting.ts
1463
- var handleSelecting = (models, model, statementParams, instructions, options) => {
1464
- let loadedFields = [];
1465
- let expandColumns = false;
1466
- let statement = "*";
1467
- let isJoining = false;
1468
- if (instructions.including) {
1469
- const flatObject = flatten(instructions.including);
1470
- instructions.including = {};
1471
- for (const [key, value] of Object.entries(flatObject)) {
1472
- const symbol = getSymbol(value);
1473
- if (symbol?.type === "query") {
1474
- const { queryModel, queryInstructions } = splitQuery(symbol.value);
1475
- const subQueryModel = getModelBySlug(models, queryModel);
1476
- isJoining = true;
1477
- expandColumns = Boolean(options?.expandColumns || queryInstructions?.selecting);
1478
- const tableAlias = composeIncludedTableAlias(key);
1479
- const single = queryModel !== subQueryModel.pluralSlug;
1480
- if (!single) {
1481
- model.tableAlias = `sub_${model.table}`;
1482
- }
1483
- const queryModelFields = queryInstructions?.selecting ? subQueryModel.fields.filter((field) => {
1484
- return queryInstructions.selecting?.includes(field.slug);
1485
- }) : (
1486
- // Exclude link fields with cardinality "many", since those don't exist as columns.
1487
- subQueryModel.fields.filter((field) => {
1488
- return !(field.type === "link" && field.kind === "many");
1489
- })
1490
- );
1491
- for (const field of queryModelFields) {
1492
- loadedFields.push({ ...field, parentField: key });
1493
- if (expandColumns) {
1494
- const newValue2 = parseFieldExpression(
1495
- { ...subQueryModel, tableAlias },
1496
- "including",
1497
- `${QUERY_SYMBOLS.FIELD}${field.slug}`
1498
- );
1499
- instructions.including[`${tableAlias}.${field.slug}`] = newValue2;
1500
- }
1501
- }
1502
- continue;
1503
- }
1504
- let newValue = value;
1505
- if (symbol?.type === "expression") {
1506
- newValue = `(${parseFieldExpression(model, "including", symbol.value)})`;
1507
- } else {
1508
- newValue = prepareStatementValue(statementParams, value);
1393
+ var getSystemModels = (models, model) => {
1394
+ const addedModels = [];
1395
+ for (const field of model.fields || []) {
1396
+ if (field.type === "link" && !field.slug.startsWith("ronin.")) {
1397
+ const relatedModel = getModelBySlug(models, field.target);
1398
+ let fieldSlug = relatedModel.slug;
1399
+ if (field.kind === "many") {
1400
+ fieldSlug = composeAssociationModelSlug(model, field);
1401
+ addedModels.push({
1402
+ pluralSlug: fieldSlug,
1403
+ slug: fieldSlug,
1404
+ system: {
1405
+ model: model.id,
1406
+ associationSlug: field.slug
1407
+ },
1408
+ fields: [
1409
+ {
1410
+ slug: "source",
1411
+ type: "link",
1412
+ target: model.slug
1413
+ },
1414
+ {
1415
+ slug: "target",
1416
+ type: "link",
1417
+ target: relatedModel.slug
1418
+ }
1419
+ ]
1420
+ });
1509
1421
  }
1510
- instructions.including[key] = newValue;
1511
- loadedFields.push({
1512
- slug: key,
1513
- type: RAW_FIELD_TYPES.includes(typeof value) ? typeof value : "string"
1514
- });
1515
1422
  }
1516
1423
  }
1517
- if (expandColumns) {
1518
- instructions.selecting = model.fields.filter((field) => !(field.type === "link" && field.kind === "many")).map((field) => field.slug);
1424
+ return addedModels.map((model2) => addDefaultModelAttributes(model2, true));
1425
+ };
1426
+ var typesInSQLite = {
1427
+ link: "TEXT",
1428
+ string: "TEXT",
1429
+ date: "DATETIME",
1430
+ blob: "TEXT",
1431
+ boolean: "BOOLEAN",
1432
+ number: "INTEGER",
1433
+ json: "TEXT"
1434
+ };
1435
+ var getFieldStatement = (models, model, field) => {
1436
+ let statement = `"${field.slug}" ${typesInSQLite[field.type || "string"]}`;
1437
+ if (field.slug === "id") statement += " PRIMARY KEY";
1438
+ if (field.unique === true) statement += " UNIQUE";
1439
+ if (field.required === true) statement += " NOT NULL";
1440
+ if (typeof field.defaultValue !== "undefined") {
1441
+ const symbol = getSymbol(field.defaultValue);
1442
+ let value = typeof field.defaultValue === "string" ? `'${field.defaultValue}'` : field.defaultValue;
1443
+ if (symbol) value = `(${parseFieldExpression(model, "to", symbol.value)})`;
1444
+ statement += ` DEFAULT ${value}`;
1519
1445
  }
1520
- if (instructions.selecting) {
1521
- const usableModel = expandColumns ? { ...model, tableAlias: model.tableAlias || model.table } : model;
1522
- const selectedFields = [];
1523
- statement = instructions.selecting.map((slug) => {
1524
- const { field, fieldSelector } = getFieldFromModel(usableModel, slug, {
1525
- instructionName: "selecting"
1526
- });
1527
- selectedFields.push(field);
1528
- return fieldSelector;
1529
- }).join(", ");
1530
- loadedFields = [...selectedFields, ...loadedFields];
1531
- } else {
1532
- loadedFields = [
1533
- ...model.fields.filter(
1534
- (field) => !(field.type === "link" && field.kind === "many")
1535
- ),
1536
- ...loadedFields
1537
- ];
1446
+ if (field.type === "string" && field.collation) {
1447
+ statement += ` COLLATE ${field.collation}`;
1538
1448
  }
1539
- if (instructions.including && Object.keys(instructions.including).length > 0) {
1540
- statement += ", ";
1541
- statement += Object.entries(instructions.including).map(([key, value]) => `${value} as "${key}"`).join(", ");
1449
+ if (field.type === "number" && field.increment === true) {
1450
+ statement += " AUTOINCREMENT";
1542
1451
  }
1543
- return { columns: statement, isJoining, loadedFields };
1544
- };
1545
-
1546
- // src/instructions/to.ts
1547
- var handleTo = (models, model, statementParams, queryType, dependencyStatements, instructions, parentModel) => {
1548
- const { with: withInstruction, to: toInstruction } = instructions;
1549
- const defaultFields = {};
1550
- if (queryType === "set" || toInstruction.ronin) {
1551
- defaultFields.ronin = {
1552
- // If records are being updated, bump their update time.
1553
- ...queryType === "set" ? { updatedAt: CURRENT_TIME_EXPRESSION } : {},
1554
- // Allow for overwriting the default values provided above.
1555
- ...toInstruction.ronin
1556
- };
1452
+ if (typeof field.check !== "undefined") {
1453
+ const symbol = getSymbol(field.check);
1454
+ statement += ` CHECK (${parseFieldExpression(model, "to", symbol?.value)})`;
1557
1455
  }
1558
- const symbol = getSymbol(toInstruction);
1559
- if (symbol?.type === "query") {
1560
- const { queryModel: subQueryModelSlug, queryInstructions: subQueryInstructions } = splitQuery(symbol.value);
1561
- const subQueryModel = getModelBySlug(models, subQueryModelSlug);
1562
- if (subQueryInstructions?.selecting) {
1563
- const currentFields = new Set(subQueryInstructions.selecting);
1564
- currentFields.add("id");
1565
- subQueryInstructions.selecting = Array.from(currentFields);
1456
+ if (typeof field.computedAs !== "undefined") {
1457
+ const { kind, value } = field.computedAs;
1458
+ const symbol = getSymbol(value);
1459
+ statement += ` GENERATED ALWAYS AS (${parseFieldExpression(model, "to", symbol?.value)}) ${kind}`;
1460
+ }
1461
+ if (field.type === "link") {
1462
+ if (field.kind === "many") return null;
1463
+ const actions = field.actions || {};
1464
+ const modelList = models.some((item) => item.slug === model.slug) ? models : [...models, model];
1465
+ const targetTable = getModelBySlug(modelList, field.target).table;
1466
+ statement += ` REFERENCES ${targetTable}("id")`;
1467
+ for (const trigger in actions) {
1468
+ if (!Object.hasOwn(actions, trigger)) continue;
1469
+ const triggerName = trigger.toUpperCase().slice(2);
1470
+ const action = actions[trigger];
1471
+ statement += ` ON ${triggerName} ${action}`;
1566
1472
  }
1567
- const subQuerySelectedFields = subQueryInstructions?.selecting;
1568
- const subQueryIncludedFields = subQueryInstructions?.including;
1569
- const subQueryFields = [
1570
- ...subQuerySelectedFields || (subQueryModel.fields || []).map((field) => field.slug),
1571
- ...subQueryIncludedFields ? Object.keys(
1572
- flatten(subQueryIncludedFields || {})
1573
- ) : []
1473
+ }
1474
+ return statement;
1475
+ };
1476
+ var PLURAL_MODEL_ENTITIES = {
1477
+ field: "fields",
1478
+ index: "indexes",
1479
+ trigger: "triggers",
1480
+ preset: "presets"
1481
+ };
1482
+ var PLURAL_MODEL_ENTITIES_VALUES = Object.values(PLURAL_MODEL_ENTITIES);
1483
+ var formatModelEntity = (type, entities) => {
1484
+ const entries = entities?.map((entity) => {
1485
+ const { slug, ...rest } = "slug" in entity ? entity : { slug: `${type}Slug`, ...entity };
1486
+ return [slug, rest];
1487
+ });
1488
+ return entries ? Object.fromEntries(entries) : void 0;
1489
+ };
1490
+ var handleSystemModel = (models, dependencyStatements, action, systemModel, newModel) => {
1491
+ const { system: _, ...systemModelClean } = systemModel;
1492
+ const query = {
1493
+ [action]: { model: action === "create" ? systemModelClean : systemModelClean.slug }
1494
+ };
1495
+ if (action === "alter" && newModel && "alter" in query && query.alter) {
1496
+ const { system: _2, ...newModelClean } = newModel;
1497
+ query.alter.to = newModelClean;
1498
+ }
1499
+ const statement = compileQueryInput(query, models, []);
1500
+ dependencyStatements.push(...statement.dependencies);
1501
+ };
1502
+ var handleSystemModels = (models, dependencyStatements, previousModel, newModel) => {
1503
+ const currentSystemModels = models.filter(({ system }) => {
1504
+ return system?.model === newModel.id;
1505
+ });
1506
+ const newSystemModels = getSystemModels(models, newModel);
1507
+ const matchSystemModels = (oldSystemModel, newSystemModel) => {
1508
+ const conditions = [
1509
+ oldSystemModel.system?.model === newSystemModel.system?.model
1574
1510
  ];
1575
- for (const field of subQueryFields || []) {
1576
- getFieldFromModel(model, field, { instructionName: "to" });
1577
- }
1578
- let statement2 = "";
1579
- if (subQuerySelectedFields) {
1580
- const columns = subQueryFields.map((field) => {
1581
- return getFieldFromModel(model, field, { instructionName: "to" }).fieldSelector;
1511
+ if (oldSystemModel.system?.associationSlug) {
1512
+ const oldFieldIndex = previousModel.fields.findIndex((item) => {
1513
+ return item.slug === newSystemModel.system?.associationSlug;
1582
1514
  });
1583
- statement2 = `(${columns.join(", ")}) `;
1515
+ const newFieldIndex = newModel.fields.findIndex((item) => {
1516
+ return item.slug === oldSystemModel.system?.associationSlug;
1517
+ });
1518
+ conditions.push(oldFieldIndex === newFieldIndex);
1584
1519
  }
1585
- statement2 += compileQueryInput(symbol.value, models, statementParams).main.statement;
1586
- return statement2;
1587
- }
1588
- Object.assign(toInstruction, defaultFields);
1589
- for (const fieldSlug in toInstruction) {
1590
- if (!Object.hasOwn(toInstruction, fieldSlug)) continue;
1591
- const fieldValue = toInstruction[fieldSlug];
1592
- const fieldDetails = getFieldFromModel(
1593
- model,
1594
- fieldSlug,
1595
- { instructionName: "to" },
1596
- false
1597
- );
1598
- if (fieldDetails?.field.type === "link" && fieldDetails.field.kind === "many") {
1599
- delete toInstruction[fieldSlug];
1600
- const associativeModelSlug = composeAssociationModelSlug(model, fieldDetails.field);
1601
- const composeStatement = (subQueryType, value) => {
1602
- const source = queryType === "add" ? { id: toInstruction.id } : withInstruction;
1603
- const recordDetails = { source };
1604
- if (value) recordDetails.target = value;
1605
- return compileQueryInput(
1606
- {
1607
- [subQueryType]: {
1608
- [associativeModelSlug]: subQueryType === "add" ? { to: recordDetails } : { with: recordDetails }
1609
- }
1610
- },
1611
- models,
1612
- [],
1613
- { returning: false }
1614
- ).main;
1615
- };
1616
- if (Array.isArray(fieldValue)) {
1617
- dependencyStatements.push(composeStatement("remove"));
1618
- for (const record of fieldValue) {
1619
- dependencyStatements.push(composeStatement("add", record));
1620
- }
1621
- } else if (isObject(fieldValue)) {
1622
- const value = fieldValue;
1623
- for (const recordToAdd of value.containing || []) {
1624
- dependencyStatements.push(composeStatement("add", recordToAdd));
1625
- }
1626
- for (const recordToRemove of value.notContaining || []) {
1627
- dependencyStatements.push(composeStatement("remove", recordToRemove));
1628
- }
1520
+ return conditions.every((condition) => condition === true);
1521
+ };
1522
+ for (const systemModel of currentSystemModels) {
1523
+ const exists = newSystemModels.find(matchSystemModels.bind(null, systemModel));
1524
+ if (exists) {
1525
+ if (exists.slug !== systemModel.slug) {
1526
+ handleSystemModel(models, dependencyStatements, "alter", systemModel, exists);
1629
1527
  }
1528
+ continue;
1630
1529
  }
1530
+ handleSystemModel(models, dependencyStatements, "drop", systemModel);
1631
1531
  }
1632
- let statement = composeConditions(models, model, statementParams, "to", toInstruction, {
1633
- parentModel,
1634
- type: queryType === "add" ? "fields" : void 0
1635
- });
1636
- if (queryType === "add") {
1637
- const deepStatement = composeConditions(
1638
- models,
1639
- model,
1640
- statementParams,
1641
- "to",
1642
- toInstruction,
1643
- {
1644
- parentModel,
1645
- type: "values"
1646
- }
1647
- );
1648
- statement = `(${statement}) VALUES (${deepStatement})`;
1649
- } else if (queryType === "set") {
1650
- statement = `SET ${statement}`;
1532
+ for (const systemModel of newSystemModels) {
1533
+ const exists = currentSystemModels.find(matchSystemModels.bind(null, systemModel));
1534
+ if (exists) continue;
1535
+ handleSystemModel(models, dependencyStatements, "create", systemModel);
1651
1536
  }
1652
- return statement;
1653
1537
  };
1654
-
1655
- // src/utils/index.ts
1656
- var compileQueryInput = (defaultQuery, models, statementParams, options) => {
1657
- const dependencyStatements = [];
1658
- const query = transformMetaQuery(
1659
- models,
1660
- dependencyStatements,
1661
- statementParams,
1662
- defaultQuery
1663
- );
1664
- if (query === null)
1665
- return { dependencies: [], main: dependencyStatements[0], loadedFields: [] };
1666
- const parsedQuery = splitQuery(query);
1667
- const { queryType, queryModel, queryInstructions } = parsedQuery;
1668
- const model = getModelBySlug(models, queryModel);
1669
- const single = queryModel !== model.pluralSlug;
1670
- let instructions = formatIdentifiers(model, queryInstructions);
1671
- const returning = options?.returning ?? true;
1672
- if (instructions && Object.hasOwn(instructions, "for")) {
1673
- instructions = handleFor(model, instructions);
1674
- }
1675
- const { columns, isJoining, loadedFields } = handleSelecting(
1676
- models,
1677
- model,
1678
- statementParams,
1679
- {
1680
- selecting: instructions?.selecting,
1681
- including: instructions?.including
1682
- },
1683
- options
1684
- );
1685
- let statement = "";
1686
- switch (queryType) {
1687
- case "get":
1688
- statement += `SELECT ${columns} FROM `;
1689
- break;
1690
- case "set":
1691
- statement += "UPDATE ";
1692
- break;
1693
- case "add":
1694
- statement += "INSERT INTO ";
1695
- break;
1696
- case "remove":
1697
- statement += "DELETE FROM ";
1698
- break;
1699
- case "count":
1700
- statement += `SELECT COUNT(${columns}) FROM `;
1701
- break;
1538
+ var transformMetaQuery = (models, dependencyStatements, statementParams, query) => {
1539
+ const { queryType } = splitQuery(query);
1540
+ const subAltering = "alter" in query && query.alter && !("to" in query.alter);
1541
+ const action = subAltering && query.alter ? Object.keys(query.alter).filter((key) => key !== "model")[0] : queryType;
1542
+ const actionReadable = action === "create" ? "creating" : action === "alter" ? "altering" : "dropping";
1543
+ const entity = subAltering && query.alter ? Object.keys(query.alter[action])[0] : "model";
1544
+ let slug = entity === "model" && action === "create" ? null : query[queryType].model;
1545
+ let modelSlug = slug;
1546
+ let jsonValue;
1547
+ if ("create" in query && query.create) {
1548
+ const init = query.create.model;
1549
+ jsonValue = "to" in query.create ? { slug: init, ...query.create.to } : init;
1550
+ slug = modelSlug = jsonValue.slug;
1702
1551
  }
1703
- let isJoiningMultipleRows = false;
1704
- if (isJoining) {
1705
- const { statement: including, tableSubQuery } = handleIncluding(
1706
- models,
1707
- model,
1708
- statementParams,
1709
- instructions?.including
1710
- );
1711
- if (tableSubQuery) {
1712
- statement += `(${tableSubQuery}) as ${model.tableAlias} `;
1713
- isJoiningMultipleRows = true;
1552
+ if ("alter" in query && query.alter) {
1553
+ if ("to" in query.alter) {
1554
+ jsonValue = query.alter.to;
1714
1555
  } else {
1715
- statement += `"${model.table}" `;
1556
+ slug = query.alter[action][entity];
1557
+ if ("create" in query.alter) {
1558
+ const item = query.alter.create[entity];
1559
+ slug = item.slug || `${entity}Slug`;
1560
+ jsonValue = { slug, ...item };
1561
+ }
1562
+ if ("alter" in query.alter && query.alter.alter) jsonValue = query.alter.alter.to;
1716
1563
  }
1717
- statement += `${including} `;
1718
- } else {
1719
- statement += `"${model.table}" `;
1720
1564
  }
1721
- if (queryType === "add" || queryType === "set") {
1722
- if (!isObject(instructions.to) || Object.keys(instructions.to).length === 0) {
1723
- throw new RoninError({
1724
- message: `When using a \`${queryType}\` query, the \`to\` instruction must be a non-empty object.`,
1725
- code: "INVALID_TO_VALUE",
1726
- queries: [query]
1565
+ if (!(modelSlug && slug)) return query;
1566
+ const model = action === "create" && entity === "model" ? null : getModelBySlug(models, modelSlug);
1567
+ if (entity === "model") {
1568
+ let queryTypeDetails = {};
1569
+ if (action === "create") {
1570
+ const newModel = jsonValue;
1571
+ const modelWithAttributes = addDefaultModelAttributes(newModel, true);
1572
+ const modelWithFields = addDefaultModelFields(modelWithAttributes, true);
1573
+ const modelWithPresets = addDefaultModelPresets(
1574
+ [...models, modelWithFields],
1575
+ modelWithFields
1576
+ );
1577
+ modelWithPresets.fields = modelWithPresets.fields.map((field2) => ({
1578
+ ...field2,
1579
+ // Default field type.
1580
+ type: field2.type || "string",
1581
+ // Default field name.
1582
+ name: field2.name || slugToName(field2.slug)
1583
+ }));
1584
+ const columns = modelWithPresets.fields.map((field2) => getFieldStatement(models, modelWithPresets, field2)).filter(Boolean);
1585
+ const entities = Object.fromEntries(
1586
+ Object.entries(PLURAL_MODEL_ENTITIES).map(([type, pluralType2]) => {
1587
+ const list = modelWithPresets[pluralType2];
1588
+ return [pluralType2, formatModelEntity(type, list)];
1589
+ })
1590
+ );
1591
+ dependencyStatements.push({
1592
+ statement: `CREATE TABLE "${modelWithPresets.table}" (${columns.join(", ")})`,
1593
+ params: []
1594
+ });
1595
+ models.push(modelWithPresets);
1596
+ const modelWithObjects = Object.assign({}, modelWithPresets);
1597
+ for (const entity2 in entities) {
1598
+ if (!Object.hasOwn(entities, entity2)) continue;
1599
+ Object.defineProperty(modelWithObjects, entity2, { value: entities[entity2] });
1600
+ }
1601
+ queryTypeDetails = { to: modelWithObjects };
1602
+ getSystemModels(models, modelWithPresets).map((systemModel) => {
1603
+ return handleSystemModel(models, dependencyStatements, "create", systemModel);
1604
+ });
1605
+ }
1606
+ if (action === "alter" && model) {
1607
+ const modelBeforeUpdate2 = structuredClone(model);
1608
+ const newModel = jsonValue;
1609
+ const modelWithAttributes = addDefaultModelAttributes(newModel, false);
1610
+ const modelWithFields = addDefaultModelFields(modelWithAttributes, false);
1611
+ const modelWithPresets = addDefaultModelPresets(models, modelWithFields);
1612
+ const newTableName = modelWithPresets.table;
1613
+ if (newTableName) {
1614
+ dependencyStatements.push({
1615
+ statement: `ALTER TABLE "${model.table}" RENAME TO "${newTableName}"`,
1616
+ params: []
1617
+ });
1618
+ }
1619
+ Object.assign(model, modelWithPresets);
1620
+ queryTypeDetails = {
1621
+ with: {
1622
+ slug
1623
+ },
1624
+ to: modelWithPresets
1625
+ };
1626
+ handleSystemModels(models, dependencyStatements, modelBeforeUpdate2, model);
1627
+ }
1628
+ if (action === "drop" && model) {
1629
+ models.splice(models.indexOf(model), 1);
1630
+ dependencyStatements.push({ statement: `DROP TABLE "${model.table}"`, params: [] });
1631
+ queryTypeDetails = { with: { slug } };
1632
+ models.filter(({ system }) => system?.model === model.id).map((systemModel) => {
1633
+ return handleSystemModel(models, dependencyStatements, "drop", systemModel);
1727
1634
  });
1728
1635
  }
1729
- const toStatement = handleTo(
1730
- models,
1731
- model,
1732
- statementParams,
1733
- queryType,
1734
- dependencyStatements,
1735
- { with: instructions.with, to: instructions.to },
1736
- options?.parentModel
1737
- );
1738
- statement += `${toStatement} `;
1636
+ const modelSlug2 = "to" in queryTypeDetails ? queryTypeDetails?.to?.slug : "with" in queryTypeDetails ? queryTypeDetails?.with?.slug : void 0;
1637
+ if (modelSlug2 === "model") return null;
1638
+ const queryTypeAction = action === "create" ? "add" : action === "alter" ? "set" : "remove";
1639
+ return {
1640
+ [queryTypeAction]: {
1641
+ model: queryTypeDetails
1642
+ }
1643
+ };
1739
1644
  }
1740
- const conditions = [];
1741
- if (queryType !== "add" && instructions && Object.hasOwn(instructions, "with")) {
1742
- const withStatement = handleWith(
1743
- models,
1744
- model,
1745
- statementParams,
1746
- instructions.with,
1747
- options?.parentModel
1748
- );
1749
- if (withStatement.length > 0) conditions.push(withStatement);
1645
+ const modelBeforeUpdate = structuredClone(model);
1646
+ const existingModel = model;
1647
+ const pluralType = PLURAL_MODEL_ENTITIES[entity];
1648
+ const targetEntityIndex = existingModel[pluralType]?.findIndex(
1649
+ (entity2) => entity2.slug === slug
1650
+ );
1651
+ if ((action === "alter" || action === "drop") && (typeof targetEntityIndex === "undefined" || targetEntityIndex === -1)) {
1652
+ throw new RoninError({
1653
+ message: `No ${entity} with slug "${slug}" defined in model "${existingModel.name}".`,
1654
+ code: MODEL_ENTITY_ERROR_CODES[entity]
1655
+ });
1750
1656
  }
1751
- if ((queryType === "get" || queryType === "count") && !single && instructions?.limitedTo) {
1752
- instructions = instructions || {};
1753
- instructions.orderedBy = instructions.orderedBy || {};
1754
- instructions.orderedBy.ascending = instructions.orderedBy.ascending || [];
1755
- instructions.orderedBy.descending = instructions.orderedBy.descending || [];
1756
- if (![
1757
- ...instructions.orderedBy.ascending,
1758
- ...instructions.orderedBy.descending
1759
- ].includes("ronin.createdAt")) {
1760
- instructions.orderedBy.descending.push("ronin.createdAt");
1761
- }
1657
+ const existingEntity = existingModel[pluralType]?.[targetEntityIndex];
1658
+ if (action === "create" && existingEntity) {
1659
+ throw new RoninError({
1660
+ message: `A ${entity} with the slug "${slug}" already exists.`,
1661
+ code: "EXISTING_MODEL_ENTITY",
1662
+ fields: ["slug"]
1663
+ });
1762
1664
  }
1763
- if (instructions && (Object.hasOwn(instructions, "before") || Object.hasOwn(instructions, "after"))) {
1764
- if (single) {
1765
- throw new RoninError({
1766
- message: "The `before` and `after` instructions are not supported when querying for a single record.",
1767
- code: "INVALID_BEFORE_OR_AFTER_INSTRUCTION",
1768
- queries: [query]
1665
+ if (entity === "field") {
1666
+ const statement = `ALTER TABLE "${existingModel.table}"`;
1667
+ const existingField = existingEntity;
1668
+ const existingLinkField = existingField?.type === "link" && existingField.kind === "many";
1669
+ if (action === "create") {
1670
+ const field2 = jsonValue;
1671
+ field2.type = field2.type || "string";
1672
+ field2.name = field2.name || slugToName(field2.slug);
1673
+ const fieldStatement = getFieldStatement(models, existingModel, field2);
1674
+ if (fieldStatement) {
1675
+ dependencyStatements.push({
1676
+ statement: `${statement} ADD COLUMN ${fieldStatement}`,
1677
+ params: []
1678
+ });
1679
+ }
1680
+ } else if (action === "alter") {
1681
+ const field2 = jsonValue;
1682
+ const newSlug = field2.slug;
1683
+ if (newSlug) {
1684
+ field2.name = field2.name || slugToName(field2.slug);
1685
+ if (!existingLinkField) {
1686
+ dependencyStatements.push({
1687
+ statement: `${statement} RENAME COLUMN "${slug}" TO "${newSlug}"`,
1688
+ params: []
1689
+ });
1690
+ }
1691
+ }
1692
+ } else if (action === "drop" && !existingLinkField) {
1693
+ const systemFields = getSystemFields(existingModel.idPrefix);
1694
+ const isSystemField = systemFields.some((field2) => field2.slug === slug);
1695
+ if (isSystemField) {
1696
+ throw new RoninError({
1697
+ message: `The ${entity} "${slug}" is a system ${entity} and cannot be removed.`,
1698
+ code: "REQUIRED_MODEL_ENTITY"
1699
+ });
1700
+ }
1701
+ dependencyStatements.push({
1702
+ statement: `${statement} DROP COLUMN "${slug}"`,
1703
+ params: []
1769
1704
  });
1770
1705
  }
1771
- const beforeAndAfterStatement = handleBeforeOrAfter(model, statementParams, {
1772
- before: instructions.before,
1773
- after: instructions.after,
1774
- with: instructions.with,
1775
- orderedBy: instructions.orderedBy,
1776
- limitedTo: instructions.limitedTo
1777
- });
1778
- conditions.push(beforeAndAfterStatement);
1779
1706
  }
1780
- if (conditions.length > 0) {
1781
- if (conditions.length === 1) {
1782
- statement += `WHERE ${conditions[0]} `;
1783
- } else {
1784
- statement += `WHERE (${conditions.join(" ")}) `;
1707
+ const statementAction = action.toUpperCase();
1708
+ if (entity === "index") {
1709
+ const index = jsonValue;
1710
+ const indexName = convertToSnakeCase(slug);
1711
+ let statement = `${statementAction}${index?.unique ? " UNIQUE" : ""} INDEX "${indexName}"`;
1712
+ if (action === "create") {
1713
+ if (!Array.isArray(index.fields) || index.fields.length === 0) {
1714
+ throw new RoninError({
1715
+ message: `When ${actionReadable} ${PLURAL_MODEL_ENTITIES[entity]}, at least one field must be provided.`,
1716
+ code: "INVALID_MODEL_VALUE",
1717
+ fields: ["fields"]
1718
+ });
1719
+ }
1720
+ const columns = index.fields.map((field2) => {
1721
+ let fieldSelector = "";
1722
+ if ("slug" in field2) {
1723
+ ({ fieldSelector } = getFieldFromModel(existingModel, field2.slug, {
1724
+ modelEntityType: "index",
1725
+ modelEntityName: indexName
1726
+ }));
1727
+ } else if ("expression" in field2) {
1728
+ fieldSelector = parseFieldExpression(existingModel, "to", field2.expression);
1729
+ }
1730
+ if (field2.collation) fieldSelector += ` COLLATE ${field2.collation}`;
1731
+ if (field2.order) fieldSelector += ` ${field2.order}`;
1732
+ return fieldSelector;
1733
+ });
1734
+ statement += ` ON "${existingModel.table}" (${columns.join(", ")})`;
1735
+ if (index.filter) {
1736
+ const withStatement = handleWith(models, existingModel, null, index.filter);
1737
+ statement += ` WHERE (${withStatement})`;
1738
+ }
1785
1739
  }
1740
+ dependencyStatements.push({ statement, params: [] });
1786
1741
  }
1787
- if (instructions?.orderedBy) {
1788
- const orderedByStatement = handleOrderedBy(model, instructions.orderedBy);
1789
- statement += `${orderedByStatement} `;
1790
- }
1791
- if (queryType === "get" && !isJoiningMultipleRows && (single || instructions?.limitedTo)) {
1792
- statement += handleLimitedTo(single, instructions?.limitedTo);
1742
+ if (entity === "trigger") {
1743
+ const triggerName = convertToSnakeCase(slug);
1744
+ let statement = `${statementAction} TRIGGER "${triggerName}"`;
1745
+ if (action === "create") {
1746
+ const trigger = jsonValue;
1747
+ const statementParts = [`${trigger.when} ${trigger.action}`];
1748
+ if (trigger.fields) {
1749
+ if (trigger.action !== "UPDATE") {
1750
+ throw new RoninError({
1751
+ message: `When ${actionReadable} ${PLURAL_MODEL_ENTITIES[entity]}, targeting specific fields requires the \`UPDATE\` action.`,
1752
+ code: "INVALID_MODEL_VALUE",
1753
+ fields: ["action"]
1754
+ });
1755
+ }
1756
+ const fieldSelectors = trigger.fields.map((field2) => {
1757
+ return getFieldFromModel(existingModel, field2.slug, {
1758
+ modelEntityType: "trigger",
1759
+ modelEntityName: triggerName
1760
+ }).fieldSelector;
1761
+ });
1762
+ statementParts.push(`OF (${fieldSelectors.join(", ")})`);
1763
+ }
1764
+ statementParts.push("ON", `"${existingModel.table}"`);
1765
+ if (trigger.filter || trigger.effects.some((query2) => findInObject(query2, QUERY_SYMBOLS.FIELD))) {
1766
+ statementParts.push("FOR EACH ROW");
1767
+ }
1768
+ if (trigger.filter) {
1769
+ const tableAlias = trigger.action === "DELETE" ? QUERY_SYMBOLS.FIELD_PARENT_OLD : QUERY_SYMBOLS.FIELD_PARENT_NEW;
1770
+ const withStatement = handleWith(
1771
+ models,
1772
+ { ...existingModel, tableAlias },
1773
+ null,
1774
+ trigger.filter
1775
+ );
1776
+ statementParts.push("WHEN", `(${withStatement})`);
1777
+ }
1778
+ const effectStatements = trigger.effects.map((effectQuery) => {
1779
+ return compileQueryInput(effectQuery, models, null, {
1780
+ returning: false,
1781
+ parentModel: existingModel
1782
+ }).main.statement;
1783
+ });
1784
+ statementParts.push("BEGIN");
1785
+ statementParts.push(`${effectStatements.join("; ")};`);
1786
+ statementParts.push("END");
1787
+ statement += ` ${statementParts.join(" ")}`;
1788
+ }
1789
+ dependencyStatements.push({ statement, params: [] });
1793
1790
  }
1794
- if (["add", "set", "remove"].includes(queryType) && returning) {
1795
- statement += "RETURNING * ";
1791
+ const field = `${QUERY_SYMBOLS.FIELD}${pluralType}`;
1792
+ let json;
1793
+ switch (action) {
1794
+ case "create": {
1795
+ const value = prepareStatementValue(statementParams, jsonValue);
1796
+ json = `json_insert(${field}, '$.${slug}', ${value})`;
1797
+ existingModel[pluralType] = [
1798
+ ...existingModel[pluralType] || [],
1799
+ jsonValue
1800
+ ];
1801
+ break;
1802
+ }
1803
+ case "alter": {
1804
+ const value = prepareStatementValue(statementParams, jsonValue);
1805
+ json = `json_set(${field}, '$.${slug}', json_patch(json_extract(${field}, '$.${slug}'), ${value}))`;
1806
+ const targetEntity = existingModel[pluralType];
1807
+ Object.assign(targetEntity[targetEntityIndex], jsonValue);
1808
+ break;
1809
+ }
1810
+ case "drop": {
1811
+ json = `json_remove(${field}, '$.${slug}')`;
1812
+ const targetEntity = existingModel[pluralType];
1813
+ targetEntity.splice(targetEntityIndex, 1);
1814
+ }
1796
1815
  }
1797
- const mainStatement = {
1798
- statement: statement.trimEnd(),
1799
- params: statementParams || []
1800
- };
1801
- if (returning) mainStatement.returning = true;
1816
+ handleSystemModels(models, dependencyStatements, modelBeforeUpdate, existingModel);
1802
1817
  return {
1803
- dependencies: dependencyStatements,
1804
- main: mainStatement,
1805
- loadedFields
1818
+ set: {
1819
+ model: {
1820
+ with: { slug: modelSlug },
1821
+ to: {
1822
+ [pluralType]: { [QUERY_SYMBOLS.EXPRESSION]: json }
1823
+ }
1824
+ }
1825
+ }
1806
1826
  };
1807
1827
  };
1808
1828
 
@@ -1825,21 +1845,25 @@ var Transaction = class {
1825
1845
  * @returns The composed SQL statements.
1826
1846
  */
1827
1847
  compileQueries = (queries, models, options) => {
1828
- const modelList = [
1829
- ROOT_MODEL,
1830
- ...models.flatMap((model) => getSystemModels(models, model)),
1831
- ...models
1848
+ const modelsWithAttributes = [ROOT_MODEL, ...models].map((model) => {
1849
+ return addDefaultModelAttributes(model, true);
1850
+ });
1851
+ const modelsWithFields = [
1852
+ ...modelsWithAttributes.flatMap((model) => {
1853
+ return getSystemModels(modelsWithAttributes, model);
1854
+ }),
1855
+ ...modelsWithAttributes
1832
1856
  ].map((model) => {
1833
1857
  return addDefaultModelFields(model, true);
1834
1858
  });
1835
- const modelListWithPresets = modelList.map((model) => {
1836
- return addDefaultModelPresets(modelList, model);
1859
+ const modelsWithPresets = modelsWithFields.map((model) => {
1860
+ return addDefaultModelPresets(modelsWithFields, model);
1837
1861
  });
1838
1862
  const statements = [];
1839
1863
  for (const query of queries) {
1840
1864
  const result = compileQueryInput(
1841
1865
  query,
1842
- modelListWithPresets,
1866
+ modelsWithPresets,
1843
1867
  options?.inlineParams ? null : [],
1844
1868
  { expandColumns: options?.expandColumns }
1845
1869
  );
@@ -1853,7 +1877,7 @@ var Transaction = class {
1853
1877
  }))
1854
1878
  );
1855
1879
  }
1856
- this.models = modelListWithPresets;
1880
+ this.models = modelsWithPresets;
1857
1881
  return statements;
1858
1882
  };
1859
1883
  formatRows(fields, rows, single, isMeta) {