@ronin/compiler 0.13.10 → 0.13.11-leo-ron-1099-experimental-306

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 +1562 -1535
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -150,1659 +150,1682 @@ 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
  }
280
- }
281
- }
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
296
+ Object.assign(instructions, {
297
+ [instructionName]: replacedForFilter[instructionName]
288
298
  });
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);
299
+ }
293
300
  }
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
778
  };
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;
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
+ let conditionMatcher = "=";
788
+ let conditionValue = value;
789
+ if (options.condition) {
790
+ [conditionMatcher, conditionValue] = WITH_CONDITIONS[options.condition](value);
897
791
  }
898
- const statement = compileQueryInput(query, models, []);
899
- dependencyStatements.push(...statement.dependencies);
900
- };
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
- })
792
+ if (symbol) {
793
+ if (symbol?.type === "expression") {
794
+ conditionValue = parseFieldExpression(
795
+ model,
796
+ instructionName,
797
+ symbol.value,
798
+ options.parentModel
952
799
  );
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
- }
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
800
  }
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
- });
801
+ if (symbol.type === "query" && collectStatementValue) {
802
+ conditionValue = `(${compileQueryInput(symbol.value, models, statementParams).main.statement})`;
994
803
  }
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
- };
804
+ } else if (collectStatementValue) {
805
+ conditionValue = prepareStatementValue(statementParams, conditionValue);
1003
806
  }
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
- });
807
+ if (options.type === "fields") return conditionSelector;
808
+ if (options.type === "values") return conditionValue;
809
+ return `${conditionSelector} ${conditionMatcher} ${conditionValue}`;
810
+ };
811
+ var composeConditions = (models, model, statementParams, instructionName, value, options) => {
812
+ const isNested = isObject(value) && Object.keys(value).length > 0;
813
+ if (isNested && Object.keys(value).every((key) => key in WITH_CONDITIONS)) {
814
+ const conditions = Object.entries(value).map(
815
+ ([conditionType, checkValue]) => composeConditions(models, model, statementParams, instructionName, checkValue, {
816
+ ...options,
817
+ condition: conditionType
818
+ })
819
+ );
820
+ return conditions.join(" AND ");
1015
821
  }
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"]
822
+ if (options.fieldSlug) {
823
+ const childField = model.fields.some(({ slug }) => {
824
+ return slug.includes(".") && slug.split(".")[0] === options.fieldSlug;
1022
825
  });
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;
826
+ if (!childField) {
827
+ const fieldDetails = getFieldFromModel(model, options.fieldSlug, {
828
+ instructionName
1092
829
  });
1093
- statement += ` ON "${existingModel.table}" (${columns.join(", ")})`;
1094
- if (index.filter) {
1095
- const withStatement = handleWith(models, existingModel, null, index.filter);
1096
- statement += ` WHERE (${withStatement})`;
830
+ const { field: modelField } = fieldDetails || {};
831
+ const consumeJSON = modelField?.type === "json" && instructionName === "to";
832
+ if (modelField && !(isObject(value) || Array.isArray(value)) || getSymbol(value) || consumeJSON) {
833
+ return composeFieldValues(
834
+ models,
835
+ model,
836
+ statementParams,
837
+ instructionName,
838
+ value,
839
+ { ...options, fieldSlug: options.fieldSlug }
840
+ );
1097
841
  }
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
- });
842
+ if (modelField?.type === "link" && isNested) {
843
+ const keys = Object.keys(value);
844
+ const values = Object.values(value);
845
+ let recordTarget;
846
+ if (keys.length === 1 && keys[0] === "id") {
847
+ recordTarget = values[0];
848
+ } else {
849
+ const relatedModel = getModelBySlug(models, modelField.target);
850
+ const subQuery = {
851
+ get: {
852
+ [relatedModel.slug]: {
853
+ with: value,
854
+ selecting: ["id"]
855
+ }
856
+ }
857
+ };
858
+ recordTarget = {
859
+ [QUERY_SYMBOLS.QUERY]: subQuery
860
+ };
1114
861
  }
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(
862
+ return composeConditions(
1130
863
  models,
1131
- { ...existingModel, tableAlias },
1132
- null,
1133
- trigger.filter
864
+ model,
865
+ statementParams,
866
+ instructionName,
867
+ recordTarget,
868
+ options
1134
869
  );
1135
- statementParts.push("WHEN", `(${withStatement})`);
1136
870
  }
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
871
  }
1148
- dependencyStatements.push({ statement, params: [] });
1149
872
  }
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
- }
873
+ if (isNested) {
874
+ const conditions = Object.entries(value).map(([field, value2]) => {
875
+ const nestedFieldSlug = options.fieldSlug ? `${options.fieldSlug}.${field}` : field;
876
+ return composeConditions(models, model, statementParams, instructionName, value2, {
877
+ ...options,
878
+ fieldSlug: nestedFieldSlug
879
+ });
880
+ });
881
+ const joiner = instructionName === "to" ? ", " : " AND ";
882
+ if (instructionName === "to") return `${conditions.join(joiner)}`;
883
+ return conditions.length === 1 ? conditions[0] : options.fieldSlug ? `(${conditions.join(joiner)})` : conditions.join(joiner);
884
+ }
885
+ if (Array.isArray(value)) {
886
+ const conditions = value.map(
887
+ (filter) => composeConditions(models, model, statementParams, instructionName, filter, options)
888
+ );
889
+ return conditions.join(" OR ");
890
+ }
891
+ throw new RoninError({
892
+ 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.`,
893
+ code: "INVALID_WITH_VALUE",
894
+ queries: null
895
+ });
896
+ };
897
+ var formatIdentifiers = ({ identifiers }, queryInstructions) => {
898
+ if (!queryInstructions) return queryInstructions;
899
+ const type = "with" in queryInstructions ? "with" : null;
900
+ if (!type) return queryInstructions;
901
+ const nestedInstructions = queryInstructions[type];
902
+ if (!nestedInstructions || Array.isArray(nestedInstructions))
903
+ return queryInstructions;
904
+ const newNestedInstructions = { ...nestedInstructions };
905
+ for (const oldKey of Object.keys(newNestedInstructions)) {
906
+ if (oldKey !== "nameIdentifier" && oldKey !== "slugIdentifier") continue;
907
+ const identifierName = oldKey === "nameIdentifier" ? "name" : "slug";
908
+ const value = newNestedInstructions[oldKey];
909
+ const newKey = identifiers[identifierName];
910
+ newNestedInstructions[newKey] = value;
911
+ delete newNestedInstructions[oldKey];
912
+ }
913
+ return {
914
+ ...queryInstructions,
915
+ [type]: newNestedInstructions
916
+ };
917
+ };
918
+
919
+ // src/instructions/with.ts
920
+ var getMatcher = (value, negative) => {
921
+ if (negative) {
922
+ if (value === null) return "IS NOT";
923
+ return "!=";
924
+ }
925
+ if (value === null) return "IS";
926
+ return "=";
927
+ };
928
+ var WITH_CONDITIONS = {
929
+ being: (value) => [getMatcher(value, false), value],
930
+ notBeing: (value) => [getMatcher(value, true), value],
931
+ startingWith: (value) => ["LIKE", `${value}%`],
932
+ notStartingWith: (value) => ["NOT LIKE", `${value}%`],
933
+ endingWith: (value) => ["LIKE", `%${value}`],
934
+ notEndingWith: (value) => ["NOT LIKE", `%${value}`],
935
+ containing: (value) => ["LIKE", `%${value}%`],
936
+ notContaining: (value) => ["NOT LIKE", `%${value}%`],
937
+ greaterThan: (value) => [">", value],
938
+ greaterOrEqual: (value) => [">=", value],
939
+ lessThan: (value) => ["<", value],
940
+ lessOrEqual: (value) => ["<=", value]
941
+ };
942
+ var handleWith = (models, model, statementParams, instruction, parentModel) => {
943
+ const subStatement = composeConditions(
944
+ models,
945
+ model,
946
+ statementParams,
947
+ "with",
948
+ instruction,
949
+ { parentModel }
950
+ );
951
+ return `(${subStatement})`;
952
+ };
953
+
954
+ // node_modules/title/dist/esm/lower-case.js
955
+ var conjunctions = [
956
+ "for",
957
+ "and",
958
+ "nor",
959
+ "but",
960
+ "or",
961
+ "yet",
962
+ "so"
963
+ ];
964
+ var articles = [
965
+ "a",
966
+ "an",
967
+ "the"
968
+ ];
969
+ var prepositions = [
970
+ "aboard",
971
+ "about",
972
+ "above",
973
+ "across",
974
+ "after",
975
+ "against",
976
+ "along",
977
+ "amid",
978
+ "among",
979
+ "anti",
980
+ "around",
981
+ "as",
982
+ "at",
983
+ "before",
984
+ "behind",
985
+ "below",
986
+ "beneath",
987
+ "beside",
988
+ "besides",
989
+ "between",
990
+ "beyond",
991
+ "but",
992
+ "by",
993
+ "concerning",
994
+ "considering",
995
+ "despite",
996
+ "down",
997
+ "during",
998
+ "except",
999
+ "excepting",
1000
+ "excluding",
1001
+ "following",
1002
+ "for",
1003
+ "from",
1004
+ "in",
1005
+ "inside",
1006
+ "into",
1007
+ "like",
1008
+ "minus",
1009
+ "near",
1010
+ "of",
1011
+ "off",
1012
+ "on",
1013
+ "onto",
1014
+ "opposite",
1015
+ "over",
1016
+ "past",
1017
+ "per",
1018
+ "plus",
1019
+ "regarding",
1020
+ "round",
1021
+ "save",
1022
+ "since",
1023
+ "than",
1024
+ "through",
1025
+ "to",
1026
+ "toward",
1027
+ "towards",
1028
+ "under",
1029
+ "underneath",
1030
+ "unlike",
1031
+ "until",
1032
+ "up",
1033
+ "upon",
1034
+ "versus",
1035
+ "via",
1036
+ "with",
1037
+ "within",
1038
+ "without"
1039
+ ];
1040
+ var lowerCase = /* @__PURE__ */ new Set([
1041
+ ...conjunctions,
1042
+ ...articles,
1043
+ ...prepositions
1044
+ ]);
1045
+
1046
+ // node_modules/title/dist/esm/specials.js
1047
+ var specials = [
1048
+ "ZEIT",
1049
+ "ZEIT Inc.",
1050
+ "Vercel",
1051
+ "Vercel Inc.",
1052
+ "CLI",
1053
+ "API",
1054
+ "HTTP",
1055
+ "HTTPS",
1056
+ "JSX",
1057
+ "DNS",
1058
+ "URL",
1059
+ "now.sh",
1060
+ "now.json",
1061
+ "vercel.app",
1062
+ "vercel.json",
1063
+ "CI",
1064
+ "CD",
1065
+ "CDN",
1066
+ "package.json",
1067
+ "package.lock",
1068
+ "yarn.lock",
1069
+ "GitHub",
1070
+ "GitLab",
1071
+ "CSS",
1072
+ "Sass",
1073
+ "JS",
1074
+ "JavaScript",
1075
+ "TypeScript",
1076
+ "HTML",
1077
+ "WordPress",
1078
+ "Next.js",
1079
+ "Node.js",
1080
+ "Webpack",
1081
+ "Docker",
1082
+ "Bash",
1083
+ "Kubernetes",
1084
+ "SWR",
1085
+ "TinaCMS",
1086
+ "UI",
1087
+ "UX",
1088
+ "TS",
1089
+ "TSX",
1090
+ "iPhone",
1091
+ "iPad",
1092
+ "watchOS",
1093
+ "iOS",
1094
+ "iPadOS",
1095
+ "macOS",
1096
+ "PHP",
1097
+ "composer.json",
1098
+ "composer.lock",
1099
+ "CMS",
1100
+ "SQL",
1101
+ "C",
1102
+ "C#",
1103
+ "GraphQL",
1104
+ "GraphiQL",
1105
+ "JWT",
1106
+ "JWTs"
1107
+ ];
1108
+
1109
+ // node_modules/title/dist/esm/index.js
1110
+ var word = `[^\\s'\u2019\\(\\)!?;:"-]`;
1111
+ var regex = new RegExp(`(?:(?:(\\s?(?:^|[.\\(\\)!?;:"-])\\s*)(${word}))|(${word}))(${word}*[\u2019']*${word}*)`, "g");
1112
+ var convertToRegExp = (specials2) => specials2.map((s) => [new RegExp(`\\b${s}\\b`, "gi"), s]);
1113
+ function parseMatch(match) {
1114
+ const firstCharacter = match[0];
1115
+ if (/\s/.test(firstCharacter)) {
1116
+ return match.slice(1);
1117
+ }
1118
+ if (/[\(\)]/.test(firstCharacter)) {
1119
+ return null;
1174
1120
  }
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;
1121
+ return match;
1122
+ }
1123
+ var src_default = (str, options = {}) => {
1124
+ str = str.toLowerCase().replace(regex, (m, lead = "", forced, lower, rest, offset, string) => {
1125
+ const isLastWord = m.length + offset >= string.length;
1126
+ const parsedMatch = parseMatch(m);
1127
+ if (!parsedMatch) {
1128
+ return m;
1201
1129
  }
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
- }
1130
+ if (!forced) {
1131
+ const fullLower = lower + rest;
1132
+ if (lowerCase.has(fullLower) && !isLastWord) {
1133
+ return parsedMatch;
1216
1134
  }
1217
1135
  }
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;
1136
+ return lead + (lower || forced).toUpperCase() + rest;
1236
1137
  });
1237
- return cursors.map((cursor) => encodeURIComponent(String(cursor))).join(CURSOR_SEPARATOR);
1138
+ const customSpecials = options.special || [];
1139
+ const replace = [...specials, ...customSpecials];
1140
+ const replaceRegExp = convertToRegExp(replace);
1141
+ replaceRegExp.forEach(([pattern, s]) => {
1142
+ str = str.replace(pattern, s);
1143
+ });
1144
+ return str;
1238
1145
  };
1239
1146
 
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
- });
1147
+ // src/model/defaults.ts
1148
+ var slugToName = (slug) => {
1149
+ const name = slug.replace(/([a-z])([A-Z])/g, "$1 $2");
1150
+ return src_default(name);
1151
+ };
1152
+ var VOWELS = ["a", "e", "i", "o", "u"];
1153
+ var pluralize = (word2) => {
1154
+ const lastLetter = word2.slice(-1).toLowerCase();
1155
+ const secondLastLetter = word2.slice(-2, -1).toLowerCase();
1156
+ if (lastLetter === "y" && !VOWELS.includes(secondLastLetter)) {
1157
+ return `${word2.slice(0, -1)}ies`;
1262
1158
  }
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(" "));
1159
+ if (lastLetter === "s" || word2.slice(-2).toLowerCase() === "ch" || word2.slice(-2).toLowerCase() === "sh" || word2.slice(-2).toLowerCase() === "ex") {
1160
+ return `${word2}es`;
1319
1161
  }
