typeorm-extensions 2.2.0-rc.1 → 2.2.0-rc.10

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.
Files changed (112) hide show
  1. package/README.md +70 -39
  2. package/dist/decorators/calculated-column.decorator.d.ts +7 -3
  3. package/dist/decorators/calculated-column.decorator.js +35 -6
  4. package/dist/decorators/generated-column.decorator.d.ts +17 -0
  5. package/dist/decorators/generated-column.decorator.js +15 -0
  6. package/dist/decorators/index.d.ts +1 -0
  7. package/dist/decorators/index.js +1 -0
  8. package/dist/extensions/calculated-column.extension.d.ts +3 -3
  9. package/dist/extensions/calculated-column.extension.js +3 -16
  10. package/dist/extensions/cursor-pagination.extension.d.ts +2 -1
  11. package/dist/extensions/cursor-pagination.extension.js +7 -13
  12. package/dist/extensions/get-many-with-totals.extension.js +10 -29
  13. package/dist/extensions/join-lateral.extension.js +4 -6
  14. package/dist/extensions/order.extension.d.ts +2 -2
  15. package/dist/extensions/pagination.extension.d.ts +1 -1
  16. package/dist/extensions/period-filter.extension.d.ts +4 -0
  17. package/dist/extensions/period-filter.extension.js +1 -9
  18. package/dist/extensions/typed/index.d.ts +2 -0
  19. package/dist/extensions/typed/index.js +2 -0
  20. package/dist/extensions/typed/typed-calculated/typed-calculated-column.extension.d.ts +19 -0
  21. package/dist/extensions/typed/typed-calculated/typed-calculated-column.extension.js +17 -0
  22. package/dist/extensions/typed/typed-distinct/typed-distinct.extension.d.ts +6 -4
  23. package/dist/extensions/typed/typed-distinct/typed-distinct.extension.js +9 -2
  24. package/dist/extensions/typed/typed-group-by/typed-group-by.extension.d.ts +8 -7
  25. package/dist/extensions/typed/typed-group-by/typed-group-by.extension.js +1 -1
  26. package/dist/extensions/typed/typed-having/typed-having.extension.d.ts +4 -4
  27. package/dist/extensions/typed/typed-having/typed-having.extension.js +1 -1
  28. package/dist/extensions/typed/typed-joins/typed-join-and-map.extension.d.ts +19 -0
  29. package/dist/extensions/typed/typed-joins/typed-join-and-map.extension.js +23 -0
  30. package/dist/extensions/typed/typed-joins/typed-joins.extension.d.ts +4 -4
  31. package/dist/extensions/typed/typed-joins/typed-joins.extension.js +11 -22
  32. package/dist/extensions/typed/typed-order-by/typed-order-by-array-position.extension.d.ts +2 -2
  33. package/dist/extensions/typed/typed-order-by/typed-order-by.extension.d.ts +4 -2
  34. package/dist/extensions/typed/typed-order-by/typed-order-by.extension.js +18 -17
  35. package/dist/extensions/typed/typed-returning/typed-returning.extension.js +1 -9
  36. package/dist/extensions/typed/typed-root.extension.d.ts +26 -4
  37. package/dist/extensions/typed/typed-select/typed-select.extension.d.ts +6 -6
  38. package/dist/extensions/typed/typed-select/typed-select.extension.js +3 -9
  39. package/dist/extensions/typed/typed-where/typed-period-filter.extension.d.ts +5 -1
  40. package/dist/extensions/typed/typed-where/typed-period-filter.extension.js +1 -9
  41. package/dist/extensions/typed/typed-where/typed-where-array.extension.d.ts +13 -9
  42. package/dist/extensions/typed/typed-where/typed-where-array.extension.js +4 -16
  43. package/dist/extensions/typed/typed-where/typed-where-ilike.extension.d.ts +10 -6
  44. package/dist/extensions/typed/typed-where/typed-where-ilike.extension.js +1 -9
  45. package/dist/extensions/typed/typed-where/typed-where-is-any.extension.d.ts +10 -6
  46. package/dist/extensions/typed/typed-where/typed-where-is-any.extension.js +3 -20
  47. package/dist/extensions/typed/typed-where/typed-where-is-distinct-from.extension.d.ts +10 -6
  48. package/dist/extensions/typed/typed-where/typed-where-is-distinct-from.extension.js +3 -20
  49. package/dist/extensions/typed/typed-where/typed-where-is-in.extension.d.ts +10 -6
  50. package/dist/extensions/typed/typed-where/typed-where-is-in.extension.js +3 -21
  51. package/dist/extensions/typed/typed-where/typed-where-json.extension.d.ts +10 -6
  52. package/dist/extensions/typed/typed-where/typed-where-json.extension.js +4 -21
  53. package/dist/extensions/typed/typed-where/typed-where-lower.extension.d.ts +10 -6
  54. package/dist/extensions/typed/typed-where/typed-where-lower.extension.js +1 -9
  55. package/dist/extensions/typed/typed-where/typed-where-similar.extension.d.ts +8 -4
  56. package/dist/extensions/typed/typed-where/typed-where-similar.extension.js +1 -8
  57. package/dist/extensions/typed/typed-where/typed-where.extension.d.ts +7 -3
  58. package/dist/extensions/typed/typed-where/typed-where.extension.js +2 -10
  59. package/dist/extensions/union.extension.js +1 -1
  60. package/dist/extensions/where-array.extension.d.ts +4 -0
  61. package/dist/extensions/where-array.extension.js +1 -9
  62. package/dist/extensions/where-exists.extension.js +4 -15
  63. package/dist/extensions/where-is-any.extension.d.ts +41 -128
  64. package/dist/extensions/where-is-any.extension.js +1 -9
  65. package/dist/extensions/where-is-distinct-from.extension.d.ts +4 -0
  66. package/dist/extensions/where-is-distinct-from.extension.js +1 -9
  67. package/dist/extensions/where-is-in.extension.d.ts +38 -116
  68. package/dist/extensions/where-is-in.extension.js +11 -27
  69. package/dist/extensions/where-json.extension.d.ts +4 -0
  70. package/dist/extensions/where-json.extension.js +1 -9
  71. package/dist/extensions/where-relation-exists.extension.d.ts +6 -6
  72. package/dist/extensions/where-relation-exists.extension.js +5 -25
  73. package/dist/helpers.d.ts +5 -0
  74. package/dist/helpers.js +12 -3
  75. package/dist/index.d.ts +1 -2
  76. package/dist/index.js +1 -4
  77. package/dist/types/extensions/cursor-pagination.types.d.ts +5 -0
  78. package/dist/types/extensions/get-many-with-totals.types.d.ts +4 -0
  79. package/dist/types/extensions/index.d.ts +1 -0
  80. package/dist/types/extensions/index.js +1 -0
  81. package/dist/types/extensions/typed-group-by.types.d.ts +2 -2
  82. package/dist/types/extensions/typed-join-and-map.types.d.ts +6 -0
  83. package/dist/types/extensions/typed-join-and-map.types.js +2 -0
  84. package/dist/types/modules/typed-root.types.d.ts +11 -9
  85. package/dist/utils/calculated-column.utils.d.ts +11 -0
  86. package/dist/utils/calculated-column.utils.js +33 -4
  87. package/dist/utils/common.utils.d.ts +2 -0
  88. package/dist/utils/common.utils.js +4 -1
  89. package/dist/utils/cursor.utils.js +4 -6
  90. package/dist/utils/json-path.utils.js +2 -2
  91. package/dist/utils/pagination.utils.d.ts +7 -0
  92. package/dist/utils/pagination.utils.js +20 -10
  93. package/dist/utils/parameter.utils.d.ts +10 -0
  94. package/dist/utils/parameter.utils.js +22 -1
  95. package/dist/utils/period.utils.js +2 -1
  96. package/dist/utils/prototype.utils.d.ts +9 -0
  97. package/dist/utils/prototype.utils.js +12 -2
  98. package/dist/utils/query-builder.utils.d.ts +43 -4
  99. package/dist/utils/query-builder.utils.js +239 -36
  100. package/dist/utils/reserved-qualifier-guard.utils.d.ts +36 -0
  101. package/dist/utils/reserved-qualifier-guard.utils.js +98 -0
  102. package/dist/utils/reserved-qualifier.utils.d.ts +33 -0
  103. package/dist/utils/reserved-qualifier.utils.js +252 -0
  104. package/dist/utils/table.utils.d.ts +27 -11
  105. package/dist/utils/table.utils.js +58 -23
  106. package/dist/utils/time-zone.utils.d.ts +8 -0
  107. package/dist/utils/time-zone.utils.js +15 -5
  108. package/dist/utils/typed-where.utils.d.ts +9 -0
  109. package/dist/utils/typed-where.utils.js +17 -0
  110. package/dist/utils/where.utils.d.ts +2 -0
  111. package/dist/utils/where.utils.js +21 -14
  112. package/package.json +2 -2
