typeorm-extensions 1.3.5 → 2.1.0

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 (107) hide show
  1. package/README.md +12 -10
  2. package/dist/constants/index.d.ts +1 -1
  3. package/dist/constants/index.js +2 -2
  4. package/dist/extensions/cursor-pagination.extension.d.ts +18 -0
  5. package/dist/extensions/cursor-pagination.extension.js +108 -0
  6. package/dist/extensions/index.d.ts +7 -0
  7. package/dist/extensions/index.js +7 -0
  8. package/dist/extensions/order-by-array-position.extension.d.ts +24 -0
  9. package/dist/extensions/order-by-array-position.extension.js +18 -0
  10. package/dist/extensions/order.extension.d.ts +3 -2
  11. package/dist/extensions/order.extension.js +16 -13
  12. package/dist/extensions/period-filter.extension.d.ts +29 -0
  13. package/dist/extensions/period-filter.extension.js +23 -0
  14. package/dist/extensions/typed/index.d.ts +10 -0
  15. package/dist/extensions/typed/index.js +10 -0
  16. package/dist/extensions/typed/typed-distinct/typed-distinct.extension.js +2 -2
  17. package/dist/extensions/typed/typed-joins/typed-joins.extension.js +2 -1
  18. package/dist/extensions/typed/typed-order-by/typed-order-by-array-position.extension.d.ts +20 -0
  19. package/dist/extensions/typed/typed-order-by/typed-order-by-array-position.extension.js +21 -0
  20. package/dist/extensions/typed/typed-order-by/typed-order-by.extension.d.ts +4 -3
  21. package/dist/extensions/typed/typed-order-by/typed-order-by.extension.js +7 -6
  22. package/dist/extensions/typed/typed-returning/typed-returning.extension.d.ts +27 -0
  23. package/dist/extensions/typed/typed-returning/typed-returning.extension.js +38 -0
  24. package/dist/extensions/typed/typed-select/typed-select.extension.d.ts +6 -6
  25. package/dist/extensions/typed/typed-select/typed-select.extension.js +11 -8
  26. package/dist/extensions/typed/typed-where/typed-period-filter.extension.d.ts +31 -0
  27. package/dist/extensions/typed/typed-where/typed-period-filter.extension.js +25 -0
  28. package/dist/extensions/typed/typed-where/typed-where-array.extension.d.ts +68 -0
  29. package/dist/extensions/typed/typed-where/typed-where-array.extension.js +41 -0
  30. package/dist/extensions/typed/typed-where/typed-where-ilike.extension.d.ts +62 -0
  31. package/dist/extensions/typed/typed-where/typed-where-ilike.extension.js +84 -0
  32. package/dist/extensions/typed/typed-where/typed-where-is-any.extension.d.ts +53 -0
  33. package/dist/extensions/typed/typed-where/typed-where-is-any.extension.js +38 -0
  34. package/dist/extensions/typed/typed-where/typed-where-is-distinct-from.extension.d.ts +53 -0
  35. package/dist/extensions/typed/typed-where/typed-where-is-distinct-from.extension.js +38 -0
  36. package/dist/extensions/typed/typed-where/typed-where-is-in.extension.d.ts +36 -119
  37. package/dist/extensions/typed/typed-where/typed-where-is-in.extension.js +14 -29
  38. package/dist/extensions/typed/typed-where/typed-where-json.extension.d.ts +53 -0
  39. package/dist/extensions/typed/typed-where/typed-where-json.extension.js +39 -0
  40. package/dist/extensions/typed/typed-where/typed-where-lower.extension.d.ts +59 -0
  41. package/dist/extensions/typed/typed-where/typed-where-lower.extension.js +86 -0
  42. package/dist/extensions/typed/typed-where/typed-where-similar.extension.d.ts +54 -0
  43. package/dist/extensions/typed/typed-where/typed-where-similar.extension.js +73 -0
  44. package/dist/extensions/typed/typed-where/typed-where.extension.d.ts +30 -74
  45. package/dist/extensions/typed/typed-where/typed-where.extension.js +13 -21
  46. package/dist/extensions/where-array.extension.d.ts +70 -0
  47. package/dist/extensions/where-array.extension.js +47 -0
  48. package/dist/extensions/where-is-any.extension.d.ts +142 -0
  49. package/dist/extensions/where-is-any.extension.js +44 -0
  50. package/dist/extensions/where-is-distinct-from.extension.d.ts +56 -0
  51. package/dist/extensions/where-is-distinct-from.extension.js +55 -0
  52. package/dist/extensions/where-json.extension.d.ts +55 -0
  53. package/dist/extensions/where-json.extension.js +64 -0
  54. package/dist/helpers.d.ts +2 -0
  55. package/dist/helpers.js +23 -0
  56. package/dist/index.d.ts +3 -0
  57. package/dist/index.js +4 -0
  58. package/dist/types/extensions/cursor-pagination.types.d.ts +16 -0
  59. package/dist/types/extensions/cursor-pagination.types.js +2 -0
  60. package/dist/types/extensions/get-many-with-totals.types.d.ts +3 -1
  61. package/dist/types/extensions/index.d.ts +4 -0
  62. package/dist/types/extensions/index.js +4 -0
  63. package/dist/types/extensions/order.types.d.ts +1 -1
  64. package/dist/types/extensions/period.types.d.ts +15 -0
  65. package/dist/types/extensions/period.types.js +2 -0
  66. package/dist/types/extensions/search.types.d.ts +68 -0
  67. package/dist/types/extensions/search.types.js +2 -0
  68. package/dist/types/extensions/typed-order-by.types.d.ts +3 -0
  69. package/dist/types/extensions/typed-order-by.types.js +2 -0
  70. package/dist/types/extensions/typed-where.types.d.ts +4 -4
  71. package/dist/types/extensions/where.types.d.ts +1 -0
  72. package/dist/types/interfaces/config.interface.d.ts +17 -0
  73. package/dist/types/interfaces/config.interface.js +2 -0
  74. package/dist/types/interfaces/cursor-pagination.interface.d.ts +16 -0
  75. package/dist/types/interfaces/cursor-pagination.interface.js +2 -0
  76. package/dist/types/interfaces/index.d.ts +3 -0
  77. package/dist/types/interfaces/index.js +3 -0
  78. package/dist/types/interfaces/period.interface.d.ts +12 -0
  79. package/dist/types/interfaces/period.interface.js +2 -0
  80. package/dist/types/modules/proxy-callback.types.d.ts +50 -3
  81. package/dist/types/modules/typed-params.types.d.ts +45 -0
  82. package/dist/types/modules/typed-params.types.js +2 -0
  83. package/dist/types/util-types/index.d.ts +3 -1
  84. package/dist/utils/cursor.utils.d.ts +31 -0
  85. package/dist/utils/cursor.utils.js +130 -0
  86. package/dist/utils/json-path.utils.d.ts +22 -0
  87. package/dist/utils/json-path.utils.js +57 -0
  88. package/dist/utils/order.utils.d.ts +15 -0
  89. package/dist/utils/order.utils.js +45 -0
  90. package/dist/utils/period.utils.d.ts +9 -0
  91. package/dist/utils/period.utils.js +82 -0
  92. package/dist/utils/proxy-key.utils.d.ts +19 -3
  93. package/dist/utils/proxy-key.utils.js +40 -15
  94. package/dist/utils/query-builder.utils.d.ts +48 -0
  95. package/dist/utils/query-builder.utils.js +257 -0
  96. package/dist/utils/search.utils.d.ts +27 -0
  97. package/dist/utils/search.utils.js +83 -0
  98. package/dist/utils/sql.utils.d.ts +29 -1
  99. package/dist/utils/sql.utils.js +91 -9
  100. package/dist/utils/table.utils.d.ts +86 -0
  101. package/dist/utils/table.utils.js +124 -0
  102. package/dist/utils/where.utils.d.ts +18 -1
  103. package/dist/utils/where.utils.js +62 -9
  104. package/helpers/package.json +7 -0
  105. package/package.json +5 -4
  106. package/dist/utils/identifier.utils.d.ts +0 -15
  107. package/dist/utils/identifier.utils.js +0 -45
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildKeysetCondition = exports.getTextCastType = exports.supportsRowValueComparison = exports.decodeCursor = exports.encodeCursor = exports.InvalidCursorError = void 0;
4
+ const sql_utils_1 = require("./sql.utils");
5
+ /** A cursor that can't be decoded or doesn't belong to the query's order (e.g. tampered with). */
6
+ class InvalidCursorError extends TypeError {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = 'InvalidCursorError';
10
+ }
11
+ }
12
+ exports.InvalidCursorError = InvalidCursorError;
13
+ const isDate = (value) => Object.prototype.toString.call(value) === '[object Date]';
14
+ const toCursorValue = (value) => {
15
+ if (isDate(value)) {
16
+ return { $date: value.toISOString() };
17
+ }
18
+ if (typeof value === 'bigint') {
19
+ return value.toString();
20
+ }
21
+ return value;
22
+ };
23
+ // base64url of UTF-8 text, with the globals of Node 16+ and browsers (no Buffer, no @types/node)
24
+ const toBase64Url = (text) => {
25
+ let binary = '';
26
+ new TextEncoder().encode(text).forEach((byte) => (binary += String.fromCharCode(byte)));
27
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
28
+ };
29
+ const fromBase64Url = (value) => {
30
+ if (!/^[A-Za-z0-9_-]*$/.test(value)) {
31
+ throw new InvalidCursorError('Invalid cursor: not base64url');
32
+ }
33
+ const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
34
+ const binary = atob(base64 + '='.repeat((4 - (base64.length % 4)) % 4));
35
+ return new TextDecoder('utf-8', { fatal: true }).decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
36
+ };
37
+ /** base64url-encoded JSON of the key fields and the key values of the last row. */
38
+ function encodeCursor(fields, values) {
39
+ const payload = { k: fields, v: values.map(toCursorValue) };
40
+ return toBase64Url(JSON.stringify(payload));
41
+ }
42
+ exports.encodeCursor = encodeCursor;
43
+ const isCursorValue = (value) => typeof value === 'string' ||
44
+ (typeof value === 'number' && Number.isFinite(value)) ||
45
+ typeof value === 'boolean' ||
46
+ (typeof value === 'object' &&
47
+ value !== null &&
48
+ typeof value.$date === 'string');
49
+ /** Key values of a cursor made for `fields`; throws `InvalidCursorError` otherwise. */
50
+ function decodeCursor(cursor, fields) {
51
+ let payload;
52
+ try {
53
+ payload = JSON.parse(fromBase64Url(cursor));
54
+ }
55
+ catch {
56
+ throw new InvalidCursorError('Invalid cursor: it could not be decoded');
57
+ }
58
+ if (!payload ||
59
+ !Array.isArray(payload.k) ||
60
+ !Array.isArray(payload.v) ||
61
+ payload.k.length !== fields.length ||
62
+ payload.v.length !== fields.length ||
63
+ payload.k.some((field, i) => field !== fields[i]) ||
64
+ !payload.v.every(isCursorValue)) {
65
+ throw new InvalidCursorError(`Invalid cursor: it was not made for the order (${fields.join(', ')})`);
66
+ }
67
+ return payload.v.map((value) => {
68
+ if (typeof value !== 'object') {
69
+ return value;
70
+ }
71
+ const date = new Date(value.$date);
72
+ if (isNaN(date.getTime())) {
73
+ throw new InvalidCursorError('Invalid cursor: invalid date');
74
+ }
75
+ return date;
76
+ });
77
+ }
78
+ exports.decodeCursor = decodeCursor;
79
+ /** Databases that compare row values: `(a, b) < (1, 2)`. */
80
+ const supportsRowValueComparison = (driver) => driver === 'postgres' ||
81
+ driver === 'aurora-postgres' ||
82
+ driver === 'cockroachdb' ||
83
+ (0, sql_utils_1.isMysqlLikeDriver)(driver) ||
84
+ (0, sql_utils_1.isSqliteLikeDriver)(driver);
85
+ exports.supportsRowValueComparison = supportsRowValueComparison;
86
+ /**
87
+ * Type to read a date / time column as text with, so the cursor keeps the database's own precision
88
+ * (PostgreSQL timestamps have microseconds, a JS Date only milliseconds). `undefined` when unknown.
89
+ */
90
+ const getTextCastType = (driver) => {
91
+ if (driver === 'postgres' || driver === 'aurora-postgres' || driver === 'cockroachdb' || (0, sql_utils_1.isSqliteLikeDriver)(driver)) {
92
+ return 'text';
93
+ }
94
+ if ((0, sql_utils_1.isMysqlLikeDriver)(driver)) {
95
+ return 'char';
96
+ }
97
+ if (driver === 'mssql') {
98
+ return 'nvarchar(max)';
99
+ }
100
+ return undefined;
101
+ };
102
+ exports.getTextCastType = getTextCastType;
103
+ /**
104
+ * WHERE condition for the rows after the cursor:
105
+ * - same direction everywhere, on databases with row values: `(a, b) > (:a, :b)`
106
+ * - otherwise: `a >= :a AND (a > :a OR (a = :a AND b < :b))`, where the leading bound lets an index on `a` be used
107
+ */
108
+ function buildKeysetCondition(keys, useRowValues) {
109
+ const operator = (key) => (key.direction === 'ASC' ? '>' : '<');
110
+ if (keys.length === 1) {
111
+ const [key] = keys;
112
+ return `${key.column} ${operator(key)} :${key.paramName}`;
113
+ }
114
+ if (useRowValues && keys.every((key) => key.direction === keys[0].direction)) {
115
+ const columns = keys.map((key) => key.column).join(', ');
116
+ const params = keys.map((key) => `:${key.paramName}`).join(', ');
117
+ return `(${columns}) ${operator(keys[0])} (${params})`;
118
+ }
119
+ const expand = (index) => {
120
+ const key = keys[index];
121
+ const after = `${key.column} ${operator(key)} :${key.paramName}`;
122
+ if (index === keys.length - 1) {
123
+ return after;
124
+ }
125
+ return `(${after} OR (${key.column} = :${key.paramName} AND ${expand(index + 1)}))`;
126
+ };
127
+ const [first] = keys;
128
+ return `(${first.column} ${operator(first)}= :${first.paramName} AND ${expand(0)})`;
129
+ }
130
+ exports.buildKeysetCondition = buildKeysetCondition;
@@ -0,0 +1,22 @@
1
+ import type { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
2
+ import type { DatabaseType } from 'typeorm';
3
+ /** `->>` gives the value as text (as an SQL value on SQLite), `->` as JSON. */
4
+ export type JsonAccessor = 'text' | 'json';
5
+ export declare const isJsonColumn: (column: ColumnMetadata) => boolean;
6
+ /** `-> 'settings' ->> 'theme'`: every key but the last one with `->`, the last one with `jsonOperator`. */
7
+ export declare function buildJsonPath(fieldNames: readonly string[], jsonOperator: '->>' | '->'): string;
8
+ export interface JsonPathSqlOptions {
9
+ /** Defaults to `'text'`. */
10
+ accessor?: JsonAccessor;
11
+ /** The value the path is compared with: on PostgreSQL a `'text'` path is cast to its type. */
12
+ value?: unknown;
13
+ /** Compares a `'text'` path as a timestamp whatever `value` is, since a period also takes date strings. */
14
+ asTimestamp?: boolean;
15
+ }
16
+ /**
17
+ * SQL for a path inside a JSON column: `"exec"."details" -> 'meta' ->> 'endTs'`, cast on PostgreSQL to the
18
+ * type of the compared value (`CAST(… AS numeric)`). A `simple-json` column is text there, so it is cast to
19
+ * `jsonb` first. SQLite (3.38+) has the same operators, and `->>` gives SQL values, so it needs no cast, except
20
+ * for a timestamp: there it is rewritten in the format TypeORM stores dates and binds `Date` parameters in.
21
+ */
22
+ export declare function toJsonPathSql(driver: DatabaseType | undefined, columnSql: string, column: ColumnMetadata, jsonPath: readonly string[], { accessor, value, asTimestamp }?: JsonPathSqlOptions): string;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toJsonPathSql = exports.buildJsonPath = exports.isJsonColumn = void 0;
4
+ const sql_utils_1 = require("./sql.utils");
5
+ const JSON_COLUMN_TYPES = ['json', 'jsonb', 'simple-json'];
6
+ const isJsonColumn = (column) => JSON_COLUMN_TYPES.includes(String(column.type));
7
+ exports.isJsonColumn = isJsonColumn;
8
+ /** Wraps a JSON key in single quotes, doubling any quote inside it (standard SQL string literal). */
9
+ const toJsonKeyLiteral = (fieldName) => `'${String(fieldName).replace(/'/g, "''")}'`;
10
+ /** `-> 'settings' ->> 'theme'`: every key but the last one with `->`, the last one with `jsonOperator`. */
11
+ function buildJsonPath(fieldNames, jsonOperator) {
12
+ return fieldNames
13
+ .map((fieldName, index) => `${index === fieldNames.length - 1 ? jsonOperator : '->'} ${toJsonKeyLiteral(fieldName)}`)
14
+ .join(' ');
15
+ }
16
+ exports.buildJsonPath = buildJsonPath;
17
+ const isDate = (value) => Object.prototype.toString.call(value) === '[object Date]';
18
+ /**
19
+ * PostgreSQL type to compare a `->>` value (text) with `value` as: text compares `'9' >= '10'` as true.
20
+ * An array (IN, ANY) is typed by its first value; strings and unknown values stay text.
21
+ */
22
+ function getJsonTextCast(value) {
23
+ const sample = Array.isArray(value) ? value.find((item) => item !== null && item !== undefined) : value;
24
+ if (typeof sample === 'number' || typeof sample === 'bigint') {
25
+ return 'numeric';
26
+ }
27
+ if (typeof sample === 'boolean') {
28
+ return 'boolean';
29
+ }
30
+ if (isDate(sample)) {
31
+ return 'timestamptz';
32
+ }
33
+ return undefined;
34
+ }
35
+ /**
36
+ * SQL for a path inside a JSON column: `"exec"."details" -> 'meta' ->> 'endTs'`, cast on PostgreSQL to the
37
+ * type of the compared value (`CAST(… AS numeric)`). A `simple-json` column is text there, so it is cast to
38
+ * `jsonb` first. SQLite (3.38+) has the same operators, and `->>` gives SQL values, so it needs no cast, except
39
+ * for a timestamp: there it is rewritten in the format TypeORM stores dates and binds `Date` parameters in.
40
+ */
41
+ function toJsonPathSql(driver, columnSql, column, jsonPath, { accessor = 'text', value, asTimestamp = false } = {}) {
42
+ const path = buildJsonPath(jsonPath, accessor === 'json' ? '->' : '->>');
43
+ const isTimestamp = asTimestamp && accessor === 'text';
44
+ if ((0, sql_utils_1.isSqliteLikeDriver)(driver)) {
45
+ const sql = `${columnSql} ${path}`;
46
+ return isTimestamp ? `strftime('%Y-%m-%d %H:%M:%f', ${sql})` : sql;
47
+ }
48
+ if (!(0, sql_utils_1.isPostgresLikeDriver)(driver)) {
49
+ throw new TypeError(`Typed keys into a JSON column (${column.propertyPath}.${jsonPath.join('.')}) are supported on PostgreSQL and SQLite, ` +
50
+ `not on ${driver !== null && driver !== void 0 ? driver : 'a query without a connection'}`);
51
+ }
52
+ const source = String(column.type) === 'simple-json' ? `CAST(${columnSql} AS jsonb)` : columnSql;
53
+ const sql = `${source} ${path}`;
54
+ const cast = isTimestamp ? 'timestamptz' : accessor === 'text' ? getJsonTextCast(value) : undefined;
55
+ return cast ? `CAST(${sql} AS ${cast})` : sql;
56
+ }
57
+ exports.toJsonPathSql = toJsonPathSql;
@@ -0,0 +1,15 @@
1
+ import type { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
2
+ import type { OrderDirection } from '../types/interfaces/ordering.interface';
3
+ import type { ArgsType } from '../types/util-types';
4
+ /**
5
+ * `'ASC'` / `'DESC'` for any `OrderDirection`: `'asc'` / `'desc'` in any case, `1` / `-1`,
6
+ * `true` (ASC) / `false` (DESC). Anything else is the default direction (ASC).
7
+ */
8
+ export declare function toSqlOrderDirection(direction: OrderDirection | undefined | null): 'ASC' | 'DESC';
9
+ /**
10
+ * Selects `array_position(:values, column)` under a generated alias and orders by that alias.
11
+ * TypeORM can't order by a function expression together with `take` and joins
12
+ * (`"array_position(…" alias was not found`), but it can order by a selection alias.
13
+ * `column` is already quoted.
14
+ */
15
+ export declare function attachOrderByArrayPosition<Entity extends ObjectLiteral>(builder: SelectQueryBuilder<Entity>, column: string, values: ArgsType, arrayType: string | undefined, replace: boolean): SelectQueryBuilder<Entity>;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.attachOrderByArrayPosition = exports.toSqlOrderDirection = void 0;
4
+ const constants_1 = require("../constants");
5
+ const sql_utils_1 = require("./sql.utils");
6
+ /**
7
+ * `'ASC'` / `'DESC'` for any `OrderDirection`: `'asc'` / `'desc'` in any case, `1` / `-1`,
8
+ * `true` (ASC) / `false` (DESC). Anything else is the default direction (ASC).
9
+ */
10
+ function toSqlOrderDirection(direction) {
11
+ if (typeof direction === 'boolean') {
12
+ return direction ? 'ASC' : 'DESC';
13
+ }
14
+ if (typeof direction === 'number') {
15
+ return direction === -1 ? 'DESC' : 'ASC';
16
+ }
17
+ if (typeof direction === 'string' && /asc|desc/i.test(direction)) {
18
+ return direction.toUpperCase();
19
+ }
20
+ return constants_1.defaultOrderDirection;
21
+ }
22
+ exports.toSqlOrderDirection = toSqlOrderDirection;
23
+ /**
24
+ * Selects `array_position(:values, column)` under a generated alias and orders by that alias.
25
+ * TypeORM can't order by a function expression together with `take` and joins
26
+ * (`"array_position(…" alias was not found`), but it can order by a selection alias.
27
+ * `column` is already quoted.
28
+ */
29
+ function attachOrderByArrayPosition(builder, column, values, arrayType, replace) {
30
+ if (!Array.isArray(values)) {
31
+ throw new TypeError(`orderByArrayPosition on "${column}" expects an array of values, got ${values}`);
32
+ }
33
+ const castTo = arrayType === undefined ? undefined : (0, sql_utils_1.assertArrayType)(arrayType);
34
+ const paramName = (0, sql_utils_1.createUniqueParameterName)('position');
35
+ // lower case: PostgreSQL folds the unquoted alias in ORDER BY
36
+ const selectionAlias = `array_${paramName}`.toLowerCase();
37
+ const parameter = castTo ? `CAST(:${paramName} AS ${castTo})` : `:${paramName}`;
38
+ builder
39
+ .addSelect(`array_position(${parameter}, ${column})`, selectionAlias)
40
+ .setParameter(paramName, values);
41
+ return replace
42
+ ? builder.orderBy(selectionAlias, 'ASC', 'NULLS LAST')
43
+ : builder.addOrderBy(selectionAlias, 'ASC', 'NULLS LAST');
44
+ }
45
+ exports.attachOrderByArrayPosition = attachOrderByArrayPosition;
@@ -0,0 +1,9 @@
1
+ import type { WhereExpressionBuilder } from 'typeorm';
2
+ import type { PeriodFilter } from '../types/interfaces/period.interface';
3
+ import type { ApplyPeriodOptions } from '../types/extensions/period.types';
4
+ /**
5
+ * Adds `column >= :periodFrom` and `column <= :periodTo` conditions (or their exclusive versions) with
6
+ * AND, so the filter narrows whatever WHERE conditions the query already has, OR ones included:
7
+ * `(a OR b) AND column >= …`. `column` is already quoted.
8
+ */
9
+ export declare function attachPeriodFilter<QB extends WhereExpressionBuilder>(builder: QB, column: string, periodFilter: PeriodFilter | null | undefined, options?: ApplyPeriodOptions): QB;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.attachPeriodFilter = void 0;
4
+ const sql_utils_1 = require("./sql.utils");
5
+ const where_utils_1 = require("./where.utils");
6
+ const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
7
+ /** `'2024-01-31'` as a UTC midnight Date, or a `TypeError` if it is not a calendar date (`'2024-02-30'`). */
8
+ function parseDateOnly(value, name) {
9
+ const day = new Date(`${value}T00:00:00.000Z`);
10
+ if (isNaN(day.getTime()) || day.toISOString().slice(0, 10) !== value) {
11
+ throw new TypeError(`Invalid ${name} "${value}": not a calendar date`);
12
+ }
13
+ return day;
14
+ }
15
+ /** A usable boundary, `undefined` for a missing one (`undefined`, `null`, `''`). */
16
+ function normalizeBoundary(value, name) {
17
+ if (value === undefined || value === null || value === '') {
18
+ return undefined;
19
+ }
20
+ if (Object.prototype.toString.call(value) === '[object Date]') {
21
+ if (isNaN(value.getTime())) {
22
+ throw new TypeError(`Invalid ${name}: invalid Date`);
23
+ }
24
+ return value;
25
+ }
26
+ if (typeof value !== 'string') {
27
+ throw new TypeError(`Invalid ${name}: expected a Date or a string, got ${typeof value}`);
28
+ }
29
+ if (DATE_ONLY.test(value)) {
30
+ parseDateOnly(value, name);
31
+ }
32
+ return value;
33
+ }
34
+ /** The day after a date-only string, or `undefined` past year 9999. */
35
+ function getNextDay(value, name) {
36
+ const day = parseDateOnly(value, name);
37
+ day.setUTCDate(day.getUTCDate() + 1);
38
+ const next = day.toISOString().slice(0, 10);
39
+ return DATE_ONLY.test(next) ? next : undefined;
40
+ }
41
+ /**
42
+ * The comparison for one end of the period. A date-only string stands for the whole day,
43
+ * so excluding the start or including the end moves the bound to the next day:
44
+ * `> '2024-01-01'` would still match timestamps later on January 1st.
45
+ */
46
+ function getBoundCondition(boundary, name, include) {
47
+ const isFrom = name === 'periodFrom';
48
+ const nextDay = typeof boundary === 'string' && DATE_ONLY.test(boundary) && include !== isFrom
49
+ ? getNextDay(boundary, name)
50
+ : undefined;
51
+ if (nextDay !== undefined) {
52
+ // excluded start day: `>= next day`; included end day: `< next day`
53
+ return { operator: isFrom ? '>=' : '<', value: nextDay };
54
+ }
55
+ if (isFrom) {
56
+ return { operator: include ? '>=' : '>', value: boundary };
57
+ }
58
+ return { operator: include ? '<=' : '<', value: boundary };
59
+ }
60
+ /**
61
+ * Adds `column >= :periodFrom` and `column <= :periodTo` conditions (or their exclusive versions) with
62
+ * AND, so the filter narrows whatever WHERE conditions the query already has, OR ones included:
63
+ * `(a OR b) AND column >= …`. `column` is already quoted.
64
+ */
65
+ function attachPeriodFilter(builder, column, periodFilter, options) {
66
+ const { includeFrom = true, includeTo = true } = options !== null && options !== void 0 ? options : {};
67
+ const bounds = [
68
+ { name: 'periodFrom', include: includeFrom },
69
+ { name: 'periodTo', include: includeTo },
70
+ ];
71
+ for (const { name, include } of bounds) {
72
+ const boundary = normalizeBoundary(periodFilter === null || periodFilter === void 0 ? void 0 : periodFilter[name], name);
73
+ if (boundary === undefined) {
74
+ continue;
75
+ }
76
+ const { operator, value } = getBoundCondition(boundary, name, include);
77
+ const paramName = (0, sql_utils_1.createUniqueParameterName)(name);
78
+ (0, where_utils_1.andWhereNarrowing)(builder, `${column} ${operator} :${paramName}`, { [paramName]: value });
79
+ }
80
+ return builder;
81
+ }
82
+ exports.attachPeriodFilter = attachPeriodFilter;
@@ -3,7 +3,7 @@ import { KeyProxyCallback } from '../types/modules/proxy-callback.types';
3
3
  export declare class KeyHolder {
4
4
  private _keys;
5
5
  constructor(_keys?: string[]);
6
- toSql(length?: number, useDoubleQuotes?: boolean): string;
6
+ toSql(length?: number, useQuotes?: boolean): string;
7
7
  toString(length?: number): string;
8
8
  valueOf(): string;
9
9
  [Symbol.toPrimitive](): string;
@@ -19,6 +19,22 @@ export declare class KeyProxy {
19
19
  static defaultHandler: ProxyHandler<KeyHolder>;
20
20
  static create<T extends ObjectLiteral>(): T;
21
21
  }
22
- export declare const getKeyHolder: <Type extends ObjectLiteral>(callback: KeyHolder | KeyProxyCallback<Type>) => KeyHolder | Array<KeyHolder>;
22
+ /** Typed keys are `column` or `alias.column`: a deeper path can't be mapped to a query alias. */
23
+ export declare const MAX_KEY_DEPTH = 2;
24
+ export declare const createKeyTooDeepError: (keys: string[]) => TypeError;
25
+ /**
26
+ * Keys of a typed callback. Paths deeper than `alias.column` throw, unless `allowDeepPaths` is set by a caller
27
+ * that checks them against the query's metadata (paths into JSON columns).
28
+ */
29
+ export declare const getKeyHolder: <Type extends ObjectLiteral>(callback: KeyHolder | KeyProxyCallback<Type>, allowDeepPaths?: boolean) => KeyHolder | Array<KeyHolder>;
23
30
  export declare const getKey: <Type extends ObjectLiteral>(callback: string | KeyHolder | KeyProxyCallback<Type>) => string;
24
- export declare const getSqlKeyFromProxyCallback: <Type extends ObjectLiteral>(callback: string | KeyHolder | KeyProxyCallback<Type>, addPrefix?: string, useDoubleQuotes?: boolean) => string | string[];
31
+ /**
32
+ * Alias to prefix a single-level key with: a string, or a function called only when it is needed,
33
+ * e.g. `() => query.alias`, which throws while the query has no main alias (before `from()`).
34
+ */
35
+ export type KeyPrefix = string | (() => string | undefined);
36
+ /**
37
+ * SQL key of a typed callback: `(u) => u.id` → `prefix.id`, `(u) => u.images.url` → `images.url`
38
+ * (an `alias.column` path keeps its own alias). Strings are returned as is.
39
+ */
40
+ export declare const getSqlKeyFromProxyCallback: <Type extends ObjectLiteral>(callback: string | KeyHolder | KeyProxyCallback<Type>, addPrefix?: KeyPrefix, useQuotes?: boolean) => string | string[];
@@ -1,15 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getSqlKeyFromProxyCallback = exports.getKey = exports.getKeyHolder = exports.KeyProxy = exports.TypedProxy = exports.KeyHolder = void 0;
3
+ exports.getSqlKeyFromProxyCallback = exports.getKey = exports.getKeyHolder = exports.createKeyTooDeepError = exports.MAX_KEY_DEPTH = exports.KeyProxy = exports.TypedProxy = exports.KeyHolder = void 0;
4
4
  const sql_utils_1 = require("./sql.utils");
5
5
  class KeyHolder {
6
6
  constructor(_keys = []) {
7
7
  this._keys = _keys;
8
8
  this._keys = [..._keys];
9
9
  }
10
- toSql(length = 2, useDoubleQuotes) {
10
+ toSql(length = 2, useQuotes) {
11
11
  const string = this.toString(length);
12
- const sql = useDoubleQuotes ? (0, sql_utils_1.stringToSQLIdentifier)(string) : string;
12
+ const sql = useQuotes ? (0, sql_utils_1.stringToSQLIdentifier)(string) : string;
13
13
  return sql;
14
14
  }
15
15
  toString(length = 2) {
@@ -69,14 +69,33 @@ KeyProxy.defaultHandler = {
69
69
  return newLayer;
70
70
  },
71
71
  };
72
- const getKeyHolder = (callback) => {
72
+ /** Typed keys are `column` or `alias.column`: a deeper path can't be mapped to a query alias. */
73
+ exports.MAX_KEY_DEPTH = 2;
74
+ const createKeyTooDeepError = (keys) => new TypeError(`Typed key "${keys.join('.')}" is ${keys.length} levels deep, but typed methods support only ` +
75
+ `"column" or "alias.column", or a path into a JSON column in where methods. Join the nested relation and ` +
76
+ `address it by its alias with an alias-keyed type, e.g. type Joins = { someAlias: SomeEntity }; (j) => j.someAlias.column`);
77
+ exports.createKeyTooDeepError = createKeyTooDeepError;
78
+ const assertKeyDepth = (keyHolder) => {
79
+ const keys = keyHolder.getKeys();
80
+ if (keys.length > exports.MAX_KEY_DEPTH) {
81
+ throw (0, exports.createKeyTooDeepError)(keys);
82
+ }
83
+ return keyHolder;
84
+ };
85
+ /**
86
+ * Keys of a typed callback. Paths deeper than `alias.column` throw, unless `allowDeepPaths` is set by a caller
87
+ * that checks them against the query's metadata (paths into JSON columns).
88
+ */
89
+ const getKeyHolder = (callback, allowDeepPaths = false) => {
90
+ const check = allowDeepPaths ? (keyHolder) => keyHolder : assertKeyDepth;
73
91
  if (typeof callback !== 'function') {
74
92
  if (callback instanceof KeyHolder) {
75
- return callback;
93
+ return check(callback);
76
94
  }
77
95
  }
78
96
  const keyHolder = callback(KeyProxy.create());
79
- return keyHolder;
97
+ // fail loudly instead of silently dropping levels beyond `alias.column`
98
+ return Array.isArray(keyHolder) ? keyHolder.map(check) : check(keyHolder);
80
99
  };
81
100
  exports.getKeyHolder = getKeyHolder;
82
101
  const getKey = (callback) => {
@@ -86,26 +105,32 @@ const getKey = (callback) => {
86
105
  return keyHolder.toString();
87
106
  };
88
107
  exports.getKey = getKey;
89
- const getSqlKeyFromProxyCallback = (callback, addPrefix, useDoubleQuotes) => {
108
+ /**
109
+ * SQL key of a typed callback: `(u) => u.id` → `prefix.id`, `(u) => u.images.url` → `images.url`
110
+ * (an `alias.column` path keeps its own alias). Strings are returned as is.
111
+ */
112
+ const getSqlKeyFromProxyCallback = (callback, addPrefix, useQuotes) => {
90
113
  if (typeof callback === 'string')
91
114
  return callback;
92
115
  const keyHolder = (0, exports.getKeyHolder)(callback);
93
116
  if (Array.isArray(keyHolder)) {
94
- return keyHolder.map((holder) => (0, exports.getSqlKeyFromProxyCallback)(holder, addPrefix));
117
+ return keyHolder.map((holder) => (0, exports.getSqlKeyFromProxyCallback)(holder, addPrefix, useQuotes));
95
118
  }
96
119
  // if select is like this: (proxy) => proxy.relation.column
97
120
  if (keyHolder.getKeys().length > 1) {
98
121
  // return unchanged
99
- return keyHolder.toSql(undefined, useDoubleQuotes);
122
+ return keyHolder.toSql(undefined, useQuotes);
100
123
  }
101
124
  // else if select is one layer depth like this: (proxy) => proxy.column
102
- // then add alias before the key
103
- if (typeof addPrefix === 'string' && addPrefix.length >= 1) {
104
- keyHolder.unshift(addPrefix);
125
+ // then add alias before the key; resolve the alias only now, a path above doesn't need it
126
+ const prefix = typeof addPrefix === 'function' ? addPrefix() : addPrefix;
127
+ if (typeof prefix === 'string' && prefix.length >= 1) {
128
+ // a new holder: a KeyHolder passed in by the caller must stay as it was
129
+ return new KeyHolder([prefix, ...keyHolder.getKeys()]).toSql(undefined, useQuotes);
105
130
  }
106
- return keyHolder.toSql(undefined, useDoubleQuotes);
131
+ return keyHolder.toSql(undefined, useQuotes);
107
132
  };
108
133
  exports.getSqlKeyFromProxyCallback = getSqlKeyFromProxyCallback;
109
134
  // example of typed select
110
- // const key = getSqlKey<UserEntity>((proxy) => proxy.sessions);
111
- // console.log(key);
135
+ // const key = getKey<UserEntity>((proxy) => proxy.sessions);
136
+ // console.log(key); // "sessions"
@@ -0,0 +1,48 @@
1
+ import type { ObjectLiteral, WhereExpressionBuilder } from 'typeorm';
2
+ import type { KeyProxyCallback } from '../types/modules/proxy-callback.types';
3
+ import { JsonPathSqlOptions } from './json-path.utils';
4
+ /**
5
+ * Database name of an entity property's column (`@Column({ name })`), looked up in the metadata of
6
+ * a query alias (the main alias when `aliasName` is undefined). `undefined` when the alias has no
7
+ * entity or the property isn't a column.
8
+ */
9
+ export declare function getColumnDatabaseName(builder: WhereExpressionBuilder, aliasName: string | undefined, propertyPath: string): string | undefined;
10
+ /**
11
+ * SQL for a field passed as a string to the untyped methods (`whereIsIn('user.first-name', …)`).
12
+ *
13
+ * A string can't tell a column from an expression: `user.first-name` may be the column `first-name`
14
+ * or `user.first` minus `name`. The query builder knows its aliases and their entities, though:
15
+ * - `alias.column`, where `alias` is an alias of the query and `column` one of its entity's columns
16
+ * (by database name or property path): the column, quoted by the driver, `"user"."first-name"`.
17
+ * Property paths are mapped to their columns (`user.createdAt` → `"user"."created_at"`).
18
+ * UPDATE / DELETE give the bare column, as they have no alias.
19
+ * - `column`, a database column name of the main entity: the quoted column, `"first-name"`. Property
20
+ * names aren't mapped without an alias, as `status` may also be a column of a joined table.
21
+ * - anything else (unknown aliases, subqueries, expressions such as `LOWER(u.email)` or
22
+ * `u.price - u.discount`): quoted only when it is a path of plain identifiers, as
23
+ * `stringToSQLIdentifier` does, in the driver's quotes.
24
+ */
25
+ export declare function toFieldSql(builder: WhereExpressionBuilder, field: string): string;
26
+ /**
27
+ * SQL key for a typed callback on `builder`: `"alias"."column"`, or `"column"` on UPDATE / DELETE.
28
+ *
29
+ * The property is mapped to its database column name (`@Column({ name })`, embedded columns)
30
+ * through the metadata of the alias it belongs to (see `resolveTypedKey`). Aliases without
31
+ * metadata (subqueries) and unknown properties keep the property name. A path into a JSON column
32
+ * gives its JSON accessor, cast on PostgreSQL to the type of `jsonOptions.value`.
33
+ */
34
+ export declare function getTypedColumnKey<Type extends ObjectLiteral>(builder: WhereExpressionBuilder, callback: KeyProxyCallback<Type>, jsonOptions?: JsonPathSqlOptions): string | string[];
35
+ /**
36
+ * Quoted column of a typed callback that must select exactly one column
37
+ * (`whereIsAnyTyped`, `whereArrayContainsTyped`, …). Throws a `TypeError` for several columns:
38
+ * one condition per column has no single obvious meaning (AND? OR?), so don't guess.
39
+ */
40
+ export declare function getTypedSingleColumnKey<Type extends ObjectLiteral>(builder: WhereExpressionBuilder, callback: KeyProxyCallback<Type>, methodName: string, jsonOptions?: JsonPathSqlOptions): string;
41
+ /**
42
+ * Key for typed select / order / distinct on a SelectQueryBuilder.
43
+ *
44
+ * Without quotes it is `alias.propertyPath` (`user.name`, `images.url`, `p.address.city`),
45
+ * which TypeORM maps to the column itself. With `useQuotes` it is the quoted database column
46
+ * (`"t"."ticket_status"`), since TypeORM doesn't map quoted keys. Strings are kept as is.
47
+ */
48
+ export declare function getTypedSelectionKey<Type extends ObjectLiteral>(builder: WhereExpressionBuilder, selection: KeyProxyCallback<Type> | string, useQuotes?: boolean): string | string[];