1320
- return `${clause}(${conditions.join(" OR ")})`;
1162
+ return `${word2}s`;
1163
+ };
1164
+ var modelAttributes = [
1165
+ ["pluralSlug", "slug", pluralize],
1166
+ ["name", "slug", slugToName],
1167
+ ["pluralName", "pluralSlug", slugToName],
1168
+ ["idPrefix", "slug", (slug) => slug.slice(0, 3)],
1169
+ ["table", "pluralSlug", convertToSnakeCase]
1170
+ ];
1171
+ var getModelIdentifier = () => {
1172
+ return `mod_${Array.from(crypto.getRandomValues(new Uint8Array(12))).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16).toLowerCase()}`;
1321
1173
  };
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)
1174
+ var addDefaultModelAttributes = (model, isNew) => {
1175
+ const copiedModel = { ...model };
1176
+ if (isNew && !copiedModel.id) copiedModel.id = getModelIdentifier();
1177
+ for (const [setting, base, generator] of modelAttributes) {
1178
+ if (copiedModel[setting] || !copiedModel[base]) continue;
1179
+ copiedModel[setting] = generator(copiedModel[base]);
1180
+ }
1181
+ const newFields = copiedModel.fields || [];
1182
+ if (isNew || newFields.length > 0) {
1183
+ if (!copiedModel.identifiers) copiedModel.identifiers = {};
1184
+ if (!copiedModel.identifiers.name) {
1185
+ const suitableField = newFields.find(
1186
+ (field) => field.type === "string" && field.required === true && ["name"].includes(field.slug)
1342
1187
  );
1188
+ copiedModel.identifiers.name = suitableField?.slug || "id";
1343
1189
  }
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
- });
1190
+ if (!copiedModel.identifiers.slug) {
1191
+ const suitableField = newFields.find(
1192
+ (field) => field.type === "string" && field.unique === true && field.required === true && ["slug", "handle"].includes(field.slug)
1193
+ );
1194
+ copiedModel.identifiers.slug = suitableField?.slug || "id";
1367
1195
  }
