@ronin/compiler 0.10.3 → 0.11.0

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,7 +5984,8 @@ 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: {
@@ -6005,6 +6006,45 @@ type AmountResult = {
6005
6006
  };
6006
6007
  type Result = SingleRecordResult | MultipleRecordResult | AmountResult;
6007
6008
 
6009
+ /**
6010
+ * A list of placeholders that can be located inside queries after those queries were
6011
+ * serialized into JSON objects.
6012
+ *
6013
+ * These placeholders are used to represent special keys and values. For example, if a
6014
+ * query is nested into a query, the nested query will be marked with `__RONIN_QUERY`,
6015
+ * which allows for distinguishing that nested query from an object of instructions.
6016
+ */
6017
+ declare const QUERY_SYMBOLS: {
6018
+ readonly QUERY: "__RONIN_QUERY";
6019
+ readonly EXPRESSION: "__RONIN_EXPRESSION";
6020
+ readonly FIELD: "__RONIN_FIELD_";
6021
+ readonly FIELD_PARENT: "__RONIN_FIELD_PARENT_";
6022
+ readonly FIELD_PARENT_OLD: "__RONIN_FIELD_PARENT_OLD_";
6023
+ readonly FIELD_PARENT_NEW: "__RONIN_FIELD_PARENT_NEW_";
6024
+ readonly VALUE: "__RONIN_VALUE";
6025
+ };
6026
+ 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';
6027
+ interface Issue {
6028
+ message: string;
6029
+ path: Array<string | number>;
6030
+ }
6031
+ interface Details {
6032
+ message: string;
6033
+ code: RoninErrorCode;
6034
+ field?: string;
6035
+ fields?: Array<string>;
6036
+ issues?: Array<Issue>;
6037
+ queries?: Array<Query> | null;
6038
+ }
6039
+ declare class RoninError extends Error {
6040
+ code: Details['code'];
6041
+ field?: Details['field'];
6042
+ fields?: Details['fields'];
6043
+ issues?: Details['issues'];
6044
+ queries?: Details['queries'];
6045
+ constructor(details: Details);
6046
+ }
6047
+
6008
6048
  interface TransactionOptions {
6009
6049
  /** A list of models that already exist in the database. */
6010
6050
  models?: Array<PublicModel>;
@@ -6020,6 +6060,7 @@ declare class Transaction {
6020
6060
  statements: Array<Statement>;
6021
6061
  models: Array<Model>;
6022
6062
  private queries;
6063
+ private fields;
6023
6064
  constructor(queries: Array<Query>, options?: TransactionOptions);
6024
6065
  /**
6025
6066
  * Composes SQL statements for the provided RONIN queries.
@@ -6031,10 +6072,11 @@ declare class Transaction {
6031
6072
  * @returns The composed SQL statements.
6032
6073
  */
6033
6074
  private compileQueries;
6034
- private formatRecord;
6035
- prepareResults(results: Array<Array<Row>>): Array<Result>;
6075
+ private formatRow;
6076
+ formatResults(results: Array<Array<RawRow>>, raw?: true): Array<Result>;
6077
+ formatResults(results: Array<Array<ObjectRow>>, raw?: false): Array<Result>;
6036
6078
  }
6037
6079
 
6038
6080
  declare const CLEAN_ROOT_MODEL: PublicModel;
6039
6081
 
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 };
6082
+ 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;
@@ -133,15 +156,15 @@ var prepareStatementValue = (statementParams, value) => {
133
156
  };
134
157
  var parseFieldExpression = (model, instructionName, expression, parentModel) => {
135
158
  return expression.replace(RONIN_MODEL_FIELD_REGEX, (match) => {
136
- let toReplace = RONIN_MODEL_SYMBOLS.FIELD;
159
+ let toReplace = QUERY_SYMBOLS.FIELD;
137
160
  let rootModel = model;
138
- if (match.startsWith(RONIN_MODEL_SYMBOLS.FIELD_PARENT)) {
161
+ if (match.startsWith(QUERY_SYMBOLS.FIELD_PARENT)) {
139
162
  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;
163
+ toReplace = QUERY_SYMBOLS.FIELD_PARENT;
164
+ if (match.startsWith(QUERY_SYMBOLS.FIELD_PARENT_OLD)) {
165
+ rootModel.tableAlias = toReplace = QUERY_SYMBOLS.FIELD_PARENT_OLD;
166
+ } else if (match.startsWith(QUERY_SYMBOLS.FIELD_PARENT_NEW)) {
167
+ rootModel.tableAlias = toReplace = QUERY_SYMBOLS.FIELD_PARENT_NEW;
145
168
  }
146
169
  }
147
170
  const fieldSlug = match.replace(toReplace, "");
@@ -190,47 +213,52 @@ var composeConditions = (models, model, statementParams, instructionName, value,
190
213
  return conditions.join(" AND ");
191
214
  }
192
215
  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"]
216
+ const childField = model.fields.some(({ slug }) => {
217
+ return slug.includes(".") && slug.split(".")[0] === options.fieldSlug;
218
+ });
219
+ if (!childField) {
220
+ const fieldDetails = getFieldFromModel(model, options.fieldSlug, instructionName);
221
+ const { field: modelField } = fieldDetails || {};
222
+ const consumeJSON = modelField?.type === "json" && instructionName === "to";
223
+ if (modelField && !(isObject(value) || Array.isArray(value)) || getSymbol(value) || consumeJSON) {
224
+ return composeFieldValues(
225
+ models,
226
+ model,
227
+ statementParams,
228
+ instructionName,
229
+ value,
230
+ { ...options, fieldSlug: options.fieldSlug }
231
+ );
232
+ }
233
+ if (modelField?.type === "link" && isNested) {
234
+ const keys = Object.keys(value);
235
+ const values = Object.values(value);
236
+ let recordTarget;
237
+ if (keys.length === 1 && keys[0] === "id") {
238
+ recordTarget = values[0];
239
+ } else {
240
+ const relatedModel = getModelBySlug(models, modelField.target);
241
+ const subQuery = {
242
+ get: {
243
+ [relatedModel.slug]: {
244
+ with: value,
245
+ selecting: ["id"]
246
+ }
219
247
  }
220
- }
221
- };
222
- recordTarget = {
223
- [RONIN_MODEL_SYMBOLS.QUERY]: subQuery
224
- };
248
+ };
249
+ recordTarget = {
250
+ [QUERY_SYMBOLS.QUERY]: subQuery
251
+ };
252
+ }
253
+ return composeConditions(
254
+ models,
255
+ model,
256
+ statementParams,
257
+ instructionName,
258
+ recordTarget,
259
+ options
260
+ );
225
261
  }
226
- return composeConditions(
227
- models,
228
- model,
229
- statementParams,
230
- instructionName,
231
- recordTarget,
232
- options
233
- );
234
262
  }
235
263
  }
236
264
  if (isNested) {
@@ -278,23 +306,6 @@ var formatIdentifiers = ({ identifiers }, queryInstructions) => {
278
306
  [type]: newNestedInstructions
279
307
  };
280
308
  };
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
309
 
299
310
  // src/instructions/with.ts
300
311
  var getMatcher = (value, negative) => {
@@ -347,7 +358,7 @@ var getModelBySlug = (models, slug) => {
347
358
  };
348
359
  var composeAssociationModelSlug = (model, field) => convertToCamelCase(`ronin_link_${model.slug}_${field.slug}`);
349
360
  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)}.` : "";
361
+ const symbol = model.tableAlias?.startsWith(QUERY_SYMBOLS.FIELD_PARENT) ? `${model.tableAlias.replace(QUERY_SYMBOLS.FIELD_PARENT, "").slice(0, -1)}.` : "";
351
362
  const tablePrefix = symbol || (model.tableAlias ? `"${model.tableAlias}".` : "");
352
363
  if (field.type === "json" && instructionName !== "to") {
353
364
  const dotParts = fieldPath.split(".");
@@ -357,7 +368,7 @@ var getFieldSelector = (model, field, fieldPath, instructionName) => {
357
368
  }
358
369
  return `${tablePrefix}"${fieldPath}"`;
359
370
  };
360
- var getFieldFromModel = (model, fieldPath, instructionName) => {
371
+ function getFieldFromModel(model, fieldPath, instructionName, shouldThrow = true) {
361
372
  const errorPrefix = `Field "${fieldPath}" defined for \`${instructionName}\``;
362
373
  const modelFields = model.fields || [];
363
374
  let modelField;
@@ -375,16 +386,19 @@ var getFieldFromModel = (model, fieldPath, instructionName) => {
375
386
  }
376
387
  modelField = modelFields.find((field) => field.slug === fieldPath);
377
388
  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
- });
389
+ if (shouldThrow) {
390
+ throw new RoninError({
391
+ message: `${errorPrefix} does not exist in model "${model.name}".`,
392
+ code: "FIELD_NOT_FOUND",
393
+ field: fieldPath,
394
+ queries: null
395
+ });
396
+ }
397
+ return null;
384
398
  }
