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

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 (115) 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 +15 -2
  11. package/dist/extensions/cursor-pagination.extension.js +90 -13
  12. package/dist/extensions/get-many-with-totals.extension.js +12 -31
  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 +7 -1
  16. package/dist/extensions/pagination.extension.js +5 -12
  17. package/dist/extensions/period-filter.extension.d.ts +4 -0
  18. package/dist/extensions/period-filter.extension.js +1 -9
  19. package/dist/extensions/typed/index.d.ts +2 -0
  20. package/dist/extensions/typed/index.js +2 -0
  21. package/dist/extensions/typed/typed-calculated/typed-calculated-column.extension.d.ts +19 -0
  22. package/dist/extensions/typed/typed-calculated/typed-calculated-column.extension.js +17 -0
  23. package/dist/extensions/typed/typed-distinct/typed-distinct.extension.d.ts +6 -4
  24. package/dist/extensions/typed/typed-distinct/typed-distinct.extension.js +9 -2
  25. package/dist/extensions/typed/typed-group-by/typed-group-by.extension.d.ts +8 -7
  26. package/dist/extensions/typed/typed-group-by/typed-group-by.extension.js +1 -1
  27. package/dist/extensions/typed/typed-having/typed-having.extension.d.ts +4 -4
  28. package/dist/extensions/typed/typed-having/typed-having.extension.js +1 -1
  29. package/dist/extensions/typed/typed-joins/typed-join-and-map.extension.d.ts +19 -0
  30. package/dist/extensions/typed/typed-joins/typed-join-and-map.extension.js +23 -0
  31. package/dist/extensions/typed/typed-joins/typed-joins.extension.d.ts +4 -4
  32. package/dist/extensions/typed/typed-joins/typed-joins.extension.js +11 -22
  33. package/dist/extensions/typed/typed-order-by/typed-order-by-array-position.extension.d.ts +2 -2
  34. package/dist/extensions/typed/typed-order-by/typed-order-by.extension.d.ts +4 -2
  35. package/dist/extensions/typed/typed-order-by/typed-order-by.extension.js +18 -17
  36. package/dist/extensions/typed/typed-returning/typed-returning.extension.js +1 -9
  37. package/dist/extensions/typed/typed-root.extension.d.ts +26 -4
  38. package/dist/extensions/typed/typed-select/typed-select.extension.d.ts +6 -6
  39. package/dist/extensions/typed/typed-select/typed-select.extension.js +3 -9
  40. package/dist/extensions/typed/typed-where/typed-period-filter.extension.d.ts +5 -1
  41. package/dist/extensions/typed/typed-where/typed-period-filter.extension.js +1 -9
  42. package/dist/extensions/typed/typed-where/typed-where-array.extension.d.ts +13 -9
  43. package/dist/extensions/typed/typed-where/typed-where-array.extension.js +4 -16
  44. package/dist/extensions/typed/typed-where/typed-where-ilike.extension.d.ts +10 -6
  45. package/dist/extensions/typed/typed-where/typed-where-ilike.extension.js +1 -9
  46. package/dist/extensions/typed/typed-where/typed-where-is-any.extension.d.ts +10 -6
  47. package/dist/extensions/typed/typed-where/typed-where-is-any.extension.js +3 -20
  48. package/dist/extensions/typed/typed-where/typed-where-is-distinct-from.extension.d.ts +10 -6
  49. package/dist/extensions/typed/typed-where/typed-where-is-distinct-from.extension.js +3 -20
  50. package/dist/extensions/typed/typed-where/typed-where-is-in.extension.d.ts +10 -6
  51. package/dist/extensions/typed/typed-where/typed-where-is-in.extension.js +3 -21
  52. package/dist/extensions/typed/typed-where/typed-where-json.extension.d.ts +10 -6
  53. package/dist/extensions/typed/typed-where/typed-where-json.extension.js +4 -21
  54. package/dist/extensions/typed/typed-where/typed-where-lower.extension.d.ts +10 -6
  55. package/dist/extensions/typed/typed-where/typed-where-lower.extension.js +1 -9
  56. package/dist/extensions/typed/typed-where/typed-where-similar.extension.d.ts +8 -4
  57. package/dist/extensions/typed/typed-where/typed-where-similar.extension.js +1 -8
  58. package/dist/extensions/typed/typed-where/typed-where.extension.d.ts +7 -3
  59. package/dist/extensions/typed/typed-where/typed-where.extension.js +2 -10
  60. package/dist/extensions/union.extension.js +1 -1
  61. package/dist/extensions/where-array.extension.d.ts +4 -0
  62. package/dist/extensions/where-array.extension.js +1 -9
  63. package/dist/extensions/where-exists.extension.js +4 -15
  64. package/dist/extensions/where-is-any.extension.d.ts +41 -128
  65. package/dist/extensions/where-is-any.extension.js +1 -9
  66. package/dist/extensions/where-is-distinct-from.extension.d.ts +4 -0
  67. package/dist/extensions/where-is-distinct-from.extension.js +1 -9
  68. package/dist/extensions/where-is-in.extension.d.ts +38 -116
  69. package/dist/extensions/where-is-in.extension.js +11 -27
  70. package/dist/extensions/where-json.extension.d.ts +4 -0
  71. package/dist/extensions/where-json.extension.js +1 -9
  72. package/dist/extensions/where-relation-exists.extension.d.ts +6 -6
  73. package/dist/extensions/where-relation-exists.extension.js +5 -25
  74. package/dist/helpers.d.ts +5 -0
  75. package/dist/helpers.js +12 -3
  76. package/dist/index.d.ts +1 -2
  77. package/dist/index.js +1 -4
  78. package/dist/types/extensions/cursor-pagination.types.d.ts +16 -1
  79. package/dist/types/extensions/get-many-with-totals.types.d.ts +4 -0
  80. package/dist/types/extensions/index.d.ts +1 -0
  81. package/dist/types/extensions/index.js +1 -0
  82. package/dist/types/extensions/typed-group-by.types.d.ts +2 -2
  83. package/dist/types/extensions/typed-join-and-map.types.d.ts +6 -0
  84. package/dist/types/extensions/typed-join-and-map.types.js +2 -0
  85. package/dist/types/interfaces/cursor-pagination.interface.d.ts +11 -0
  86. package/dist/types/modules/typed-root.types.d.ts +11 -9
  87. package/dist/utils/calculated-column.utils.d.ts +11 -0
  88. package/dist/utils/calculated-column.utils.js +33 -4
  89. package/dist/utils/common.utils.d.ts +2 -0
  90. package/dist/utils/common.utils.js +4 -1
  91. package/dist/utils/cursor.utils.d.ts +5 -0
  92. package/dist/utils/cursor.utils.js +18 -7
  93. package/dist/utils/json-path.utils.js +2 -2
  94. package/dist/utils/pagination.utils.d.ts +10 -0
  95. package/dist/utils/pagination.utils.js +27 -10
  96. package/dist/utils/parameter.utils.d.ts +10 -0
  97. package/dist/utils/parameter.utils.js +22 -1
  98. package/dist/utils/period.utils.js +2 -1
  99. package/dist/utils/prototype.utils.d.ts +9 -0
  100. package/dist/utils/prototype.utils.js +12 -2
  101. package/dist/utils/query-builder.utils.d.ts +43 -4
  102. package/dist/utils/query-builder.utils.js +239 -36
  103. package/dist/utils/reserved-qualifier-guard.utils.d.ts +36 -0
  104. package/dist/utils/reserved-qualifier-guard.utils.js +98 -0
  105. package/dist/utils/reserved-qualifier.utils.d.ts +33 -0
  106. package/dist/utils/reserved-qualifier.utils.js +252 -0
  107. package/dist/utils/table.utils.d.ts +27 -11
  108. package/dist/utils/table.utils.js +58 -23
  109. package/dist/utils/time-zone.utils.d.ts +8 -0
  110. package/dist/utils/time-zone.utils.js +15 -5
  111. package/dist/utils/typed-where.utils.d.ts +9 -0
  112. package/dist/utils/typed-where.utils.js +17 -0
  113. package/dist/utils/where.utils.d.ts +2 -0
  114. package/dist/utils/where.utils.js +21 -14
  115. package/package.json +2 -2