1368
1196
  }
1369
- return instructions;
1197
+ return copiedModel;
1370
1198
  };
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
1199
+ var addDefaultModelFields = (model, isNew) => {
1200
+ const copiedModel = { ...model };
1201
+ const newFields = copiedModel.fields || [];
1202
+ if (isNew || newFields.length > 0) {
1203
+ copiedModel.fields = [...getSystemFields(copiedModel.idPrefix), ...newFields];
1204
+ }
1205
+ return copiedModel;
1206
+ };
1207
+ var addDefaultModelPresets = (list, model) => {
1208
+ const defaultPresets = [];
1209
+ for (const field of model.fields || []) {
1210
+ if (field.type === "link" && !field.slug.startsWith("ronin.")) {
1211
+ const relatedModel = getModelBySlug(list, field.target);
1212
+ if (field.kind === "many") continue;
1213
+ defaultPresets.push({
1214
+ instructions: {
1215
+ including: {
1216
+ [field.slug]: {
1217
+ [QUERY_SYMBOLS.QUERY]: {
1218
+ get: {
1219
+ [relatedModel.slug]: {
1220
+ with: {
1221
+ // Compare the `id` field of the related model to the link field on
1222
+ // the root model (`field.slug`).
1223
+ id: {
1224
+ [QUERY_SYMBOLS.EXPRESSION]: `${QUERY_SYMBOLS.FIELD_PARENT}${field.slug}`
1225
+ }
1226
+ }
1227
+ }
1228
+ }
1229
+ }
1230
+ }
1399
1231
  }
1400
1232
  },
1401
- models,
1402
- statementParams
1403
- );
1404
- relatedTableSelector = `(${subSelect.main.statement})`;
1233
+ slug: field.slug
1234
+ });
1405
1235
  }
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
1236
+ }
1237
+ const childModels = list.map((subModel) => {
1238
+ const field = subModel.fields?.find((field2) => {
1239
+ return field2.type === "link" && field2.target === model.slug;
1240
+ });
1241
+ if (!field) return null;
1242
+ return { model: subModel, field };
1243
+ }).filter((match) => match !== null);
1244
+ for (const childMatch of childModels) {
1245
+ const { model: childModel, field: childField } = childMatch;
1246
+ const pluralSlug = childModel.pluralSlug;
1247
+ const presetSlug = childModel.system?.associationSlug || pluralSlug;
1248
+ defaultPresets.push({
1249
+ instructions: {
1250
+ including: {
1251
+ [presetSlug]: {
1252
+ [QUERY_SYMBOLS.QUERY]: {
1253
+ get: {
1254
+ [pluralSlug]: {
1255
+ with: {
1256
+ [childField.slug]: {
1257
+ [QUERY_SYMBOLS.EXPRESSION]: `${QUERY_SYMBOLS.FIELD_PARENT}id`
1258
+ }
1259
+ }
1260
+ }
1261
+ }
1262
+ }
1263
+ }
1417
1264
  }
1418
- );
1419
- statement += ` ON (${subStatement})`;
1420
- }
1421
- if (!single) tableSubQuery = `SELECT * FROM "${model.table}" LIMIT 1`;
1265
+ },
1266
+ slug: presetSlug
1267
+ });
1422
1268
  }
