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

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,26 @@ var expand = (obj) => {
108
131
  var getProperty = (obj, path) => {
109
132
  return path.split(".").reduce((acc, key) => acc?.[key], obj);
110
133
  };
134
+ var setProperty = (obj, path, value) => {
135
+ if (!obj) return setProperty({}, path, value);
136
+ if (!path || value === void 0) return obj;
137
+ const segments = path.split(/[.[\]]/g).filter((x) => !!x.trim());
138
+ const _set = (node) => {
139
+ if (segments.length > 1) {
140
+ const key = segments.shift();
141
+ const nextIsNum = !Number.isNaN(Number.parseInt(segments[0]));
142
+ if (typeof node[key] !== "object" || node[key] === null) {
143
+ node[key] = nextIsNum ? [] : {};
144
+ }
145
+ _set(node[key]);
146
+ } else {
147
+ node[segments[0]] = value;
148
+ }
149
+ };
150
+ const cloned = structuredClone(obj);
151
+ _set(cloned);
152
+ return cloned;
153
+ };
111
154
  var splitQuery = (query) => {
112
155
  const queryType = Object.keys(query)[0];
113
156
  const queryModel = Object.keys(query[queryType])[0];
@@ -133,15 +176,15 @@ var prepareStatementValue = (statementParams, value) => {
133
176
  };
134
177
  var parseFieldExpression = (model, instructionName, expression, parentModel) => {
135
178
  return expression.replace(RONIN_MODEL_FIELD_REGEX, (match) => {
136
- let toReplace = RONIN_MODEL_SYMBOLS.FIELD;
179
+ let toReplace = QUERY_SYMBOLS.FIELD;
137
180
  let rootModel = model;
138
- if (match.startsWith(RONIN_MODEL_SYMBOLS.FIELD_PARENT)) {
181
+ if (match.startsWith(QUERY_SYMBOLS.FIELD_PARENT)) {
139
182
  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;
183
+ toReplace = QUERY_SYMBOLS.FIELD_PARENT;
184
+ if (match.startsWith(QUERY_SYMBOLS.FIELD_PARENT_OLD)) {
185
+ rootModel.tableAlias = toReplace = QUERY_SYMBOLS.FIELD_PARENT_OLD;
186
+ } else if (match.startsWith(QUERY_SYMBOLS.FIELD_PARENT_NEW)) {
187
+ rootModel.tableAlias = toReplace = QUERY_SYMBOLS.FIELD_PARENT_NEW;
145
188
  }
146
189
  }
147
190
  const fieldSlug = match.replace(toReplace, "");
@@ -190,47 +233,52 @@ var composeConditions = (models, model, statementParams, instructionName, value,
190
233
  return conditions.join(" AND ");
191
234
  }
192
235
  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"]
236
+ const childField = model.fields.some(({ slug }) => {
237
+ return slug.includes(".") && slug.split(".")[0] === options.fieldSlug;
238
+ });
239
+ if (!childField) {
240
+ const fieldDetails = getFieldFromModel(model, options.fieldSlug, instructionName);
241
+ const { field: modelField } = fieldDetails || {};
242
+ const consumeJSON = modelField?.type === "json" && instructionName === "to";
243
+ if (modelField && !(isObject(value) || Array.isArray(value)) || getSymbol(value) || consumeJSON) {
244
+ return composeFieldValues(
245
+ models,
246
+ model,
247
+ statementParams,
248
+ instructionName,
249
+ value,
250
+ { ...options, fieldSlug: options.fieldSlug }
251
+ );
252
+ }
253
+ if (modelField?.type === "link" && isNested) {
254
+ const keys = Object.keys(value);
255
+ const values = Object.values(value);
256
+ let recordTarget;
257
+ if (keys.length === 1 && keys[0] === "id") {
258
+ recordTarget = values[0];
259
+ } else {
260
+ const relatedModel = getModelBySlug(models, modelField.target);
261
+ const subQuery = {
262
+ get: {
263
+ [relatedModel.slug]: {
264
+ with: value,
265
+ selecting: ["id"]
266
+ }
219
267
  }
220
- }
221
- };
222
- recordTarget = {
223
- [RONIN_MODEL_SYMBOLS.QUERY]: subQuery
224
- };
268
+ };
269
+ recordTarget = {
270
+ [QUERY_SYMBOLS.QUERY]: subQuery
271
+ };
272
+ }
273
+ return composeConditions(
274
+ models,
275
+ model,
276
+ statementParams,
277
+ instructionName,
278
+ recordTarget,
279
+ options
280
+ );
225
281
  }
226
- return composeConditions(
227
- models,
228
- model,
229
- statementParams,
230
- instructionName,
231
- recordTarget,
232
- options
233
- );
234
282
  }
235
283
  }
236
284
  if (isNested) {
@@ -278,23 +326,6 @@ var formatIdentifiers = ({ identifiers }, queryInstructions) => {
278
326
  [type]: newNestedInstructions
279
327
  };
280
328
  };
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
329
 
299
330
  // src/instructions/with.ts
300
331
  var getMatcher = (value, negative) => {
@@ -347,7 +378,7 @@ var getModelBySlug = (models, slug) => {
347
378
  };
348
379
  var composeAssociationModelSlug = (model, field) => convertToCamelCase(`ronin_link_${model.slug}_${field.slug}`);
349
380
  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)}.` : "";
381
+ const symbol = model.tableAlias?.startsWith(QUERY_SYMBOLS.FIELD_PARENT) ? `${model.tableAlias.replace(QUERY_SYMBOLS.FIELD_PARENT, "").slice(0, -1)}.` : "";
351
382
  const tablePrefix = symbol || (model.tableAlias ? `"${model.tableAlias}".` : "");
352
383
  if (field.type === "json" && instructionName !== "to") {
353
384
  const dotParts = fieldPath.split(".");
@@ -357,7 +388,7 @@ var getFieldSelector = (model, field, fieldPath, instructionName) => {
357
388
  }
358
389
  return `${tablePrefix}"${fieldPath}"`;
359
390
  };
360
- var getFieldFromModel = (model, fieldPath, instructionName) => {
391
+ function getFieldFromModel(model, fieldPath, instructionName, shouldThrow = true) {
361
392
  const errorPrefix = `Field "${fieldPath}" defined for \`${instructionName}\``;
362
393
  const modelFields = model.fields || [];
363
394
  let modelField;
@@ -375,16 +406,19 @@ var getFieldFromModel = (model, fieldPath, instructionName) => {
375
406
  }
376
407
  modelField = modelFields.find((field) => field.slug === fieldPath);
377
408
  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
- });
409
+ if (shouldThrow) {
410
+ throw new RoninError({
411
+ message: `${errorPrefix} does not exist in model "${model.name}".`,
412
+ code: "FIELD_NOT_FOUND",
413
+ field: fieldPath,
414
+ queries: null
415
+ });
416
+ }
417
+ return null;
384
418
  }
