@ronin/compiler 0.10.3 → 0.11.0

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,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": {