@@ -1,9 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getTypedSelectionKey = exports.getTypedSingleColumnKey = exports.isTypedJsonPath = exports.getTypedColumnKey = exports.toFieldSql = exports.findFieldColumn = exports.getColumnDatabaseName = void 0;
3
+ exports.getQueryRunner = exports.withoutPaging = exports.getAppliedOrderBy = exports.getTypedSelectionKey = exports.resolveTypedProperty = exports.getTypedSingleColumnKey = exports.isTypedJsonPath = exports.getTypedColumnKey = exports.findTypedCalculatedProperty = exports.toFieldSql = exports.findFieldColumn = exports.getColumnDatabaseName = exports.resolveDeferredKeys = exports.listQueryAliases = void 0;
4
+ const typeorm_1 = require("typeorm");
5
+ const QueryExpressionMap_1 = require("typeorm/query-builder/QueryExpressionMap");
4
6
  const proxy_key_utils_1 = require("./proxy-key.utils");
5
7
  const json_path_utils_1 = require("./json-path.utils");
6
8
  const sql_utils_1 = require("./sql.utils");
9
+ const calculated_column_utils_1 = require("./calculated-column.utils");
7
10
  /**
8
11
  * Whether single-level typed keys are qualified with the main alias (`user.firstName`).
9
12
  *
@@ -30,13 +33,131 @@ function requireMainAliasName(queryBuilder, key) {
30
33
  }
31
34
  return mainAlias.name;
32
35
  }
36
+ /**
37
+ * The aliases of the query and of the queries it is part of, its own first. The builder of a `Brackets` callback
38
+ * has the outer query's main alias but an empty alias list: the aliases, joins included, are on its parent. A
39
+ * subquery sees the aliases of the outer query too.
40
+ */
41
+ function listQueryAliases(builder) {
42
+ var _a;
43
+ const aliases = [];
44
+ // parentQueryBuilder is protected in TypeORM's types
45
+ for (let query = builder; query; query = query.parentQueryBuilder) {
46
+ const { aliases: own = [], mainAlias } = (_a = query.expressionMap) !== null && _a !== void 0 ? _a : {};
47
+ aliases.push(...own, ...(mainAlias ? [mainAlias] : []));
48
+ }
49
+ return aliases;
50
+ }
51
+ exports.listQueryAliases = listQueryAliases;
52
+ /** An alias of the query by name, or of a query it is part of (see `listQueryAliases`). */
53
+ function findQueryAlias(queryBuilder, aliasName) {
54
+ return listQueryAliases(queryBuilder).find((candidate) => candidate.name === aliasName);
55
+ }
56
+ // private-use characters: no identifier or SQL that TypeORM builds contains them
57
+ const DEFERRED_KEY_START = '\uE000';
58
+ const DEFERRED_PROPERTY = '\uE001';
59
+ const DEFERRED_FIELD = '\uE002';
60
+ const DEFERRED_UNQUOTED = '\uE004';
61
+ const DEFERRED_KEY_END = '\uE003';
62
+ const DEFERRED_KEY = /\uE000([^\uE000-\uE004]+)([\uE001\uE002\uE004])([^\uE000-\uE004]+)\uE003/g;
63
+ const TWO_PART_FIELD = /^([A-Za-z_][\w$]*)\.([A-Za-z_][\w$]*)$/;
64
+ /**
65
+ * Placeholder for a column of an alias the query doesn't have yet, e.g. a join added after the call: resolved when
66
+ * the alias is added (`resolveAliasKeys`) or, at the latest, when the SQL is built (`resolveDeferredKeys`).
67
+ */
68
+ function deferColumnKey(aliasName, separator, name) {
69
+ return `${DEFERRED_KEY_START}${aliasName}${separator}${name}${DEFERRED_KEY_END}`;
70
+ }
71
+ /**
72
+ * SQL of a deferred key for `alias`, undefined while the query doesn't have it:
73
+ * - a quoted key: the database column through the alias' entity, quoted, `"usr"."receipts_email"`; the name
74
+ * quoted without an entity;
75
+ * - an unquoted one (select, order, group by, distinct without `useQuotes`): `alias.property` on an alias with an
76
+ * entity, which TypeORM maps and reads as a column of the entity; quoted on an alias without one (a subquery, a
77
+ * CTE), as on an alias known at the call, and on an alias the query never has, which only raw SQL adds (a LATERAL
78
+ * subquery in the condition of a raw join): TypeORM can't map it, and PostgreSQL would fold it to lower case.
79
+ */
80
+ function toDeferredKeySql(alias, aliasName, separator, name, escape) {
81
+ const metadata = metadataOf(alias);
82
+ if (separator === DEFERRED_UNQUOTED) {
83
+ return metadata ? `${aliasName}.${name}` : `${escape(aliasName)}.${escape(name)}`;
84
+ }
85
+ const column = metadata && (separator === DEFERRED_FIELD ? findColumn(metadata, name) : metadata.findColumnWithPropertyPath(name));
86
+ const columnName = column && !column.isVirtualProperty ? column.databaseName : name;
87
+ return `${escape(aliasName)}.${escape(columnName)}`;
88
+ }
89
+ /** Replaces placeholders of `deferColumnKey` in SQL built by `builder`, through the aliases it has by then. */
90
+ function resolveDeferredKeys(builder, statement) {
91
+ if (typeof statement !== 'string' || !statement.includes(DEFERRED_KEY_START)) {
92
+ return statement;
93
+ }
94
+ const queryBuilder = builder;
95
+ const escape = (identifier) => queryBuilder.escape(identifier);
96
+ return statement.replace(DEFERRED_KEY, (_match, aliasName, separator, name) => toDeferredKeySql(findQueryAlias(queryBuilder, aliasName), aliasName, separator, name, escape));
97
+ }
98
+ exports.resolveDeferredKeys = resolveDeferredKeys;
99
+ /**
100
+ * Resolves the placeholders of a new alias in select, order, group by and distinct on, which TypeORM reads before it
101
+ * builds the SQL: a select of `alias.property` is how it knows to fill the property of a joined entity.
102
+ */
103
+ function resolveAliasKeys(expressionMap, alias) {
104
+ var _a;
105
+ // `connection` (TypeORM 0.3) or `dataSource` (1.x), protected in its types; read only when a key is found
106
+ const { connection, dataSource } = expressionMap;
107
+ const escape = (identifier) => (connection !== null && connection !== void 0 ? connection : dataSource).driver.escape(identifier);
108
+ const hasKey = (statement) => typeof statement === 'string' && statement.includes(DEFERRED_KEY_START);
109
+ const resolve = (statement) => hasKey(statement)
110
+ ? statement.replace(DEFERRED_KEY, (match, aliasName, separator, name) => aliasName === alias.name ? toDeferredKeySql(alias, aliasName, separator, name, escape) : match)
111
+ : statement;
112
+ // new objects and arrays: a clone shares them with the query it was cloned from
113
+ if (expressionMap.selects.some((select) => hasKey(select.selection))) {
114
+ expressionMap.selects = expressionMap.selects.map((select) => hasKey(select.selection) ? { ...select, selection: resolve(select.selection) } : select);
115
+ }
116
+ if (expressionMap.groupBys.some(hasKey)) {
117
+ expressionMap.groupBys = expressionMap.groupBys.map(resolve);
118
+ }
119
+ if ((_a = expressionMap.selectDistinctOn) === null || _a === void 0 ? void 0 : _a.some(hasKey)) {
120
+ expressionMap.selectDistinctOn = expressionMap.selectDistinctOn.map(resolve);
121
+ }
122
+ if (Object.keys(expressionMap.orderBys).some(hasKey)) {
123
+ expressionMap.orderBys = Object.fromEntries(Object.entries(expressionMap.orderBys).map(([key, order]) => [resolve(key), order]));
124
+ }
125
+ }
126
+ const DEFERRED_KEYS_INSTALLED = Symbol.for('typeorm-extensions.deferredKeys');
127
+ function installDeferredKeyResolution() {
128
+ const builderPrototype = typeorm_1.QueryBuilder.prototype;
129
+ const mapPrototype = QueryExpressionMap_1.QueryExpressionMap.prototype;
130
+ if (builderPrototype[DEFERRED_KEYS_INSTALLED]) {
131
+ return;
132
+ }
133
+ builderPrototype[DEFERRED_KEYS_INSTALLED] = true;
134
+ // TypeORM 0.3.12 maps property names in each part of the query, later versions once in the whole query; keys are
135
+ // resolved first, so an unquoted `alias.property` is mapped too
136
+ for (const method of ['replacePropertyNames', 'replacePropertyNamesForTheWholeQuery']) {
137
+ const original = builderPrototype[method];
138
+ if (typeof original === 'function') {
139
+ builderPrototype[method] = function (statement) {
140
+ return original.call(this, resolveDeferredKeys(this, statement));
141
+ };
142
+ }
143
+ }
144
+ const createAlias = mapPrototype.createAlias;
145
+ if (typeof createAlias === 'function') {
146
+ mapPrototype.createAlias = function (...args) {
147
+ const alias = createAlias.apply(this, args);
148
+ if (alias === null || alias === void 0 ? void 0 : alias.name) {
149
+ resolveAliasKeys(this, alias);
150
+ }
151
+ return alias;
152
+ };
153
+ }
154
+ }
155
+ installDeferredKeyResolution();
156
+ const metadataOf = (alias) => (alias === null || alias === void 0 ? void 0 : alias.hasMetadata) ? alias.metadata : undefined;
33
157
  /** Entity metadata of a query alias (the main alias when `aliasName` is undefined), if it has any. */
