@ronin/compiler 0.10.3-leo-ron-1083-experimental-219 → 0.10.3-leo-ron-1083-experimental-221

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
@@ -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: {
@@ -6059,6 +6060,7 @@ declare class Transaction {
6059
6060
  statements: Array<Statement>;
6060
6061
  models: Array<Model>;
6061
6062
  private queries;
6063
+ private fields;
6062
6064
  constructor(queries: Array<Query>, options?: TransactionOptions);
6063
6065
  /**
6064
6066
  * Composes SQL statements for the provided RONIN queries.
@@ -6070,8 +6072,9 @@ declare class Transaction {
6070
6072
  * @returns The composed SQL statements.
6071
6073
  */
6072
6074
  private compileQueries;
6073
- private formatRecord;
6074
- 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>;
6075
6078
  }
6076
6079
 
6077
6080
  declare const CLEAN_ROOT_MODEL: PublicModel;
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ var RONIN_MODEL_FIELD_REGEX = new RegExp(
20
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",
@@ -117,7 +118,11 @@ var omit = (obj, properties) => Object.fromEntries(
117
118
  var expand = (obj) => {
118
119
  return Object.entries(obj).reduce((res, [key, val]) => {
119
120
  key.split(".").reduce((acc, part, i, arr) => {
120
- 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
+ }
121
126
  return acc[part];
122
127
  }, res);
123
128
  return res;
@@ -208,47 +213,52 @@ var composeConditions = (models, model, statementParams, instructionName, value,
208
213
  return conditions.join(" AND ");
209
214
  }
210
215
  if (options.fieldSlug) {
211
- const fieldDetails = getFieldFromModel(model, options.fieldSlug, instructionName);
212
- const { field: modelField } = fieldDetails;
213
- const consumeJSON = modelField.type === "json" && instructionName === "to";
214
- if (!(isObject(value) || Array.isArray(value)) || getSymbol(value) || consumeJSON) {
215
- return composeFieldValues(
216
- models,
217
- model,
218
- statementParams,
219
- instructionName,
220
- value,
221
- { ...options, fieldSlug: options.fieldSlug }
222
- );
223
- }
224
- if (modelField.type === "link" && isNested) {
225
- const keys = Object.keys(value);
226
- const values = Object.values(value);
227
- let recordTarget;
228
- if (keys.length === 1 && keys[0] === "id") {
229
- recordTarget = values[0];
230
- } else {
231
- const relatedModel = getModelBySlug(models, modelField.target);
232
- const subQuery = {
233
- get: {
234
- [relatedModel.slug]: {
235
- with: value,
236
- 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
+ }
237
247
  }
238
- }
239
- };
240
- recordTarget = {
241
- [QUERY_SYMBOLS.QUERY]: subQuery
242
- };
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
+ );
243
261
  }
244
- return composeConditions(
245
- models,
246
- model,
247
- statementParams,
248
- instructionName,
249
- recordTarget,
250
- options
251
- );
252
262
  }
253
263
  }
254
264
  if (isNested) {
@@ -358,7 +368,7 @@ var getFieldSelector = (model, field, fieldPath, instructionName) => {
358
368
  }
359
369
  return `${tablePrefix}"${fieldPath}"`;
360
370
  };
361
- var getFieldFromModel = (model, fieldPath, instructionName) => {
371
+ function getFieldFromModel(model, fieldPath, instructionName, shouldThrow = true) {
362
372
  const errorPrefix = `Field "${fieldPath}" defined for \`${instructionName}\``;
363
373
  const modelFields = model.fields || [];
364
374
  let modelField;
@@ -376,16 +386,19 @@ var getFieldFromModel = (model, fieldPath, instructionName) => {
376
386
  }
377
387
  modelField = modelFields.find((field) => field.slug === fieldPath);
378
388
  if (!modelField) {
379
- throw new RoninError({
380
- message: `${errorPrefix} does not exist in model "${model.name}".`,
381
- code: "FIELD_NOT_FOUND",
382
- field: fieldPath,
383
- queries: null
384
- });
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;
385
398
  }
386
399
  const fieldSelector = getFieldSelector(model, modelField, fieldPath, instructionName);
387
400
  return { field: modelField, fieldSelector };
388
- };
401
+ }
389
402
  var slugToName = (slug) => {
390
403
  const name = slug.replace(/([a-z])([A-Z])/g, "$1 $2");
391
404
  return title(name);
@@ -441,11 +454,6 @@ var SYSTEM_FIELDS = [
441
454
  slug: "id",
442
455
  displayAs: "single-line"
443
456
  },
444
- {
445
- name: "RONIN",
446
- type: "group",
447
- slug: "ronin"
448
- },
449
457
  {
450
458
  name: "RONIN - Locked",
451
459
  type: "boolean",
@@ -489,7 +497,6 @@ var ROOT_MODEL = {
489
497
  { slug: "pluralSlug", type: "string" },
490
498
  { slug: "idPrefix", type: "string" },
491
499
  { slug: "table", type: "string" },
492
- { slug: "identifiers", type: "group" },
493
500
  { slug: "identifiers.name", type: "string" },
494
501
  { slug: "identifiers.slug", type: "string" },
495
502
  // Providing an empty object as a default value allows us to use `json_insert`
@@ -611,7 +618,6 @@ var typesInSQLite = {
611
618
  json: "TEXT"
612
619
  };
613
620
  var getFieldStatement = (models, model, field) => {
614
- if (field.type === "group") return null;
615
621
  let statement = `"${field.slug}" ${typesInSQLite[field.type]}`;
616
622
  if (field.slug === "id") statement += " PRIMARY KEY";
617
623
  if (field.unique === true) statement += " UNIQUE";
@@ -1103,7 +1109,7 @@ var handleIncluding = (models, model, statementParams, instruction) => {
1103
1109
  const relatedModel = getModelBySlug(models, queryModel);
1104
1110
  let joinType = "LEFT";
1105
1111
  let relatedTableSelector = `"${relatedModel.table}"`;
1106
- const tableAlias = `including_${ephemeralFieldSlug}`;
1112
+ const tableAlias = composeIncludedTableAlias(ephemeralFieldSlug);
1107
1113
  const single = queryModel !== relatedModel.pluralSlug;
1108
1114
  if (!modifiableQueryInstructions?.with) {
1109
1115
  joinType = "CROSS";
@@ -1185,49 +1191,69 @@ var handleOrderedBy = (model, instruction) => {
1185
1191
 
1186
1192
  // src/instructions/selecting.ts
1187
1193
  var handleSelecting = (models, model, statementParams, instructions, options) => {
1194
+ let loadedFields = [];
1195
+ let statement = "*";
1188
1196
  let isJoining = false;
1189
- let statement = instructions.selecting ? instructions.selecting.map((slug) => {
1190
- return getFieldFromModel(model, slug, "selecting").fieldSelector;
1191
- }).join(", ") : "*";
1192
1197
  if (instructions.including) {
1193
1198
  const flatObject = flatten(instructions.including);
1194
- const filteredObject = Object.entries(flatObject).flatMap(([key, value]) => {
1199
+ instructions.including = {};
1200
+ for (const [key, value] of Object.entries(flatObject)) {
1195
1201
  const symbol = getSymbol(value);
1196
1202
  if (symbol?.type === "query") {
1197
1203
  isJoining = true;
1198
- if (!options?.expandColumns) return null;
1199
- const { queryModel: queryModelSlug } = splitQuery(symbol.value);
1200
- const queryModel = getModelBySlug(models, queryModelSlug);
1201
- const tableName = `including_${key}`;
1202
- const duplicatedFields = queryModel.fields.filter((field) => {
1203
- if (field.type === "group") return null;
1204
- return model.fields.some((modelField) => modelField.slug === field.slug);
1205
- }).filter((item) => item !== null);
1206
- return duplicatedFields.map((field) => {
1207
- const value2 = parseFieldExpression(
1208
- { ...queryModel, tableAlias: tableName },
1209
- "including",
1210
- `${QUERY_SYMBOLS.FIELD}${field.slug}`
1211
- );
1212
- return {
1213
- key: `${tableName}.${field.slug}`,
1214
- value: value2
1215
- };
1216
- });
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
+ }
1220
+ }
1221
+ continue;
1217
1222
  }
1223
+ let newValue = value;
1218
1224
  if (symbol?.type === "expression") {
1219
- value = `(${parseFieldExpression(model, "including", symbol.value)})`;
1225
+ newValue = `(${parseFieldExpression(model, "including", symbol.value)})`;
1220
1226
  } else {
1221
- value = prepareStatementValue(statementParams, value);
1227
+ newValue = prepareStatementValue(statementParams, value);
1222
1228
  }
1223
- return { key, value };
1224
- }).filter((entry) => entry !== null).map((entry) => [entry.key, entry.value]);
1225
- if (filteredObject.length > 0) {
1226
- statement += ", ";
1227
- statement += filteredObject.map(([key, value]) => `${value} as "${key}"`).join(", ");
1229
+ instructions.including[key] = newValue;
1228
1230
  }
1229
1231
  }
1230
- 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 };
1231
1257
  };
1232
1258
 
1233
1259
  // src/instructions/to.ts
@@ -1294,8 +1320,8 @@ var handleTo = (models, model, statementParams, queryType, dependencyStatements,
1294
1320
  Object.assign(toInstruction, defaultFields);
1295
1321
  for (const fieldSlug in toInstruction) {
1296
1322
  const fieldValue = toInstruction[fieldSlug];
1297
- const fieldDetails = getFieldFromModel(model, fieldSlug, "to");
1298
- 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") {
1299
1325
  delete toInstruction[fieldSlug];
1300
1326
  const associativeModelSlug = composeAssociationModelSlug(model, fieldDetails.field);
1301
1327
  const composeStatement = (subQueryType, value) => {
@@ -1360,7 +1386,8 @@ var compileQueryInput = (defaultQuery, models, statementParams, options) => {
1360
1386
  statementParams,
1361
1387
  defaultQuery
1362
1388
  );
1363
- if (query === null) return { dependencies: [], main: dependencyStatements[0] };
1389
+ if (query === null)
1390
+ return { dependencies: [], main: dependencyStatements[0], loadedFields: [] };
1364
1391
  const parsedQuery = splitQuery(query);
1365
1392
  const { queryType, queryModel, queryInstructions } = parsedQuery;
1366
1393
  const model = getModelBySlug(models, queryModel);
@@ -1370,7 +1397,7 @@ var compileQueryInput = (defaultQuery, models, statementParams, options) => {
1370
1397
  if (instructions && Object.hasOwn(instructions, "for")) {
1371
1398
  instructions = handleFor(model, instructions);
1372
1399
  }
1373
- const { columns, isJoining } = handleSelecting(
1400
+ const { columns, isJoining, loadedFields } = handleSelecting(
1374
1401
  models,
1375
1402
  model,
1376
1403
  statementParams,
@@ -1499,7 +1526,8 @@ var compileQueryInput = (defaultQuery, models, statementParams, options) => {
1499
1526
  if (returning) mainStatement.returning = true;
1500
1527
  return {
1501
1528
  dependencies: dependencyStatements,
1502
- main: mainStatement
1529
+ main: mainStatement,
1530
+ loadedFields
1503
1531
  };
1504
1532
  };
1505
1533
 
@@ -1508,6 +1536,7 @@ var Transaction = class {
1508
1536
  statements;
1509
1537
  models = [];
1510
1538
  queries;
1539
+ fields = [];
1511
1540
  constructor(queries, options) {
1512
1541
  const models = options?.models || [];
1513
1542
  this.statements = this.compileQueries(queries, models, options);
@@ -1544,42 +1573,67 @@ var Transaction = class {
1544
1573
  );
1545
1574
  dependencyStatements.push(...result.dependencies);
1546
1575
  mainStatements.push(result.main);
1576
+ this.fields.push(result.loadedFields);
1547
1577
  }
1548
1578
  this.models = modelListWithPresets;
1549
1579
  return [...dependencyStatements, ...mainStatements];
1550
1580
  };
1551
- formatRecord(model, record) {
1552
- const formattedRecord = { ...record };
1553
- for (const key in record) {
1554
- 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
+ }
1555
1592
  if (field.type === "json") {
1556
- formattedRecord[key] = JSON.parse(record[key]);
1557
- continue;
1593
+ newValue = JSON.parse(value);
1558
1594
  }
1559
- formattedRecord[key] = record[key];
1595
+ record[newSlug] = newValue;
1560
1596
  }
1561
- return expand(formattedRecord);
1597
+ return expand(record);
1562
1598
  }
1563
- 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) {
1564
1612
  const relevantResults = results.filter((_, index) => {
1565
1613
  return this.statements[index].returning;
1566
1614
  });
1567
- 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) => {
1568
1623
  const query = this.queries.at(-index);
1624
+ const fields = this.fields.at(-index);
1569
1625
  const { queryType, queryModel, queryInstructions } = splitQuery(query);
1570
1626
  const model = getModelBySlug(this.models, queryModel);
1571
1627
  if (queryType === "count") {
1572
- return { amount: result[0]["COUNT(*)"] };
1628
+ return { amount: rows[0][0] };
1573
1629
  }
1574
1630
  const single = queryModel !== model.pluralSlug;
1575
1631
  if (single) {
1576
- return { record: this.formatRecord(model, result[0]) };
1632
+ return { record: this.formatRow(fields, rows[0]) };
1577
1633
  }
1578
1634
  const pageSize = queryInstructions?.limitedTo;
1579
1635
  const output = {
1580
- records: result.map((resultItem) => {
1581
- return this.formatRecord(model, resultItem);
1582
- })
1636
+ records: rows.map((row) => this.formatRow(fields, row))
1583
1637
  };
1584
1638
  if (pageSize && output.records.length > 0) {
1585
1639
  if (queryInstructions?.before || queryInstructions?.after) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ronin/compiler",
3
- "version": "0.10.3-leo-ron-1083-experimental-219",
3
+ "version": "0.10.3-leo-ron-1083-experimental-221",
4
4
  "type": "module",
5
5
  "description": "Compiles RONIN queries to SQL statements.",
6
6
  "publishConfig": {