1423
- return { statement, tableSubQuery };
1269
+ if (Object.keys(defaultPresets).length > 0) {
1270
+ model.presets = [...defaultPresets, ...model.presets || []];
1271
+ }
1272
+ return model;
1424
1273
  };
1425
1274
 
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} `;
1275
+ // src/model/index.ts
1276
+ var getModelBySlug = (models, slug) => {
1277
+ const model = models.find((model2) => {
1278
+ return model2.slug === slug || model2.pluralSlug === slug;
1279
+ });
1280
+ if (!model) {
1281
+ throw new RoninError({
1282
+ message: `No matching model with either Slug or Plural Slug of "${slug}" could be found.`,
1283
+ code: "MODEL_NOT_FOUND"
1284
+ });
1285
+ }
1286
+ return model;
1432
1287
  };
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 += ", ";
1288
+ var composeAssociationModelSlug = (model, field) => convertToCamelCase(`ronin_link_${model.slug}_${field.slug}`);
1289
+ var getFieldSelector = (model, field, fieldPath, writing) => {
1290
+ const symbol = model.tableAlias?.startsWith(QUERY_SYMBOLS.FIELD_PARENT) ? `${model.tableAlias.replace(QUERY_SYMBOLS.FIELD_PARENT, "").slice(0, -1)}.` : "";
1291
+ const tablePrefix = symbol || (model.tableAlias ? `"${model.tableAlias}".` : "");
1292
+ if (field.type === "json" && !writing) {
1293
+ const dotParts = fieldPath.split(".");
1294
+ const columnName = tablePrefix + dotParts.shift();
1295
+ const jsonField = dotParts.join(".");
1296
+ return `json_extract(${columnName}, '$.${jsonField}')`;
1297
+ }
1298
+ return `${tablePrefix}"${fieldPath}"`;
1299
+ };
1300
+ function getFieldFromModel(model, fieldPath, source, shouldThrow = true) {
1301
+ const writingField = "instructionName" in source ? source.instructionName === "to" : true;
1302
+ const errorTarget = "instructionName" in source ? `\`${source.instructionName}\`` : `${source.modelEntityType} "${source.modelEntityName}"`;
1303
+ const errorPrefix = `Field "${fieldPath}" defined for ${errorTarget}`;
1304
+ const modelFields = model.fields || [];
1305
+ let modelField;
1306
+ if (fieldPath.includes(".")) {
1307
+ modelField = modelFields.find((field) => field.slug === fieldPath.split(".")[0]);
1308
+ if (modelField?.type === "json") {
1309
+ const fieldSelector2 = getFieldSelector(model, modelField, fieldPath, writingField);
1310
+ return { field: modelField, fieldSelector: fieldSelector2 };
1444
1311
  }
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;
1312
+ }
1313
+ modelField = modelFields.find((field) => field.slug === fieldPath);
1314
+ if (!modelField) {
1315
+ if (shouldThrow) {
1316
+ throw new RoninError({
1317
+ message: `${errorPrefix} does not exist in model "${model.name}".`,
1318
+ code: "FIELD_NOT_FOUND",
1319
+ field: fieldPath,
1320
+ queries: null
1321
+ });
1322
+ }
1323
+ return null;
1324
+ }
1325
+ const fieldSelector = getFieldSelector(model, modelField, fieldPath, writingField);
1326
+ return { field: modelField, fieldSelector };
1327
+ }
1328
+ var getSystemFields = (idPrefix = "rec") => [
1329
+ {
1330
+ name: "ID",
1331
+ type: "string",
1332
+ slug: "id",
1333
+ defaultValue: {
1334
+ // Since default values in SQLite cannot rely on other columns, we unfortunately
1335
+ // cannot rely on the `idPrefix` column here. Instead, we need to inject it directly
1336
+ // into the expression as a static string.
1337
+ [QUERY_SYMBOLS.EXPRESSION]: `'${idPrefix}_' || lower(substr(hex(randomblob(12)), 1, 16))`
1450
1338
  }
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}`;
1339
+ },
1340
+ {
1341
+ name: "RONIN - Locked",
1342
+ type: "boolean",
1343
+ slug: "ronin.locked"
1344
+ },
1345
+ {
1346
+ name: "RONIN - Created At",
1347
+ type: "date",
1348
+ slug: "ronin.createdAt",
1349
+ defaultValue: CURRENT_TIME_EXPRESSION
1350
+ },
1351
+ {
1352
+ name: "RONIN - Created By",
1353
+ type: "string",
1354
+ slug: "ronin.createdBy"
1355
+ },
1356
+ {
1357
+ name: "RONIN - Updated At",
1358
+ type: "date",
1359
+ slug: "ronin.updatedAt",
1360
+ defaultValue: CURRENT_TIME_EXPRESSION
1361
+ },
1362
+ {
1363
+ name: "RONIN - Updated By",
1364
+ type: "string",
1365
+ slug: "ronin.updatedBy"
1458
1366
  }
1459
- return `ORDER BY ${statement}`;
1367
+ ];
1368
+ var ROOT_MODEL = {
1369
+ slug: "model",
1370
+ identifiers: {
1371
+ name: "name",
1372
+ slug: "slug"
1373
+ },
1374
+ // This name mimics the `sqlite_schema` table in SQLite.
1375
+ table: "ronin_schema",
1376
+ // Indicates that the model was automatically generated by RONIN.
1377
+ system: { model: "root" },
1378
+ fields: [
1379
+ { slug: "name", type: "string" },
1380
+ { slug: "pluralName", type: "string" },
1381
+ { slug: "slug", type: "string" },
1382
+ { slug: "pluralSlug", type: "string" },
1383
+ { slug: "idPrefix", type: "string" },
1384
+ { slug: "table", type: "string" },
1385
+ { slug: "identifiers.name", type: "string" },
1386
+ { slug: "identifiers.slug", type: "string" },
1387
+ // Providing an empty object as a default value allows us to use `json_insert`
1388
+ // without needing to fall back to an empty object in the insertion statement,
1389
+ // which makes the statement shorter.
1390
+ { slug: "fields", type: "json", defaultValue: "{}" },
1391
+ { slug: "indexes", type: "json", defaultValue: "{}" },
1392
+ { slug: "triggers", type: "json", defaultValue: "{}" },
1393
+ { slug: "presets", type: "json", defaultValue: "{}" }
1394
+ ]
1460
1395
  };
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);
1396
+ var getSystemModels = (models, model) => {
1397
+ const addedModels = [];
1398
+ for (const field of model.fields || []) {
1399
+ if (field.type === "link" && !field.slug.startsWith("ronin.")) {
1400
+ const relatedModel = getModelBySlug(models, field.target);
1401
+ let fieldSlug = relatedModel.slug;
1402
+ if (field.kind === "many") {
1403
+ fieldSlug = composeAssociationModelSlug(model, field);
1404
+ addedModels.push({
1405
+ pluralSlug: fieldSlug,
1406
+ slug: fieldSlug,
1407
+ system: {
1408
+ model: model.id,
1409
+ associationSlug: field.slug
1410
+ },
1411
+ fields: [
1412
+ {
1413
+ slug: "source",
1414
+ type: "link",
1415
+ target: model.slug
1416
+ },
1417
+ {
1418
+ slug: "target",
1419
+ type: "link",
1420
+ target: relatedModel.slug
1421
+ }
1422
+ ]
1423
+ });
1509
1424
  }
1510
- instructions.including[key] = newValue;
1511
- loadedFields.push({
1512
- slug: key,
1513
- type: RAW_FIELD_TYPES.includes(typeof value) ? typeof value : "string"
1514
- });
1515
1425
  }
1516
1426
  }
1517
- if (expandColumns) {
1518
- instructions.selecting = model.fields.filter((field) => !(field.type === "link" && field.kind === "many")).map((field) => field.slug);
1427
+ return addedModels.map((model2) => addDefaultModelAttributes(model2, true));
1428
+ };
1429
+ var typesInSQLite = {
1430
+ link: "TEXT",
1431
+ string: "TEXT",
1432
+ date: "DATETIME",
1433
+ blob: "TEXT",
1434
+ boolean: "BOOLEAN",
1435
+ number: "INTEGER",
1436
+ json: "TEXT"
1437
+ };
1438
+ var getFieldStatement = (models, model, field) => {
1439
+ let statement = `"${field.slug}" ${typesInSQLite[field.type || "string"]}`;
1440
+ if (field.slug === "id") statement += " PRIMARY KEY";
1441
+ if (field.unique === true) statement += " UNIQUE";
1442
+ if (field.required === true) statement += " NOT NULL";
1443
+ if (typeof field.defaultValue !== "undefined") {
1444
+ const symbol = getSymbol(field.defaultValue);
1445
+ let value = typeof field.defaultValue === "string" ? `'${field.defaultValue}'` : field.defaultValue;
1446
+ if (symbol) value = `(${parseFieldExpression(model, "to", symbol.value)})`;
1447
+ statement += ` DEFAULT ${value}`;
1519
1448
  }
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
- ];
1449
+ if (field.type === "string" && field.collation) {
1450
+ statement += ` COLLATE ${field.collation}`;
1538
1451
  }
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(", ");
1452
+ if (field.type === "number" && field.increment === true) {
1453
+ statement += " AUTOINCREMENT";
1542
1454
  }
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
- };
1455
+ if (typeof field.check !== "undefined") {
1456
+ const symbol = getSymbol(field.check);
1457
+ statement += ` CHECK (${parseFieldExpression(model, "to", symbol?.value)})`;
1557
1458
  }
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);
1459
+ if (typeof field.computedAs !== "undefined") {
1460
+ const { kind, value } = field.computedAs;
1461
+ const symbol = getSymbol(value);
1462
+ statement += ` GENERATED ALWAYS AS (${parseFieldExpression(model, "to", symbol?.value)}) ${kind}`;
1463
+ }
1464
+ if (field.type === "link") {
1465
+ if (field.kind === "many") return null;
1466
+ const actions = field.actions || {};
1467
+ const modelList = models.some((item) => item.slug === model.slug) ? models : [...models, model];
1468
+ const targetTable = getModelBySlug(modelList, field.target).table;
1469
+ statement += ` REFERENCES ${targetTable}("id")`;
1470
+ for (const trigger in actions) {
1471
+ if (!Object.hasOwn(actions, trigger)) continue;
1472
+ const triggerName = trigger.toUpperCase().slice(2);
1473
+ const action = actions[trigger];
1474
+ statement += ` ON ${triggerName} ${action}`;
1566
1475
  }
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
- ) : []
1476
+ }
1477
+ return statement;
1478
+ };
1479
+ var PLURAL_MODEL_ENTITIES = {
1480
+ field: "fields",
1481
+ index: "indexes",
1482
+ trigger: "triggers",
1483
+ preset: "presets"
1484
+ };
1485
+ var PLURAL_MODEL_ENTITIES_VALUES = Object.values(PLURAL_MODEL_ENTITIES);
1486
+ var formatModelEntity = (type, entities) => {
1487
+ const entries = entities?.map((entity) => {
1488
+ const { slug, ...rest } = "slug" in entity ? entity : { slug: `${type}Slug`, ...entity };
1489
+ return [slug, rest];
1490
+ });
1491
+ return entries ? Object.fromEntries(entries) : void 0;
1492
+ };
1493
+ var handleSystemModel = (models, dependencyStatements, action, systemModel, newModel) => {
1494
+ const { system: _, ...systemModelClean } = systemModel;
1495
+ const query = {
1496
+ [action]: { model: action === "create" ? systemModelClean : systemModelClean.slug }
1497
+ };
1498
+ if (action === "alter" && newModel && "alter" in query && query.alter) {
1499
+ const { system: _2, ...newModelClean } = newModel;
1500
+ query.alter.to = newModelClean;
1501
+ }
1502
+ const statement = compileQueryInput(query, models, []);
1503
+ dependencyStatements.push(...statement.dependencies);
1504
+ };
1505
+ var handleSystemModels = (models, dependencyStatements, previousModel, newModel) => {
1506
+ const currentSystemModels = models.filter(({ system }) => {
1507
+ return system?.model === newModel.id;
1508
+ });
1509
+ const newSystemModels = getSystemModels(models, newModel);
1510
+ const matchSystemModels = (oldSystemModel, newSystemModel) => {
1511
+ const conditions = [
1512
+ oldSystemModel.system?.model === newSystemModel.system?.model
1574
1513
  ];
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;
1514
+ if (oldSystemModel.system?.associationSlug) {
1515
+ const oldFieldIndex = previousModel.fields.findIndex((item) => {
1516
+ return item.slug === newSystemModel.system?.associationSlug;
1582
1517
  });
1583
- statement2 = `(${columns.join(", ")}) `;
1518
+ const newFieldIndex = newModel.fields.findIndex((item) => {
1519
+ return item.slug === oldSystemModel.system?.associationSlug;
1520
+ });
1521
+ conditions.push(oldFieldIndex === newFieldIndex);
1584
1522
  }
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
- }
1523
+ return conditions.every((condition) => condition === true);
1524
+ };
1525
+ for (const systemModel of currentSystemModels) {
1526
+ const exists = newSystemModels.find(matchSystemModels.bind(null, systemModel));
1527
+ if (exists) {
1528
+ if (exists.slug !== systemModel.slug) {
1529
+ handleSystemModel(models, dependencyStatements, "alter", systemModel, exists);
1629
1530
  }
1531
+ continue;
1630
1532
  }
1533
+ handleSystemModel(models, dependencyStatements, "drop", systemModel);
1631
1534
  }
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}`;
1535
+ for (const systemModel of newSystemModels) {
1536
+ const exists = currentSystemModels.find(matchSystemModels.bind(null, systemModel));
1537
+ if (exists) continue;
1538
+ handleSystemModel(models, dependencyStatements, "create", systemModel);
1651
1539
  }
1652
- return statement;
1653
1540
  };
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;
1541
+ var transformMetaQuery = (models, dependencyStatements, statementParams, query) => {
1542
+ const { queryType } = splitQuery(query);
1543
+ const subAltering = "alter" in query && query.alter && !("to" in query.alter);
1544
+ const action = subAltering && query.alter ? Object.keys(query.alter).filter((key) => key !== "model")[0] : queryType;
1545
+ const actionReadable = action === "create" ? "creating" : action === "alter" ? "altering" : "dropping";
1546
+ const entity = subAltering && query.alter ? Object.keys(query.alter[action])[0] : "model";
1547
+ let slug = entity === "model" && action === "create" ? null : query[queryType].model;
1548
+ let modelSlug = slug;
1549
+ let jsonValue;
1550
+ if ("create" in query && query.create) {
1551
+ const init = query.create.model;
1552
+ jsonValue = "to" in query.create ? { slug: init, ...query.create.to } : init;
1553
+ slug = modelSlug = jsonValue.slug;
1702
1554
  }
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;
1555
+ if ("alter" in query && query.alter) {
1556
+ if ("to" in query.alter) {
1557
+ jsonValue = query.alter.to;
1714
1558
  } else {
1715
- statement += `"${model.table}" `;
1559
+ slug = query.alter[action][entity];
1560
+ if ("create" in query.alter) {
1561
+ const item = query.alter.create[entity];
1562
+ slug = item.slug || `${entity}Slug`;
1563
+ jsonValue = { slug, ...item };
1564
+ }
1565
+ if ("alter" in query.alter && query.alter.alter) jsonValue = query.alter.alter.to;
1716
1566
  }
1717
- statement += `${including} `;
1718
- } else {
1719
- statement += `"${model.table}" `;
1720
1567
  }
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]
1568
+ if (!(modelSlug && slug)) return query;
1569
+ const model = action === "create" && entity === "model" ? null : getModelBySlug(models, modelSlug);
1570
+ if (entity === "model") {
1571
+ let queryTypeDetails = {};
1572
+ if (action === "create") {
1573
+ const newModel = jsonValue;
1574
+ const modelWithAttributes = addDefaultModelAttributes(newModel, true);
1575
+ const modelWithFields = addDefaultModelFields(modelWithAttributes, true);
1576
+ const modelWithPresets = addDefaultModelPresets(
1577
+ [...models, modelWithFields],
1578
+ modelWithFields
1579
+ );
1580
+ modelWithPresets.fields = modelWithPresets.fields.map((field2) => ({
1581
+ ...field2,
1582
+ // Default field type.
1583
+ type: field2.type || "string",
1584
+ // Default field name.
1585
+ name: field2.name || slugToName(field2.slug)
1586
+ }));
1587
+ const columns = modelWithPresets.fields.map((field2) => getFieldStatement(models, modelWithPresets, field2)).filter(Boolean);
1588
+ const entities = Object.fromEntries(
1589
+ Object.entries(PLURAL_MODEL_ENTITIES).map(([type, pluralType2]) => {
1590
+ const list = modelWithPresets[pluralType2];
1591
+ return [pluralType2, formatModelEntity(type, list)];
1592
+ })
1593
+ );
1594
+ dependencyStatements.push({
1595
+ statement: `CREATE TABLE "${modelWithPresets.table}" (${columns.join(", ")})`,
1596
+ params: []
1597
+ });
1598
+ models.push(modelWithPresets);
1599
+ const modelWithObjects = Object.assign({}, modelWithPresets);
1600
+ for (const entity2 in entities) {
1601
+ if (!Object.hasOwn(entities, entity2)) continue;
1602
+ Object.defineProperty(modelWithObjects, entity2, { value: entities[entity2] });
1603
+ }
1604
+ queryTypeDetails = { to: modelWithObjects };
1605
+ getSystemModels(models, modelWithPresets).map((systemModel) => {
1606
+ return handleSystemModel(models, dependencyStatements, "create", systemModel);
1607
+ });
1608
+ }
1609
+ if (action === "alter" && model) {
1610
+ const modelBeforeUpdate2 = structuredClone(model);
1611
+ const newModel = jsonValue;
1612
+ const modelWithAttributes = addDefaultModelAttributes(newModel, false);
1613
+ const modelWithFields = addDefaultModelFields(modelWithAttributes, false);
1614
+ const modelWithPresets = addDefaultModelPresets(models, modelWithFields);
1615
+ const newTableName = modelWithPresets.table;
1616
+ if (newTableName) {
1617
+ dependencyStatements.push({
1618
+ statement: `ALTER TABLE "${model.table}" RENAME TO "${newTableName}"`,
1619
+ params: []
1620
+ });
1621
+ }
1622
+ Object.assign(model, modelWithPresets);
1623
+ queryTypeDetails = {
1624
+ with: {
1625
+ slug
1626
+ },
1627
+ to: modelWithPresets
1628
+ };
1629
+ handleSystemModels(models, dependencyStatements, modelBeforeUpdate2, model);
1630
+ }
1631
+ if (action === "drop" && model) {
1632
+ models.splice(models.indexOf(model), 1);
1633
+ dependencyStatements.push({ statement: `DROP TABLE "${model.table}"`, params: [] });
1634
+ queryTypeDetails = { with: { slug } };
1635
+ models.filter(({ system }) => system?.model === model.id).map((systemModel) => {
1636
+ return handleSystemModel(models, dependencyStatements, "drop", systemModel);
1727
1637
  });
1728
1638
  }
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} `;
1639
+ const modelSlug2 = "to" in queryTypeDetails ? queryTypeDetails?.to?.slug : "with" in queryTypeDetails ? queryTypeDetails?.with?.slug : void 0;
1640
+ if (modelSlug2 === "model") return null;
1641
+ const queryTypeAction = action === "create" ? "add" : action === "alter" ? "set" : "remove";
1642
+ return {
1643
+ [queryTypeAction]: {
1644
+ model: queryTypeDetails
1645
+ }
1646
+ };
1739
1647
  }
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);
1648
+ const modelBeforeUpdate = structuredClone(model);
1649
+ const existingModel = model;
1650
+ const pluralType = PLURAL_MODEL_ENTITIES[entity];
1651
+ const targetEntityIndex = existingModel[pluralType]?.findIndex(
1652
+ (entity2) => entity2.slug === slug
1653
+ );
1654
+ if ((action === "alter" || action === "drop") && (typeof targetEntityIndex === "undefined" || targetEntityIndex === -1)) {
1655
+ throw new RoninError({
1656
+ message: `No ${entity} with slug "${slug}" defined in model "${existingModel.name}".`,
1657
+ code: MODEL_ENTITY_ERROR_CODES[entity]
1658
+ });
1750
1659
  }
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
- }
1660
+ const existingEntity = existingModel[pluralType]?.[targetEntityIndex];
1661
+ if (action === "create" && existingEntity) {
1662
+ throw new RoninError({
1663
+ message: `A ${entity} with the slug "${slug}" already exists.`,
1664
+ code: "EXISTING_MODEL_ENTITY",
1665
+ fields: ["slug"]
1666
+ });
1762
1667
  }
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]
1668
+ if (entity === "field") {
1669
+ const statement = `ALTER TABLE "${existingModel.table}"`;
1670
+ const existingField = existingEntity;
1671
+ const existingLinkField = existingField?.type === "link" && existingField.kind === "many";
1672
+ if (action === "create") {
1673
+ const field2 = jsonValue;
1674
+ field2.type = field2.type || "string";
1675
+ field2.name = field2.name || slugToName(field2.slug);
1676
+ const fieldStatement = getFieldStatement(models, existingModel, field2);
1677
+ if (fieldStatement) {
1678
+ dependencyStatements.push({
1679
+ statement: `${statement} ADD COLUMN ${fieldStatement}`,
1680
+ params: []
1681
+ });
1682
+ }
1683
+ } else if (action === "alter") {
1684
+ const field2 = jsonValue;
1685
+ const newSlug = field2.slug;
1686
+ if (newSlug) {
1687
+ field2.name = field2.name || slugToName(field2.slug);
1688
+ if (!existingLinkField) {
1689
+ dependencyStatements.push({
1690
+ statement: `${statement} RENAME COLUMN "${slug}" TO "${newSlug}"`,
1691
+ params: []
1692
+ });
1693
+ }
1694
+ }
1695
+ } else if (action === "drop" && !existingLinkField) {
1696
+ const systemFields = getSystemFields(existingModel.idPrefix);
1697
+ const isSystemField = systemFields.some((field2) => field2.slug === slug);
1698
+ if (isSystemField) {
1699
+ throw new RoninError({
1700
+ message: `The ${entity} "${slug}" is a system ${entity} and cannot be removed.`,
1701
+ code: "REQUIRED_MODEL_ENTITY"
1702
+ });
1703
+ }
1704
+ dependencyStatements.push({
1705
+ statement: `${statement} DROP COLUMN "${slug}"`,
1706
+ params: []
1769
1707
  });
1770
1708
  }
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
1709
  }
1780
- if (conditions.length > 0) {
1781
- if (conditions.length === 1) {
1782
- statement += `WHERE ${conditions[0]} `;
1783
- } else {
1784
- statement += `WHERE (${conditions.join(" ")}) `;
1710
+ const statementAction = action.toUpperCase();
1711
+ if (entity === "index") {
1712
+ const index = jsonValue;
1713
+ const indexName = convertToSnakeCase(slug);
1714
+ let statement = `${statementAction}${index?.unique ? " UNIQUE" : ""} INDEX "${indexName}"`;
1715
+ if (action === "create") {
1716
+ if (!Array.isArray(index.fields) || index.fields.length === 0) {
1717
+ throw new RoninError({
1718
+ message: `When ${actionReadable} ${PLURAL_MODEL_ENTITIES[entity]}, at least one field must be provided.`,
1719
+ code: "INVALID_MODEL_VALUE",
1720
+ fields: ["fields"]
1721
+ });
1722
+ }
1723
+ const columns = index.fields.map((field2) => {
1724
+ let fieldSelector = "";
1725
+ if ("slug" in field2) {
1726
+ ({ fieldSelector } = getFieldFromModel(existingModel, field2.slug, {
1727
+ modelEntityType: "index",
1728
+ modelEntityName: indexName
1729
+ }));
1730
+ } else if ("expression" in field2) {
1731
+ fieldSelector = parseFieldExpression(existingModel, "to", field2.expression);
1732
+ }
1733
+ if (field2.collation) fieldSelector += ` COLLATE ${field2.collation}`;
1734
+ if (field2.order) fieldSelector += ` ${field2.order}`;
1735
+ return fieldSelector;
1736
+ });
1737
+ statement += ` ON "${existingModel.table}" (${columns.join(", ")})`;
1738
+ if (index.filter) {
1739
+ const withStatement = handleWith(models, existingModel, null, index.filter);
1740
+ statement += ` WHERE (${withStatement})`;
1741
+ }
1785
1742
  }
1743
+ dependencyStatements.push({ statement, params: [] });
1786
1744
  }
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);
1745
+ if (entity === "trigger") {
1746
+ const triggerName = convertToSnakeCase(slug);
1747
+ let statement = `${statementAction} TRIGGER "${triggerName}"`;
1748
+ if (action === "create") {
1749
+ const trigger = jsonValue;
1750
+ const statementParts = [`${trigger.when} ${trigger.action}`];
1751
+ if (trigger.fields) {
1752
+ if (trigger.action !== "UPDATE") {
1753
+ throw new RoninError({
1754
+ message: `When ${actionReadable} ${PLURAL_MODEL_ENTITIES[entity]}, targeting specific fields requires the \`UPDATE\` action.`,
1755
+ code: "INVALID_MODEL_VALUE",
1756
+ fields: ["action"]
1757
+ });
1758
+ }
1759
+ const fieldSelectors = trigger.fields.map((field2) => {
1760
+ return getFieldFromModel(existingModel, field2.slug, {
1761
+ modelEntityType: "trigger",
1762
+ modelEntityName: triggerName
1763
+ }).fieldSelector;
1764
+ });
1765
+ statementParts.push(`OF (${fieldSelectors.join(", ")})`);
1766
+ }
1767
+ statementParts.push("ON", `"${existingModel.table}"`);
1768
+ if (trigger.filter || trigger.effects.some((query2) => findInObject(query2, QUERY_SYMBOLS.FIELD))) {
1769
+ statementParts.push("FOR EACH ROW");
1770
+ }
1771
+ if (trigger.filter) {
1772
+ const tableAlias = trigger.action === "DELETE" ? QUERY_SYMBOLS.FIELD_PARENT_OLD : QUERY_SYMBOLS.FIELD_PARENT_NEW;
1773
+ const withStatement = handleWith(
1774
+ models,
1775
+ { ...existingModel, tableAlias },
1776
+ null,
1777
+ trigger.filter
1778
+ );
1779
+ statementParts.push("WHEN", `(${withStatement})`);
1780
+ }
1781
+ const effectStatements = trigger.effects.map((effectQuery) => {
1782
+ return compileQueryInput(effectQuery, models, null, {
1783
+ returning: false,
1784
+ parentModel: existingModel
1785
+ }).main.statement;
1786
+ });
1787
+ statementParts.push("BEGIN");
1788
+ statementParts.push(`${effectStatements.join("; ")};`);
1789
+ statementParts.push("END");
1790
+ statement += ` ${statementParts.join(" ")}`;
1791
+ }
1792
+ dependencyStatements.push({ statement, params: [] });
1793
1793
  }