@@ -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[];
@@ -0,0 +1,252 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findBareReservedQualifiers = exports.scanReservedQualifiers = exports.toReservedWordSet = exports.COCKROACHDB_RESERVED_KEY_WORDS = exports.POSTGRES_RESERVED_KEY_WORDS = void 0;
4
+ /**
5
+ * The reserved key words of PostgreSQL, "reserved" in Appendix C of its documentation (`SYSTEM_USER` since
6
+ * PostgreSQL 16). None can be a table, an alias or a schema without quotes, so none is followed by a `.` in valid
7
+ * SQL. The words "reserved (can be function or type)" (`LEFT`, `JOIN`, `BINARY`, …) aren't on the list: one may
8
+ * name the schema of a type, as in `CAST(x AS binary.my_type)`.
9
+ */
10
+ exports.POSTGRES_RESERVED_KEY_WORDS = Object.freeze([
11
+ 'ALL', 'ANALYSE', 'ANALYZE', 'AND', 'ANY', 'ARRAY', 'AS', 'ASC', 'ASYMMETRIC', 'BOTH', 'CASE', 'CAST', 'CHECK',
12
+ 'COLLATE', 'COLUMN', 'CONSTRAINT', 'CREATE', 'CURRENT_CATALOG', 'CURRENT_DATE', 'CURRENT_ROLE', 'CURRENT_TIME',
13
+ 'CURRENT_TIMESTAMP', 'CURRENT_USER', 'DEFAULT', 'DEFERRABLE', 'DESC', 'DISTINCT', 'DO', 'ELSE', 'END', 'EXCEPT',
14
+ 'FALSE', 'FETCH', 'FOR', 'FOREIGN', 'FROM', 'GRANT', 'GROUP', 'HAVING', 'IN', 'INITIALLY', 'INTERSECT', 'INTO',
15
+ 'LATERAL', 'LEADING', 'LIMIT', 'LOCALTIME', 'LOCALTIMESTAMP', 'NOT', 'NULL', 'OFFSET', 'ON', 'ONLY', 'OR', 'ORDER',
16
+ 'PLACING', 'PRIMARY', 'REFERENCES', 'RETURNING', 'SELECT', 'SESSION_USER', 'SOME', 'SYMMETRIC', 'SYSTEM_USER',
17
+ 'TABLE', 'THEN', 'TO', 'TRAILING', 'TRUE', 'UNION', 'UNIQUE', 'USER', 'USING', 'VARIADIC', 'WHEN', 'WHERE',
18
+ 'WINDOW', 'WITH',
19
+ ]);
20
+ /** The reserved key words of CockroachDB: PostgreSQL's without `SYSTEM_USER`, with `CONCURRENTLY` and `CURRENT_SCHEMA`. */
21
+ exports.COCKROACHDB_RESERVED_KEY_WORDS = Object.freeze([
22
+ ...exports.POSTGRES_RESERVED_KEY_WORDS.filter((word) => word !== 'SYSTEM_USER'),
23
+ 'CONCURRENTLY',
24
+ 'CURRENT_SCHEMA',
25
+ ]);
26
+ const KEY_WORD = /^[A-Za-z_][A-Za-z0-9_$]*$/;
27
+ const wordSets = new WeakMap();
28
+ /** The words in lower case; cached for a frozen list, such as the defaults, which can't change afterwards. */
29
+ function toReservedWordSet(words) {
30
+ const cached = wordSets.get(words);
31
+ if (cached) {
32
+ return cached;
33
+ }
34
+ if (!Array.isArray(words)) {
35
+ throw new TypeError(`Reserved words must be an array of key words, got ${typeof words}`);
36
+ }
37
+ for (const word of words) {
38
+ if (typeof word !== 'string' || !KEY_WORD.test(word)) {
39
+ throw new TypeError(`Invalid reserved word ${JSON.stringify(word)}: expected a key word, such as "order"`);
40
+ }
41
+ }
42
+ const set = new Set(words.map((word) => word.toLowerCase()));
43
+ if (Object.isFrozen(words)) {
44
+ wordSets.set(words, set);
45
+ }
46
+ return set;
47
+ }
48
+ exports.toReservedWordSet = toReservedWordSet;
49
+ const TAB = 9;
50
+ const CARRIAGE_RETURN = 13;
51
+ const LINE_FEED = 10;
52
+ const SPACE = 32;
53
+ const DOUBLE_QUOTE = 34;
54
+ const DOLLAR = 36;
55
+ const QUOTE = 39;
56
+ const STAR = 42;
57
+ const DASH = 45;
58
+ const DOT = 46;
59
+ const SLASH = 47;
60
+ const COLON = 58;
61
+ const BACKSLASH = 92;
62
+ const isSpace = (code) => code === SPACE || (code >= TAB && code <= CARRIAGE_RETURN);
63
+ const isDigit = (code) => code >= 48 && code <= 57;
64
+ // as PostgreSQL reads identifiers: letters, `_` and every non-ASCII character; then digits and `$` too
65
+ const isIdentifierStart = (code) => (code >= 97 && code <= 122) || (code >= 65 && code <= 90) || code === 95 || code >= 128;
66
+ const isIdentifierPart = (code) => isIdentifierStart(code) || isDigit(code) || code === DOLLAR;
67
+ /** Index after the string literal whose opening quote is at `start`; `E'…'` strings take backslash escapes. */
68
+ function skipString(sql, start, backslashEscapes) {
69
+ for (let i = start + 1; i < sql.length; i++) {
70
+ const code = sql.charCodeAt(i);
71
+ if (backslashEscapes && code === BACKSLASH) {
72
+ i++;
73
+ }
74
+ else if (code === QUOTE) {
75
+ if (sql.charCodeAt(i + 1) !== QUOTE) {
76
+ return i + 1;
77
+ }
78
+ i++;
79
+ }
80
+ }
81
+ return sql.length;
82
+ }
83
+ function skipQuotedIdentifier(sql, start) {
84
+ for (let i = start + 1; i < sql.length; i++) {
85
+ if (sql.charCodeAt(i) === DOUBLE_QUOTE) {
86
+ if (sql.charCodeAt(i + 1) !== DOUBLE_QUOTE) {
87
+ return i + 1;
88
+ }
89
+ i++;
90
+ }
91
+ }
92
+ return sql.length;
93
+ }
94
+ function skipLineComment(sql, start) {
95
+ for (let i = start + 2; i < sql.length; i++) {
96
+ const code = sql.charCodeAt(i);
97
+ if (code === LINE_FEED || code === CARRIAGE_RETURN) {
98
+ return i;
99
+ }
100
+ }
101
+ return sql.length;
102
+ }
103
+ // PostgreSQL nests block comments: `/* a /* b */ c */` is one comment
104
+ function skipBlockComment(sql, start) {
105
+ let depth = 0;
106
+ let i = start;
107
+ while (i < sql.length) {
108
+ const code = sql.charCodeAt(i);
109
+ const next = sql.charCodeAt(i + 1);
110
+ if (code === SLASH && next === STAR) {
111
+ depth++;
112
+ i += 2;
113
+ }
114
+ else if (code === STAR && next === SLASH) {
115
+ depth--;
116
+ i += 2;
117
+ if (depth === 0) {
118
+ return i;
119
+ }
120
+ }
121
+ else {
122
+ i++;
123
+ }
124
+ }
125
+ return sql.length;
126
+ }
127
+ /** Index after the opening tag of a dollar-quoted string at `start` (`$$`, `$tag$`); -1 for a parameter (`$1`). */
128
+ function dollarTagEnd(sql, start) {
129
+ let i = start + 1;
130
+ if (isIdentifierStart(sql.charCodeAt(i))) {
131
+ do {
132
+ i++;
133
+ } while (isIdentifierStart(sql.charCodeAt(i)) || isDigit(sql.charCodeAt(i)));
134
+ }
135
+ return sql.charCodeAt(i) === DOLLAR ? i + 1 : -1;
136
+ }
137
+ /**
138
+ * Whether the word from `start` to `end` qualifies a name: a dot follows it, after optional whitespace, and not a
139
+ * number (`THEN .5`); and it isn't itself a name after a dot (`public.order.id`), next to a quoted name or an
140
+ * identifier character, or a named parameter (`:order`, but `::` is a cast).
141
+ */
142
+ function isQualifier(sql, start, end, previous) {
143
+ let next = end;
144
+ while (isSpace(sql.charCodeAt(next))) {
145
+ next++;
146
+ }
147
+ if (sql.charCodeAt(next) !== DOT || isDigit(sql.charCodeAt(next + 1)) || previous === DOT) {
148
+ return false;
149
+ }
150
+ const before = sql.charCodeAt(start - 1);
151
+ return (before !== DOUBLE_QUOTE &&
152
+ !isIdentifierPart(before) &&
153
+ !(before === COLON && sql.charCodeAt(start - 2) !== COLON));
154
+ }
155
+ const SNIPPET_CONTEXT = 30;
156
+ // cut at whitespace, not inside a word, when the context has some
157
+ function snippetAt(sql, start, end) {
158
+ let from = Math.max(0, start - SNIPPET_CONTEXT);
159
+ let to = Math.min(sql.length, end + SNIPPET_CONTEXT);
160
+ if (from > 0) {
161
+ const space = sql.slice(from, start).search(/\s/);
162
+ if (space !== -1) {
163
+ from += space + 1;
164
+ }
165
+ }
166
+ if (to < sql.length) {
167
+ const space = sql.slice(end, to).search(/\s\S*$/);
168
+ if (space !== -1) {
169
+ to = end + space;
170
+ }
171
+ }
172
+ return `${from > 0 ? '…' : ''}${sql.slice(from, to).replace(/\s+/g, ' ')}${to < sql.length ? '…' : ''}`;
173
+ }
174
+ /** `findBareReservedQualifiers` with the words as a set of lower-case words (`toReservedWordSet`). */
175
+ function scanReservedQualifiers(sql, words) {
176
+ const found = [];
177
+ const length = sql.length;
178
+ // the last character outside whitespace and comments
179
+ let previous = -1;
180
+ let i = 0;
181
+ while (i < length) {
182
+ const code = sql.charCodeAt(i);
183
+ if (isSpace(code)) {
184
+ i++;
185
+ }
186
+ else if (code === DASH && sql.charCodeAt(i + 1) === DASH) {
187
+ i = skipLineComment(sql, i);
188
+ }
189
+ else if (code === SLASH && sql.charCodeAt(i + 1) === STAR) {
190
+ i = skipBlockComment(sql, i);
191
+ }
192
+ else if (code === QUOTE) {
193
+ i = skipString(sql, i, false);
194
+ previous = code;
195
+ }
196
+ else if (code === DOUBLE_QUOTE) {
197
+ i = skipQuotedIdentifier(sql, i);
198
+ previous = code;
199
+ }
200
+ else if (code === DOLLAR) {
201
+ const tagEnd = dollarTagEnd(sql, i);
202
+ if (tagEnd === -1) {
203
+ do {
204
+ i++;
205
+ } while (isDigit(sql.charCodeAt(i)));
206
+ }
207
+ else {
208
+ const close = sql.indexOf(sql.slice(i, tagEnd), tagEnd);
209
+ i = close === -1 ? length : close + (tagEnd - i);
210
+ }
211
+ previous = code;
212
+ }
213
+ else if (isIdentifierPart(code)) {
214
+ const start = i;
215
+ do {
216
+ i++;
217
+ } while (isIdentifierPart(sql.charCodeAt(i)));
218
+ // E'…', the string literal with backslash escapes
219
+ if (i - start === 1 && (code === 69 || code === 101) && sql.charCodeAt(i) === QUOTE) {
220
+ i = skipString(sql, i, true);
221
+ previous = QUOTE;
222
+ continue;
223
+ }
224
+ if (isIdentifierStart(code) &&
225
+ isQualifier(sql, start, i, previous) &&
226
+ words.has(sql.slice(start, i).toLowerCase())) {
227
+ found.push({ word: sql.slice(start, i), index: start, snippet: snippetAt(sql, start, i) });
228
+ }
229
+ previous = code;
230
+ }
231
+ else {
232
+ previous = code;
233
+ i++;
234
+ }
235
+ }
236
+ return found;
237
+ }
238
+ exports.scanReservedQualifiers = scanReservedQualifiers;
239
+ /**
240
+ * Reserved words that qualify a name without quotes, as in `order."userId"` or `user.id`: PostgreSQL reads such a
241
+ * word as the key word, not as an alias, and rejects the statement (`syntax error at or near "order"`).
242
+ *
243
+ * Words in string literals (`E'…'` and dollar-quoted ones too), quoted identifiers, comments and parameters are
244
+ * skipped, and so are words after a dot (`public.order`) and a number after one (`THEN .5`). Case-insensitive;
245
+ * whitespace may come before the dot (`group . name`).
246
+ *
247
+ * @param words the words to look for, `POSTGRES_RESERVED_KEY_WORDS` by default
248
+ */
249
+ function findBareReservedQualifiers(sql, words = exports.POSTGRES_RESERVED_KEY_WORDS) {
250
+ return scanReservedQualifiers(sql, toReservedWordSet(words));
251
+ }
252
+ exports.findBareReservedQualifiers = findBareReservedQualifiers;
@@ -74,17 +74,7 @@ export type TableHelperTimeZone<T> = string | {
74
74
  /** The units of PostgreSQL's `date_trunc`. */
75
75
  export type DateTruncUnit = 'microseconds' | 'milliseconds' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' | 'decade' | 'century' | 'millennium';
76
76
  export type JsonFieldRest<V> = [NonNullable<V>] extends [Record<string, unknown>] ? JsonFieldTuple<NonNullable<V>> : string[];
77
- /**
78
- * Creates a type-safe SQL helper for building column references, WHERE expressions,
79
- * JOIN clauses, and JSONB accessors for a given entity type.
80
- *
81
- * @example
82
- * const t = tableHelper<UserEntity>('user');
83
- * t.column('email') // → "user"."email"
84
- * t.where('id', '= :id') // → "user"."id" = :id
85
- * t.jsonColumn('metadata', '->>', 'role') // → "user"."metadata" ->> 'role'
86
- */
87
- export declare const tableHelper: <T extends ObjectLiteral>(alias?: string, options?: TableHelperOptions) => {
77
+ declare const createTableHelper: <T extends ObjectLiteral>(alias?: string, options?: TableHelperOptions, databaseNames?: ReadonlyMap<string, string>) => {
88
78
  alias: string | undefined;
89
79
  /**
90
80
  * Returns a quoted SQL column identifier, e.g. `"alias"."column"`. The column is always quoted as one
@@ -193,4 +183,30 @@ export declare const tableHelper: <T extends ObjectLiteral>(alias?: string, opti
193
183
  */
194
184
  dateTrunc(unit: DateTruncUnit, value: TableHelperExpression<T>, timeZone?: TableHelperTimeZone<T> | undefined): string;
195
185
  };
186
+ type TableHelperMemberName = keyof ReturnType<typeof createTableHelper>;
187
+ /**
188
+ * The helper `tableHelper` returns: its methods, and each column of the table as a read-only property,
189
+ * `t.email` for `t.column('email')`. A column named like a member (`alias`, `count`, …) is the member there.
190
+ */
191
+ export type TableHelper<T extends ObjectLiteral> = ReturnType<typeof createTableHelper<T>> & {
192
+ readonly [K in Exclude<ColumnName<T>, TableHelperMemberName>]: string;
193
+ };
194
+ /** An entity class, abstract ones included. */
195
+ export type EntityClass<T> = abstract new (...args: never[]) => T;
196
+ /**
197
+ * Creates a type-safe SQL helper for building column references, WHERE expressions,
198
+ * JOIN clauses, and JSONB accessors for a given entity type.
199
+ *
200
+ * Given the entity class, it takes property names and writes the column names the entity maps them to:
201
+ * `tableHelper(UserEntity, 'user').adminEmail` is `"user"."email"` for `@Column({ name: 'email' }) adminEmail`.
202
+ *
203
+ * @example
204
+ * const t = tableHelper<UserEntity>('user');
205
+ * t.column('email') // → "user"."email"
206
+ * t.email // → "user"."email", the same
207
+ * t.where('id', '= :id') // → "user"."id" = :id
208
+ * t.jsonColumn('metadata', '->>', 'role') // → "user"."metadata" ->> 'role'
209
+ */
210
+ export declare function tableHelper<T extends ObjectLiteral>(entity: EntityClass<T>, alias?: string, options?: TableHelperOptions): TableHelper<T>;
211
+ export declare function tableHelper<T extends ObjectLiteral>(alias?: string, options?: TableHelperOptions): TableHelper<T>;
196
212
  export {};
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.tableHelper = void 0;
4
+ const typeorm_1 = require("typeorm");
4
5
  const sql_utils_1 = require("./sql.utils");
5
6
  const json_path_utils_1 = require("./json-path.utils");
7
+ const time_zone_utils_1 = require("./time-zone.utils");
6
8
  /**
7
9
  * The argument of an aggregate and the clause after it for a `filter` written with `syntax`:
8
10
  * `CASE WHEN filter THEN argument END`, or `argument` and ` FILTER (WHERE filter)`.
@@ -44,9 +46,9 @@ const DATE_TRUNC_UNITS = [
44
46
  'century',
45
47
  'millennium',
46
48
  ];
47
- function assertPostgres(method, driver) {
49
+ function assertPostgres(method, driver, hint) {
48
50
  if (driver !== undefined && !(0, sql_utils_1.isPostgresLikeDriver)(driver)) {
49
- throw new TypeError(`tableHelper.${method}(): PostgreSQL only, the driver is "${driver}"`);
51
+ throw new TypeError(`tableHelper.${method}(): PostgreSQL only, the driver is "${driver}"${hint ? `: ${hint}` : ''}`);
50
52
  }
51
53
  }
52
54
  function toExpression(value, toColumn) {
@@ -57,7 +59,7 @@ function toTimeZone(method, timeZone, toColumn) {
57
59
  if (!timeZone.trim()) {
58
60
  throw new TypeError(`tableHelper.${method}(): the time zone is empty`);
59
61
  }
60
- return sqlString(toPostgresZone(timeZone));
62
+ return sqlString(toPostgresZone(method, timeZone));
61
63
  }
62
64
  if ('sql' in timeZone) {
63
65
  return timeZone.sql;
@@ -65,19 +67,18 @@ function toTimeZone(method, timeZone, toColumn) {
65
67
  const column = toColumn(timeZone.column);
66
68
  return timeZone.fallback === undefined
67
69
  ? column
68
- : `COALESCE(${column}, ${sqlString(toPostgresZone(timeZone.fallback))})`;
70
+ : `COALESCE(${column}, ${sqlString(toPostgresZone(method, timeZone.fallback))})`;
69
71
  }
70
- const ISO_OFFSET = /^([+-])(\d{2}):?(\d{2})?$/;
71
72
  /**
72
73
  * PostgreSQL reads `'+02:00'` as a POSIX zone, whose offsets count west: UTC−2. An ISO 8601 offset is
73
74
  * written as the POSIX zone of the same offset, `'<+0200>-02:00'`, so `'+02:00'` is UTC+2 as everywhere else.
74
75
  */
75
- function toPostgresZone(timeZone) {
76
- const offset = ISO_OFFSET.exec(timeZone.trim());
76
+ function toPostgresZone(method, timeZone) {
77
+ const offset = (0, time_zone_utils_1.parseUtcOffset)(timeZone.trim(), `tableHelper.${method}(): "${timeZone}" is not a UTC offset`);
77
78
  if (offset === null) {
78
79
  return timeZone;
79
80
  }
80
- const [, sign, hours, minutes = '00'] = offset;
81
+ const { sign, hours, minutes } = offset;
81
82
  return `<${sign}${hours}${minutes}>${sign === '+' ? '-' : '+'}${hours}:${minutes}`;
82
83
  }
83
84
  /** The JSON functions of a database: PostgreSQL's unless the driver is MySQL / MariaDB or SQLite. */
@@ -102,26 +103,17 @@ function jsonFunctions(driver, jsonb) {
102
103
  }
103
104
  // MySQL reads a backslash in a string as an escape (unless NO_BACKSLASH_ESCAPES): `\'` would end the literal
104
105
  const sqlString = (value, driver) => `'${((0, sql_utils_1.isMysqlLikeDriver)(driver) ? value.replace(/\\/g, '\\\\') : value).replace(/'/g, "''")}'`;
105
- /**
106
- * Creates a type-safe SQL helper for building column references, WHERE expressions,
107
- * JOIN clauses, and JSONB accessors for a given entity type.
108
- *
109
- * @example
110
- * const t = tableHelper<UserEntity>('user');
111
- * t.column('email') // → "user"."email"
112
- * t.where('id', '= :id') // → "user"."id" = :id
113
- * t.jsonColumn('metadata', '->>', 'role') // → "user"."metadata" ->> 'role'
114
- */
115
- const tableHelper = (alias, options = {}) => ({
106
+ const createTableHelper = (alias, options = {}, databaseNames) => ({
116
107
  alias,
117
108
  /**
118
109
  * Returns a quoted SQL column identifier, e.g. `"alias"."column"`. The column is always quoted as one
119
110
  * identifier, so names with other characters work too: `"user"."first-name"`.
120
111
  */
121
112
  column(columnName, alias) {
113
+ var _a;
122
114
  const { useQuotes = true, driver } = options;
123
115
  const tableAlias = alias !== null && alias !== void 0 ? alias : this.alias;
124
- const column = String(columnName);
116
+ const column = (_a = databaseNames === null || databaseNames === void 0 ? void 0 : databaseNames.get(String(columnName))) !== null && _a !== void 0 ? _a : String(columnName);
125
117
  if (!useQuotes) {
126
118
  return [tableAlias, column].filter(Boolean).join('.');
127
119
  }
@@ -208,9 +200,7 @@ const tableHelper = (alias, options = {}) => ({
208
200
  arrayAgg(columnName, aggOptions = {}) {
209
201
  var _a;
210
202
  const { distinct = false, orderBy, filter } = aggOptions;
211
- if (options.driver !== undefined && !(0, sql_utils_1.isPostgresLikeDriver)(options.driver)) {
212
- throw new TypeError(`tableHelper.arrayAgg(): arrays are PostgreSQL's, the driver is "${options.driver}": use jsonAgg`);
213
- }
203
+ assertPostgres('arrayAgg', options.driver, 'use jsonAgg');
214
204
  const order = orderBy ? aggregateOrder('arrayAgg', orderBy, (column) => this.column(column)) : undefined;
215
205
  // PostgreSQL: "in an aggregate with DISTINCT, ORDER BY expressions must appear in argument list"
216
206
  if (distinct && order && order.column !== columnName) {
@@ -316,4 +306,49 @@ const tableHelper = (alias, options = {}) => ({
316
306
  return `date_trunc('${unit}', ${toExpression(value, toColumn)}${zone})`;
317
307
  }
318
308
  });
309
+ // keys that JavaScript, Node and test tools read on any object (await, JSON.stringify, util.inspect, Jest): they
310
+ // must stay undefined, as must symbols. The members of Object.prototype are in the target already
311
+ const PROTOCOL_KEYS = new Set(['then', 'toJSON', 'inspect', 'asymmetricMatch', '$$typeof', 'nodeType']);
312
+ const columnAccess = {
313
+ get(target, key, receiver) {
314
+ if (typeof key === 'symbol' || key in target || PROTOCOL_KEYS.has(key)) {
315
+ return Reflect.get(target, key, receiver);
316
+ }
317
+ return target.column(key);
318
+ },
319
+ };
320
+ const databaseNamesByEntity = new WeakMap();
321
+ // The names set with `@Column({ name })` on the class or a parent, the nearest declaration winning. Every other
322
+ // column keeps its property name, as under TypeORM's default naming strategy. Columns of embedded entities are not read
323
+ const databaseColumnNames = (entity) => {
324
+ var _a;
325
+ const cached = databaseNamesByEntity.get(entity);
326
+ if (cached) {
327
+ return cached;
328
+ }
329
+ const lineage = [];
330
+ for (let target = entity; typeof target === 'function' && target !== Function.prototype; target = Object.getPrototypeOf(target)) {
331
+ lineage.push(target);
332
+ }
333
+ const names = new Map();
334
+ const depths = new Map();
335
+ for (const { target, propertyName, options } of (0, typeorm_1.getMetadataArgsStorage)().columns) {
336
+ const depth = lineage.indexOf(target);
337
+ if (depth !== -1 && options.name && depth < ((_a = depths.get(propertyName)) !== null && _a !== void 0 ? _a : Infinity)) {
338
+ names.set(propertyName, options.name);
339
+ depths.set(propertyName, depth);
340
+ }
341
+ }
342
+ databaseNamesByEntity.set(entity, names);
343
+ return names;
344
+ };
345
+ function tableHelper(entityOrAlias, aliasOrOptions, entityOptions) {
346
+ if (entityOrAlias === undefined && typeof aliasOrOptions === 'string') {
347
+ throw new TypeError(`tableHelper(entity, '${aliasOrOptions}'): the entity class is undefined, as a circular import leaves it at module load. Create the helper where it is used`);
348
+ }
349
+ const helper = typeof entityOrAlias === 'function'
350
+ ? createTableHelper(aliasOrOptions, entityOptions, databaseColumnNames(entityOrAlias))
351
+ : createTableHelper(entityOrAlias, aliasOrOptions);
352
+ return new Proxy(helper, columnAccess);
353
+ }
319
354
  exports.tableHelper = tableHelper;
@@ -1,3 +1,10 @@
1
+ type UtcOffset = {
2
+ sign: '+' | '-';
3
+ hours: string;
4
+ minutes: string;
5
+ };
6
+ /** The parts of an ISO 8601 UTC offset (`+02:00`, `-0530`, `+02`); `null` for other text, a `TypeError` past 23:59. */
7
+ export declare function parseUtcOffset(text: string, errorMessage: string): UtcOffset | null;
1
8
  /** Throws a `TypeError` for a time zone the runtime doesn't know. */
2
9
  export declare function assertTimeZone(timeZone: string): void;
3
10
  /**
@@ -6,3 +13,4 @@ export declare function assertTimeZone(timeZone: string): void;
6
13
  * instant already: it is returned as that `Date`. Any other string throws a `TypeError` naming `name`.
7
14
  */
8
15
  export declare function readInTimeZone(value: string, timeZone: string, name: string): Date;
16
+ export {};
@@ -1,10 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.readInTimeZone = exports.assertTimeZone = void 0;
3
+ exports.readInTimeZone = exports.assertTimeZone = exports.parseUtcOffset = void 0;
4
4
  const DAY_MS = 24 * 60 * 60 * 1000;
5
5
  const MAX_CACHED_FORMATTERS = 500;
6
6
  const DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?(Z|[+-]\d{2}(?::?\d{2})?)?)?$/i;
7
7
  const OFFSET = /^([+-])(\d{2}):?(\d{2})?$/;
8
+ /** The parts of an ISO 8601 UTC offset (`+02:00`, `-0530`, `+02`); `null` for other text, a `TypeError` past 23:59. */
9
+ function parseUtcOffset(text, errorMessage) {
10
+ const offset = OFFSET.exec(text);
11
+ if (offset === null) {
12
+ return null;
13
+ }
14
+ const [, sign, hours, minutes = '00'] = offset;
15
+ if (Number(hours) > 23 || Number(minutes) > 59) {
16
+ throw new TypeError(errorMessage);
17
+ }
18
+ return { sign: sign, hours, minutes };
19
+ }
20
+ exports.parseUtcOffset = parseUtcOffset;
8
21
  const formatters = new Map();
9
22
  function getFormatter(timeZone) {
10
23
  let formatter = formatters.get(timeZone);
@@ -94,9 +107,6 @@ function getOffsetMinutes(offset, value, name) {
94
107
  if (offset.toUpperCase() === 'Z') {
95
108
  return 0;
96
109
  }
97
- const [, sign, hours, minutes = '00'] = OFFSET.exec(offset);
98
- if (Number(hours) > 23 || Number(minutes) > 59) {
99
- throw new TypeError(`Invalid ${name} "${value}": not a UTC offset`);
100
- }
110
+ const { sign, hours, minutes } = parseUtcOffset(offset, `Invalid ${name} "${value}": not a UTC offset`);
101
111
  return (sign === '-' ? -1 : 1) * (Number(hours) * 60 + Number(minutes));
102
112
  }
@@ -0,0 +1,9 @@
1
+ import { ObjectLiteral, WhereExpressionBuilder } from 'typeorm';
2
+ import type { KeyProxyCallback } from '../types/modules/proxy-callback.types';
3
+ import type { JsonPathSqlOptions } from './json-path.utils';
4
+ /**
5
+ * Calls the untyped where `method` with the single column a typed callback picks, followed by `args`, as the typed
6
+ * where methods that only add the column to their untyped counterpart do. A callback that isn't a function, from
7
+ * untyped code, leaves the builder as it is.
8
+ */
9
+ export declare function attachTypedColumnWhere<Type extends ObjectLiteral, QB extends WhereExpressionBuilder, Args extends unknown[] = unknown[]>(builder: QB, method: string, where: KeyProxyCallback<Type>, jsonOptions: JsonPathSqlOptions | undefined, ...args: Args): QB;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.attachTypedColumnWhere = void 0;
4
+ const query_builder_utils_1 = require("./query-builder.utils");
5
+ /**
6
+ * Calls the untyped where `method` with the single column a typed callback picks, followed by `args`, as the typed
7
+ * where methods that only add the column to their untyped counterpart do. A callback that isn't a function, from
8
+ * untyped code, leaves the builder as it is.
9
+ */
10
+ function attachTypedColumnWhere(builder, method, where, jsonOptions, ...args) {
11
+ if (typeof where !== 'function') {
12
+ return builder;
13
+ }
14
+ const key = (0, query_builder_utils_1.getTypedSingleColumnKey)(builder, where, `${method}Typed`, jsonOptions);
15
+ return builder[method](key, ...args);
16
+ }
17
+ exports.attachTypedColumnWhere = attachTypedColumnWhere;