typeorm-extensions 2.0.0 → 2.2.0-rc.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.
- package/LICENSE +21 -0
- package/README.md +9 -8
- package/dist/decorators/calculated-column.decorator.d.ts +30 -0
- package/dist/decorators/calculated-column.decorator.js +82 -0
- package/dist/decorators/index.d.ts +1 -0
- package/dist/decorators/index.js +17 -0
- package/dist/extensions/calculated-column.extension.d.ts +33 -0
- package/dist/extensions/calculated-column.extension.js +33 -0
- package/dist/extensions/cursor-pagination.extension.d.ts +2 -1
- package/dist/extensions/cursor-pagination.extension.js +4 -3
- package/dist/extensions/get-many-with-totals.extension.d.ts +9 -1
- package/dist/extensions/get-many-with-totals.extension.js +158 -10
- package/dist/extensions/index.d.ts +7 -0
- package/dist/extensions/index.js +7 -0
- package/dist/extensions/join-lateral.extension.d.ts +19 -0
- package/dist/extensions/join-lateral.extension.js +50 -0
- package/dist/extensions/join-unnest.extension.d.ts +19 -0
- package/dist/extensions/join-unnest.extension.js +40 -0
- package/dist/extensions/order.extension.js +18 -7
- package/dist/extensions/query-returning.extension.d.ts +29 -0
- package/dist/extensions/query-returning.extension.js +50 -0
- package/dist/extensions/typed/index.d.ts +3 -0
- package/dist/extensions/typed/index.js +3 -2
- package/dist/extensions/typed/typed-distinct/typed-distinct.extension.d.ts +3 -2
- package/dist/extensions/typed/typed-group-by/typed-group-by.extension.d.ts +24 -0
- package/dist/extensions/typed/typed-group-by/typed-group-by.extension.js +31 -0
- package/dist/extensions/typed/typed-having/typed-having.extension.d.ts +36 -0
- package/dist/extensions/typed/typed-having/typed-having.extension.js +42 -0
- package/dist/extensions/typed/typed-joins/typed-joins.extension.d.ts +5 -4
- package/dist/extensions/typed/typed-order-by/typed-order-by-array-position.extension.d.ts +3 -2
- package/dist/extensions/typed/typed-order-by/typed-order-by-array-position.extension.js +10 -2
- package/dist/extensions/typed/typed-order-by/typed-order-by.extension.d.ts +3 -2
- package/dist/extensions/typed/typed-order-by/typed-order-by.extension.js +3 -2
- package/dist/extensions/typed/typed-root.extension.d.ts +25 -0
- package/dist/extensions/typed/typed-root.extension.js +13 -0
- package/dist/extensions/typed/typed-select/typed-select.extension.d.ts +5 -4
- package/dist/extensions/typed/typed-select/typed-select.extension.js +3 -2
- package/dist/extensions/typed/typed-where/typed-period-filter.extension.d.ts +2 -1
- package/dist/extensions/typed/typed-where/typed-period-filter.extension.js +1 -1
- package/dist/extensions/typed/typed-where/typed-where-array.extension.d.ts +10 -9
- package/dist/extensions/typed/typed-where/typed-where-array.extension.js +32 -0
- package/dist/extensions/typed/typed-where/typed-where-ilike.extension.d.ts +7 -6
- package/dist/extensions/typed/typed-where/typed-where-is-any.extension.d.ts +39 -129
- package/dist/extensions/typed/typed-where/typed-where-is-any.extension.js +2 -1
- package/dist/extensions/typed/typed-where/typed-where-is-distinct-from.extension.d.ts +7 -6
- package/dist/extensions/typed/typed-where/typed-where-is-distinct-from.extension.js +1 -1
- package/dist/extensions/typed/typed-where/typed-where-is-in.extension.d.ts +36 -117
- package/dist/extensions/typed/typed-where/typed-where-is-in.extension.js +11 -19
- package/dist/extensions/typed/typed-where/typed-where-json.extension.d.ts +7 -6
- package/dist/extensions/typed/typed-where/typed-where-json.extension.js +1 -1
- package/dist/extensions/typed/typed-where/typed-where-lower.extension.d.ts +7 -6
- package/dist/extensions/typed/typed-where/typed-where-similar.extension.d.ts +5 -4
- package/dist/extensions/typed/typed-where/typed-where.extension.d.ts +30 -73
- package/dist/extensions/typed/typed-where/typed-where.extension.js +10 -10
- package/dist/extensions/union.extension.d.ts +22 -0
- package/dist/extensions/union.extension.js +52 -0
- package/dist/extensions/where-exists.extension.d.ts +50 -0
- package/dist/extensions/where-exists.extension.js +64 -0
- package/dist/extensions/where-relation-exists.extension.d.ts +38 -0
- package/dist/extensions/where-relation-exists.extension.js +95 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/types/extensions/cursor-pagination.types.d.ts +5 -0
- package/dist/types/extensions/get-many-with-totals.types.d.ts +19 -0
- package/dist/types/extensions/index.d.ts +5 -0
- package/dist/types/extensions/index.js +5 -0
- package/dist/types/extensions/join-lateral.types.d.ts +6 -0
- package/dist/types/extensions/join-lateral.types.js +2 -0
- package/dist/types/extensions/period.types.d.ts +8 -0
- package/dist/types/extensions/query-returning.types.d.ts +11 -0
- package/dist/types/extensions/query-returning.types.js +2 -0
- package/dist/types/extensions/typed-group-by.types.d.ts +8 -0
- package/dist/types/extensions/typed-group-by.types.js +2 -0
- package/dist/types/extensions/typed-having.types.d.ts +2 -0
- package/dist/types/extensions/typed-having.types.js +2 -0
- package/dist/types/extensions/union.types.d.ts +12 -0
- package/dist/types/extensions/union.types.js +2 -0
- package/dist/types/extensions/where.types.d.ts +1 -0
- package/dist/types/interfaces/config.interface.d.ts +17 -0
- package/dist/types/interfaces/config.interface.js +2 -0
- package/dist/types/interfaces/index.d.ts +1 -0
- package/dist/types/interfaces/index.js +1 -0
- package/dist/types/modules/proxy-callback.types.d.ts +28 -4
- package/dist/types/modules/typed-params.types.d.ts +45 -0
- package/dist/types/modules/typed-params.types.js +2 -0
- package/dist/types/modules/typed-root.types.d.ts +13 -0
- package/dist/types/modules/typed-root.types.js +2 -0
- package/dist/utils/calculated-column.utils.d.ts +21 -0
- package/dist/utils/calculated-column.utils.js +63 -0
- package/dist/utils/json-path.utils.d.ts +22 -0
- package/dist/utils/json-path.utils.js +57 -0
- package/dist/utils/parameter.utils.d.ts +7 -0
- package/dist/utils/parameter.utils.js +21 -0
- package/dist/utils/period.utils.js +7 -2
- package/dist/utils/prototype.utils.d.ts +5 -1
- package/dist/utils/prototype.utils.js +4 -1
- package/dist/utils/proxy-key.utils.d.ts +9 -4
- package/dist/utils/proxy-key.utils.js +21 -14
- package/dist/utils/query-builder.utils.d.ts +13 -3
- package/dist/utils/query-builder.utils.js +95 -18
- package/dist/utils/sql.utils.d.ts +2 -0
- package/dist/utils/sql.utils.js +5 -1
- package/dist/utils/table.utils.d.ts +112 -2
- package/dist/utils/table.utils.js +206 -22
- package/dist/utils/time-zone.utils.d.ts +8 -0
- package/dist/utils/time-zone.utils.js +102 -0
- package/dist/utils/where.utils.d.ts +2 -2
- package/dist/utils/where.utils.js +23 -2
- package/package.json +4 -3
|
@@ -2,18 +2,106 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.tableHelper = void 0;
|
|
4
4
|
const sql_utils_1 = require("./sql.utils");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const json_path_utils_1 = require("./json-path.utils");
|
|
6
|
+
/**
|
|
7
|
+
* The argument of an aggregate and the clause after it for a `filter` written with `syntax`:
|
|
8
|
+
* `CASE WHEN filter THEN argument END`, or `argument` and ` FILTER (WHERE filter)`.
|
|
9
|
+
*/
|
|
10
|
+
function filtered(method, argument, filter, syntax = 'case', driver) {
|
|
11
|
+
if (syntax !== 'case' && syntax !== 'filter') {
|
|
12
|
+
throw new TypeError(`tableHelper.${method}(): syntax is 'case' or 'filter', got "${String(syntax)}"`);
|
|
13
|
+
}
|
|
14
|
+
if (filter && syntax === 'filter' && (0, sql_utils_1.isMysqlLikeDriver)(driver)) {
|
|
15
|
+
throw new TypeError(`tableHelper.${method}(): MySQL / MariaDB have no FILTER (WHERE …), use syntax 'case'`);
|
|
16
|
+
}
|
|
17
|
+
if (!filter) {
|
|
18
|
+
return { argument, suffix: '' };
|
|
19
|
+
}
|
|
20
|
+
return syntax === 'case'
|
|
21
|
+
? { argument: `CASE WHEN ${filter} THEN ${argument} END`, suffix: '' }
|
|
22
|
+
: { argument, suffix: ` FILTER (WHERE ${filter})` };
|
|
23
|
+
}
|
|
24
|
+
/** `orderBy` of an aggregate: its column, and the SQL after the argument, ` ORDER BY column [direction]`. */
|
|
25
|
+
function aggregateOrder(method, orderBy, toSql) {
|
|
26
|
+
const [column, direction] = (typeof orderBy === 'string' ? [orderBy] : orderBy);
|
|
27
|
+
if (direction !== undefined && direction !== 'ASC' && direction !== 'DESC') {
|
|
28
|
+
throw new TypeError(`tableHelper.${method}(): the order is 'ASC' or 'DESC', got "${String(direction)}"`);
|
|
29
|
+
}
|
|
30
|
+
return { column, sql: ` ORDER BY ${toSql(column)}${direction ? ` ${direction}` : ''}` };
|
|
8
31
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
32
|
+
const DATE_TRUNC_UNITS = [
|
|
33
|
+
'microseconds',
|
|
34
|
+
'milliseconds',
|
|
35
|
+
'second',
|
|
36
|
+
'minute',
|
|
37
|
+
'hour',
|
|
38
|
+
'day',
|
|
39
|
+
'week',
|
|
40
|
+
'month',
|
|
41
|
+
'quarter',
|
|
42
|
+
'year',
|
|
43
|
+
'decade',
|
|
44
|
+
'century',
|
|
45
|
+
'millennium',
|
|
46
|
+
];
|
|
47
|
+
function assertPostgres(method, driver) {
|
|
48
|
+
if (driver !== undefined && !(0, sql_utils_1.isPostgresLikeDriver)(driver)) {
|
|
49
|
+
throw new TypeError(`tableHelper.${method}(): PostgreSQL only, the driver is "${driver}"`);
|
|
14
50
|
}
|
|
15
|
-
return result;
|
|
16
51
|
}
|
|
52
|
+
function toExpression(value, toColumn) {
|
|
53
|
+
return typeof value === 'string' ? toColumn(value) : value.sql;
|
|
54
|
+
}
|
|
55
|
+
function toTimeZone(method, timeZone, toColumn) {
|
|
56
|
+
if (typeof timeZone === 'string') {
|
|
57
|
+
if (!timeZone.trim()) {
|
|
58
|
+
throw new TypeError(`tableHelper.${method}(): the time zone is empty`);
|
|
59
|
+
}
|
|
60
|
+
return sqlString(toPostgresZone(timeZone));
|
|
61
|
+
}
|
|
62
|
+
if ('sql' in timeZone) {
|
|
63
|
+
return timeZone.sql;
|
|
64
|
+
}
|
|
65
|
+
const column = toColumn(timeZone.column);
|
|
66
|
+
return timeZone.fallback === undefined
|
|
67
|
+
? column
|
|
68
|
+
: `COALESCE(${column}, ${sqlString(toPostgresZone(timeZone.fallback))})`;
|
|
69
|
+
}
|
|
70
|
+
const ISO_OFFSET = /^([+-])(\d{2}):?(\d{2})?$/;
|
|
71
|
+
/**
|
|
72
|
+
* PostgreSQL reads `'+02:00'` as a POSIX zone, whose offsets count west: UTC−2. An ISO 8601 offset is
|
|
73
|
+
* written as the POSIX zone of the same offset, `'<+0200>-02:00'`, so `'+02:00'` is UTC+2 as everywhere else.
|
|
74
|
+
*/
|
|
75
|
+
function toPostgresZone(timeZone) {
|
|
76
|
+
const offset = ISO_OFFSET.exec(timeZone.trim());
|
|
77
|
+
if (offset === null) {
|
|
78
|
+
return timeZone;
|
|
79
|
+
}
|
|
80
|
+
const [, sign, hours, minutes = '00'] = offset;
|
|
81
|
+
return `<${sign}${hours}${minutes}>${sign === '+' ? '-' : '+'}${hours}:${minutes}`;
|
|
82
|
+
}
|
|
83
|
+
/** The JSON functions of a database: PostgreSQL's unless the driver is MySQL / MariaDB or SQLite. */
|
|
84
|
+
function jsonFunctions(driver, jsonb) {
|
|
85
|
+
const mysql = (0, sql_utils_1.isMysqlLikeDriver)(driver);
|
|
86
|
+
if (jsonb && (mysql || (0, sql_utils_1.isSqliteLikeDriver)(driver))) {
|
|
87
|
+
throw new TypeError(`tableHelper: jsonb is PostgreSQL's, the driver is "${driver}"`);
|
|
88
|
+
}
|
|
89
|
+
if (mysql) {
|
|
90
|
+
return { mysql, object: 'JSON_OBJECT', agg: 'JSON_ARRAYAGG', emptyArray: 'JSON_ARRAY()' };
|
|
91
|
+
}
|
|
92
|
+
if ((0, sql_utils_1.isSqliteLikeDriver)(driver)) {
|
|
93
|
+
return { mysql, object: 'json_object', agg: 'json_group_array', emptyArray: 'json_array()' };
|
|
94
|
+
}
|
|
95
|
+
// PostgreSQL's when no driver is given; SQL Server, Oracle and the rest have functions of their own
|
|
96
|
+
if (driver !== undefined && !(0, sql_utils_1.isPostgresLikeDriver)(driver)) {
|
|
97
|
+
throw new TypeError(`tableHelper: the JSON functions are PostgreSQL's, MySQL / MariaDB's and SQLite's, the driver is "${driver}"`);
|
|
98
|
+
}
|
|
99
|
+
const type = jsonb ? 'jsonb' : 'json';
|
|
100
|
+
// CAST, not `::json`: TypeORM would read `:json` as a parameter
|
|
101
|
+
return { mysql, object: `${type}_build_object`, agg: `${type}_agg`, emptyArray: `CAST('[]' AS ${type})` };
|
|
102
|
+
}
|
|
103
|
+
// MySQL reads a backslash in a string as an escape (unless NO_BACKSLASH_ESCAPES): `\'` would end the literal
|
|
104
|
+
const sqlString = (value, driver) => `'${((0, sql_utils_1.isMysqlLikeDriver)(driver) ? value.replace(/\\/g, '\\\\') : value).replace(/'/g, "''")}'`;
|
|
17
105
|
/**
|
|
18
106
|
* Creates a type-safe SQL helper for building column references, WHERE expressions,
|
|
19
107
|
* JOIN clauses, and JSONB accessors for a given entity type.
|
|
@@ -31,13 +119,13 @@ const tableHelper = (alias, options = {}) => ({
|
|
|
31
119
|
* identifier, so names with other characters work too: `"user"."first-name"`.
|
|
32
120
|
*/
|
|
33
121
|
column(columnName, alias) {
|
|
34
|
-
const { useQuotes = true } = options;
|
|
122
|
+
const { useQuotes = true, driver } = options;
|
|
35
123
|
const tableAlias = alias !== null && alias !== void 0 ? alias : this.alias;
|
|
36
124
|
const column = String(columnName);
|
|
37
125
|
if (!useQuotes) {
|
|
38
126
|
return [tableAlias, column].filter(Boolean).join('.');
|
|
39
127
|
}
|
|
40
|
-
return [tableAlias ? (0, sql_utils_1.quoteIdentifierPath)(tableAlias) : undefined, (0, sql_utils_1.quoteIdentifier)(column)]
|
|
128
|
+
return [tableAlias ? (0, sql_utils_1.quoteIdentifierPath)(tableAlias, driver) : undefined, (0, sql_utils_1.quoteIdentifier)(column, driver)]
|
|
41
129
|
.filter(Boolean)
|
|
42
130
|
.join('.');
|
|
43
131
|
},
|
|
@@ -72,7 +160,8 @@ const tableHelper = (alias, options = {}) => ({
|
|
|
72
160
|
return this.where(columnName, 'IS NOT NULL');
|
|
73
161
|
},
|
|
74
162
|
/**
|
|
75
|
-
* Returns `CAST(COUNT("alias"."column") AS integer)`: the rows where the column isn't `NULL`.
|
|
163
|
+
* Returns `CAST(COUNT("alias"."column") AS integer)`: the rows where the column isn't `NULL`. With the `driver` option
|
|
164
|
+
* MySQL casts `AS SIGNED`, a `BIGINT`, which its TypeORM driver returns as a string by default.
|
|
76
165
|
* Without a column it is `COUNT(*)`, all the rows of the result, in every database. To count the rows
|
|
77
166
|
* of a table joined with LEFT JOIN, count one of its non-null columns: `images.count('id')`.
|
|
78
167
|
* @example
|
|
@@ -87,21 +176,85 @@ const tableHelper = (alias, options = {}) => ({
|
|
|
87
176
|
throw new TypeError('tableHelper.count(): the second argument is an options object, e.g. { distinct: true } or { filter: "…" }');
|
|
88
177
|
}
|
|
89
178
|
const { distinct = false, filter, syntax = 'case' } = countOptions;
|
|
90
|
-
if (syntax !== 'case' && syntax !== 'filter') {
|
|
91
|
-
throw new TypeError(`tableHelper.count(): syntax is 'case' or 'filter', got "${String(syntax)}"`);
|
|
92
|
-
}
|
|
93
179
|
const column = columnName ? this.column(columnName) : undefined;
|
|
94
180
|
if (distinct && !column) {
|
|
95
181
|
throw new TypeError("tableHelper.count({ distinct: true }) needs a column, e.g. count('email', { distinct: true })");
|
|
96
182
|
}
|
|
97
183
|
// `COUNT(*)`, not `COUNT("alias".*)`, which only PostgreSQL accepts;
|
|
98
184
|
// a CASE can't return `*`: without a column, it counts 1 per matching row
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
185
|
+
const { argument, suffix } = filtered('count', column !== null && column !== void 0 ? column : (filter && syntax === 'case' ? '1' : '*'), filter, syntax, options.driver);
|
|
186
|
+
// `CAST(… AS integer)` is a syntax error on MySQL
|
|
187
|
+
return this.cast(`COUNT(${distinct ? 'DISTINCT ' : ''}${argument})${suffix}`, (0, sql_utils_1.isMysqlLikeDriver)(options.driver) ? 'SIGNED' : 'integer');
|
|
188
|
+
},
|
|
189
|
+
/**
|
|
190
|
+
* Returns `COALESCE(SUM("alias"."column"), 0)`: 0, not `NULL`, for a group without rows or a LEFT JOIN
|
|
191
|
+
* without a match. Only a column whose property is a number. PostgreSQL sums integers into a `bigint`
|
|
192
|
+
* and MySQL into a `DECIMAL`, which the drivers return as strings.
|
|
193
|
+
* @example
|
|
194
|
+
* t.sum('amountCents', { filter: t.where('status', "= 'paid'") })
|
|
195
|
+
* // COALESCE(SUM(CASE WHEN "order"."status" = 'paid' THEN "order"."amountCents" END), 0)
|
|
196
|
+
*/
|
|
197
|
+
sum(columnName, sumOptions = {}) {
|
|
198
|
+
const { argument, suffix } = filtered('sum', this.column(columnName), sumOptions.filter, sumOptions.syntax, options.driver);
|
|
199
|
+
return `COALESCE(SUM(${argument})${suffix}, 0)`;
|
|
200
|
+
},
|
|
201
|
+
/**
|
|
202
|
+
* Returns the PostgreSQL array of a column over the rows of a group, `{}` for a group without rows.
|
|
203
|
+
* Other databases have no arrays and throw a `TypeError`: use `jsonAgg` there.
|
|
204
|
+
* @example
|
|
205
|
+
* images.arrayAgg('id', { distinct: true, orderBy: ['id', 'DESC'], filter: images.isNotNull('id') })
|
|
206
|
+
* // COALESCE(array_agg(DISTINCT "image"."id" ORDER BY "image"."id" DESC) FILTER (WHERE "image"."id" IS NOT NULL), '{}')
|
|
207
|
+
*/
|
|
208
|
+
arrayAgg(columnName, aggOptions = {}) {
|
|
209
|
+
var _a;
|
|
210
|
+
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
|
+
}
|
|
214
|
+
const order = orderBy ? aggregateOrder('arrayAgg', orderBy, (column) => this.column(column)) : undefined;
|
|
215
|
+
// PostgreSQL: "in an aggregate with DISTINCT, ORDER BY expressions must appear in argument list"
|
|
216
|
+
if (distinct && order && order.column !== columnName) {
|
|
217
|
+
throw new TypeError(`tableHelper.arrayAgg(): with distinct, the array can only be ordered by "${columnName}", got "${order.column}"`);
|
|
103
218
|
}
|
|
104
|
-
|
|
219
|
+
const aggregate = `array_agg(${distinct ? 'DISTINCT ' : ''}${this.column(columnName)}${(_a = order === null || order === void 0 ? void 0 : order.sql) !== null && _a !== void 0 ? _a : ''})`;
|
|
220
|
+
// the untyped '{}' takes the type of the array
|
|
221
|
+
return `COALESCE(${aggregate}${filter ? ` FILTER (WHERE ${filter})` : ''}, '{}')`;
|
|
222
|
+
},
|
|
223
|
+
/**
|
|
224
|
+
* Returns a JSON object of columns, keyed by the column names or by the keys of a `{ key: column }` map.
|
|
225
|
+
* Every column is checked against the entity type.
|
|
226
|
+
* @example
|
|
227
|
+
* t.jsonBuildObject(['id', 'url']) // json_build_object('id', "image"."id", 'url', "image"."url")
|
|
228
|
+
* t.jsonBuildObject({ imageId: 'id', src: 'url' }) // json_build_object('imageId', "image"."id", 'src', "image"."url")
|
|
229
|
+
* // MySQL: JSON_OBJECT(…), SQLite: json_object(…); { jsonb: true }: jsonb_build_object(…)
|
|
230
|
+
*/
|
|
231
|
+
jsonBuildObject(columns, jsonOptions = {}) {
|
|
232
|
+
const entries = Array.isArray(columns)
|
|
233
|
+
? columns.map((column) => [column, column])
|
|
234
|
+
: Object.entries(columns);
|
|
235
|
+
if (!entries.length) {
|
|
236
|
+
throw new TypeError('tableHelper.jsonBuildObject() needs at least one column');
|
|
237
|
+
}
|
|
238
|
+
const pairs = entries.map(([key, column]) => `${sqlString(key, options.driver)}, ${this.column(column)}`);
|
|
239
|
+
return `${jsonFunctions(options.driver, jsonOptions.jsonb).object}(${pairs.join(', ')})`;
|
|
240
|
+
},
|
|
241
|
+
/**
|
|
242
|
+
* Returns the JSON array of an expression over the rows of a group, `[]` for a group without rows
|
|
243
|
+
* (the aggregate alone gives `NULL` there). Use it with GROUP BY, or in a subquery.
|
|
244
|
+
* @example
|
|
245
|
+
* images.jsonAgg(images.jsonBuildObject(['id', 'url']), { orderBy: ['id', 'DESC'], filter: images.isNotNull('id') })
|
|
246
|
+
* // COALESCE(json_agg(json_build_object(…) ORDER BY "image"."id" DESC) FILTER (WHERE "image"."id" IS NOT NULL), CAST('[]' AS json))
|
|
247
|
+
* // MySQL: COALESCE(JSON_ARRAYAGG(…), JSON_ARRAY()), without ORDER BY and FILTER; SQLite: json_group_array(…)
|
|
248
|
+
*/
|
|
249
|
+
jsonAgg(expression, aggOptions = {}) {
|
|
250
|
+
const { orderBy, filter, jsonb } = aggOptions;
|
|
251
|
+
const json = jsonFunctions(options.driver, jsonb);
|
|
252
|
+
if (json.mysql && (orderBy || filter)) {
|
|
253
|
+
throw new TypeError("tableHelper.jsonAgg(): MySQL's JSON_ARRAYAGG takes no ORDER BY and no FILTER");
|
|
254
|
+
}
|
|
255
|
+
const order = orderBy ? aggregateOrder('jsonAgg', orderBy, (column) => this.column(column)).sql : '';
|
|
256
|
+
const aggregate = `${json.agg}(${expression}${order})${filter ? ` FILTER (WHERE ${filter})` : ''}`;
|
|
257
|
+
return `COALESCE(${aggregate}, ${json.emptyArray})`;
|
|
105
258
|
},
|
|
106
259
|
/** Returns `CAST(expression AS type)`. */
|
|
107
260
|
cast(expression, type) {
|
|
@@ -115,7 +268,7 @@ const tableHelper = (alias, options = {}) => ({
|
|
|
115
268
|
*/
|
|
116
269
|
jsonColumn(columnName, jsonOperator, ...fieldNames) {
|
|
117
270
|
const column = this.column(columnName);
|
|
118
|
-
const path = buildJsonPath(fieldNames, jsonOperator);
|
|
271
|
+
const path = (0, json_path_utils_1.buildJsonPath)(fieldNames, jsonOperator);
|
|
119
272
|
return `${column} ${path}`;
|
|
120
273
|
},
|
|
121
274
|
/**
|
|
@@ -129,7 +282,38 @@ const tableHelper = (alias, options = {}) => ({
|
|
|
129
282
|
* qb.whereTyped(u => u.metadata, `${t.jsonPath('metadata', '->>', 'role')} =`, 'admin')
|
|
130
283
|
*/
|
|
131
284
|
jsonPath(_columnName, jsonOperator, ...fieldNames) {
|
|
132
|
-
return buildJsonPath(fieldNames, jsonOperator);
|
|
285
|
+
return (0, json_path_utils_1.buildJsonPath)(fieldNames, jsonOperator);
|
|
286
|
+
},
|
|
287
|
+
/**
|
|
288
|
+
* Returns `(expression AT TIME ZONE zone)` (PostgreSQL). A `timestamptz` gives the wall-clock `timestamp`
|
|
289
|
+
* in that zone; a `timestamp` is read as wall-clock time there and gives a `timestamptz`.
|
|
290
|
+
* A zone name is written as an escaped literal, so it is safe to take from a request.
|
|
291
|
+
* @example
|
|
292
|
+
* t.atTimeZone('paidAt', 'Europe/Kyiv') // ("order"."paidAt" AT TIME ZONE 'Europe/Kyiv')
|
|
293
|
+
* t.atTimeZone('startsAt', { column: 'timezone', fallback: 'UTC' }) // ("order"."startsAt" AT TIME ZONE COALESCE("order"."timezone", 'UTC'))
|
|
294
|
+
*/
|
|
295
|
+
atTimeZone(value, timeZone) {
|
|
296
|
+
assertPostgres('atTimeZone', options.driver);
|
|
297
|
+
const toColumn = (column) => this.column(column);
|
|
298
|
+
return `(${toExpression(value, toColumn)} AT TIME ZONE ${toTimeZone('atTimeZone', timeZone, toColumn)})`;
|
|
299
|
+
},
|
|
300
|
+
/**
|
|
301
|
+
* Returns `date_trunc('unit', expression)` (PostgreSQL). With a time zone it is the three-argument form
|
|
302
|
+
* (PostgreSQL 12+): a `timestamptz` is truncated at the boundaries of that zone, e.g. its local midnight
|
|
303
|
+
* for `'day'`, and the result is a `timestamptz` again.
|
|
304
|
+
* @example
|
|
305
|
+
* t.dateTrunc('day', 'paidAt') // date_trunc('day', "order"."paidAt")
|
|
306
|
+
* t.dateTrunc('day', 'paidAt', 'Europe/Kyiv') // date_trunc('day', "order"."paidAt", 'Europe/Kyiv')
|
|
307
|
+
*/
|
|
308
|
+
dateTrunc(unit, value, timeZone) {
|
|
309
|
+
assertPostgres('dateTrunc', options.driver);
|
|
310
|
+
// the unit goes into the SQL as is
|
|
311
|
+
if (!DATE_TRUNC_UNITS.includes(unit)) {
|
|
312
|
+
throw new TypeError(`tableHelper.dateTrunc(): the unit is one of ${DATE_TRUNC_UNITS.join(', ')}, got "${String(unit)}"`);
|
|
313
|
+
}
|
|
314
|
+
const toColumn = (column) => this.column(column);
|
|
315
|
+
const zone = timeZone === undefined ? '' : `, ${toTimeZone('dateTrunc', timeZone, toColumn)}`;
|
|
316
|
+
return `date_trunc('${unit}', ${toExpression(value, toColumn)}${zone})`;
|
|
133
317
|
}
|
|
134
318
|
});
|
|
135
319
|
exports.tableHelper = tableHelper;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Throws a `TypeError` for a time zone the runtime doesn't know. */
|
|
2
|
+
export declare function assertTimeZone(timeZone: string): void;
|
|
3
|
+
/**
|
|
4
|
+
* Reads a date (`'2024-01-31'`, its midnight) or a date-time without an offset (`'2024-01-31 10:00'`) as
|
|
5
|
+
* the wall clock of `timeZone`, and returns that instant. A date-time with an offset (`Z`, `+02:00`) is an
|
|
6
|
+
* instant already: it is returned as that `Date`. Any other string throws a `TypeError` naming `name`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readInTimeZone(value: string, timeZone: string, name: string): Date;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readInTimeZone = exports.assertTimeZone = void 0;
|
|
4
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
5
|
+
const MAX_CACHED_FORMATTERS = 500;
|
|
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
|
+
const OFFSET = /^([+-])(\d{2}):?(\d{2})?$/;
|
|
8
|
+
const formatters = new Map();
|
|
9
|
+
function getFormatter(timeZone) {
|
|
10
|
+
let formatter = formatters.get(timeZone);
|
|
11
|
+
if (formatter === undefined) {
|
|
12
|
+
try {
|
|
13
|
+
formatter = new Intl.DateTimeFormat('en-US', {
|
|
14
|
+
timeZone,
|
|
15
|
+
hour12: false,
|
|
16
|
+
era: 'short',
|
|
17
|
+
year: 'numeric',
|
|
18
|
+
month: 'numeric',
|
|
19
|
+
day: 'numeric',
|
|
20
|
+
hour: 'numeric',
|
|
21
|
+
minute: 'numeric',
|
|
22
|
+
second: 'numeric',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new TypeError(`Invalid timeZone "${timeZone}": not an IANA time zone`);
|
|
27
|
+
}
|
|
28
|
+
// the keys are caller input: Intl accepts any letter case of a zone name
|
|
29
|
+
if (formatters.size >= MAX_CACHED_FORMATTERS) {
|
|
30
|
+
formatters.clear();
|
|
31
|
+
}
|
|
32
|
+
formatters.set(timeZone, formatter);
|
|
33
|
+
}
|
|
34
|
+
return formatter;
|
|
35
|
+
}
|
|
36
|
+
/** The wall clock of an instant in a time zone, as milliseconds of the same wall clock in UTC. */
|
|
37
|
+
function getWallClock(instant, timeZone) {
|
|
38
|
+
const parts = {};
|
|
39
|
+
for (const { type, value } of getFormatter(timeZone).formatToParts(new Date(instant))) {
|
|
40
|
+
parts[type] = value;
|
|
41
|
+
}
|
|
42
|
+
const year = parts.era === 'BC' ? 1 - Number(parts.year) : Number(parts.year);
|
|
43
|
+
const wallClock = new Date(0);
|
|
44
|
+
// some ICU versions write midnight as 24 with hour12: false
|
|
45
|
+
wallClock.setUTCFullYear(year, Number(parts.month) - 1, Number(parts.day));
|
|
46
|
+
wallClock.setUTCHours(Number(parts.hour) % 24, Number(parts.minute), Number(parts.second), 0);
|
|
47
|
+
return wallClock.getTime() + (((instant % 1000) + 1000) % 1000);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The instant at which the wall clock of a time zone shows `wallClock` (milliseconds of that wall clock in
|
|
51
|
+
* UTC). Like PostgreSQL: a time skipped by a forward transition takes the offset before it, and a time that
|
|
52
|
+
* occurs twice is read with the offset after the transition, the later of the two instants.
|
|
53
|
+
*/
|
|
54
|
+
function wallClockToInstant(wallClock, timeZone) {
|
|
55
|
+
const offsets = new Set([wallClock - DAY_MS, wallClock, wallClock + DAY_MS].map((instant) => getWallClock(instant, timeZone) - instant));
|
|
56
|
+
const candidates = [...offsets].map((offset) => wallClock - offset);
|
|
57
|
+
const matching = candidates.filter((instant) => getWallClock(instant, timeZone) === wallClock);
|
|
58
|
+
return Math.max(...(matching.length > 0 ? matching : candidates));
|
|
59
|
+
}
|
|
60
|
+
/** Throws a `TypeError` for a time zone the runtime doesn't know. */
|
|
61
|
+
function assertTimeZone(timeZone) {
|
|
62
|
+
getFormatter(timeZone);
|
|
63
|
+
}
|
|
64
|
+
exports.assertTimeZone = assertTimeZone;
|
|
65
|
+
/**
|
|
66
|
+
* Reads a date (`'2024-01-31'`, its midnight) or a date-time without an offset (`'2024-01-31 10:00'`) as
|
|
67
|
+
* the wall clock of `timeZone`, and returns that instant. A date-time with an offset (`Z`, `+02:00`) is an
|
|
68
|
+
* instant already: it is returned as that `Date`. Any other string throws a `TypeError` naming `name`.
|
|
69
|
+
*/
|
|
70
|
+
function readInTimeZone(value, timeZone, name) {
|
|
71
|
+
const match = DATE_TIME.exec(value);
|
|
72
|
+
if (match === null) {
|
|
73
|
+
throw new TypeError(`Invalid ${name} "${value}": with timeZone, expected a Date, a date (YYYY-MM-DD) or a date-time ` +
|
|
74
|
+
'(YYYY-MM-DD HH:mm[:ss[.sss]]) with or without an offset');
|
|
75
|
+
}
|
|
76
|
+
const [, year, month, day, hour = '0', minute = '0', second = '0', fraction = '', offset] = match;
|
|
77
|
+
const wallClock = new Date(0);
|
|
78
|
+
wallClock.setUTCFullYear(Number(year), Number(month) - 1, Number(day));
|
|
79
|
+
// a Date keeps milliseconds: finer fractions are dropped
|
|
80
|
+
wallClock.setUTCHours(Number(hour), Number(minute), Number(second), Number(fraction.padEnd(3, '0').slice(0, 3)));
|
|
81
|
+
const roundTrip = wallClock.toISOString();
|
|
82
|
+
const expected = `${year}-${month}-${day}T${hour.padStart(2, '0')}:${minute.padStart(2, '0')}:${second.padStart(2, '0')}`;
|
|
83
|
+
if (isNaN(wallClock.getTime()) || roundTrip.slice(0, 19) !== expected) {
|
|
84
|
+
throw new TypeError(`Invalid ${name} "${value}": not a calendar date and time`);
|
|
85
|
+
}
|
|
86
|
+
if (offset === undefined) {
|
|
87
|
+
return new Date(wallClockToInstant(wallClock.getTime(), timeZone));
|
|
88
|
+
}
|
|
89
|
+
// an offset makes it an instant already; as text the databases would read it each their own way
|
|
90
|
+
return new Date(wallClock.getTime() - getOffsetMinutes(offset, value, name) * 60000);
|
|
91
|
+
}
|
|
92
|
+
exports.readInTimeZone = readInTimeZone;
|
|
93
|
+
function getOffsetMinutes(offset, value, name) {
|
|
94
|
+
if (offset.toUpperCase() === 'Z') {
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
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
|
+
}
|
|
101
|
+
return (sign === '-' ? -1 : 1) * (Number(hours) * 60 + Number(minutes));
|
|
102
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ObjectLiteral, WhereExpressionBuilder } from 'typeorm';
|
|
2
|
-
import type { WhereMethods } from '../types/extensions/where.types';
|
|
2
|
+
import type { HavingMethods, WhereMethods } from '../types/extensions/where.types';
|
|
3
3
|
export declare function getValuePlaceholder<T>(value: T, paramName?: string): string;
|
|
4
4
|
/**
|
|
5
5
|
* Whether `params` is a `{ name: value }` params object (a plain object or a class instance such
|
|
@@ -12,7 +12,7 @@ export declare function isParamsObject(params: unknown): params is Record<string
|
|
|
12
12
|
* so `::type` casts and colons inside literals don't count unless they name a `params` key.
|
|
13
13
|
*/
|
|
14
14
|
export declare function hasParamPlaceholder(conditions: string, params?: unknown): boolean;
|
|
15
|
-
export declare function attachWhere<QB extends WhereExpressionBuilder>(builder: QB, method: WhereMethods, field: string, conditions: string, params?: unknown, options?: {
|
|
15
|
+
export declare function attachWhere<QB extends WhereExpressionBuilder>(builder: QB, method: WhereMethods | HavingMethods, field: string, conditions: string, params?: unknown, options?: {
|
|
16
16
|
addValue?: boolean;
|
|
17
17
|
}): QB;
|
|
18
18
|
/**
|
|
@@ -49,14 +49,35 @@ function hasParamPlaceholder(conditions, params) {
|
|
|
49
49
|
return false;
|
|
50
50
|
}
|
|
51
51
|
exports.hasParamPlaceholder = hasParamPlaceholder;
|
|
52
|
+
/**
|
|
53
|
+
* A params object whose value goes after the condition (`'='`, `{ id: 1 }`) holds that one value. With other
|
|
54
|
+
* keys only the first was used and the rest dropped; with none the column was compared with NULL.
|
|
55
|
+
*/
|
|
56
|
+
function assertSingleValueParams(field, params) {
|
|
57
|
+
if (!isParamsObject(params)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const keys = Object.keys(params);
|
|
61
|
+
if (keys.length !== 1) {
|
|
62
|
+
throw new TypeError(`Condition on "${field}": a params object without a placeholder in the condition holds the one value to compare with, ` +
|
|
63
|
+
`got ${keys.length} keys (${keys.join(', ') || 'none'}). Pass the value itself, or name the keys in the condition: '= :id'`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** The builder's `where` / `having` method, bound to it: only a SelectQueryBuilder has `having`. */
|
|
67
|
+
function addCondition(builder, method) {
|
|
68
|
+
const add = builder[method];
|
|
69
|
+
return (condition, parameters) => add.call(builder, condition, parameters);
|
|
70
|
+
}
|
|
52
71
|
function attachWhere(builder, method, field, conditions, params, options) {
|
|
53
72
|
let { addValue } = options || {};
|
|
54
73
|
if (method && typeof field === 'string' && typeof conditions === 'string') {
|
|
74
|
+
const fieldName = field;
|
|
55
75
|
field = (0, query_builder_utils_1.toFieldSql)(builder, field);
|
|
56
76
|
const hasParams = typeof params !== 'undefined';
|
|
57
77
|
// `addValue` defaults to `true` unless `conditions` already references one of the `params` keys
|
|
58
78
|
addValue = addValue !== null && addValue !== void 0 ? addValue : !hasParamPlaceholder(conditions, params);
|
|
59
79
|
if (addValue && hasParams) {
|
|
80
|
+
assertSingleValueParams(fieldName, params);
|
|
60
81
|
// pick first value from params object or pass as is (a Date or a Buffer is a value too)
|
|
61
82
|
const firstValue = isParamsObject(params) ? Object.values(params)[0] : params;
|
|
62
83
|
// 0, false, '' are valid values like any other
|
|
@@ -69,10 +90,10 @@ function attachWhere(builder, method, field, conditions, params, options) {
|
|
|
69
90
|
: undefined;
|
|
70
91
|
const queryParams = paramName && valueIsValid ? { [paramName]: firstValue } : undefined;
|
|
71
92
|
const valuePlaceholder = getValuePlaceholder(firstValue, paramName);
|
|
72
|
-
return builder
|
|
93
|
+
return addCondition(builder, method)(`${field} ${conditions} ${valuePlaceholder}`, queryParams);
|
|
73
94
|
}
|
|
74
95
|
else {
|
|
75
|
-
return builder
|
|
96
|
+
return addCondition(builder, method)(`${field} ${conditions}`, params);
|
|
76
97
|
}
|
|
77
98
|
}
|
|
78
99
|
return builder;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typeorm-extensions",
|
|
3
|
-
"version": "2.0.0",
|
|
3
|
+
"version": "2.2.0-rc.0",
|
|
4
4
|
"description": "typeorm commonly used extensions",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc -p .",
|
|
16
|
-
"lint": "eslint . --ext .ts"
|
|
16
|
+
"lint": "eslint . --ext .ts",
|
|
17
|
+
"test:types": "tsc -p test/types"
|
|
17
18
|
},
|
|
18
19
|
"keywords": [
|
|
19
20
|
"typeorm",
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
"utils"
|
|
24
25
|
],
|
|
25
26
|
"author": "koenigstag",
|
|
26
|
-
"license": "
|
|
27
|
+
"license": "MIT",
|
|
27
28
|
"peerDependencies": {
|
|
28
29
|
"typeorm": "^0.3.12 || ^1.0.0"
|
|
29
30
|
},
|