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

Sign up to get free protection for your applications and to get access to all the features.
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": {