@ronin/compiler 0.10.3 → 0.11.0-leo-ron-1083-experimental-225

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # RONIN Compiler
2
2
 
3
- This package compiles [RONIN queries](https://ronin.co/docs/queries) to SQL statements.
3
+ This package compiles [RONIN queries](https://ronin.co/docs/queries) to [SQLite](https://www.sqlite.org) statements.
4
4
 
5
5
  ## Setup
6
6
 
@@ -65,9 +65,11 @@ Once the RONIN queries have been compiled down to SQL statements, the statements
65
65
  executed and their results can be formatted by the compiler as well:
66
66
 
67
67
  ```typescript
68
- // `rows` are provided by the database engine
68
+ // Passing `rawResults` (rows being of arrays of values) provided by the database (ideal)
69
+ const results: Array<Result> = transaction.formatResults(rawResults);
69
70
 
70
- const results: Array<Result> = transaction.prepareResults(rows);
71
+ // Passing `objectResults` (rows being of objects) provided by a driver
72
+ const results: Array<Result> = transaction.formatResults(objectResults, false);
71
73
  ```
72
74
 
73
75
  #### Root Model
@@ -133,12 +135,13 @@ new Transaction(queries, {
133
135
  //
134
136
  // If the driver being used instead returns an object for every row, the driver must
135
137
  // ensure the uniqueness of every key in that object, which means prefixing duplicated
136
- // column names with the name of the respective table, if multiple tables are joined.
138
+ // column names with the name of the respective table, if multiple tables are joined
139
+ // (example for an object key: "table_name.column_name").
137
140
  //
138
141
  // Drivers that return objects for rows offer this behavior as an option that is
139
142
  // usually called "expand columns". If the driver being used does not offer such an
140
143
  // option, you can instead activate the option in the compiler, which results in longer
141
- // SQL statements because any duplicated column name is aliased.
144
+ // SQL statements because all column names are aliased.
142
145
  expandColumns: true
143
146
  });
144
147
  ```
package/dist/index.d.ts CHANGED
@@ -5867,7 +5867,7 @@ type ModelFieldBasics = {
5867
5867
  increment?: boolean;
5868
5868
  };
5869
5869
  type ModelFieldNormal = ModelFieldBasics & {
5870
- type: 'string' | 'number' | 'boolean' | 'date' | 'json' | 'group';
5870
+ type: 'string' | 'number' | 'boolean' | 'date' | 'json';
5871
5871
  };
5872
5872
  type ModelFieldReferenceAction = 'CASCADE' | 'RESTRICT' | 'SET NULL' | 'SET DEFAULT' | 'NO ACTION';
5873
5873
  type ModelFieldReference = ModelFieldBasics & {
@@ -5984,12 +5984,16 @@ type PublicModel<T extends Array<ModelField> = Array<ModelField>> = Omit<Partial
5984
5984
  identifiers?: Partial<Model['identifiers']>;
5985
5985
  };
5986
5986
 
5987
- type Row = Record<string, unknown>;
5987
+ type RawRow = Array<unknown>;
5988
+ type ObjectRow = Record<string, unknown>;
5988
5989
  type NativeRecord = Record<string, unknown> & {
5989
5990
  id: string;
5990
5991
  ronin: {
5992
+ locked: boolean;
5991
5993
  createdAt: Date;
5994
+ createdBy: string | null;
5992
5995
  updatedAt: Date;
5996
+ updatedBy: string | null;
5993
5997
  };
5994
5998
  };
5995
5999
  type SingleRecordResult = {
@@ -6005,6 +6009,45 @@ type AmountResult = {
6005
6009
  };
6006
6010
  type Result = SingleRecordResult | MultipleRecordResult | AmountResult;
6007
6011
 
6012
+ /**
6013
+ * A list of placeholders that can be located inside queries after those queries were
6014
+ * serialized into JSON objects.
6015
+ *
6016
+ * These placeholders are used to represent special keys and values. For example, if a
6017
+ * query is nested into a query, the nested query will be marked with `__RONIN_QUERY`,
6018
+ * which allows for distinguishing that nested query from an object of instructions.
6019
+ */
6020
+ declare const QUERY_SYMBOLS: {
6021
+ readonly QUERY: "__RONIN_QUERY";
6022
+ readonly EXPRESSION: "__RONIN_EXPRESSION";
6023
+ readonly FIELD: "__RONIN_FIELD_";
6024
+ readonly FIELD_PARENT: "__RONIN_FIELD_PARENT_";
6025
+ readonly FIELD_PARENT_OLD: "__RONIN_FIELD_PARENT_OLD_";
6026
+ readonly FIELD_PARENT_NEW: "__RONIN_FIELD_PARENT_NEW_";
6027
+ readonly VALUE: "__RONIN_VALUE";
6028
+ };
6029
+ type RoninErrorCode = 'MODEL_NOT_FOUND' | 'FIELD_NOT_FOUND' | 'INDEX_NOT_FOUND' | 'TRIGGER_NOT_FOUND' | 'PRESET_NOT_FOUND' | 'INVALID_WITH_VALUE' | 'INVALID_TO_VALUE' | 'INVALID_INCLUDING_VALUE' | 'INVALID_FOR_VALUE' | 'INVALID_BEFORE_OR_AFTER_INSTRUCTION' | 'INVALID_MODEL_VALUE' | 'MUTUALLY_EXCLUSIVE_INSTRUCTIONS' | 'MISSING_INSTRUCTION' | 'MISSING_FIELD';
6030
+ interface Issue {
6031
+ message: string;
6032
+ path: Array<string | number>;
6033
+ }
6034
+ interface Details {
6035
+ message: string;
6036
+ code: RoninErrorCode;
6037
+ field?: string;
6038
+ fields?: Array<string>;
6039
+ issues?: Array<Issue>;
6040
+ queries?: Array<Query> | null;
6041
+ }
6042
+ declare class RoninError extends Error {
6043
+ code: Details['code'];
6044
+ field?: Details['field'];
6045
+ fields?: Details['fields'];
6046
+ issues?: Details['issues'];
6047
+ queries?: Details['queries'];
6048
+ constructor(details: Details);
6049
+ }
6050
+
6008
6051
  interface TransactionOptions {
6009
6052
  /** A list of models that already exist in the database. */
6010
6053
  models?: Array<PublicModel>;
@@ -6020,6 +6063,7 @@ declare class Transaction {
6020
6063
  statements: Array<Statement>;
6021
6064
  models: Array<Model>;
6022
6065
  private queries;
6066
+ private fields;
6023
6067
  constructor(queries: Array<Query>, options?: TransactionOptions);
6024
6068
  /**
6025
6069
  * Composes SQL statements for the provided RONIN queries.
@@ -6031,10 +6075,11 @@ declare class Transaction {
6031
6075
  * @returns The composed SQL statements.
6032
6076
  */
6033
6077
  private compileQueries;
6034
- private formatRecord;
6035
- prepareResults(results: Array<Array<Row>>): Array<Result>;
6078
+ private formatRows;
6079
+ formatResults(results: Array<Array<RawRow>>, raw?: true): Array<Result>;
6080
+ formatResults(results: Array<Array<ObjectRow>>, raw?: false): Array<Result>;
6036
6081
  }
6037
6082
 
6038
6083
  declare const CLEAN_ROOT_MODEL: PublicModel;
6039
6084
 
6040
- export { type PublicModel as Model, type ModelField, type ModelIndex, type ModelPreset, type ModelTrigger, type Query, CLEAN_ROOT_MODEL as ROOT_MODEL, type Result, type Statement, Transaction };
6085
+ export { type PublicModel as Model, type ModelField, type ModelIndex, type ModelPreset, type ModelTrigger, QUERY_SYMBOLS, type Query, CLEAN_ROOT_MODEL as ROOT_MODEL, type Result, RoninError, type Statement, Transaction };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/utils/helpers.ts
2
2
  import { init as cuid } from "@paralleldrive/cuid2";
3
- var RONIN_MODEL_SYMBOLS = {
3
+ var QUERY_SYMBOLS = {
4
4
  // Represents a sub query.
5
5
  QUERY: "__RONIN_QUERY",
6
6
  // Represents an expression that should be evaluated.
@@ -17,9 +17,10 @@ var RONIN_MODEL_SYMBOLS = {
17
17
  VALUE: "__RONIN_VALUE"
18
18
  };
19
19
  var RONIN_MODEL_FIELD_REGEX = new RegExp(
20
- `${RONIN_MODEL_SYMBOLS.FIELD}[_a-zA-Z0-9]+`,
20
+ `${QUERY_SYMBOLS.FIELD}[_a-zA-Z0-9.]+`,
21
21
  "g"
22
22
  );
23
+ var composeIncludedTableAlias = (fieldSlug) => `including_${fieldSlug}`;
23
24
  var MODEL_ENTITY_ERROR_CODES = {
24
25
  field: "FIELD_NOT_FOUND",
25
26
  index: "INDEX_NOT_FOUND",
@@ -65,6 +66,23 @@ var convertToCamelCase = (str) => {
65
66
  return sanitize(str).split(SPLIT_REGEX).map((part, index) => index === 0 ? part.toLowerCase() : capitalize(part)).join("");
66
67
  };
67
68
  var isObject = (value) => value != null && typeof value === "object" && Array.isArray(value) === false;
69
+ var getSymbol = (value) => {
70
+ if (!isObject(value)) return null;
71
+ const objectValue = value;
72
+ if (QUERY_SYMBOLS.QUERY in objectValue) {
73
+ return {
74
+ type: "query",
75
+ value: objectValue[QUERY_SYMBOLS.QUERY]
76
+ };
77
+ }
78
+ if (QUERY_SYMBOLS.EXPRESSION in objectValue) {
79
+ return {
80
+ type: "expression",
81
+ value: objectValue[QUERY_SYMBOLS.EXPRESSION]
82
+ };
83
+ }
84
+ return null;
85
+ };
68
86
  var findInObject = (obj, pattern, replacer) => {
69
87
  let found = false;
70
88
  for (const key in obj) {
@@ -85,10 +103,11 @@ var findInObject = (obj, pattern, replacer) => {
85
103
  var flatten = (obj, prefix = "", res = {}) => {
86
104
  for (const key in obj) {
87
105
  const path = prefix ? `${prefix}.${key}` : key;
88
- if (typeof obj[key] === "object" && obj[key] !== null) {
89
- flatten(obj[key], path, res);
106
+ const value = obj[key];
107
+ if (typeof value === "object" && value !== null && !getSymbol(value)) {
108
+ flatten(value, path, res);
90
109
  } else {
91
- res[path] = obj[key];
110
+ res[path] = value;
92
111
  }
93
112
  }
94
113
  return res;
@@ -99,7 +118,11 @@ var omit = (obj, properties) => Object.fromEntries(
99
118
  var expand = (obj) => {
100
119
  return Object.entries(obj).reduce((res, [key, val]) => {
101
120
  key.split(".").reduce((acc, part, i, arr) => {
102
- acc[part] = i === arr.length - 1 ? val : acc[part] || {};
121
+ if (i === arr.length - 1) {
122
+ acc[part] = val;
123
+ } else {
124
+ acc[part] = typeof acc[part] === "object" && acc[part] !== null ? acc[part] : {};
125
+ }
103
126
  return acc[part];
104
127
  }, res);
105
128
  return res;
@@ -108,6 +131,32 @@ var expand = (obj) => {
108
131
  var getProperty = (obj, path) => {
109
132
  return path.split(".").reduce((acc, key) => acc?.[key], obj);
110
133
  };
134
+ var toInt = (value, defaultValue) => {
135
+ const def = defaultValue === void 0 ? 0 : defaultValue;
136
+ if (value === null || value === void 0) return def;
137
+ const result = Number.parseInt(value);
138
+ return Number.isNaN(result) ? def : result;
139
+ };
140
+ var setProperty = (initial, path, value) => {
141
+ if (!initial) return setProperty({}, path, value);
142
+ if (!path || value === void 0) return initial;
143
+ const segments = path.split(/[.[\]]/g).filter((x) => !!x.trim());
144
+ const _set = (node) => {
145
+ if (segments.length > 1) {
146
+ const key = segments.shift();
147
+ const nextIsNum = toInt(segments[0], null) !== null;
148
+ if (typeof node[key] !== "object" || node[key] === null) {
149
+ node[key] = nextIsNum ? [] : {};
150
+ }
151
+ _set(node[key]);
152
+ } else {
153
+ node[segments[0]] = value;
154
+ }
155
+ };
156
+ const cloned = structuredClone(initial);
157
+ _set(cloned);
158
+ return cloned;
159
+ };
111
160
  var splitQuery = (query) => {
112
161
  const queryType = Object.keys(query)[0];
113
162
  const queryModel = Object.keys(query[queryType])[0];
@@ -133,15 +182,15 @@ var prepareStatementValue = (statementParams, value) => {
133
182
  };
134
183
  var parseFieldExpression = (model, instructionName, expression, parentModel) => {
135
184
  return expression.replace(RONIN_MODEL_FIELD_REGEX, (match) => {
136
- let toReplace = RONIN_MODEL_SYMBOLS.FIELD;
185
+ let toReplace = QUERY_SYMBOLS.FIELD;
137
186
  let rootModel = model;
138
- if (match.startsWith(RONIN_MODEL_SYMBOLS.FIELD_PARENT)) {
187
+ if (match.startsWith(QUERY_SYMBOLS.FIELD_PARENT)) {
139
188
  rootModel = parentModel;
140
- toReplace = RONIN_MODEL_SYMBOLS.FIELD_PARENT;
141
- if (match.startsWith(RONIN_MODEL_SYMBOLS.FIELD_PARENT_OLD)) {
142
- rootModel.tableAlias = toReplace = RONIN_MODEL_SYMBOLS.FIELD_PARENT_OLD;
143
- } else if (match.startsWith(RONIN_MODEL_SYMBOLS.FIELD_PARENT_NEW)) {
144
- rootModel.tableAlias = toReplace = RONIN_MODEL_SYMBOLS.FIELD_PARENT_NEW;
189
+ toReplace = QUERY_SYMBOLS.FIELD_PARENT;
190
+ if (match.startsWith(QUERY_SYMBOLS.FIELD_PARENT_OLD)) {
191
+ rootModel.tableAlias = toReplace = QUERY_SYMBOLS.FIELD_PARENT_OLD;
192
+ } else if (match.startsWith(QUERY_SYMBOLS.FIELD_PARENT_NEW)) {
193
+ rootModel.tableAlias = toReplace = QUERY_SYMBOLS.FIELD_PARENT_NEW;
145
194
  }
146
195
  }
147
196
  const fieldSlug = match.replace(toReplace, "");
@@ -190,47 +239,52 @@ var composeConditions = (models, model, statementParams, instructionName, value,
190
239
  return conditions.join(" AND ");
191
240
  }
192
241
  if (options.fieldSlug) {
193
- const fieldDetails = getFieldFromModel(model, options.fieldSlug, instructionName);
194
- const { field: modelField } = fieldDetails;
195
- const consumeJSON = modelField.type === "json" && instructionName === "to";
196
- if (!(isObject(value) || Array.isArray(value)) || getSymbol(value) || consumeJSON) {
197
- return composeFieldValues(
198
- models,
199
- model,
200
- statementParams,
201
- instructionName,
202
- value,
203
- { ...options, fieldSlug: options.fieldSlug }
204
- );
205
- }
206
- if (modelField.type === "link" && isNested) {
207
- const keys = Object.keys(value);
208
- const values = Object.values(value);
209
- let recordTarget;
210
- if (keys.length === 1 && keys[0] === "id") {
211
- recordTarget = values[0];
212
- } else {
213
- const relatedModel = getModelBySlug(models, modelField.target);
214
- const subQuery = {
215
- get: {
216
- [relatedModel.slug]: {
217
- with: value,
218
- selecting: ["id"]
242
+ const childField = model.fields.some(({ slug }) => {
243
+ return slug.includes(".") && slug.split(".")[0] === options.fieldSlug;
244
+ });
245
+ if (!childField) {
246
+ const fieldDetails = getFieldFromModel(model, options.fieldSlug, instructionName);
247
+ const { field: modelField } = fieldDetails || {};
248
+ const consumeJSON = modelField?.type === "json" && instructionName === "to";
249
+ if (modelField && !(isObject(value) || Array.isArray(value)) || getSymbol(value) || consumeJSON) {
250
+ return composeFieldValues(
251
+ models,
252
+ model,
253
+ statementParams,
254
+ instructionName,
255
+ value,
256
+ { ...options, fieldSlug: options.fieldSlug }
257
+ );
258
+ }
259
+ if (modelField?.type === "link" && isNested) {
260
+ const keys = Object.keys(value);
261
+ const values = Object.values(value);
262
+ let recordTarget;
263
+ if (keys.length === 1 && keys[0] === "id") {
264
+ recordTarget = values[0];
265
+ } else {
266
+ const relatedModel = getModelBySlug(models, modelField.target);
267
+ const subQuery = {
268
+ get: {
269
+ [relatedModel.slug]: {
270
+ with: value,
271
+ selecting: ["id"]
272
+ }
219
273
  }
220
- }
221
- };
222
- recordTarget = {
223
- [RONIN_MODEL_SYMBOLS.QUERY]: subQuery
224
- };
274
+ };
275
+ recordTarget = {
276
+ [QUERY_SYMBOLS.QUERY]: subQuery
277
+ };
278
+ }
279
+ return composeConditions(
280
+ models,
281
+ model,
282
+ statementParams,
283
+ instructionName,
284
+ recordTarget,
285
+ options
286
+ );
225
287
  }
226
- return composeConditions(
227
- models,
228
- model,
229
- statementParams,
230
- instructionName,
231
- recordTarget,
232
- options
233
- );
234
288
  }
235
289
  }
236
290
  if (isNested) {
@@ -278,23 +332,6 @@ var formatIdentifiers = ({ identifiers }, queryInstructions) => {
278
332
  [type]: newNestedInstructions
279
333
  };
280
334
  };
281
- var getSymbol = (value) => {
282
- if (!isObject(value)) return null;
283
- const objectValue = value;
284
- if (RONIN_MODEL_SYMBOLS.QUERY in objectValue) {
285
- return {
286
- type: "query",
287
- value: objectValue[RONIN_MODEL_SYMBOLS.QUERY]
288
- };
289
- }
290
- if (RONIN_MODEL_SYMBOLS.EXPRESSION in objectValue) {
291
- return {
292
- type: "expression",
293
- value: objectValue[RONIN_MODEL_SYMBOLS.EXPRESSION]
294
- };
295
- }
296
- return null;
297
- };
298
335
 
299
336
  // src/instructions/with.ts
300
337
  var getMatcher = (value, negative) => {
@@ -347,7 +384,7 @@ var getModelBySlug = (models, slug) => {
347
384
  };
348
385
  var composeAssociationModelSlug = (model, field) => convertToCamelCase(`ronin_link_${model.slug}_${field.slug}`);
349
386
  var getFieldSelector = (model, field, fieldPath, instructionName) => {
350
- const symbol = model.tableAlias?.startsWith(RONIN_MODEL_SYMBOLS.FIELD_PARENT) ? `${model.tableAlias.replace(RONIN_MODEL_SYMBOLS.FIELD_PARENT, "").slice(0, -1)}.` : "";
387
+ const symbol = model.tableAlias?.startsWith(QUERY_SYMBOLS.FIELD_PARENT) ? `${model.tableAlias.replace(QUERY_SYMBOLS.FIELD_PARENT, "").slice(0, -1)}.` : "";
351
388
  const tablePrefix = symbol || (model.tableAlias ? `"${model.tableAlias}".` : "");
352
389
  if (field.type === "json" && instructionName !== "to") {
353
390
  const dotParts = fieldPath.split(".");
@@ -357,7 +394,7 @@ var getFieldSelector = (model, field, fieldPath, instructionName) => {
357
394
  }
358
395
  return `${tablePrefix}"${fieldPath}"`;
359
396
  };
360
- var getFieldFromModel = (model, fieldPath, instructionName) => {
397
+ function getFieldFromModel(model, fieldPath, instructionName, shouldThrow = true) {
361
398
  const errorPrefix = `Field "${fieldPath}" defined for \`${instructionName}\``;
362
399
  const modelFields = model.fields || [];
363
400
  let modelField;
@@ -375,16 +412,19 @@ var getFieldFromModel = (model, fieldPath, instructionName) => {
375
412
  }
376
413
  modelField = modelFields.find((field) => field.slug === fieldPath);
377
414
  if (!modelField) {
378
- throw new RoninError({
379
- message: `${errorPrefix} does not exist in model "${model.name}".`,
380
- code: "FIELD_NOT_FOUND",
381
- field: fieldPath,
382
- queries: null
383
- });
415
+ if (shouldThrow) {
416
+ throw new RoninError({
417
+ message: `${errorPrefix} does not exist in model "${model.name}".`,
418
+ code: "FIELD_NOT_FOUND",
419
+ field: fieldPath,
420
+ queries: null
421
+ });
422
+ }
423
+ return null;
384
424
  }
385
425
  const fieldSelector = getFieldSelector(model, modelField, fieldPath, instructionName);
386
426
  return { field: modelField, fieldSelector };
387
- };
427
+ }
388
428
  var slugToName = (slug) => {
389
429
  const name = slug.replace(/([a-z])([A-Z])/g, "$1 $2");
390
430
  return title(name);
@@ -440,11 +480,6 @@ var SYSTEM_FIELDS = [
440
480
  slug: "id",
441
481
  displayAs: "single-line"
442
482
  },
443
- {
444
- name: "RONIN",
445
- type: "group",
446
- slug: "ronin"
447
- },
448
483
  {
449
484
  name: "RONIN - Locked",
450
485
  type: "boolean",
@@ -488,7 +523,6 @@ var ROOT_MODEL = {
488
523
  { slug: "pluralSlug", type: "string" },
489
524
  { slug: "idPrefix", type: "string" },
490
525
  { slug: "table", type: "string" },
491
- { slug: "identifiers", type: "group" },
492
526
  { slug: "identifiers.name", type: "string" },
493
527
  { slug: "identifiers.slug", type: "string" },
494
528
  // Providing an empty object as a default value allows us to use `json_insert`
@@ -543,14 +577,14 @@ var addDefaultModelPresets = (list, model) => {
543
577
  instructions: {
544
578
  including: {
545
579
  [field.slug]: {
546
- [RONIN_MODEL_SYMBOLS.QUERY]: {
580
+ [QUERY_SYMBOLS.QUERY]: {
547
581
  get: {
548
582
  [relatedModel.slug]: {
549
583
  with: {
550
584
  // Compare the `id` field of the related model to the link field on
551
585
  // the root model (`field.slug`).
552
586
  id: {
553
- [RONIN_MODEL_SYMBOLS.EXPRESSION]: `${RONIN_MODEL_SYMBOLS.FIELD_PARENT}${field.slug}`
587
+ [QUERY_SYMBOLS.EXPRESSION]: `${QUERY_SYMBOLS.FIELD_PARENT}${field.slug}`
554
588
  }
555
589
  }
556
590
  }
@@ -578,12 +612,12 @@ var addDefaultModelPresets = (list, model) => {
578
612
  instructions: {
579
613
  including: {
580
614
  [presetSlug]: {
581
- [RONIN_MODEL_SYMBOLS.QUERY]: {
615
+ [QUERY_SYMBOLS.QUERY]: {
582
616
  get: {
583
617
  [pluralSlug]: {
584
618
  with: {
585
619
  [childField.slug]: {
586
- [RONIN_MODEL_SYMBOLS.EXPRESSION]: `${RONIN_MODEL_SYMBOLS.FIELD_PARENT}id`
620
+ [QUERY_SYMBOLS.EXPRESSION]: `${QUERY_SYMBOLS.FIELD_PARENT}id`
587
621
  }
588
622
  }
589
623
  }
@@ -610,7 +644,6 @@ var typesInSQLite = {
610
644
  json: "TEXT"
611
645
  };
612
646
  var getFieldStatement = (models, model, field) => {
613
- if (field.type === "group") return null;
614
647
  let statement = `"${field.slug}" ${typesInSQLite[field.type]}`;
615
648
  if (field.slug === "id") statement += " PRIMARY KEY";
616
649
  if (field.unique === true) statement += " UNIQUE";
@@ -850,11 +883,11 @@ var transformMetaQuery = (models, dependencyStatements, statementParams, query)
850
883
  statementParts.push(`OF (${fieldSelectors.join(", ")})`);
851
884
  }
852
885
  statementParts.push("ON", `"${existingModel.table}"`);
853
- if (trigger.filter || trigger.effects.some((query2) => findInObject(query2, RONIN_MODEL_SYMBOLS.FIELD))) {
886
+ if (trigger.filter || trigger.effects.some((query2) => findInObject(query2, QUERY_SYMBOLS.FIELD))) {
854
887
  statementParts.push("FOR EACH ROW");
855
888
  }
856
889
  if (trigger.filter) {
857
- const tableAlias = trigger.action === "DELETE" ? RONIN_MODEL_SYMBOLS.FIELD_PARENT_OLD : RONIN_MODEL_SYMBOLS.FIELD_PARENT_NEW;
890
+ const tableAlias = trigger.action === "DELETE" ? QUERY_SYMBOLS.FIELD_PARENT_OLD : QUERY_SYMBOLS.FIELD_PARENT_NEW;
858
891
  const withStatement = handleWith(
859
892
  models,
860
893
  { ...existingModel, tableAlias },
@@ -876,7 +909,7 @@ var transformMetaQuery = (models, dependencyStatements, statementParams, query)
876
909
  }
877
910
  dependencyStatements.push({ statement, params });
878
911
  }
879
- const field = `${RONIN_MODEL_SYMBOLS.FIELD}${pluralType}`;
912
+ const field = `${QUERY_SYMBOLS.FIELD}${pluralType}`;
880
913
  let json;
881
914
  switch (action) {
882
915
  case "create": {
@@ -940,7 +973,7 @@ var transformMetaQuery = (models, dependencyStatements, statementParams, query)
940
973
  model: {
941
974
  with: { slug: modelSlug },
942
975
  to: {
943
- [pluralType]: { [RONIN_MODEL_SYMBOLS.EXPRESSION]: json }
976
+ [pluralType]: { [QUERY_SYMBOLS.EXPRESSION]: json }
944
977
  }
945
978
  }
946
979
  }
@@ -1059,8 +1092,8 @@ var handleFor = (model, instructions) => {
1059
1092
  if (arg !== null) {
1060
1093
  findInObject(
1061
1094
  replacedForFilter,
1062
- RONIN_MODEL_SYMBOLS.VALUE,
1063
- (match) => match.replace(RONIN_MODEL_SYMBOLS.VALUE, arg)
1095
+ QUERY_SYMBOLS.VALUE,
1096
+ (match) => match.replace(QUERY_SYMBOLS.VALUE, arg)
1064
1097
  );
1065
1098
  }
1066
1099
  for (const subInstruction in replacedForFilter) {
@@ -1102,7 +1135,7 @@ var handleIncluding = (models, model, statementParams, instruction) => {
1102
1135
  const relatedModel = getModelBySlug(models, queryModel);
1103
1136
  let joinType = "LEFT";
1104
1137
  let relatedTableSelector = `"${relatedModel.table}"`;
1105
- const tableAlias = `including_${ephemeralFieldSlug}`;
1138
+ const tableAlias = composeIncludedTableAlias(ephemeralFieldSlug);
1106
1139
  const single = queryModel !== relatedModel.pluralSlug;
1107
1140
  if (!modifiableQueryInstructions?.with) {
1108
1141
  joinType = "CROSS";
@@ -1124,11 +1157,10 @@ var handleIncluding = (models, model, statementParams, instruction) => {
1124
1157
  relatedTableSelector = `(${subSelect.main.statement})`;
1125
1158
  }
1126
1159
  statement += `${joinType} JOIN ${relatedTableSelector} as ${tableAlias}`;
1127
- model.tableAlias = model.table;
1160
+ model.tableAlias = model.tableAlias || model.table;
1128
1161
  if (joinType === "LEFT") {
1129
1162
  if (!single) {
1130
1163
  tableSubQuery = `SELECT * FROM "${model.table}" LIMIT 1`;
1131
- model.tableAlias = `sub_${model.table}`;
1132
1164
  }
1133
1165
  const subStatement = composeConditions(
1134
1166
  models,
@@ -1183,36 +1215,74 @@ var handleOrderedBy = (model, instruction) => {
1183
1215
  };
1184
1216
 
1185
1217
  // src/instructions/selecting.ts
1186
- var handleSelecting = (model, statementParams, instructions) => {
1218
+ var handleSelecting = (models, model, statementParams, instructions, options) => {
1219
+ let loadedFields = [];
1220
+ let statement = "*";
1187
1221
  let isJoining = false;
1188
- let statement = instructions.selecting ? instructions.selecting.map((slug) => {
1189
- return getFieldFromModel(model, slug, "selecting").fieldSelector;
1190
- }).join(", ") : "*";
1191
1222
  if (instructions.including) {
1192
- const filteredObject = Object.entries(instructions.including).map(([key, value]) => {
1223
+ const flatObject = flatten(instructions.including);
1224
+ instructions.including = {};
1225
+ for (const [key, value] of Object.entries(flatObject)) {
1193
1226
  const symbol = getSymbol(value);
1194
- if (symbol) {
1195
- if (symbol.type === "query") {
1196
- isJoining = true;
1197
- return null;
1227
+ if (symbol?.type === "query") {
1228
+ isJoining = true;
1229
+ const { queryModel, queryInstructions } = splitQuery(symbol.value);
1230
+ const subQueryModel = getModelBySlug(models, queryModel);
1231
+ const tableAlias = composeIncludedTableAlias(key);
1232
+ const single = queryModel !== subQueryModel.pluralSlug;
1233
+ if (!single) {
1234
+ model.tableAlias = `sub_${model.table}`;
1198
1235
  }
1199
- if (symbol.type === "expression") {
1200
- value = parseFieldExpression(model, "including", symbol.value);
1236
+ const queryModelFields = queryInstructions?.selecting ? subQueryModel.fields.filter((field) => {
1237
+ return queryInstructions.selecting?.includes(field.slug);
1238
+ }) : subQueryModel.fields;
1239
+ for (const field of queryModelFields) {
1240
+ loadedFields.push({ ...field, parentField: key });
1241
+ if (options?.expandColumns) {
1242
+ const newValue2 = parseFieldExpression(
1243
+ { ...subQueryModel, tableAlias },
1244
+ "including",
1245
+ `${QUERY_SYMBOLS.FIELD}${field.slug}`
1246
+ );
1247
+ instructions.including[`${tableAlias}.${field.slug}`] = newValue2;
1248
+ }
1201
1249
  }
1250
+ continue;
1202
1251
  }
1203
- return [key, value];
1204
- }).filter((entry) => entry !== null);
1205
- const newObjectEntries = Object.entries(flatten(Object.fromEntries(filteredObject)));
1206
- if (newObjectEntries.length > 0) {
1207
- statement += ", ";
1208
- statement += newObjectEntries.map(([key, value]) => {
1209
- if (typeof value === "string" && value.startsWith('"'))
1210
- return `(${value}) as "${key}"`;
1211
- return `${prepareStatementValue(statementParams, value)} as "${key}"`;
1212
- }).join(", ");
1252
+ let newValue = value;
1253
+ if (symbol?.type === "expression") {
1254
+ newValue = `(${parseFieldExpression(model, "including", symbol.value)})`;
1255
+ } else {
1256
+ newValue = prepareStatementValue(statementParams, value);
1257
+ }
1258
+ instructions.including[key] = newValue;
1213
1259
  }
1214
1260
  }
1215
- return { columns: statement, isJoining };
1261
+ const expandColumns = isJoining && options?.expandColumns;
1262
+ if (expandColumns) {
1263
+ instructions.selecting = model.fields.map((field) => field.slug);
1264
+ }
1265
+ if (instructions.selecting) {
1266
+ const usableModel = expandColumns ? { ...model, tableAlias: model.tableAlias || model.table } : model;
1267
+ const selectedFields = [];
1268
+ statement = instructions.selecting.map((slug) => {
1269
+ const { field, fieldSelector } = getFieldFromModel(
1270
+ usableModel,
1271
+ slug,
1272
+ "selecting"
1273
+ );
1274
+ selectedFields.push(field);
1275
+ return fieldSelector;
1276
+ }).join(", ");
1277
+ loadedFields = [...selectedFields, ...loadedFields];
1278
+ } else {
1279
+ loadedFields = [...model.fields, ...loadedFields];
1280
+ }
1281
+ if (instructions.including && Object.keys(instructions.including).length > 0) {
1282
+ statement += ", ";
1283
+ statement += Object.entries(instructions.including).map(([key, value]) => `${value} as "${key}"`).join(", ");
1284
+ }
1285
+ return { columns: statement, isJoining, loadedFields };
1216
1286
  };
1217
1287
 
1218
1288
  // src/instructions/to.ts
@@ -1279,8 +1349,8 @@ var handleTo = (models, model, statementParams, queryType, dependencyStatements,
1279
1349
  Object.assign(toInstruction, defaultFields);
1280
1350
  for (const fieldSlug in toInstruction) {
1281
1351
  const fieldValue = toInstruction[fieldSlug];
1282
- const fieldDetails = getFieldFromModel(model, fieldSlug, "to");
1283
- if (fieldDetails.field.type === "link" && fieldDetails.field.kind === "many") {
1352
+ const fieldDetails = getFieldFromModel(model, fieldSlug, "to", false);
1353
+ if (fieldDetails?.field.type === "link" && fieldDetails.field.kind === "many") {
1284
1354
  delete toInstruction[fieldSlug];
1285
1355
  const associativeModelSlug = composeAssociationModelSlug(model, fieldDetails.field);
1286
1356
  const composeStatement = (subQueryType, value) => {
@@ -1345,7 +1415,8 @@ var compileQueryInput = (defaultQuery, models, statementParams, options) => {
1345
1415
  statementParams,
1346
1416
  defaultQuery
1347
1417
  );
1348
- if (query === null) return { dependencies: [], main: dependencyStatements[0] };
1418
+ if (query === null)
1419
+ return { dependencies: [], main: dependencyStatements[0], loadedFields: [] };
1349
1420
  const parsedQuery = splitQuery(query);
1350
1421
  const { queryType, queryModel, queryInstructions } = parsedQuery;
1351
1422
  const model = getModelBySlug(models, queryModel);
@@ -1355,10 +1426,16 @@ var compileQueryInput = (defaultQuery, models, statementParams, options) => {
1355
1426
  if (instructions && Object.hasOwn(instructions, "for")) {
1356
1427
  instructions = handleFor(model, instructions);
1357
1428
  }
1358
- const { columns, isJoining } = handleSelecting(model, statementParams, {
1359
- selecting: instructions?.selecting,
1360
- including: instructions?.including
1361
- });
1429
+ const { columns, isJoining, loadedFields } = handleSelecting(
1430
+ models,
1431
+ model,
1432
+ statementParams,
1433
+ {
1434
+ selecting: instructions?.selecting,
1435
+ including: instructions?.including
1436
+ },
1437
+ options
1438
+ );
1362
1439
  let statement = "";
1363
1440
  switch (queryType) {
1364
1441
  case "get":
@@ -1478,7 +1555,8 @@ var compileQueryInput = (defaultQuery, models, statementParams, options) => {
1478
1555
  if (returning) mainStatement.returning = true;
1479
1556
  return {
1480
1557
  dependencies: dependencyStatements,
1481
- main: mainStatement
1558
+ main: mainStatement,
1559
+ loadedFields
1482
1560
  };
1483
1561
  };
1484
1562
 
@@ -1487,6 +1565,7 @@ var Transaction = class {
1487
1565
  statements;
1488
1566
  models = [];
1489
1567
  queries;
1568
+ fields = [];
1490
1569
  constructor(queries, options) {
1491
1570
  const models = options?.models || [];
1492
1571
  this.statements = this.compileQueries(queries, models, options);
@@ -1518,46 +1597,83 @@ var Transaction = class {
1518
1597
  const result = compileQueryInput(
1519
1598
  query,
1520
1599
  modelListWithPresets,
1521
- options?.inlineParams ? null : []
1600
+ options?.inlineParams ? null : [],
1601
+ { expandColumns: options?.expandColumns }
1522
1602
  );
1523
1603
  dependencyStatements.push(...result.dependencies);
1524
1604
  mainStatements.push(result.main);
1605
+ this.fields.push(result.loadedFields);
1525
1606
  }
1526
1607
  this.models = modelListWithPresets;
1527
1608
  return [...dependencyStatements, ...mainStatements];
1528
1609
  };
1529
- formatRecord(model, record) {
1530
- const formattedRecord = { ...record };
1531
- for (const key in record) {
1532
- const { field } = getFieldFromModel(model, key, "to");
1533
- if (field.type === "json") {
1534
- formattedRecord[key] = JSON.parse(record[key]);
1535
- continue;
1610
+ formatRows(fields, rows, single) {
1611
+ const records = [];
1612
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
1613
+ const row = rows[rowIndex];
1614
+ for (let valueIndex = 0; valueIndex < row.length; valueIndex++) {
1615
+ const value = row[valueIndex];
1616
+ const field = fields[valueIndex];
1617
+ let newSlug = field.slug;
1618
+ let newValue = value;
1619
+ if (field.type === "json") {
1620
+ newValue = JSON.parse(value);
1621
+ } else if (field.type === "boolean") {
1622
+ newValue = Boolean(value);
1623
+ }
1624
+ const parentFieldSlug = field.parentField;
1625
+ if (parentFieldSlug) {
1626
+ if (rows.length === 1) {
1627
+ newSlug = `${parentFieldSlug}.${field.slug}`;
1628
+ } else {
1629
+ const fieldPath = `${parentFieldSlug}[${rowIndex}].${field.slug}`;
1630
+ records[0] = setProperty(records[0], fieldPath, newValue);
1631
+ continue;
1632
+ }
1633
+ }
1634
+ records[rowIndex] = setProperty(records[rowIndex], newSlug, newValue);
1536
1635
  }
1537
- formattedRecord[key] = record[key];
1538
1636
  }
1539
- return expand(formattedRecord);
1637
+ return single ? records[0] : records;
1540
1638
  }
1541
- prepareResults(results) {
1639
+ /**
1640
+ * Format the results returned from the database into RONIN records.
1641
+ *
1642
+ * @param results - A list of results from the database, where each result is an array
1643
+ * of rows.
1644
+ * @param raw - By default, rows are expected to be arrays of values, which is how SQL
1645
+ * databases return rows by default. If the driver being used returns rows as objects
1646
+ * instead, this option should be set to `false`.
1647
+ *
1648
+ * @returns A list of formatted RONIN results, where each result is either a single
1649
+ * RONIN record, an array of RONIN records, or a RONIN count result.
1650
+ */
1651
+ formatResults(results, raw = true) {
1542
1652
  const relevantResults = results.filter((_, index) => {
1543
1653
  return this.statements[index].returning;
1544
1654
  });
1545
- return relevantResults.map((result, index) => {
1655
+ const normalizedResults = raw ? relevantResults : relevantResults.map((rows) => {
1656
+ return rows.map((row) => {
1657
+ if (Array.isArray(row)) return row;
1658
+ if (row["COUNT(*)"]) return [row["COUNT(*)"]];
1659
+ return Object.values(row);
1660
+ });
1661
+ });
1662
+ return normalizedResults.map((rows, index) => {
1546
1663
  const query = this.queries.at(-index);
1664
+ const fields = this.fields.at(-index);
1547
1665
  const { queryType, queryModel, queryInstructions } = splitQuery(query);
1548
1666
  const model = getModelBySlug(this.models, queryModel);
1549
1667
  if (queryType === "count") {
1550
- return { amount: result[0]["COUNT(*)"] };
1668
+ return { amount: rows[0][0] };
1551
1669
  }
1552
1670
  const single = queryModel !== model.pluralSlug;
1553
1671
  if (single) {
1554
- return { record: this.formatRecord(model, result[0]) };
1672
+ return { record: rows[0] ? this.formatRows(fields, rows, single) : null };
1555
1673
  }
1556
1674
  const pageSize = queryInstructions?.limitedTo;
1557
1675
  const output = {
1558
- records: result.map((resultItem) => {
1559
- return this.formatRecord(model, resultItem);
1560
- })
1676
+ records: this.formatRows(fields, rows, single)
1561
1677
  };
1562
1678
  if (pageSize && output.records.length > 0) {
1563
1679
  if (queryInstructions?.before || queryInstructions?.after) {
@@ -1585,6 +1701,8 @@ var Transaction = class {
1585
1701
  };
1586
1702
  var CLEAN_ROOT_MODEL = omit(ROOT_MODEL, ["system"]);
1587
1703
  export {
1704
+ QUERY_SYMBOLS,
1588
1705
  CLEAN_ROOT_MODEL as ROOT_MODEL,
1706
+ RoninError,
1589
1707
  Transaction
1590
1708
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ronin/compiler",
3
- "version": "0.10.3",
3
+ "version": "0.11.0-leo-ron-1083-experimental-225",
4
4
  "type": "module",
5
5
  "description": "Compiles RONIN queries to SQL statements.",
6
6
  "publishConfig": {