1794
- if (["add", "set", "remove"].includes(queryType) && returning) {
1795
- statement += "RETURNING * ";
1794
+ const field = `${QUERY_SYMBOLS.FIELD}${pluralType}`;
1795
+ let json;
1796
+ switch (action) {
1797
+ case "create": {
1798
+ const value = prepareStatementValue(statementParams, jsonValue);
1799
+ json = `json_insert(${field}, '$.${slug}', ${value})`;
1800
+ existingModel[pluralType] = [
1801
+ ...existingModel[pluralType] || [],
1802
+ jsonValue
1803
+ ];
1804
+ break;
1805
+ }
1806
+ case "alter": {
1807
+ const value = prepareStatementValue(statementParams, jsonValue);
1808
+ json = `json_set(${field}, '$.${slug}', json_patch(json_extract(${field}, '$.${slug}'), ${value}))`;
1809
+ const targetEntity = existingModel[pluralType];
1810
+ Object.assign(targetEntity[targetEntityIndex], jsonValue);
1811
+ break;
1812
+ }
1813
+ case "drop": {
1814
+ json = `json_remove(${field}, '$.${slug}')`;
1815
+ const targetEntity = existingModel[pluralType];
1816
+ targetEntity.splice(targetEntityIndex, 1);
1817
+ }
1796
1818
  }
1797
- const mainStatement = {
1798
- statement: statement.trimEnd(),
1799
- params: statementParams || []
1800
- };
1801
- if (returning) mainStatement.returning = true;
1819
+ handleSystemModels(models, dependencyStatements, modelBeforeUpdate, existingModel);
1802
1820
  return {
1803
- dependencies: dependencyStatements,
1804
- main: mainStatement,
1805
- loadedFields
1821
+ set: {
1822
+ model: {
1823
+ with: { slug: modelSlug },
1824
+ to: {
1825
+ [pluralType]: { [QUERY_SYMBOLS.EXPRESSION]: json }
1826
+ }
1827
+ }
1828
+ }
1806
1829
  };
1807
1830
  };