34
158
  function getAliasMetadata(queryBuilder, aliasName) {
35
- const expressionMap = queryBuilder.expressionMap;
36
- const alias = aliasName === undefined
37
- ? expressionMap === null || expressionMap === void 0 ? void 0 : expressionMap.mainAlias
38
- : expressionMap === null || expressionMap === void 0 ? void 0 : expressionMap.aliases.find((queryAlias) => queryAlias.name === aliasName);
39
- return (alias === null || alias === void 0 ? void 0 : alias.hasMetadata) ? alias.metadata : undefined;
159
+ var _a;
160
+ return metadataOf(aliasName === undefined ? (_a = queryBuilder.expressionMap) === null || _a === void 0 ? void 0 : _a.mainAlias : findQueryAlias(queryBuilder, aliasName));
40
161
  }
41
162
  /**
42
163
  * Database name of an entity property's column (`@Column({ name })`), looked up in the metadata of
@@ -66,6 +187,7 @@ function findFieldColumn(builder, field) {
66
187
  const metadata = getAliasMetadata(queryBuilder, field.slice(0, dot));
67
188
  return metadata && findColumn(metadata, field.slice(dot + 1));
68
189
  }
190
+ // only by database name: an unqualified property name may mean another table's column
69
191
  return (_a = getAliasMetadata(queryBuilder, undefined)) === null || _a === void 0 ? void 0 : _a.columns.find((column) => column.databaseName === field);
70
192
  }
71
193
  exports.findFieldColumn = findFieldColumn;
@@ -85,27 +207,18 @@ exports.findFieldColumn = findFieldColumn;
85
207
  * `stringToSQLIdentifier` does, in the driver's quotes.
86
208
  */
