@ronin/compiler 0.10.3-leo-ron-1083-experimental-218 → 0.10.3-leo-ron-1083-experimental-220

Sign up to get free protection for your applications and to get access to all the features.
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
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 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-218",
3
+ "version": "0.10.3-leo-ron-1083-experimental-220",
4
4
  "type": "module",
5
5
  "description": "Compiles RONIN queries to SQL statements.",
6
6
  "publishConfig": {