@ronin/compiler 0.13.9 → 0.13.10-leo-ron-1099-experimental-304

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