typeorm-extensions 2.2.0-rc.3 → 2.2.0-rc.5

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.
@@ -5,6 +5,7 @@ const SelectQueryBuilder_1 = require("typeorm/query-builder/SelectQueryBuilder")
5
5
  const pagination_utils_1 = require("../utils/pagination.utils");
6
6
  const constants_1 = require("../constants");
7
7
  const prototype_utils_1 = require("../utils/prototype.utils");
8
+ const query_builder_utils_1 = require("../utils/query-builder.utils");
8
9
  // patching
9
10
  const extension = {
10
11
  prototype: {
@@ -111,9 +112,12 @@ async function readWithTotals(builder, paginationFilter, options, read) {
111
112
  }
112
113
  // allOrderBys, not orderBys: it is what the SQL is built from, so without an explicit order it
113
114
  // falls back to the `orderBy` of @Entity, and respects the `disable-global-order` option
114
- const orderBy = Object.entries((_a = builder.expressionMap.allOrderBys) !== null && _a !== void 0 ? _a : {}).map(([selector, value]) => typeof value === 'string'
115
- ? { selector, order: value }
116
- : { selector, order: value.order, nulls: value.nulls });
115
+ const orderBy = Object.entries((_a = builder.expressionMap.allOrderBys) !== null && _a !== void 0 ? _a : {}).map(([key, value]) => {
116
+ const selector = (0, query_builder_utils_1.resolveDeferredKeys)(builder, key);
117
+ return typeof value === 'string'
118
+ ? { selector, order: value }
119
+ : { selector, order: value.order, nulls: value.nulls };
120
+ });
117
121
  return {
118
122
  list,
119
123
  total,
@@ -5,7 +5,7 @@ const query_builder_utils_1 = require("../../../utils/query-builder.utils");
5
5
  // patching and implementation
6
6
  typeorm_1.SelectQueryBuilder.prototype.distinctOnTyped = function (...distinctOn) {
7
7
  if (Array.isArray(distinctOn) && distinctOn.length) {
8
- const keys = distinctOn.map((callback) => (0, query_builder_utils_1.getTypedSelectionKey)(this, callback)).flat();
8
+ const keys = distinctOn.map((callback) => (0, query_builder_utils_1.getTypedSelectionKey)(this, callback, undefined, 'distinctOnTyped')).flat();
9
9
  return this.distinctOn(keys);
10
10
  }
11
11
  return this;
@@ -1,11 +1,12 @@
1
1
  import { ObjectLiteral } from 'typeorm';
2
- import type { DeepUnwrapArray } from '../../../types/modules/proxy-callback.types';
2
+ import type { KeyProxyCallback } from '../../../types/modules/proxy-callback.types';
3
3
  import type { TypedGroupByOptions } from '../../../types/extensions/typed-group-by.types';
4
4
  import type { TypedRoot } from '../../../types/modules/typed-root.types';
5
- /** A value a column holds: a relation (`u => u.images`) or a JSON object is no column to group by. */
6
- type GroupedColumn = string | number | boolean | Date | ArrayBufferView | ArrayBuffer | null | undefined;
7
- /** One column, `user => user.id`, or several, `user => [user.id, user.email]`. */
8
- type GroupBySelection<Type extends ObjectLiteral> = (proxy: DeepUnwrapArray<Type>) => GroupedColumn | GroupedColumn[];
5
+ /**
6
+ * One column, `user => user.id`, or several, `user => [user.id, user.email]`. A JSON column groups as a whole;
7
+ * a relation without a join column in its table, or an embedded entity, throws a `TypeError`.
8
+ */
9
+ type GroupBySelection<Type extends ObjectLiteral> = KeyProxyCallback<Type>;
9
10
  export interface TypedGroupByMethods<Entity extends ObjectLiteral> {
10
11
  /**
11
12
  * Sets GROUP BY to the columns the callback selects: `user => user.id`, or several at once,
@@ -23,7 +23,7 @@ const extension = {
23
23
  (0, prototype_utils_1.patchPrototype)(typeorm_1.SelectQueryBuilder, extension);
24
24
  // implementation
25
25
  function toGroupByKeys(builder, selection, options) {
26
- const keys = (0, query_builder_utils_1.getTypedSelectionKey)(builder, selection, options === null || options === void 0 ? void 0 : options.useQuotes);
26
+ const keys = (0, query_builder_utils_1.getTypedSelectionKey)(builder, selection, options === null || options === void 0 ? void 0 : options.useQuotes, 'groupByTyped');
27
27
  return Array.isArray(keys) ? keys : [keys];
28
28
  }
29
29
  function addGroupByKeys(builder, keys) {
@@ -10,7 +10,7 @@ typeorm_1.SelectQueryBuilder.prototype.orderByTyped = function (selection, order
10
10
  return this.orderBy(selection.toSql(undefined, options === null || options === void 0 ? void 0 : options.useQuotes, (0, sql_utils_1.getDriverType)(this)), order, nulls);
11
11
  }
12
12
  if (typeof selection === 'function') {
13
- const key = (0, query_builder_utils_1.getTypedSelectionKey)(this, selection, options === null || options === void 0 ? void 0 : options.useQuotes);
13
+ const key = (0, query_builder_utils_1.getTypedSelectionKey)(this, selection, options === null || options === void 0 ? void 0 : options.useQuotes, 'orderByTyped');
14
14
  return this.orderBy(key, order, nulls);
15
15
  }
16
16
  return this;
@@ -20,7 +20,7 @@ typeorm_1.SelectQueryBuilder.prototype.addOrderByTyped = function (selection, or
20
20
  return this.addOrderBy(selection.toSql(undefined, options === null || options === void 0 ? void 0 : options.useQuotes, (0, sql_utils_1.getDriverType)(this)), order, nulls);
21
21
  }
22
22
  if (typeof selection === 'function') {
23
- const key = (0, query_builder_utils_1.getTypedSelectionKey)(this, selection, options === null || options === void 0 ? void 0 : options.useQuotes);
23
+ const key = (0, query_builder_utils_1.getTypedSelectionKey)(this, selection, options === null || options === void 0 ? void 0 : options.useQuotes, 'addOrderByTyped');
24
24
  return this.addOrderBy(key, order, nulls);
25
25
  }
26
26
  return this;
@@ -29,11 +29,11 @@ const attachSelectMethod = (query, method, selection, selectionAliasName, useQuo
29
29
  if (Array.isArray(selection)) {
30
30
  const arrayOfKeys = selection.flatMap((select) => select instanceof proxy_key_utils_1.KeyHolder
31
31
  ? (0, proxy_key_utils_1.getSqlKeyFromProxyCallback)(select, () => query.alias, useQuotes, (0, sql_utils_1.getDriverType)(query))
32
- : (0, query_builder_utils_1.getTypedSelectionKey)(query, select, useQuotes));
32
+ : (0, query_builder_utils_1.getTypedSelectionKey)(query, select, useQuotes, `${method}Typed`));
33
33
  return query[method](arrayOfKeys);
34
34
  }
35
35
  else if (typeof selection === 'function') {
36
- const keyArrayOrSingleKey = (0, query_builder_utils_1.getTypedSelectionKey)(query, selection, useQuotes);
36
+ const keyArrayOrSingleKey = (0, query_builder_utils_1.getTypedSelectionKey)(query, selection, useQuotes, `${method}Typed`);
37
37
  if (Array.isArray(keyArrayOrSingleKey)) {
38
38
  return query[method](keyArrayOrSingleKey);
39
39
  }
@@ -2,6 +2,11 @@ import type { ObjectLiteral, WhereExpressionBuilder } from 'typeorm';
2
2
  import type { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
3
3
  import type { KeyProxyCallback } from '../types/modules/proxy-callback.types';
4
4
  import { JsonPathSqlOptions } from './json-path.utils';
5
+ /**
6
+ * Replaces placeholders of `deferColumnKey` in SQL built by `builder`: the database column through the entity of
7
+ * the alias, quoted, `"usr"."receipts_email"`. An alias still unknown or without an entity keeps the name.
8
+ */
9
+ export declare function resolveDeferredKeys(builder: WhereExpressionBuilder, statement: string): string;
5
10
  /**
6
11
  * Database name of an entity property's column (`@Column({ name })`), looked up in the metadata of
7
12
  * a query alias (the main alias when `aliasName` is undefined). `undefined` when the alias has no
@@ -56,4 +61,4 @@ export declare function getTypedSingleColumnKey<Type extends ObjectLiteral>(buil
56
61
  * TypeORM leaves its keys as written, and PostgreSQL folds an unquoted `cte.firstName` to `firstname`.
57
62
  * Strings are kept as is.
58
63
  */
59
- export declare function getTypedSelectionKey<Type extends ObjectLiteral>(builder: WhereExpressionBuilder, selection: KeyProxyCallback<Type> | string, useQuotes?: boolean): string | string[];
64
+ export declare function getTypedSelectionKey<Type extends ObjectLiteral>(builder: WhereExpressionBuilder, selection: KeyProxyCallback<Type> | string, useQuotes?: boolean, columnsFor?: string): string | string[];
@@ -1,6 +1,7 @@
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.getTypedSelectionKey = exports.getTypedSingleColumnKey = exports.isTypedJsonPath = exports.getTypedColumnKey = exports.toFieldSql = exports.findFieldColumn = exports.getColumnDatabaseName = exports.resolveDeferredKeys = void 0;
4
+ const typeorm_1 = require("typeorm");
4
5
  const proxy_key_utils_1 = require("./proxy-key.utils");
5
6
  const json_path_utils_1 = require("./json-path.utils");
6
7
  const sql_utils_1 = require("./sql.utils");
@@ -47,6 +48,55 @@ function findQueryAlias(queryBuilder, aliasName) {
47
48
  }
48
49
  return undefined;
49
50
  }
51
+ // private-use characters: no identifier or SQL that TypeORM builds contains them
52
+ const DEFERRED_KEY_START = '\uE000';
53
+ const DEFERRED_PROPERTY = '\uE001';
54
+ const DEFERRED_FIELD = '\uE002';
55
+ const DEFERRED_KEY_END = '\uE003';
56
+ const DEFERRED_KEY = /\uE000([^\uE000-\uE003]+)([\uE001\uE002])([^\uE000-\uE003]+)\uE003/g;
57
+ const TWO_PART_FIELD = /^([A-Za-z_][\w$]*)\.([A-Za-z_][\w$]*)$/;
58
+ /**
59
+ * Placeholder for a column of an alias the query doesn't have yet, e.g. a join added after the call. TypeORM maps
60
+ * property names when it builds the SQL, and so does `resolveDeferredKeys` with these, once the alias is known.
61
+ */
62
+ function deferColumnKey(aliasName, separator, name) {
63
+ return `${DEFERRED_KEY_START}${aliasName}${separator}${name}${DEFERRED_KEY_END}`;
64
+ }
65
+ /**
66
+ * Replaces placeholders of `deferColumnKey` in SQL built by `builder`: the database column through the entity of
67
+ * the alias, quoted, `"usr"."receipts_email"`. An alias still unknown or without an entity keeps the name.
68
+ */
69
+ function resolveDeferredKeys(builder, statement) {
70
+ if (typeof statement !== 'string' || !statement.includes(DEFERRED_KEY_START)) {
71
+ return statement;
72
+ }
73
+ const queryBuilder = builder;
74
+ return statement.replace(DEFERRED_KEY, (_match, aliasName, separator, name) => {
75
+ const metadata = getAliasMetadata(queryBuilder, aliasName);
76
+ const column = metadata && (separator === DEFERRED_FIELD ? findColumn(metadata, name) : metadata.findColumnWithPropertyPath(name));
77
+ const columnName = column && !column.isVirtualProperty ? column.databaseName : name;
78
+ return `${queryBuilder.escape(aliasName)}.${queryBuilder.escape(columnName)}`;
79
+ });
80
+ }
81
+ exports.resolveDeferredKeys = resolveDeferredKeys;
82
+ const DEFERRED_KEYS_INSTALLED = Symbol.for('typeorm-extensions.deferredKeys');
83
+ // TypeORM 0.3.12 maps property names in each part of the query, later versions once in the whole query
84
+ function installDeferredKeyResolution() {
85
+ const prototype = typeorm_1.QueryBuilder.prototype;
86
+ if (prototype[DEFERRED_KEYS_INSTALLED]) {
87
+ return;
88
+ }
89
+ prototype[DEFERRED_KEYS_INSTALLED] = true;
90
+ for (const method of ['replacePropertyNames', 'replacePropertyNamesForTheWholeQuery']) {
91
+ const original = prototype[method];
92
+ if (typeof original === 'function') {
93
+ prototype[method] = function (statement) {
94
+ return resolveDeferredKeys(this, original.call(this, statement));
95
+ };
96
+ }
97
+ }
98
+ }
99
+ installDeferredKeyResolution();
50
100
  /** Entity metadata of a query alias (the main alias when `aliasName` is undefined), if it has any. */
51
101
  function getAliasMetadata(queryBuilder, aliasName) {
52
102
  var _a;
@@ -112,6 +162,10 @@ function toFieldSql(builder, field) {
112
162
  const columnSql = queryBuilder.escape(column.databaseName);
113
163
  return isAliasPrefixingEnabled(queryBuilder) ? `${queryBuilder.escape(aliasName)}.${columnSql}` : columnSql;
114
164
  }
165
+ const parts = TWO_PART_FIELD.exec(field);
166
+ if (parts && !findQueryAlias(queryBuilder, aliasName)) {
167
+ return deferColumnKey(parts[1], DEFERRED_FIELD, parts[2]);
168
+ }
115
169
  }
116
170
  else {
117
171
  // only by database name: an unqualified property name may mean another table's column
@@ -222,6 +276,9 @@ function resolveTypedKeyPath(queryBuilder, keys) {
222
276
  */
223
277
  function toQuotedColumnKey(queryBuilder, { aliasName, propertyPath, metadata, column, jsonPath }, jsonOptions) {
224
278
  var _a, _b;
279
+ if (aliasName !== undefined && !metadata && !findQueryAlias(queryBuilder, aliasName)) {
280
+ return deferColumnKey(aliasName, DEFERRED_PROPERTY, propertyPath);
281
+ }
225
282
  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;
226
283
  const columnSql = aliasName === undefined
227
284
  ? queryBuilder.escape(columnName)
@@ -268,6 +325,22 @@ function getTypedSingleColumnKey(builder, callback, methodName, jsonOptions) {
268
325
  return key;
269
326
  }
270
327
  exports.getTypedSingleColumnKey = getTypedSingleColumnKey;
328
+ /**
329
+ * Throws for a key that names no column of its table: a relation whose join column is on the other side
330
+ * (one-to-many, many-to-many, the inverse side of a one-to-one) or an embedded entity. TypeORM leaves such a key
331
+ * as written, and the database rejects it.
332
+ */
333
+ function assertTableColumnKey({ metadata, propertyPath }, keys, methodName) {
334
+ const relation = metadata === null || metadata === void 0 ? void 0 : metadata.findRelationWithPropertyPath(propertyPath);
335
+ if (relation && !relation.isManyToOne && !relation.isOneToOneOwner) {
336
+ throw new TypeError(`${methodName}: "${keys.join('.')}" is a relation without a join column in its table. ` +
337
+ `Join it and address its columns by the join alias`);
338
+ }
339
+ if (!relation && (metadata === null || metadata === void 0 ? void 0 : metadata.findEmbeddedWithPropertyPath(propertyPath))) {
340
+ throw new TypeError(`${methodName}: "${keys.join('.')}" is an embedded entity, not a column: address its columns, e.g. ` +
341
+ `"${keys.join('.')}.someColumn"`);
342
+ }
343
+ }
271
344
  // an alias the query doesn't know yet may still get metadata (a join added later), so it stays unquoted
272
345
  function isAliasWithoutMetadata(queryBuilder, aliasName) {
273
346
  const alias = aliasName === undefined ? undefined : findQueryAlias(queryBuilder, aliasName);
@@ -283,7 +356,7 @@ function isAliasWithoutMetadata(queryBuilder, aliasName) {
283
356
  * TypeORM leaves its keys as written, and PostgreSQL folds an unquoted `cte.firstName` to `firstname`.
284
357
  * Strings are kept as is.
285
358
  */
286
- function getTypedSelectionKey(builder, selection, useQuotes) {
359
+ function getTypedSelectionKey(builder, selection, useQuotes, columnsFor) {
287
360
  if (typeof selection === 'string') {
288
361
  return selection;
289
362
  }
@@ -294,6 +367,9 @@ function getTypedSelectionKey(builder, selection, useQuotes) {
294
367
  throw new TypeError(`Typed key "${keyHolder.getKeys().join('.')}" points into the JSON column "${resolved.propertyPath}": ` +
295
368
  `select, order, distinct and group by take columns only. For an expression use tableHelper().jsonColumn()`);
296
369
  }
370
+ if (columnsFor !== undefined) {
371
+ assertTableColumnKey(resolved, keyHolder.getKeys(), columnsFor);
372
+ }
297
373
  if (useQuotes !== null && useQuotes !== void 0 ? useQuotes : isAliasWithoutMetadata(queryBuilder, resolved.aliasName)) {
298
374
  return toQuotedColumnKey(queryBuilder, resolved);
299
375
  }
@@ -13,6 +13,11 @@ function getValuePlaceholder(value, paramName) {
13
13
  return `:${paramName}`;
14
14
  }
15
15
  exports.getValuePlaceholder = getValuePlaceholder;
16
+ /**
17
+ * A condition that ends with an operator, `=`, `>=`, `LIKE`, `IN`, `@>`: it needs a value after it. A word after
18
+ * `:` or `.` is a parameter or a column (`ILIKE :like`, `= t.in`), not an operator.
19
+ */
20
+ const OPERATOR_AT_END = /(?:[=<>!~@&|?]|(?<![\w$:.])(?:LIKE|ILIKE|IN|SIMILAR TO|DISTINCT FROM|BETWEEN|ANY|ALL))\s*$/i;
16
21
  const isDate = (value) => Object.prototype.toString.call(value) === '[object Date]';
17
22
  /**
18
23
  * Objects that are parameter values themselves, not `{ name: value }` params objects:
@@ -76,6 +81,10 @@ function attachWhere(builder, method, field, conditions, params, options) {
76
81
  const hasParams = typeof params !== 'undefined';
77
82
  // `addValue` defaults to `true` unless `conditions` already references one of the `params` keys
78
83
  addValue = addValue !== null && addValue !== void 0 ? addValue : !hasParamPlaceholder(conditions, params);
84
+ if (addValue && !hasParams && OPERATOR_AT_END.test(conditions)) {
85
+ throw new TypeError(`The value after "${fieldName} ${conditions.trim()}" is undefined, which leaves the condition without ` +
86
+ `an operand. Pass the value, null to compare with NULL, or a condition without one ('IS NULL')`);
87
+ }
79
88
  if (addValue && hasParams) {
80
89
  assertSingleValueParams(fieldName, params);
81
90
  // pick first value from params object or pass as is (a Date or a Buffer is a value too)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typeorm-extensions",
3
- "version": "2.2.0-rc.3",
3
+ "version": "2.2.0-rc.5",
4
4
  "description": "typeorm commonly used extensions",
5
5
  "main": "./dist/index.js",
6
6
  "types": "dist/index.d.ts",