385
419
  const fieldSelector = getFieldSelector(model, modelField, fieldPath, instructionName);
386
420
  return { field: modelField, fieldSelector };
387
- };
421
+ }
388
422
  var slugToName = (slug) => {
389
423
  const name = slug.replace(/([a-z])([A-Z])/g, "$1 $2");
390
424
  return title(name);
@@ -440,11 +474,6 @@ var SYSTEM_FIELDS = [
440
474
  slug: "id",
441
475
  displayAs: "single-line"
442
476
  },
443
- {
444
- name: "RONIN",
445
- type: "group",
446
- slug: "ronin"
447
- },
448
477
  {
449
478
  name: "RONIN - Locked",
450
479
  type: "boolean",
@@ -488,7 +517,6 @@ var ROOT_MODEL = {
488
517
  { slug: "pluralSlug", type: "string" },
489
518
  { slug: "idPrefix", type: "string" },
490
519
  { slug: "table", type: "string" },
491
- { slug: "identifiers", type: "group" },
492
520
  { slug: "identifiers.name", type: "string" },
493
521
  { slug: "identifiers.slug", type: "string" },
494
522
  // Providing an empty object as a default value allows us to use `json_insert`
@@ -543,14 +571,14 @@ var addDefaultModelPresets = (list, model) => {
543
571
  instructions: {
544
572
  including: {
545
573
  [field.slug]: {
546
- [RONIN_MODEL_SYMBOLS.QUERY]: {
574
+ [QUERY_SYMBOLS.QUERY]: {
547
575
  get: {
548
576
  [relatedModel.slug]: {
549
577
  with: {
550
578
  // Compare the `id` field of the related model to the link field on
551
579
  // the root model (`field.slug`).
552
580
  id: {
553
- [RONIN_MODEL_SYMBOLS.EXPRESSION]: `${RONIN_MODEL_SYMBOLS.FIELD_PARENT}${field.slug}`
581
+ [QUERY_SYMBOLS.EXPRESSION]: `${QUERY_SYMBOLS.FIELD_PARENT}${field.slug}`
554
582
  }
555
583
  }
556
584
  }
@@ -578,12 +606,12 @@ var addDefaultModelPresets = (list, model) => {
578
606
  instructions: {
579
607
  including: {
580
608
  [presetSlug]: {
581
- [RONIN_MODEL_SYMBOLS.QUERY]: {
609
+ [QUERY_SYMBOLS.QUERY]: {
582
610
  get: {
583
611
  [pluralSlug]: {
584
612
  with: {
585
613
  [childField.slug]: {
586
- [RONIN_MODEL_SYMBOLS.EXPRESSION]: `${RONIN_MODEL_SYMBOLS.FIELD_PARENT}id`
614
+ [QUERY_SYMBOLS.EXPRESSION]: `${QUERY_SYMBOLS.FIELD_PARENT}id`
587
615
  }
588
616
  }
589
617
  }
@@ -610,7 +638,6 @@ var typesInSQLite = {
610
638
  json: "TEXT"
611
639
  };
612
640
  var getFieldStatement = (models, model, field) => {
613
- if (field.type === "group") return null;
614
641
  let statement = `"${field.slug}" ${typesInSQLite[field.type]}`;
615
642
  if (field.slug === "id") statement += " PRIMARY KEY";
616
643
  if (field.unique === true) statement += " UNIQUE";
@@ -850,11 +877,11 @@ var transformMetaQuery = (models, dependencyStatements, statementParams, query)
850
877
  statementParts.push(`OF (${fieldSelectors.join(", ")})`);
851
878
  }
852
879
  statementParts.push("ON", `"${existingModel.table}"`);
853
- if (trigger.filter || trigger.effects.some((query2) => findInObject(query2, RONIN_MODEL_SYMBOLS.FIELD))) {
880
+ if (trigger.filter || trigger.effects.some((query2) => findInObject(query2, QUERY_SYMBOLS.FIELD))) {
854
881
  statementParts.push("FOR EACH ROW");
855
882
  }
856
883
  if (trigger.filter) {
857
- const tableAlias = trigger.action === "DELETE" ? RONIN_MODEL_SYMBOLS.FIELD_PARENT_OLD : RONIN_MODEL_SYMBOLS.FIELD_PARENT_NEW;
884
+ const tableAlias = trigger.action === "DELETE" ? QUERY_SYMBOLS.FIELD_PARENT_OLD : QUERY_SYMBOLS.FIELD_PARENT_NEW;
858
885
  const withStatement = handleWith(
859
886
  models,
860
887
  { ...existingModel, tableAlias },
@@ -876,7 +903,7 @@ var transformMetaQuery = (models, dependencyStatements, statementParams, query)
876
903
  }
877
904
  dependencyStatements.push({ statement, params });
878
905
  }
879
- const field = `${RONIN_MODEL_SYMBOLS.FIELD}${pluralType}`;
906
+ const field = `${QUERY_SYMBOLS.FIELD}${pluralType}`;
880
907
  let json;
881
908
  switch (action) {
882
909
  case "create": {
@@ -940,7 +967,7 @@ var transformMetaQuery = (models, dependencyStatements, statementParams, query)
940
967
  model: {
941
968
  with: { slug: modelSlug },
942
969
  to: {
943
- [pluralType]: { [RONIN_MODEL_SYMBOLS.EXPRESSION]: json }
970
+ [pluralType]: { [QUERY_SYMBOLS.EXPRESSION]: json }
944
971
  }
945
972
  }
946
973
  }
@@ -1059,8 +1086,8 @@ var handleFor = (model, instructions) => {
1059
1086
  if (arg !== null) {
1060
1087
  findInObject(
1061
1088
  replacedForFilter,
1062
- RONIN_MODEL_SYMBOLS.VALUE,
1063
- (match) => match.replace(RONIN_MODEL_SYMBOLS.VALUE, arg)
1089
+ QUERY_SYMBOLS.VALUE,
1090
+ (match) => match.replace(QUERY_SYMBOLS.VALUE, arg)
1064
1091
  );
1065
1092
  }
1066
1093
  for (const subInstruction in replacedForFilter) {
@@ -1102,7 +1129,7 @@ var handleIncluding = (models, model, statementParams, instruction) => {
1102
1129
  const relatedModel = getModelBySlug(models, queryModel);
1103
1130
  let joinType = "LEFT";
1104
1131
  let relatedTableSelector = `"${relatedModel.table}"`;
1105
- const tableAlias = `including_${ephemeralFieldSlug}`;
1132
+ const tableAlias = composeIncludedTableAlias(ephemeralFieldSlug);
1106
1133
  const single = queryModel !== relatedModel.pluralSlug;
1107
1134
  if (!modifiableQueryInstructions?.with) {
1108
1135
  joinType = "CROSS";
@@ -1124,11 +1151,10 @@ var handleIncluding = (models, model, statementParams, instruction) => {
1124
1151
  relatedTableSelector = `(${subSelect.main.statement})`;
1125
1152
  }
1126
1153
  statement += `${joinType} JOIN ${relatedTableSelector} as ${tableAlias}`;
1127
- model.tableAlias = model.table;
1154
+ model.tableAlias = model.tableAlias || model.table;
1128
1155
  if (joinType === "LEFT") {
1129
1156
  if (!single) {
1130
1157
  tableSubQuery = `SELECT * FROM "${model.table}" LIMIT 1`;
1131
- model.tableAlias = `sub_${model.table}`;
1132
1158
  }
1133
1159
  const subStatement = composeConditions(
1134
1160
  models,
@@ -1183,36 +1209,74 @@ var handleOrderedBy = (model, instruction) => {
1183
1209
  };
1184
1210
 
1185
1211
  // src/instructions/selecting.ts
1186
- var handleSelecting = (model, statementParams, instructions) => {
1212
+ var handleSelecting = (models, model, statementParams, instructions, options) => {
1213
+ let loadedFields = [];
1214
+ let statement = "*";
1187
1215
  let isJoining = false;
1188
- let statement = instructions.selecting ? instructions.selecting.map((slug) => {
1189
- return getFieldFromModel(model, slug, "selecting").fieldSelector;
1190
- }).join(", ") : "*";
1191
1216
  if (instructions.including) {
1192
- const filteredObject = Object.entries(instructions.including).map(([key, value]) => {
1217
+ const flatObject = flatten(instructions.including);
1218
+ instructions.including = {};
1219
+ for (const [key, value] of Object.entries(flatObject)) {
1193
1220
  const symbol = getSymbol(value);
1194
- if (symbol) {
1195
- if (symbol.type === "query") {
1196
- isJoining = true;
1197
- return null;
1221
+ if (symbol?.type === "query") {
1222
+ isJoining = true;
1223
+ const { queryModel, queryInstructions } = splitQuery(symbol.value);
1224
+ const subQueryModel = getModelBySlug(models, queryModel);
1225
+ const tableAlias = composeIncludedTableAlias(key);
1226
+ const single = queryModel !== subQueryModel.pluralSlug;
1227
+ if (!single) {
1228
+ model.tableAlias = `sub_${model.table}`;
1198
1229
  }
1199
- if (symbol.type === "expression") {
1200
- value = parseFieldExpression(model, "including", symbol.value);
1230
+ const queryModelFields = queryInstructions?.selecting ? subQueryModel.fields.filter((field) => {
1231
+ return queryInstructions.selecting?.includes(field.slug);
1232
+ }) : subQueryModel.fields;
1233
+ for (const field of queryModelFields) {
1234
+ loadedFields.push({ ...field, parentField: key });
1235
+ if (options?.expandColumns) {
1236
+ const newValue2 = parseFieldExpression(
1237
+ { ...subQueryModel, tableAlias },
1238
+ "including",
1239
+ `${QUERY_SYMBOLS.FIELD}${field.slug}`
1240
+ );
1241
+ instructions.including[`${tableAlias}.${field.slug}`] = newValue2;
1242
+ }
1201
1243
  }
1244
+ continue;
1202
1245
  }
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(", ");
1246
+ let newValue = value;
1247
+ if (symbol?.type === "expression") {
1248
+ newValue = `(${parseFieldExpression(model, "including", symbol.value)})`;
1249
+ } else {
1250
+ newValue = prepareStatementValue(statementParams, value);
1251
+ }
1252
+ instructions.including[key] = newValue;
1213
1253
  }
1214
1254
  }
1215
- return { columns: statement, isJoining };
1255
+ const expandColumns = isJoining && options?.expandColumns;
1256
+ if (expandColumns) {
1257
+ instructions.selecting = model.fields.map((field) => field.slug);
1258
+ }
1259
+ if (instructions.selecting) {
1260
+ const usableModel = expandColumns ? { ...model, tableAlias: model.tableAlias || model.table } : model;
1261
+ const selectedFields = [];
1262
+ statement = instructions.selecting.map((slug) => {
1263
+ const { field, fieldSelector } = getFieldFromModel(
1264
+ usableModel,
1265
+ slug,
1266
+ "selecting"
1267
+ );
1268
+ selectedFields.push(field);
1269
+ return fieldSelector;
1270
+ }).join(", ");
1271
+ loadedFields = [...selectedFields, ...loadedFields];
1272
+ } else {
1273
+ loadedFields = [...model.fields, ...loadedFields];
1274
+ }
1275
+ if (instructions.including && Object.keys(instructions.including).length > 0) {
1276
+ statement += ", ";
1277
+ statement += Object.entries(instructions.including).map(([key, value]) => `${value} as "${key}"`).join(", ");
1278
+ }
1279
+ return { columns: statement, isJoining, loadedFields };
1216
1280
  };
1217
1281
 
1218
1282
  // src/instructions/to.ts
@@ -1279,8 +1343,8 @@ var handleTo = (models, model, statementParams, queryType, dependencyStatements,
1279
1343
  Object.assign(toInstruction, defaultFields);
1280
1344
  for (const fieldSlug in toInstruction) {
1281
1345
  const fieldValue = toInstruction[fieldSlug];
1282
- const fieldDetails = getFieldFromModel(model, fieldSlug, "to");
1283
- if (fieldDetails.field.type === "link" && fieldDetails.field.kind === "many") {
1346
+ const fieldDetails = getFieldFromModel(model, fieldSlug, "to", false);
1347
+ if (fieldDetails?.field.type === "link" && fieldDetails.field.kind === "many") {
1284
1348
  delete toInstruction[fieldSlug];
1285
1349
  const associativeModelSlug = composeAssociationModelSlug(model, fieldDetails.field);
1286
1350
  const composeStatement = (subQueryType, value) => {
@@ -1345,7 +1409,8 @@ var compileQueryInput = (defaultQuery, models, statementParams, options) => {
1345
1409
  statementParams,
1346
1410
  defaultQuery
1347
1411
  );
1348
- if (query === null) return { dependencies: [], main: dependencyStatements[0] };
1412
+ if (query === null)
1413
+ return { dependencies: [], main: dependencyStatements[0], loadedFields: [] };
1349
1414
  const parsedQuery = splitQuery(query);
1350
1415
  const { queryType, queryModel, queryInstructions } = parsedQuery;
1351
1416
  const model = getModelBySlug(models, queryModel);
@@ -1355,10 +1420,16 @@ var compileQueryInput = (defaultQuery, models, statementParams, options) => {
1355
1420
  if (instructions && Object.hasOwn(instructions, "for")) {
1356
1421
  instructions = handleFor(model, instructions);
1357
1422
  }
1358
- const { columns, isJoining } = handleSelecting(model, statementParams, {
1359
- selecting: instructions?.selecting,
1360
- including: instructions?.including
1361
- });
1423
+ const { columns, isJoining, loadedFields } = handleSelecting(
1424
+ models,
1425
+ model,
1426
+ statementParams,
1427
+ {
1428
+ selecting: instructions?.selecting,
1429
+ including: instructions?.including
1430
+ },
1431
+ options
1432
+ );
1362
1433
  let statement = "";
1363
1434
  switch (queryType) {
1364
1435
  case "get":
@@ -1478,7 +1549,8 @@ var compileQueryInput = (defaultQuery, models, statementParams, options) => {
1478
1549
  if (returning) mainStatement.returning = true;
1479
1550
  return {
1480
1551
  dependencies: dependencyStatements,
1481
- main: mainStatement
1552
+ main: mainStatement,
1553
+ loadedFields
1482
1554
  };
1483
1555
  };
1484
1556
 
@@ -1487,6 +1559,7 @@ var Transaction = class {
1487
1559
  statements;
1488
1560
  models = [];
1489
1561
  queries;
1562
+ fields = [];
1490
1563
  constructor(queries, options) {
1491
1564
  const models = options?.models || [];
1492
1565
  this.statements = this.compileQueries(queries, models, options);
@@ -1518,46 +1591,83 @@ var Transaction = class {
1518
1591
  const result = compileQueryInput(
1519
1592
  query,
1520
1593
  modelListWithPresets,
1521
- options?.inlineParams ? null : []
1594
+ options?.inlineParams ? null : [],
1595
+ { expandColumns: options?.expandColumns }
1522
1596
  );
1523
1597
  dependencyStatements.push(...result.dependencies);
1524
1598
  mainStatements.push(result.main);
1599
+ this.fields.push(result.loadedFields);
1525
1600
  }
1526
1601
  this.models = modelListWithPresets;
1527
1602
  return [...dependencyStatements, ...mainStatements];
1528
1603
  };
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;
1604
+ formatRows(fields, rows, single) {
1605
+ const records = [];
1606
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
1607
+ const row = rows[rowIndex];
1608
+ for (let valueIndex = 0; valueIndex < row.length; valueIndex++) {
1609
+ const value = row[valueIndex];
1610
+ const field = fields[valueIndex];
1611
+ let newSlug = field.slug;
1612
+ let newValue = value;
1613
+ if (field.type === "json") {
1614
+ newValue = JSON.parse(value);
1615
+ } else if (field.type === "boolean") {
1616
+ newValue = Boolean(value);
1617
+ }
1618
+ const parentFieldSlug = field.parentField;
1619
+ if (parentFieldSlug) {
1620
+ if (rows.length === 1) {
1621
+ newSlug = `${parentFieldSlug}.${field.slug}`;
1622
+ } else {
1623
+ const fieldPath = `${parentFieldSlug}[${rowIndex}].${field.slug}`;
1624
+ records[0] = setProperty(records[0], fieldPath, newValue);
1625
+ continue;
1626
+ }
1627
+ }
1628
+ records[rowIndex] = setProperty(records[rowIndex], newSlug, newValue);
1536
1629
  }
1537
- formattedRecord[key] = record[key];
1538
1630
  }
1539
- return expand(formattedRecord);
1631
+ return single ? records[0] : records;
1540
1632
  }
1541
- prepareResults(results) {
1633
+ /**
1634
+ * Format the results returned from the database into RONIN records.
1635
+ *
1636
+ * @param results - A list of results from the database, where each result is an array
1637
+ * of rows.
1638
+ * @param raw - By default, rows are expected to be arrays of values, which is how SQL
1639
+ * databases return rows by default. If the driver being used returns rows as objects
1640
+ * instead, this option should be set to `false`.
1641
+ *
1642
+ * @returns A list of formatted RONIN results, where each result is either a single
1643
+ * RONIN record, an array of RONIN records, or a RONIN count result.
1644
+ */
1645
+ formatResults(results, raw = true) {
1542
1646
  const relevantResults = results.filter((_, index) => {
1543
1647
  return this.statements[index].returning;
1544
1648
  });
1545
- return relevantResults.map((result, index) => {
1649
+ const normalizedResults = raw ? relevantResults : relevantResults.map((rows) => {
1650
+ return rows.map((row) => {
1651
+ if (Array.isArray(row)) return row;
1652
+ if (row["COUNT(*)"]) return [row["COUNT(*)"]];
1653
+ return Object.values(row);
1654
+ });
1655
+ });
1656
+ return normalizedResults.map((rows, index) => {
1546
1657
  const query = this.queries.at(-index);
1658
+ const fields = this.fields.at(-index);
1547
1659
  const { queryType, queryModel, queryInstructions } = splitQuery(query);
1548
1660
  const model = getModelBySlug(this.models, queryModel);
1549
1661
  if (queryType === "count") {
1550
- return { amount: result[0]["COUNT(*)"] };
1662
+ return { amount: rows[0][0] };
1551
1663
  }
1552
1664
  const single = queryModel !== model.pluralSlug;
1553
1665
  if (single) {
1554
- return { record: this.formatRecord(model, result[0]) };
1666
+ return { record: rows[0] ? this.formatRows(fields, rows, single) : null };
1555
1667
  }
1556
1668
  const pageSize = queryInstructions?.limitedTo;
1557
1669
  const output = {
1558
- records: result.map((resultItem) => {
1559
- return this.formatRecord(model, resultItem);
1560
- })
1670
+ records: this.formatRows(fields, rows, single)
1561
1671
  };
1562
1672
  if (pageSize && output.records.length > 0) {
1563
1673
  if (queryInstructions?.before || queryInstructions?.after) {
@@ -1585,6 +1695,8 @@ var Transaction = class {
1585
1695
  };
1586
1696
  var CLEAN_ROOT_MODEL = omit(ROOT_MODEL, ["system"]);
1587
1697
  export {
1698
+ QUERY_SYMBOLS,
1588
1699
  CLEAN_ROOT_MODEL as ROOT_MODEL,
1700
+ RoninError,
1589
1701
  Transaction
1590
1702
  };
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-226",
4
4
  "type": "module",
5
5
  "description": "Compiles RONIN queries to SQL statements.",
6
6
  "publishConfig": {