385
399
  const fieldSelector = getFieldSelector(model, modelField, fieldPath, instructionName);
386
400
  return { field: modelField, fieldSelector };
387
- };
401
+ }
388
402
  var slugToName = (slug) => {
389
403
  const name = slug.replace(/([a-z])([A-Z])/g, "$1 $2");
390
404
  return title(name);
@@ -440,11 +454,6 @@ var SYSTEM_FIELDS = [
440
454
  slug: "id",
441
455
  displayAs: "single-line"
442
456
  },
443
- {
444
- name: "RONIN",
445
- type: "group",
446
- slug: "ronin"
447
- },
448
457
  {
449
458
  name: "RONIN - Locked",
450
459
  type: "boolean",
@@ -488,7 +497,6 @@ var ROOT_MODEL = {
488
497
  { slug: "pluralSlug", type: "string" },
489
498
  { slug: "idPrefix", type: "string" },
490
499
  { slug: "table", type: "string" },
491
- { slug: "identifiers", type: "group" },
492
500
  { slug: "identifiers.name", type: "string" },
493
501
  { slug: "identifiers.slug", type: "string" },
494
502
  // Providing an empty object as a default value allows us to use `json_insert`
@@ -543,14 +551,14 @@ var addDefaultModelPresets = (list, model) => {
543
551
  instructions: {
544
552
  including: {
545
553
  [field.slug]: {
546
- [RONIN_MODEL_SYMBOLS.QUERY]: {
554
+ [QUERY_SYMBOLS.QUERY]: {
547
555
  get: {
548
556
  [relatedModel.slug]: {
549
557
  with: {
550
558
  // Compare the `id` field of the related model to the link field on
551
559
  // the root model (`field.slug`).
552
560
  id: {
553
- [RONIN_MODEL_SYMBOLS.EXPRESSION]: `${RONIN_MODEL_SYMBOLS.FIELD_PARENT}${field.slug}`
561
+ [QUERY_SYMBOLS.EXPRESSION]: `${QUERY_SYMBOLS.FIELD_PARENT}${field.slug}`
554
562
  }
555
563
  }
556
564
  }
@@ -578,12 +586,12 @@ var addDefaultModelPresets = (list, model) => {
578
586
  instructions: {
579
587
  including: {
580
588
  [presetSlug]: {
581
- [RONIN_MODEL_SYMBOLS.QUERY]: {
589
+ [QUERY_SYMBOLS.QUERY]: {
582
590
  get: {
583
591
  [pluralSlug]: {
584
592
  with: {
585
593
  [childField.slug]: {
586
- [RONIN_MODEL_SYMBOLS.EXPRESSION]: `${RONIN_MODEL_SYMBOLS.FIELD_PARENT}id`
594
+ [QUERY_SYMBOLS.EXPRESSION]: `${QUERY_SYMBOLS.FIELD_PARENT}id`
587
595
  }
588
596
  }
589
597
  }
@@ -610,7 +618,6 @@ var typesInSQLite = {
610
618
  json: "TEXT"
611
619
  };
612
620
  var getFieldStatement = (models, model, field) => {
613
- if (field.type === "group") return null;
614
621
  let statement = `"${field.slug}" ${typesInSQLite[field.type]}`;
615
622
  if (field.slug === "id") statement += " PRIMARY KEY";
616
623
  if (field.unique === true) statement += " UNIQUE";
@@ -850,11 +857,11 @@ var transformMetaQuery = (models, dependencyStatements, statementParams, query)
850
857
  statementParts.push(`OF (${fieldSelectors.join(", ")})`);
851
858
  }
852
859
  statementParts.push("ON", `"${existingModel.table}"`);
853
- if (trigger.filter || trigger.effects.some((query2) => findInObject(query2, RONIN_MODEL_SYMBOLS.FIELD))) {
860
+ if (trigger.filter || trigger.effects.some((query2) => findInObject(query2, QUERY_SYMBOLS.FIELD))) {
854
861
  statementParts.push("FOR EACH ROW");
855
862
  }
856
863
  if (trigger.filter) {
857
- const tableAlias = trigger.action === "DELETE" ? RONIN_MODEL_SYMBOLS.FIELD_PARENT_OLD : RONIN_MODEL_SYMBOLS.FIELD_PARENT_NEW;
864
+ const tableAlias = trigger.action === "DELETE" ? QUERY_SYMBOLS.FIELD_PARENT_OLD : QUERY_SYMBOLS.FIELD_PARENT_NEW;
858
865
  const withStatement = handleWith(
859
866
  models,
860
867
  { ...existingModel, tableAlias },
@@ -876,7 +883,7 @@ var transformMetaQuery = (models, dependencyStatements, statementParams, query)
876
883
  }
877
884
  dependencyStatements.push({ statement, params });
878
885
  }
879
- const field = `${RONIN_MODEL_SYMBOLS.FIELD}${pluralType}`;
886
+ const field = `${QUERY_SYMBOLS.FIELD}${pluralType}`;
880
887
  let json;
881
888
  switch (action) {
882
889
  case "create": {
@@ -940,7 +947,7 @@ var transformMetaQuery = (models, dependencyStatements, statementParams, query)
940
947
  model: {
941
948
  with: { slug: modelSlug },
942
949
  to: {
943
- [pluralType]: { [RONIN_MODEL_SYMBOLS.EXPRESSION]: json }
950
+ [pluralType]: { [QUERY_SYMBOLS.EXPRESSION]: json }
944
951
  }
945
952
  }
946
953
  }
@@ -1059,8 +1066,8 @@ var handleFor = (model, instructions) => {
1059
1066
  if (arg !== null) {
1060
1067
  findInObject(
1061
1068
  replacedForFilter,
1062
- RONIN_MODEL_SYMBOLS.VALUE,
1063
- (match) => match.replace(RONIN_MODEL_SYMBOLS.VALUE, arg)
1069
+ QUERY_SYMBOLS.VALUE,
1070
+ (match) => match.replace(QUERY_SYMBOLS.VALUE, arg)
1064
1071
  );
1065
1072
  }
1066
1073
  for (const subInstruction in replacedForFilter) {
@@ -1102,7 +1109,7 @@ var handleIncluding = (models, model, statementParams, instruction) => {
1102
1109
  const relatedModel = getModelBySlug(models, queryModel);
1103
1110
  let joinType = "LEFT";
1104
1111
  let relatedTableSelector = `"${relatedModel.table}"`;
1105
- const tableAlias = `including_${ephemeralFieldSlug}`;
1112
+ const tableAlias = composeIncludedTableAlias(ephemeralFieldSlug);
1106
1113
  const single = queryModel !== relatedModel.pluralSlug;
1107
1114
  if (!modifiableQueryInstructions?.with) {
1108
1115
  joinType = "CROSS";
@@ -1183,36 +1190,70 @@ var handleOrderedBy = (model, instruction) => {
1183
1190
  };
1184
1191
 
1185
1192
  // src/instructions/selecting.ts
1186
- var handleSelecting = (model, statementParams, instructions) => {
1193
+ var handleSelecting = (models, model, statementParams, instructions, options) => {
1194
+ let loadedFields = [];
1195
+ let statement = "*";
1187
1196
  let isJoining = false;
1188
- let statement = instructions.selecting ? instructions.selecting.map((slug) => {
1189
- return getFieldFromModel(model, slug, "selecting").fieldSelector;
1190
- }).join(", ") : "*";
1191
1197
  if (instructions.including) {
1192
- const filteredObject = Object.entries(instructions.including).map(([key, value]) => {
1198
+ const flatObject = flatten(instructions.including);
1199
+ instructions.including = {};
1200
+ for (const [key, value] of Object.entries(flatObject)) {
1193
1201
  const symbol = getSymbol(value);
1194
- if (symbol) {
1195
- if (symbol.type === "query") {
1196
- isJoining = true;
1197
- return null;
1198
- }
1199
- if (symbol.type === "expression") {
1200
- value = parseFieldExpression(model, "including", symbol.value);
1202
+ if (symbol?.type === "query") {
1203
+ isJoining = true;
1204
+ const { queryModel, queryInstructions } = splitQuery(symbol.value);
1205
+ const subQueryModel = getModelBySlug(models, queryModel);
1206
+ const tableName = composeIncludedTableAlias(key);
1207
+ const queryModelFields = queryInstructions?.selecting ? subQueryModel.fields.filter((field) => {
1208
+ return queryInstructions.selecting?.includes(field.slug);
1209
+ }) : subQueryModel.fields;
1210
+ for (const field of queryModelFields) {
1211
+ loadedFields.push({ ...field, parentField: key });
1212
+ if (options?.expandColumns) {
1213
+ const newValue2 = parseFieldExpression(
1214
+ { ...subQueryModel, tableAlias: tableName },
1215
+ "including",
1216
+ `${QUERY_SYMBOLS.FIELD}${field.slug}`
1217
+ );
1218
+ instructions.including[`${tableName}.${field.slug}`] = newValue2;
1219
+ }
1201
1220
  }
1221
+ continue;
1202
1222
  }
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(", ");
1223
+ let newValue = value;
1224
+ if (symbol?.type === "expression") {
1225
+ newValue = `(${parseFieldExpression(model, "including", symbol.value)})`;
1226
+ } else {
1227
+ newValue = prepareStatementValue(statementParams, value);
1228
+ }
1229
+ instructions.including[key] = newValue;
1213
1230
  }
1214
1231
  }
1215
- return { columns: statement, isJoining };
1232
+ const expandColumns = isJoining && options?.expandColumns;
1233
+ if (expandColumns) {
1234
+ instructions.selecting = model.fields.map((field) => field.slug);
1235
+ }
1236
+ if (instructions.selecting) {
1237
+ const usableModel = expandColumns ? { ...model, tableAlias: model.tableAlias || model.table } : model;
1238
+ const selectedFields = [];
1239
+ statement = instructions.selecting.map((slug) => {
1240
+ const { field, fieldSelector } = getFieldFromModel(
1241
+ usableModel,
1242
+ slug,
1243
+ "selecting"
1244
+ );
1245
+ selectedFields.push(field);
1246
+ return fieldSelector;
1247
+ }).join(", ");
1248
+ loadedFields = [...selectedFields, ...loadedFields];
1249
+ } else {
1250
+ loadedFields = [...model.fields, ...loadedFields];
1251
+ }
1252
+ if (instructions.including && Object.keys(instructions.including).length > 0) {
1253
+ statement += ", ";
1254
+ statement += Object.entries(instructions.including).map(([key, value]) => `${value} as "${key}"`).join(", ");
1255
+ }
1256
+ return { columns: statement, isJoining, loadedFields };
1216
1257
  };
1217
1258
 
1218
1259
  // src/instructions/to.ts
@@ -1279,8 +1320,8 @@ var handleTo = (models, model, statementParams, queryType, dependencyStatements,
1279
1320
  Object.assign(toInstruction, defaultFields);
1280
1321
  for (const fieldSlug in toInstruction) {
1281
1322
  const fieldValue = toInstruction[fieldSlug];
1282
- const fieldDetails = getFieldFromModel(model, fieldSlug, "to");
1283
- if (fieldDetails.field.type === "link" && fieldDetails.field.kind === "many") {
1323
+ const fieldDetails = getFieldFromModel(model, fieldSlug, "to", false);
1324
+ if (fieldDetails?.field.type === "link" && fieldDetails.field.kind === "many") {
1284
1325
  delete toInstruction[fieldSlug];
1285
1326
  const associativeModelSlug = composeAssociationModelSlug(model, fieldDetails.field);
1286
1327
  const composeStatement = (subQueryType, value) => {
@@ -1345,7 +1386,8 @@ var compileQueryInput = (defaultQuery, models, statementParams, options) => {
1345
1386
  statementParams,
1346
1387
  defaultQuery
1347
1388
  );
1348
- if (query === null) return { dependencies: [], main: dependencyStatements[0] };
1389
+ if (query === null)
1390
+ return { dependencies: [], main: dependencyStatements[0], loadedFields: [] };
1349
1391
  const parsedQuery = splitQuery(query);
1350
1392
  const { queryType, queryModel, queryInstructions } = parsedQuery;
1351
1393
  const model = getModelBySlug(models, queryModel);
@@ -1355,10 +1397,16 @@ var compileQueryInput = (defaultQuery, models, statementParams, options) => {
1355
1397
  if (instructions && Object.hasOwn(instructions, "for")) {
1356
1398
  instructions = handleFor(model, instructions);
1357
1399
  }
1358
- const { columns, isJoining } = handleSelecting(model, statementParams, {
1359
- selecting: instructions?.selecting,
1360
- including: instructions?.including
1361
- });
1400
+ const { columns, isJoining, loadedFields } = handleSelecting(
1401
+ models,
1402
+ model,
1403
+ statementParams,
1404
+ {
1405
+ selecting: instructions?.selecting,
1406
+ including: instructions?.including
1407
+ },
1408
+ options
1409
+ );
1362
1410
  let statement = "";
1363
1411
  switch (queryType) {
1364
1412
  case "get":
@@ -1478,7 +1526,8 @@ var compileQueryInput = (defaultQuery, models, statementParams, options) => {
1478
1526
  if (returning) mainStatement.returning = true;
1479
1527
  return {
1480
1528
  dependencies: dependencyStatements,
1481
- main: mainStatement
1529
+ main: mainStatement,
1530
+ loadedFields
1482
1531
  };
1483
1532
  };
1484
1533
 
@@ -1487,6 +1536,7 @@ var Transaction = class {
1487
1536
  statements;
1488
1537
  models = [];
1489
1538
  queries;
1539
+ fields = [];
1490
1540
  constructor(queries, options) {
1491
1541
  const models = options?.models || [];
1492
1542
  this.statements = this.compileQueries(queries, models, options);
@@ -1518,46 +1568,72 @@ var Transaction = class {
1518
1568
  const result = compileQueryInput(
1519
1569
  query,
1520
1570
  modelListWithPresets,
1521
- options?.inlineParams ? null : []
1571
+ options?.inlineParams ? null : [],
1572
+ { expandColumns: options?.expandColumns }
1522
1573
  );
1523
1574
  dependencyStatements.push(...result.dependencies);
1524
1575
  mainStatements.push(result.main);
1576
+ this.fields.push(result.loadedFields);
1525
1577
  }
1526
1578
  this.models = modelListWithPresets;
1527
1579
  return [...dependencyStatements, ...mainStatements];
1528
1580
  };
1529
- formatRecord(model, record) {
1530
- const formattedRecord = { ...record };
1531
- for (const key in record) {
1532
- const { field } = getFieldFromModel(model, key, "to");
1581
+ formatRow(fields, row) {
1582
+ const record = {};
1583
+ for (let index = 0; index < row.length; index++) {
1584
+ const value = row[index];
1585
+ const field = fields[index];
1586
+ let newSlug = field.slug;
1587
+ let newValue = value;
1588
+ const parentFieldSlug = field.parentField;
1589
+ if (parentFieldSlug) {
1590
+ newSlug = `${parentFieldSlug}.${field.slug}`;
1591
+ }
1533
1592
  if (field.type === "json") {
1534
- formattedRecord[key] = JSON.parse(record[key]);
1535
- continue;
1593
+ newValue = JSON.parse(value);
1536
1594
  }
1537
- formattedRecord[key] = record[key];
1595
+ record[newSlug] = newValue;
1538
1596
  }
1539
- return expand(formattedRecord);
1597
+ return expand(record);
1540
1598
  }
1541
- prepareResults(results) {
1599
+ /**
1600
+ * Format the results returned from the database into RONIN records.
1601
+ *
1602
+ * @param results - A list of results from the database, where each result is an array
1603
+ * of rows.
1604
+ * @param raw - By default, rows are expected to be arrays of values, which is how SQL
1605
+ * databases return rows by default. If the driver being used returns rows as objects
1606
+ * instead, this option should be set to `false`.
1607
+ *
1608
+ * @returns A list of formatted RONIN results, where each result is either a single
1609
+ * RONIN record, an array of RONIN records, or a RONIN count result.
1610
+ */
1611
+ formatResults(results, raw = true) {
1542
1612
  const relevantResults = results.filter((_, index) => {
1543
1613
  return this.statements[index].returning;
1544
1614
  });
1545
- return relevantResults.map((result, index) => {
1615
+ const normalizedResults = raw ? relevantResults : relevantResults.map((rows) => {
1616
+ return rows.map((row) => {
1617
+ if (Array.isArray(row)) return row;
1618
+ if (row["COUNT(*)"]) return [row["COUNT(*)"]];
1619
+ return Object.values(row);
1620
+ });
1621
+ });
1622
+ return normalizedResults.map((rows, index) => {
1546
1623
  const query = this.queries.at(-index);
1624
+ const fields = this.fields.at(-index);
1547
1625
  const { queryType, queryModel, queryInstructions } = splitQuery(query);
1548
1626
  const model = getModelBySlug(this.models, queryModel);
1549
1627
  if (queryType === "count") {
1550
- return { amount: result[0]["COUNT(*)"] };
1628
+ return { amount: rows[0][0] };
1551
1629
  }
1552
1630
  const single = queryModel !== model.pluralSlug;
1553
1631
  if (single) {
1554
- return { record: this.formatRecord(model, result[0]) };
1632
+ return { record: this.formatRow(fields, rows[0]) };
1555
1633
  }
1556
1634
  const pageSize = queryInstructions?.limitedTo;
1557
1635
  const output = {
1558
- records: result.map((resultItem) => {
1559
- return this.formatRecord(model, resultItem);
1560
- })
1636
+ records: rows.map((row) => this.formatRow(fields, row))
1561
1637
  };
1562
1638
  if (pageSize && output.records.length > 0) {
1563
1639
  if (queryInstructions?.before || queryInstructions?.after) {
@@ -1585,6 +1661,8 @@ var Transaction = class {
1585
1661
  };
1586
1662
  var CLEAN_ROOT_MODEL = omit(ROOT_MODEL, ["system"]);
1587
1663
  export {
1664
+ QUERY_SYMBOLS,
1588
1665
  CLEAN_ROOT_MODEL as ROOT_MODEL,
1666
+ RoninError,
1589
1667
  Transaction
1590
1668
  };
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",
4
4
  "type": "module",
5
5
  "description": "Compiles RONIN queries to SQL statements.",
6
6
  "publishConfig": {