1808
1831
 
@@ -1825,21 +1848,25 @@ var Transaction = class {
1825
1848
  * @returns The composed SQL statements.
1826
1849
  */
1827
1850
  compileQueries = (queries, models, options) => {
1828
- const modelList = [
1829
- ROOT_MODEL,
1830
- ...models.flatMap((model) => getSystemModels(models, model)),
1831
- ...models
1851
+ const modelsWithAttributes = [ROOT_MODEL, ...models].map((model) => {
1852
+ return addDefaultModelAttributes(model, true);
1853
+ });
1854
+ const modelsWithFields = [
1855
+ ...modelsWithAttributes.flatMap((model) => {
1856
+ return getSystemModels(modelsWithAttributes, model);
1857
+ }),
1858
+ ...modelsWithAttributes
1832
1859
  ].map((model) => {
1833
1860
  return addDefaultModelFields(model, true);
1834
1861
  });
1835
- const modelListWithPresets = modelList.map((model) => {
1836
- return addDefaultModelPresets(modelList, model);
1862
+ const modelsWithPresets = modelsWithFields.map((model) => {
1863
+ return addDefaultModelPresets(modelsWithFields, model);
1837
1864
  });
1838
1865
  const statements = [];
1839
1866
  for (const query of queries) {
1840
1867
  const result = compileQueryInput(
1841
1868
  query,
1842
- modelListWithPresets,
1869
+ modelsWithPresets,
1843
1870
  options?.inlineParams ? null : [],
1844
1871
  { expandColumns: options?.expandColumns }
1845
1872
  );
@@ -1853,7 +1880,7 @@ var Transaction = class {
1853
1880
  }))
1854
1881
  );
1855
1882
  }
1856
- this.models = modelListWithPresets;
1883
+ this.models = modelsWithPresets;
1857
1884
  return statements;
1858
1885
  };
1859
1886
  formatRows(fields, rows, single, isMeta) {