87
209
  function toFieldSql(builder, field) {
88
- var _a;
89
210
  const queryBuilder = builder;
211
+ const column = findFieldColumn(builder, field);
90
212
  const dot = field.indexOf('.');
91
- if (dot > 0) {
92
- const aliasName = field.slice(0, dot);
93
- const metadata = ((_a = queryBuilder.expressionMap) === null || _a === void 0 ? void 0 : _a.aliases.some((alias) => alias.name === aliasName))
94
- ? getAliasMetadata(queryBuilder, aliasName)
95
- : undefined;
96
- const column = metadata && findColumn(metadata, field.slice(dot + 1));
97
- if (column) {
98
- const columnSql = queryBuilder.escape(column.databaseName);
99
- return isAliasPrefixingEnabled(queryBuilder) ? `${queryBuilder.escape(aliasName)}.${columnSql}` : columnSql;
100
- }
213
+ if (column) {
214
+ const columnSql = queryBuilder.escape(column.databaseName);
215
+ return dot > 0 && isAliasPrefixingEnabled(queryBuilder)
216
+ ? `${queryBuilder.escape(field.slice(0, dot))}.${columnSql}`
217
+ : columnSql;
101
218
  }
102
- else {
103
- // only by database name: an unqualified property name may mean another table's column
104
- const metadata = getAliasMetadata(queryBuilder, undefined);
105
- const column = metadata === null || metadata === void 0 ? void 0 : metadata.columns.find((candidate) => candidate.databaseName === field);
106
- if (column) {
107
- return queryBuilder.escape(column.databaseName);
108
- }
219
+ const parts = dot > 0 ? TWO_PART_FIELD.exec(field) : null;
220
+ if (parts && !findQueryAlias(queryBuilder, parts[1])) {
221
+ return deferColumnKey(parts[1], DEFERRED_FIELD, parts[2]);
109
222
  }
110
223
  return (0, sql_utils_1.stringToSQLIdentifier)(field, (0, sql_utils_1.getDriverType)(builder));
111
224
  }
@@ -146,27 +259,33 @@ function findColumnPath(metadata, keys) {
146
259
  * before `from()` the property name is used as the column name.
147
260
  */
148
261
  function resolveTypedKey(queryBuilder, keys) {
149
- var _a, _b;
150
262
  const resolved = resolveTypedKeyPath(queryBuilder, keys);
151
263
  // deeper than `alias.column` only inside a JSON column: a nested relation can't be mapped to an alias
152
264
  if (!resolved.jsonPath && keys.length > proxy_key_utils_1.MAX_KEY_DEPTH) {
153
265
  throw (0, proxy_key_utils_1.createKeyTooDeepError)(keys);
154
266
  }
155
- const column = (_a = resolved.column) !== null && _a !== void 0 ? _a : (_b = resolved.metadata) === null || _b === void 0 ? void 0 : _b.findColumnWithPropertyPath(resolved.propertyPath);
156
267
  // a @CalculatedColumn: TypeORM would write "alias"."property", a column the table doesn't have
157
- if ((column === null || column === void 0 ? void 0 : column.isVirtualProperty) && !column.query) {
268
+ if ((0, calculated_column_utils_1.isCalculated)(resolved.column)) {
158
269
  throw new TypeError(`${keys.join('.')} is a @CalculatedColumn, not a column of the table: use the SQL expression that fills it`);
159
270
  }
160
271
  return resolved;
161
272
  }
273
+ /** The alias and property of the `@CalculatedColumn` a typed key picks; undefined for any other key. */
274
+ function findTypedCalculatedProperty(builder, keyHolder) {
275
+ const resolved = resolveTypedKeyPath(builder, keyHolder.getKeys());
276
+ return (0, calculated_column_utils_1.isCalculated)(resolved.column) ? { aliasName: resolved.aliasName, propertyPath: resolved.propertyPath } : undefined;
277
+ }
278
+ exports.findTypedCalculatedProperty = findTypedCalculatedProperty;
162
279
  function resolveTypedKeyPath(queryBuilder, keys) {
163
280
  const prefixing = isAliasPrefixingEnabled(queryBuilder);
164
281
  if (keys.length === 1) {
165
282
  const aliasName = prefixing ? requireMainAliasName(queryBuilder, keys[0]) : undefined;
283
+ const metadata = getAliasMetadata(queryBuilder, aliasName);
166
284
  return {
167
285
  aliasName,
168
286
  propertyPath: keys[0],
169
- metadata: getAliasMetadata(queryBuilder, aliasName),
287
+ metadata,
288
+ column: metadata === null || metadata === void 0 ? void 0 : metadata.findColumnWithPropertyPath(keys[0]),
170
289
  };
171
290
  }
172
291
  const [firstKey, ...restKeys] = keys;
@@ -182,7 +301,7 @@ function resolveTypedKeyPath(queryBuilder, keys) {
182
301
  ...findColumnPath(metadata, restKeys),
183
302
  };
184
303
  }
185
- const queryAlias = expressionMap === null || expressionMap === void 0 ? void 0 : expressionMap.aliases.find((alias) => alias.name === firstKey);
304
+ const queryAlias = findQueryAlias(queryBuilder, firstKey);
186
305
  if (!queryAlias && mainAlias) {
187
306
  const mainMetadata = getAliasMetadata(queryBuilder, undefined);
188
307
  const columnPath = findColumnPath(mainMetadata, keys);
@@ -194,7 +313,7 @@ function resolveTypedKeyPath(queryBuilder, keys) {
194
313
  };
195
314
  }
196
315
  }
197
- const metadata = (queryAlias === null || queryAlias === void 0 ? void 0 : queryAlias.hasMetadata) ? queryAlias.metadata : undefined;
316
+ const metadata = metadataOf(queryAlias);
198
317
  return {
199
318
  aliasName: firstKey,
200
319
  propertyPath: restKeys.join('.'),
@@ -207,8 +326,11 @@ function resolveTypedKeyPath(queryBuilder, keys) {
207
326
  * inside a JSON column: `"exec"."details" -> 'meta' ->> 'endTs'` (see `toJsonPathSql`).
208
327
  */
209
328
  function toQuotedColumnKey(queryBuilder, { aliasName, propertyPath, metadata, column, jsonPath }, jsonOptions) {
210
- var _a, _b;
211
- const columnName = (_b = (_a = (column !== null && column !== void 0 ? column : metadata === null || metadata === void 0 ? void 0 : metadata.findColumnWithPropertyPath(propertyPath))) === null || _a === void 0 ? void 0 : _a.databaseName) !== null && _b !== void 0 ? _b : propertyPath;
329
+ var _a;
330
+ if (aliasName !== undefined && !metadata && !findQueryAlias(queryBuilder, aliasName)) {
331
+ return deferColumnKey(aliasName, DEFERRED_PROPERTY, propertyPath);
332
+ }
333
+ const columnName = (_a = column === null || column === void 0 ? void 0 : column.databaseName) !== null && _a !== void 0 ? _a : propertyPath;
212
334
  const columnSql = aliasName === undefined
213
335
  ? queryBuilder.escape(columnName)
214
336
  : `${queryBuilder.escape(aliasName)}.${queryBuilder.escape(columnName)}`;
@@ -255,13 +377,51 @@ function getTypedSingleColumnKey(builder, callback, methodName, jsonOptions) {
255
377
  }
256
378
  exports.getTypedSingleColumnKey = getTypedSingleColumnKey;
257
379
  /**
258
- * Key for typed select / order / distinct on a SelectQueryBuilder.
380
+ * The alias and the entity property a typed callback picks, for methods that take a property rather than a column
381
+ * (a `@CalculatedColumn`): `u => u.imagesCount` is on the main alias, `q => q.owner.imagesCount` on `owner`.
382
+ */
383
+ function resolveTypedProperty(builder, callback, methodName) {
384
+ const keyHolder = (0, proxy_key_utils_1.getKeyHolder)(callback);
385
+ if (Array.isArray(keyHolder)) {
386
+ throw new TypeError(`${methodName} expects the callback to pick a single property, got ${keyHolder.length}`);
387
+ }
388
+ const { aliasName, propertyPath } = resolveTypedKeyPath(builder, keyHolder.getKeys());
389
+ return { aliasName, propertyPath };
390
+ }
391
+ exports.resolveTypedProperty = resolveTypedProperty;
392
+ /**
393
+ * Throws for a key that names no column of its table: a relation whose join column is on the other side
394
+ * (one-to-many, many-to-many, the inverse side of a one-to-one) or an embedded entity. TypeORM leaves such a key
395
+ * as written, and the database rejects it.
396
+ */
397
+ function assertTableColumnKey({ metadata, propertyPath }, keys, methodName) {
398
+ const relation = metadata === null || metadata === void 0 ? void 0 : metadata.findRelationWithPropertyPath(propertyPath);
399
+ if (relation && !relation.isManyToOne && !relation.isOneToOneOwner) {
400
+ throw new TypeError(`${methodName}: "${keys.join('.')}" is a relation without a join column in its table. ` +
401
+ `Join it and address its columns by the join alias`);
402
+ }
403
+ if (!relation && (metadata === null || metadata === void 0 ? void 0 : metadata.findEmbeddedWithPropertyPath(propertyPath))) {
404
+ throw new TypeError(`${methodName}: "${keys.join('.')}" is an embedded entity, not a column: address its columns, e.g. ` +
405
+ `"${keys.join('.')}.someColumn"`);
406
+ }
407
+ }
408
+ // an alias the query doesn't know yet is resolved when it is added (`resolveAliasKeys`)
409
+ function isAliasWithoutMetadata(queryBuilder, aliasName) {
410
+ const alias = aliasName === undefined ? undefined : findQueryAlias(queryBuilder, aliasName);
411
+ return alias !== undefined && !alias.hasMetadata;
412
+ }
413
+ /**
414
+ * Key for typed select / order / distinct / group by on a SelectQueryBuilder.
259
415
  *
260
416
  * Without quotes it is `alias.propertyPath` (`user.name`, `images.url`, `p.address.city`),
261
417
  * which TypeORM maps to the column itself. With `useQuotes` it is the quoted database column
262
- * (`"t"."ticket_status"`), since TypeORM doesn't map quoted keys. Strings are kept as is.
418
+ * (`"t"."ticket_status"`), since TypeORM doesn't map quoted keys. An alias of the query without
419
+ * metadata (a subquery or a CTE in FROM or a join), or one the query still doesn't have when the SQL
420
+ * is built, is quoted unless `useQuotes` is `false`: TypeORM leaves its keys as written, and
421
+ * PostgreSQL folds an unquoted `cte.firstName` to `firstname`.
422
+ * Strings are kept as is.
263
423
  */
264
- function getTypedSelectionKey(builder, selection, useQuotes) {
424
+ function getTypedSelectionKey(builder, selection, useQuotes, columnsFor) {
265
425
  if (typeof selection === 'string') {
266
426
  return selection;
267
427
  }
@@ -272,9 +432,20 @@ function getTypedSelectionKey(builder, selection, useQuotes) {
272
432
  throw new TypeError(`Typed key "${keyHolder.getKeys().join('.')}" points into the JSON column "${resolved.propertyPath}": ` +
273
433
  `select, order, distinct and group by take columns only. For an expression use tableHelper().jsonColumn()`);
274
434
  }
275
- if (useQuotes) {
435
+ if (columnsFor !== undefined) {
436
+ assertTableColumnKey(resolved, keyHolder.getKeys(), columnsFor);
437
+ }
438
+ if (useQuotes !== null && useQuotes !== void 0 ? useQuotes : isAliasWithoutMetadata(queryBuilder, resolved.aliasName)) {
276
439
  return toQuotedColumnKey(queryBuilder, resolved);
277
440
  }
441
+ // an alias added later decides then: TypeORM maps an entity's keys, a subquery's are quoted
442
+ if (useQuotes === undefined &&
443
+ columnsFor !== undefined &&
444
+ resolved.aliasName !== undefined &&
445
+ !resolved.metadata &&
446
+ !findQueryAlias(queryBuilder, resolved.aliasName)) {
447
+ return deferColumnKey(resolved.aliasName, DEFERRED_UNQUOTED, resolved.propertyPath);
448
+ }
278
449
  return resolved.aliasName === undefined
279
450
  ? resolved.propertyPath
280
451
  : `${resolved.aliasName}.${resolved.propertyPath}`;
@@ -283,3 +454,35 @@ function getTypedSelectionKey(builder, selection, useQuotes) {
283
454
  return Array.isArray(keyHolder) ? keyHolder.map(toKey) : toKey(keyHolder);
284
455
  }
285
456
  exports.getTypedSelectionKey = getTypedSelectionKey;
457
+ /**
458
+ * The ORDER BY a query is built with, in the order it was applied. `allOrderBys`, not `orderBys`: it is what the SQL
459
+ * is built from, so without an explicit order it falls back to the `orderBy` of @Entity, and respects the
460
+ * `disable-global-order` option.
461
+ */
462
+ function getAppliedOrderBy(builder) {
463
+ var _a;
464
+ return Object.entries((_a = builder.expressionMap.allOrderBys) !== null && _a !== void 0 ? _a : {}).map(([key, value]) => {
465
+ const selector = resolveDeferredKeys(builder, key);
466
+ return typeof value === 'string'
467
+ ? { selector, order: value }
468
+ : { selector, order: value.order, nulls: value.nulls };
469
+ });
470
+ }
471
+ exports.getAppliedOrderBy = getAppliedOrderBy;
472
+ /** The query without paging: no ORDER BY (nor the entity's default one), LIMIT, OFFSET, take or skip. */
473
+ function withoutPaging(query) {
474
+ return query
475
+ .orderBy()
476
+ .offset(undefined)
477
+ .limit(undefined)
478
+ .skip(undefined)
479
+ .take(undefined)
480
+ .setOption('disable-global-order');
481
+ }
482
+ exports.withoutPaging = withoutPaging;
483
+ /** The query runner a builder was created with, from a transactional manager; `undefined` for one from the pool. */
484
+ function getQueryRunner(builder) {
485
+ // queryRunner is protected in TypeORM's types
486
+ return builder.queryRunner;
487
+ }
488
+ exports.getQueryRunner = getQueryRunner;
@@ -0,0 +1,36 @@
1
+ import { ReservedQualifier } from './reserved-qualifier.utils';
2
+ /** The SQL of a query names a reserved word as a qualifier without quotes; see `enableReservedQualifierGuard`. */
3
+ export declare class ReservedQualifierError extends Error {
4
+ /** The SQL of the query, as the driver gets it. */
5
+ readonly sql: string;
6
+ /** Each reserved word used as a qualifier, in the order of the SQL. */
7
+ readonly qualifiers: ReservedQualifier[];
8
+ constructor(sql: string, qualifiers: ReservedQualifier[]);
9
+ }
10
+ type GuardWordsOption = {
11
+ /**
12
+ * The words to report, instead of the default list of the database (`POSTGRES_RESERVED_KEY_WORDS`,
13
+ * `COCKROACHDB_RESERVED_KEY_WORDS` on CockroachDB). Spread a default list to extend it.
14
+ */
15
+ words?: readonly string[];
16
+ };
17
+ export type ReservedQualifierGuardOptions = GuardWordsOption & ({
18
+ /** `'throw'` (the default): the query rejects with a `ReservedQualifierError` before it runs. */
19
+ mode?: 'throw';
20
+ } | {
21
+ /** `'warn'`: the query runs, and the error is passed to `onViolation`. */
22
+ mode: 'warn';
23
+ /** Reports the error the query would throw in `'throw'` mode; `console.warn` by default. */
24
+ onViolation?: (violation: ReservedQualifierError) => void;
25
+ });
26
+ /**
27
+ * Checks the SQL of every query builder on PostgreSQL and CockroachDB for a reserved word used as a qualifier
28
+ * without quotes, such as `order."userId"` or `user.id` (`findBareReservedQualifiers`), before the query runs.
29
+ * TypeORM quotes the aliases it writes, so such a word comes from SQL written by hand, which PostgreSQL rejects.
30
+ *
31
+ * Off by default. A second call replaces the options. Other databases aren't checked.
32
+ */
33
+ export declare function enableReservedQualifierGuard(options?: ReservedQualifierGuardOptions): void;
34
+ /** Turns the check of `enableReservedQualifierGuard` off. */
35
+ export declare function disableReservedQualifierGuard(): void;
36
+ export {};
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.disableReservedQualifierGuard = exports.enableReservedQualifierGuard = exports.ReservedQualifierError = void 0;
4
+ const typeorm_1 = require("typeorm");
5
+ const reserved_qualifier_utils_1 = require("./reserved-qualifier.utils");
6
+ const sql_utils_1 = require("./sql.utils");
7
+ const NAME_AFTER_DOT = /^\s*\.\s*("(?:[^"]|"")*"|[A-Za-z_][\w$]*|\*)/;
8
+ /** `"order"."userId"` for `order."userId"` / `order.userId`, `"order".*` for `order.*`. */
9
+ function toQuotedReference(sql, { word, index }) {
10
+ var _a, _b;
11
+ const name = (_b = (_a = NAME_AFTER_DOT.exec(sql.slice(index + word.length))) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 'column';
12
+ const quotedName = name.startsWith('"') || name === '*' ? name : `"${name}"`;
13
+ return `"${word}".${quotedName}`;
14
+ }
15
+ /** The SQL of a query names a reserved word as a qualifier without quotes; see `enableReservedQualifierGuard`. */
16
+ class ReservedQualifierError extends Error {
17
+ constructor(sql, qualifiers) {
18
+ const [first] = qualifiers;
19
+ const found = qualifiers.map(({ word, snippet }) => `"${word}" in ${snippet}`).join('; ');
20
+ super(`Reserved word used as a qualifier without quotes: ${found}. ` +
21
+ `PostgreSQL reads it as the key word, not as an alias, and rejects the query. ` +
22
+ (first
23
+ ? `Quote both halves, ${toQuotedReference(sql, first)}, or build the reference with tableHelper or the typed methods`
24
+ : ''));
25
+ this.name = 'ReservedQualifierError';
26
+ this.sql = sql;
27
+ this.qualifiers = qualifiers;
28
+ }
29
+ }
30
+ exports.ReservedQualifierError = ReservedQualifierError;
31
+ let guard;
32
+ let installed = false;
33
+ let lastScan;
34
+ const reportViolation = (violation) => console.warn(violation);
35
+ function checkQuery(queryBuilder, sql, state) {
36
+ var _a;
37
+ const driver = (0, sql_utils_1.getDriverType)(queryBuilder);
38
+ if (typeof sql !== 'string' || !(0, sql_utils_1.isPostgresLikeDriver)(driver)) {
39
+ return;
40
+ }
41
+ const words = (_a = state.words) !== null && _a !== void 0 ? _a : (0, reserved_qualifier_utils_1.toReservedWordSet)(driver === 'cockroachdb' ? reserved_qualifier_utils_1.COCKROACHDB_RESERVED_KEY_WORDS : reserved_qualifier_utils_1.POSTGRES_RESERVED_KEY_WORDS);
42
+ // the same SQL again, as a query run in a loop: one scan
43
+ const qualifiers = lastScan && lastScan.sql === sql && lastScan.words === words
44
+ ? lastScan.qualifiers
45
+ : (0, reserved_qualifier_utils_1.scanReservedQualifiers)(sql, words);
46
+ lastScan = { sql, words, qualifiers };
47
+ if (qualifiers.length === 0) {
48
+ return;
49
+ }
50
+ const violation = new ReservedQualifierError(sql, qualifiers);
51
+ if (state.mode === 'throw') {
52
+ throw violation;
53
+ }
54
+ state.onViolation(violation);
55
+ }
56
+ // every query of every builder gets its SQL here before it runs: getMany, getRawMany, getOne, getCount, stream,
57
+ // execute; getSql() too. getQuery() isn't checked: a subquery's SQL is checked as part of the query it is in
58
+ function installGuard() {
59
+ if (installed) {
60
+ return;
61
+ }
62
+ installed = true;
63
+ const getQueryAndParameters = typeorm_1.QueryBuilder.prototype.getQueryAndParameters;
64
+ typeorm_1.QueryBuilder.prototype.getQueryAndParameters = function () {
65
+ const queryAndParameters = getQueryAndParameters.call(this);
66
+ if (guard) {
67
+ checkQuery(this, queryAndParameters[0], guard);
68
+ }
69
+ return queryAndParameters;
70
+ };
71
+ }
72
+ /**
73
+ * Checks the SQL of every query builder on PostgreSQL and CockroachDB for a reserved word used as a qualifier
74
+ * without quotes, such as `order."userId"` or `user.id` (`findBareReservedQualifiers`), before the query runs.
75
+ * TypeORM quotes the aliases it writes, so such a word comes from SQL written by hand, which PostgreSQL rejects.
76
+ *
77
+ * Off by default. A second call replaces the options. Other databases aren't checked.
78
+ */
79
+ function enableReservedQualifierGuard(options = {}) {
80
+ var _a;
81
+ const mode = (_a = options.mode) !== null && _a !== void 0 ? _a : 'throw';
82
+ if (mode !== 'throw' && mode !== 'warn') {
83
+ throw new TypeError(`Invalid mode ${JSON.stringify(mode)} of the reserved qualifier guard: 'throw' or 'warn'`);
84
+ }
85
+ guard = {
86
+ mode,
87
+ words: options.words === undefined ? undefined : (0, reserved_qualifier_utils_1.toReservedWordSet)(options.words),
88
+ onViolation: (options.mode === 'warn' && options.onViolation) || reportViolation,
89
+ };
90
+ installGuard();
91
+ }
92
+ exports.enableReservedQualifierGuard = enableReservedQualifierGuard;
93
+ /** Turns the check of `enableReservedQualifierGuard` off. */
94
+ function disableReservedQualifierGuard() {
95
+ guard = undefined;
96
+ lastScan = undefined;
97
+ }
98
+ exports.disableReservedQualifierGuard = disableReservedQualifierGuard;
@@ -0,0 +1,33 @@
1
+ /** A reserved word that qualifies a name without quotes, as in `order."userId"`. */
2
+ export interface ReservedQualifier {
3
+ /** The word as the SQL has it: `order`, `ORDER`. */
4
+ word: string;
5
+ /** Offset of the word in the SQL. */
6
+ index: number;
7
+ /** The SQL around the word, on one line. */
8
+ snippet: string;
9
+ }
10
+ /**
11
+ * The reserved key words of PostgreSQL, "reserved" in Appendix C of its documentation (`SYSTEM_USER` since
12
+ * PostgreSQL 16). None can be a table, an alias or a schema without quotes, so none is followed by a `.` in valid
13
+ * SQL. The words "reserved (can be function or type)" (`LEFT`, `JOIN`, `BINARY`, …) aren't on the list: one may
14
+ * name the schema of a type, as in `CAST(x AS binary.my_type)`.
15
+ */
16
+ export declare const POSTGRES_RESERVED_KEY_WORDS: readonly string[];
17
+ /** The reserved key words of CockroachDB: PostgreSQL's without `SYSTEM_USER`, with `CONCURRENTLY` and `CURRENT_SCHEMA`. */
18
+ export declare const COCKROACHDB_RESERVED_KEY_WORDS: readonly string[];
19
+ /** The words in lower case; cached for a frozen list, such as the defaults, which can't change afterwards. */
20
+ export declare function toReservedWordSet(words: readonly string[]): ReadonlySet<string>;
21
+ /** `findBareReservedQualifiers` with the words as a set of lower-case words (`toReservedWordSet`). */
22
+ export declare function scanReservedQualifiers(sql: string, words: ReadonlySet<string>): ReservedQualifier[];
23
+ /**
24
+ * Reserved words that qualify a name without quotes, as in `order."userId"` or `user.id`: PostgreSQL reads such a
25
+ * word as the key word, not as an alias, and rejects the statement (`syntax error at or near "order"`).
26
+ *
27
+ * Words in string literals (`E'…'` and dollar-quoted ones too), quoted identifiers, comments and parameters are
28
+ * skipped, and so are words after a dot (`public.order`) and a number after one (`THEN .5`). Case-insensitive;
29
+ * whitespace may come before the dot (`group . name`).
30
+ *
31
+ * @param words the words to look for, `POSTGRES_RESERVED_KEY_WORDS` by default
32
+ */
33
+ export declare function findBareReservedQualifiers(sql: string, words?: readonly string[]): ReservedQualifier[];