typeorm-extensions 2.1.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 +6 -3
- 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-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 +7 -6
- 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-in.extension.d.ts +7 -6
- package/dist/extensions/typed/typed-where/typed-where-json.extension.d.ts +7 -6
- 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 +4 -3
- 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 +2 -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/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/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 +3 -3
- package/dist/utils/proxy-key.utils.js +7 -7
- package/dist/utils/query-builder.utils.d.ts +8 -0
- package/dist/utils/query-builder.utils.js +30 -2
- package/dist/utils/table.utils.d.ts +112 -2
- package/dist/utils/table.utils.js +206 -11
- 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 +2 -2
|
@@ -7,9 +7,9 @@ class KeyHolder {
|
|
|
7
7
|
this._keys = _keys;
|
|
8
8
|
this._keys = [..._keys];
|
|
9
9
|
}
|
|
10
|
-
toSql(length = 2, useQuotes) {
|
|
10
|
+
toSql(length = 2, useQuotes, driver) {
|
|
11
11
|
const string = this.toString(length);
|
|
12
|
-
const sql = useQuotes ? (0, sql_utils_1.stringToSQLIdentifier)(string) : string;
|
|
12
|
+
const sql = useQuotes ? (0, sql_utils_1.stringToSQLIdentifier)(string, driver) : string;
|
|
13
13
|
return sql;
|
|
14
14
|
}
|
|
15
15
|
toString(length = 2) {
|
|
@@ -109,26 +109,26 @@ exports.getKey = getKey;
|
|
|
109
109
|
* SQL key of a typed callback: `(u) => u.id` → `prefix.id`, `(u) => u.images.url` → `images.url`
|
|
110
110
|
* (an `alias.column` path keeps its own alias). Strings are returned as is.
|
|
111
111
|
*/
|
|
112
|
-
const getSqlKeyFromProxyCallback = (callback, addPrefix, useQuotes) => {
|
|
112
|
+
const getSqlKeyFromProxyCallback = (callback, addPrefix, useQuotes, driver) => {
|
|
113
113
|
if (typeof callback === 'string')
|
|
114
114
|
return callback;
|
|
115
115
|
const keyHolder = (0, exports.getKeyHolder)(callback);
|
|
116
116
|
if (Array.isArray(keyHolder)) {
|
|
117
|
-
return keyHolder.map((holder) => (0, exports.getSqlKeyFromProxyCallback)(holder, addPrefix, useQuotes));
|
|
117
|
+
return keyHolder.map((holder) => (0, exports.getSqlKeyFromProxyCallback)(holder, addPrefix, useQuotes, driver));
|
|
118
118
|
}
|
|
119
119
|
// if select is like this: (proxy) => proxy.relation.column
|
|
120
120
|
if (keyHolder.getKeys().length > 1) {
|
|
121
121
|
// return unchanged
|
|
122
|
-
return keyHolder.toSql(undefined, useQuotes);
|
|
122
|
+
return keyHolder.toSql(undefined, useQuotes, driver);
|
|
123
123
|
}
|
|
124
124
|
// else if select is one layer depth like this: (proxy) => proxy.column
|
|
125
125
|
// then add alias before the key; resolve the alias only now, a path above doesn't need it
|
|
126
126
|
const prefix = typeof addPrefix === 'function' ? addPrefix() : addPrefix;
|
|
127
127
|
if (typeof prefix === 'string' && prefix.length >= 1) {
|
|
128
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);
|
|
129
|
+
return new KeyHolder([prefix, ...keyHolder.getKeys()]).toSql(undefined, useQuotes, driver);
|
|
130
130
|
}
|
|
131
|
-
return keyHolder.toSql(undefined, useQuotes);
|
|
131
|
+
return keyHolder.toSql(undefined, useQuotes, driver);
|
|
132
132
|
};
|
|
133
133
|
exports.getSqlKeyFromProxyCallback = getSqlKeyFromProxyCallback;
|
|
134
134
|
// example of typed select
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ObjectLiteral, WhereExpressionBuilder } from 'typeorm';
|
|
2
|
+
import type { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
|
|
2
3
|
import type { KeyProxyCallback } from '../types/modules/proxy-callback.types';
|
|
3
4
|
import { JsonPathSqlOptions } from './json-path.utils';
|
|
4
5
|
/**
|
|
@@ -7,6 +8,11 @@ import { JsonPathSqlOptions } from './json-path.utils';
|
|
|
7
8
|
* entity or the property isn't a column.
|
|
8
9
|
*/
|
|
9
10
|
export declare function getColumnDatabaseName(builder: WhereExpressionBuilder, aliasName: string | undefined, propertyPath: string): string | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* The entity column a field names, the way `toFieldSql` resolves it: `alias.property` or
|
|
13
|
+
* `alias.column_name` on an alias with an entity, or a database column name of the main alias.
|
|
14
|
+
*/
|
|
15
|
+
export declare function findFieldColumn(builder: WhereExpressionBuilder, field: string): ColumnMetadata | undefined;
|
|
10
16
|
/**
|
|
11
17
|
* SQL for a field passed as a string to the untyped methods (`whereIsIn('user.first-name', …)`).
|
|
12
18
|
*
|
|
@@ -32,6 +38,8 @@ export declare function toFieldSql(builder: WhereExpressionBuilder, field: strin
|
|
|
32
38
|
* gives its JSON accessor, cast on PostgreSQL to the type of `jsonOptions.value`.
|
|
33
39
|
*/
|
|
34
40
|
export declare function getTypedColumnKey<Type extends ObjectLiteral>(builder: WhereExpressionBuilder, callback: KeyProxyCallback<Type>, jsonOptions?: JsonPathSqlOptions): string | string[];
|
|
41
|
+
/** Whether a typed callback selects a path inside a JSON column, `u => u.metadata.tags`, rather than a column. */
|
|
42
|
+
export declare function isTypedJsonPath<Type extends ObjectLiteral>(builder: WhereExpressionBuilder, callback: KeyProxyCallback<Type>): boolean;
|
|
35
43
|
/**
|
|
36
44
|
* Quoted column of a typed callback that must select exactly one column
|
|
37
45
|
* (`whereIsAnyTyped`, `whereArrayContainsTyped`, …). Throws a `TypeError` for several columns:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getTypedSelectionKey = exports.getTypedSingleColumnKey = exports.getTypedColumnKey = exports.toFieldSql = exports.getColumnDatabaseName = void 0;
|
|
3
|
+
exports.getTypedSelectionKey = exports.getTypedSingleColumnKey = exports.isTypedJsonPath = exports.getTypedColumnKey = exports.toFieldSql = exports.findFieldColumn = exports.getColumnDatabaseName = void 0;
|
|
4
4
|
const proxy_key_utils_1 = require("./proxy-key.utils");
|
|
5
5
|
const json_path_utils_1 = require("./json-path.utils");
|
|
6
6
|
const sql_utils_1 = require("./sql.utils");
|
|
@@ -54,6 +54,21 @@ function findColumn(metadata, name) {
|
|
|
54
54
|
var _a;
|
|
55
55
|
return ((_a = metadata.columns.find((column) => column.databaseName === name)) !== null && _a !== void 0 ? _a : metadata.findColumnWithPropertyPath(name));
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* The entity column a field names, the way `toFieldSql` resolves it: `alias.property` or
|
|
59
|
+
* `alias.column_name` on an alias with an entity, or a database column name of the main alias.
|
|
60
|
+
*/
|
|
61
|
+
function findFieldColumn(builder, field) {
|
|
62
|
+
var _a;
|
|
63
|
+
const queryBuilder = builder;
|
|
64
|
+
const dot = field.indexOf('.');
|
|
65
|
+
if (dot > 0) {
|
|
66
|
+
const metadata = getAliasMetadata(queryBuilder, field.slice(0, dot));
|
|
67
|
+
return metadata && findColumn(metadata, field.slice(dot + 1));
|
|
68
|
+
}
|
|
69
|
+
return (_a = getAliasMetadata(queryBuilder, undefined)) === null || _a === void 0 ? void 0 : _a.columns.find((column) => column.databaseName === field);
|
|
70
|
+
}
|
|
71
|
+
exports.findFieldColumn = findFieldColumn;
|
|
57
72
|
/**
|
|
58
73
|
* SQL for a field passed as a string to the untyped methods (`whereIsIn('user.first-name', …)`).
|
|
59
74
|
*
|
|
@@ -131,11 +146,17 @@ function findColumnPath(metadata, keys) {
|
|
|
131
146
|
* before `from()` the property name is used as the column name.
|
|
132
147
|
*/
|
|
133
148
|
function resolveTypedKey(queryBuilder, keys) {
|
|
149
|
+
var _a, _b;
|
|
134
150
|
const resolved = resolveTypedKeyPath(queryBuilder, keys);
|
|
135
151
|
// deeper than `alias.column` only inside a JSON column: a nested relation can't be mapped to an alias
|
|
136
152
|
if (!resolved.jsonPath && keys.length > proxy_key_utils_1.MAX_KEY_DEPTH) {
|
|
137
153
|
throw (0, proxy_key_utils_1.createKeyTooDeepError)(keys);
|
|
138
154
|
}
|
|
155
|
+
const column = (_a = resolved.column) !== null && _a !== void 0 ? _a : (_b = resolved.metadata) === null || _b === void 0 ? void 0 : _b.findColumnWithPropertyPath(resolved.propertyPath);
|
|
156
|
+
// a @CalculatedColumn: TypeORM would write "alias"."property", a column the table doesn't have
|
|
157
|
+
if ((column === null || column === void 0 ? void 0 : column.isVirtualProperty) && !column.query) {
|
|
158
|
+
throw new TypeError(`${keys.join('.')} is a @CalculatedColumn, not a column of the table: use the SQL expression that fills it`);
|
|
159
|
+
}
|
|
139
160
|
return resolved;
|
|
140
161
|
}
|
|
141
162
|
function resolveTypedKeyPath(queryBuilder, keys) {
|
|
@@ -213,6 +234,13 @@ function getTypedColumnKey(builder, callback, jsonOptions) {
|
|
|
213
234
|
return Array.isArray(keyHolder) ? keyHolder.map(toKey) : toKey(keyHolder);
|
|
214
235
|
}
|
|
215
236
|
exports.getTypedColumnKey = getTypedColumnKey;
|
|
237
|
+
/** Whether a typed callback selects a path inside a JSON column, `u => u.metadata.tags`, rather than a column. */
|
|
238
|
+
function isTypedJsonPath(builder, callback) {
|
|
239
|
+
const keyHolder = (0, proxy_key_utils_1.getKeyHolder)(callback, true);
|
|
240
|
+
return (!Array.isArray(keyHolder) &&
|
|
241
|
+
Boolean(resolveTypedKey(builder, keyHolder.getKeys()).jsonPath));
|
|
242
|
+
}
|
|
243
|
+
exports.isTypedJsonPath = isTypedJsonPath;
|
|
216
244
|
/**
|
|
217
245
|
* Quoted column of a typed callback that must select exactly one column
|
|
218
246
|
* (`whereIsAnyTyped`, `whereArrayContainsTyped`, …). Throws a `TypeError` for several columns:
|
|
@@ -242,7 +270,7 @@ function getTypedSelectionKey(builder, selection, useQuotes) {
|
|
|
242
270
|
const resolved = resolveTypedKey(queryBuilder, keyHolder.getKeys());
|
|
243
271
|
if (resolved.jsonPath) {
|
|
244
272
|
throw new TypeError(`Typed key "${keyHolder.getKeys().join('.')}" points into the JSON column "${resolved.propertyPath}": ` +
|
|
245
|
-
`select, order and
|
|
273
|
+
`select, order, distinct and group by take columns only. For an expression use tableHelper().jsonColumn()`);
|
|
246
274
|
}
|
|
247
275
|
if (useQuotes) {
|
|
248
276
|
return toQuotedColumnKey(queryBuilder, resolved);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { ObjectLiteral } from "typeorm";
|
|
1
|
+
import { DatabaseType, ObjectLiteral } from "typeorm";
|
|
2
2
|
export type TableHelperOptions = {
|
|
3
3
|
useQuotes?: boolean;
|
|
4
|
+
/** The database the SQL is for: MySQL and MariaDB quote identifiers with backticks. Double quotes without it. */
|
|
5
|
+
driver?: DatabaseType;
|
|
4
6
|
};
|
|
5
7
|
type JsonFieldTuple<T, Depth extends 0[] = []> = Depth['length'] extends 4 ? [string, ...string[]] : {
|
|
6
8
|
[K in keyof T & string]: [NonNullable<T[K]>] extends [Record<string, unknown>] ? [K, ...JsonFieldTuple<NonNullable<T[K]>, [...Depth, 0]>] | [K] : [K];
|
|
@@ -17,6 +19,60 @@ export type TableHelperCountOptions = {
|
|
|
17
19
|
*/
|
|
18
20
|
syntax?: 'case' | 'filter';
|
|
19
21
|
};
|
|
22
|
+
export type TableHelperJsonOptions = {
|
|
23
|
+
/** `jsonb_build_object` / `jsonb_agg` instead of the `json` functions. PostgreSQL only. */
|
|
24
|
+
jsonb?: boolean;
|
|
25
|
+
};
|
|
26
|
+
type ColumnName<T> = keyof T & string;
|
|
27
|
+
export type TableHelperJsonAggOptions<T> = TableHelperJsonOptions & {
|
|
28
|
+
/** Orders the array by a column of the helper's table, ascending or in the given direction. Not on MySQL. */
|
|
29
|
+
orderBy?: AggregateOrder<T>;
|
|
30
|
+
/**
|
|
31
|
+
* Aggregates only the rows that match, e.g. `images.isNotNull('id')`: after a LEFT JOIN without a match
|
|
32
|
+
* the row has `NULL` columns, which would otherwise come out as `[{"id": null}]`. Not on MySQL.
|
|
33
|
+
*/
|
|
34
|
+
filter?: string;
|
|
35
|
+
};
|
|
36
|
+
/** Keys of the entity whose values are numbers. */
|
|
37
|
+
type NumericColumnName<T> = {
|
|
38
|
+
[K in keyof T & string]: [NonNullable<T[K]>] extends [number | bigint] ? K : never;
|
|
39
|
+
}[keyof T & string];
|
|
40
|
+
type AggregateOrder<T> = ColumnName<T> | readonly [ColumnName<T>, 'ASC' | 'DESC'];
|
|
41
|
+
export type TableHelperSumOptions = {
|
|
42
|
+
/** Sums only the rows that match this condition, e.g. `t.where('status', "= 'paid'")`. */
|
|
43
|
+
filter?: string;
|
|
44
|
+
/** How `filter` is written, as for `count`: `'case'` (default) or `'filter'` (PostgreSQL and SQLite 3.30+). */
|
|
45
|
+
syntax?: 'case' | 'filter';
|
|
46
|
+
};
|
|
47
|
+
export type TableHelperArrayAggOptions<T> = {
|
|
48
|
+
/** `array_agg(DISTINCT column)`: each value once. Then `orderBy` can only be the aggregated column. */
|
|
49
|
+
distinct?: boolean;
|
|
50
|
+
/** Orders the array by a column of the helper's table, ascending or in the given direction. */
|
|
51
|
+
orderBy?: AggregateOrder<T>;
|
|
52
|
+
/** Aggregates only the rows that match, e.g. `images.isNotNull('id')` after a LEFT JOIN without a match. */
|
|
53
|
+
filter?: string;
|
|
54
|
+
};
|
|
55
|
+
/** A column of the helper's table whose property is a `Date`. */
|
|
56
|
+
type DateColumnName<T> = {
|
|
57
|
+
[K in keyof T & string]: [NonNullable<T[K]>] extends [Date] ? K : never;
|
|
58
|
+
}[keyof T & string];
|
|
59
|
+
/** A date column of the helper's table, or an SQL expression: `{ sql: 'MAX("order"."paidAt")' }`. */
|
|
60
|
+
export type TableHelperExpression<T> = DateColumnName<T> | {
|
|
61
|
+
sql: string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* A time zone: a name or an ISO 8601 offset (`'Europe/Kyiv'`, `'UTC'`, `'+02:00'` = UTC+2), written into the SQL as an escaped string
|
|
65
|
+
* literal; a column of the helper's table, `{ column: 'timezone' }`, with an optional `fallback` zone for `NULL`;
|
|
66
|
+
* or an SQL expression, `{ sql: services.column('timezone') }`.
|
|
67
|
+
*/
|
|
68
|
+
export type TableHelperTimeZone<T> = string | {
|
|
69
|
+
column: ColumnName<T>;
|
|
70
|
+
fallback?: string;
|
|
71
|
+
} | {
|
|
72
|
+
sql: string;
|
|
73
|
+
};
|
|
74
|
+
/** The units of PostgreSQL's `date_trunc`. */
|
|
75
|
+
export type DateTruncUnit = 'microseconds' | 'milliseconds' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' | 'decade' | 'century' | 'millennium';
|
|
20
76
|
export type JsonFieldRest<V> = [NonNullable<V>] extends [Record<string, unknown>] ? JsonFieldTuple<NonNullable<V>> : string[];
|
|
21
77
|
/**
|
|
22
78
|
* Creates a type-safe SQL helper for building column references, WHERE expressions,
|
|
@@ -51,7 +107,8 @@ export declare const tableHelper: <T extends ObjectLiteral>(alias?: string, opti
|
|
|
51
107
|
/** Returns `"alias"."column" IS NOT NULL`. */
|
|
52
108
|
isNotNull(columnName: keyof T): string;
|
|
53
109
|
/**
|
|
54
|
-
* Returns `CAST(COUNT("alias"."column") AS integer)`: the rows where the column isn't `NULL`.
|
|
110
|
+
* Returns `CAST(COUNT("alias"."column") AS integer)`: the rows where the column isn't `NULL`. With the `driver` option
|
|
111
|
+
* MySQL casts `AS SIGNED`, a `BIGINT`, which its TypeORM driver returns as a string by default.
|
|
55
112
|
* Without a column it is `COUNT(*)`, all the rows of the result, in every database. To count the rows
|
|
56
113
|
* of a table joined with LEFT JOIN, count one of its non-null columns: `images.count('id')`.
|
|
57
114
|
* @example
|
|
@@ -62,6 +119,41 @@ export declare const tableHelper: <T extends ObjectLiteral>(alias?: string, opti
|
|
|
62
119
|
* // CAST(COUNT(*) FILTER (WHERE "user"."status" = 'active') AS integer)
|
|
63
120
|
*/
|
|
64
121
|
count(columnName?: keyof T | undefined, countOptions?: TableHelperCountOptions): string;
|
|
122
|
+
/**
|
|
123
|
+
* Returns `COALESCE(SUM("alias"."column"), 0)`: 0, not `NULL`, for a group without rows or a LEFT JOIN
|
|
124
|
+
* without a match. Only a column whose property is a number. PostgreSQL sums integers into a `bigint`
|
|
125
|
+
* and MySQL into a `DECIMAL`, which the drivers return as strings.
|
|
126
|
+
* @example
|
|
127
|
+
* t.sum('amountCents', { filter: t.where('status', "= 'paid'") })
|
|
128
|
+
* // COALESCE(SUM(CASE WHEN "order"."status" = 'paid' THEN "order"."amountCents" END), 0)
|
|
129
|
+
*/
|
|
130
|
+
sum(columnName: NumericColumnName<T>, sumOptions?: TableHelperSumOptions): string;
|
|
131
|
+
/**
|
|
132
|
+
* Returns the PostgreSQL array of a column over the rows of a group, `{}` for a group without rows.
|
|
133
|
+
* Other databases have no arrays and throw a `TypeError`: use `jsonAgg` there.
|
|
134
|
+
* @example
|
|
135
|
+
* images.arrayAgg('id', { distinct: true, orderBy: ['id', 'DESC'], filter: images.isNotNull('id') })
|
|
136
|
+
* // COALESCE(array_agg(DISTINCT "image"."id" ORDER BY "image"."id" DESC) FILTER (WHERE "image"."id" IS NOT NULL), '{}')
|
|
137
|
+
*/
|
|
138
|
+
arrayAgg(columnName: ColumnName<T>, aggOptions?: TableHelperArrayAggOptions<T>): string;
|
|
139
|
+
/**
|
|
140
|
+
* Returns a JSON object of columns, keyed by the column names or by the keys of a `{ key: column }` map.
|
|
141
|
+
* Every column is checked against the entity type.
|
|
142
|
+
* @example
|
|
143
|
+
* t.jsonBuildObject(['id', 'url']) // json_build_object('id', "image"."id", 'url', "image"."url")
|
|
144
|
+
* t.jsonBuildObject({ imageId: 'id', src: 'url' }) // json_build_object('imageId', "image"."id", 'src', "image"."url")
|
|
145
|
+
* // MySQL: JSON_OBJECT(…), SQLite: json_object(…); { jsonb: true }: jsonb_build_object(…)
|
|
146
|
+
*/
|
|
147
|
+
jsonBuildObject(columns: readonly ColumnName<T>[] | Readonly<Record<string, ColumnName<T>>>, jsonOptions?: TableHelperJsonOptions): string;
|
|
148
|
+
/**
|
|
149
|
+
* Returns the JSON array of an expression over the rows of a group, `[]` for a group without rows
|
|
150
|
+
* (the aggregate alone gives `NULL` there). Use it with GROUP BY, or in a subquery.
|
|
151
|
+
* @example
|
|
152
|
+
* images.jsonAgg(images.jsonBuildObject(['id', 'url']), { orderBy: ['id', 'DESC'], filter: images.isNotNull('id') })
|
|
153
|
+
* // COALESCE(json_agg(json_build_object(…) ORDER BY "image"."id" DESC) FILTER (WHERE "image"."id" IS NOT NULL), CAST('[]' AS json))
|
|
154
|
+
* // MySQL: COALESCE(JSON_ARRAYAGG(…), JSON_ARRAY()), without ORDER BY and FILTER; SQLite: json_group_array(…)
|
|
155
|
+
*/
|
|
156
|
+
jsonAgg(expression: string, aggOptions?: TableHelperJsonAggOptions<T>): string;
|
|
65
157
|
/** Returns `CAST(expression AS type)`. */
|
|
66
158
|
cast(expression: string, type: string): string;
|
|
67
159
|
/**
|
|
@@ -82,5 +174,23 @@ export declare const tableHelper: <T extends ObjectLiteral>(alias?: string, opti
|
|
|
82
174
|
* qb.whereTyped(u => u.metadata, `${t.jsonPath('metadata', '->>', 'role')} =`, 'admin')
|
|
83
175
|
*/
|
|
84
176
|
jsonPath<K_1 extends keyof T>(_columnName: K_1, jsonOperator: '->>' | '->', ...fieldNames: JsonFieldRest<T[K_1]>): string;
|
|
177
|
+
/**
|
|
178
|
+
* Returns `(expression AT TIME ZONE zone)` (PostgreSQL). A `timestamptz` gives the wall-clock `timestamp`
|
|
179
|
+
* in that zone; a `timestamp` is read as wall-clock time there and gives a `timestamptz`.
|
|
180
|
+
* A zone name is written as an escaped literal, so it is safe to take from a request.
|
|
181
|
+
* @example
|
|
182
|
+
* t.atTimeZone('paidAt', 'Europe/Kyiv') // ("order"."paidAt" AT TIME ZONE 'Europe/Kyiv')
|
|
183
|
+
* t.atTimeZone('startsAt', { column: 'timezone', fallback: 'UTC' }) // ("order"."startsAt" AT TIME ZONE COALESCE("order"."timezone", 'UTC'))
|
|
184
|
+
*/
|
|
185
|
+
atTimeZone(value: TableHelperExpression<T>, timeZone: TableHelperTimeZone<T>): string;
|
|
186
|
+
/**
|
|
187
|
+
* Returns `date_trunc('unit', expression)` (PostgreSQL). With a time zone it is the three-argument form
|
|
188
|
+
* (PostgreSQL 12+): a `timestamptz` is truncated at the boundaries of that zone, e.g. its local midnight
|
|
189
|
+
* for `'day'`, and the result is a `timestamptz` again.
|
|
190
|
+
* @example
|
|
191
|
+
* t.dateTrunc('day', 'paidAt') // date_trunc('day', "order"."paidAt")
|
|
192
|
+
* t.dateTrunc('day', 'paidAt', 'Europe/Kyiv') // date_trunc('day', "order"."paidAt", 'Europe/Kyiv')
|
|
193
|
+
*/
|
|
194
|
+
dateTrunc(unit: DateTruncUnit, value: TableHelperExpression<T>, timeZone?: TableHelperTimeZone<T> | undefined): string;
|
|
85
195
|
};
|
|
86
196
|
export {};
|
|
@@ -3,6 +3,105 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.tableHelper = void 0;
|
|
4
4
|
const sql_utils_1 = require("./sql.utils");
|
|
5
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}` : ''}` };
|
|
31
|
+
}
|
|
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}"`);
|
|
50
|
+
}
|
|
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, "''")}'`;
|
|
6
105
|
/**
|
|
7
106
|
* Creates a type-safe SQL helper for building column references, WHERE expressions,
|
|
8
107
|
* JOIN clauses, and JSONB accessors for a given entity type.
|
|
@@ -20,13 +119,13 @@ const tableHelper = (alias, options = {}) => ({
|
|
|
20
119
|
* identifier, so names with other characters work too: `"user"."first-name"`.
|
|
21
120
|
*/
|
|
22
121
|
column(columnName, alias) {
|
|
23
|
-
const { useQuotes = true } = options;
|
|
122
|
+
const { useQuotes = true, driver } = options;
|
|
24
123
|
const tableAlias = alias !== null && alias !== void 0 ? alias : this.alias;
|
|
25
124
|
const column = String(columnName);
|
|
26
125
|
if (!useQuotes) {
|
|
27
126
|
return [tableAlias, column].filter(Boolean).join('.');
|
|
28
127
|
}
|
|
29
|
-
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)]
|
|
30
129
|
.filter(Boolean)
|
|
31
130
|
.join('.');
|
|
32
131
|
},
|
|
@@ -61,7 +160,8 @@ const tableHelper = (alias, options = {}) => ({
|
|
|
61
160
|
return this.where(columnName, 'IS NOT NULL');
|
|
62
161
|
},
|
|
63
162
|
/**
|
|
64
|
-
* 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.
|
|
65
165
|
* Without a column it is `COUNT(*)`, all the rows of the result, in every database. To count the rows
|
|
66
166
|
* of a table joined with LEFT JOIN, count one of its non-null columns: `images.count('id')`.
|
|
67
167
|
* @example
|
|
@@ -76,21 +176,85 @@ const tableHelper = (alias, options = {}) => ({
|
|
|
76
176
|
throw new TypeError('tableHelper.count(): the second argument is an options object, e.g. { distinct: true } or { filter: "…" }');
|
|
77
177
|
}
|
|
78
178
|
const { distinct = false, filter, syntax = 'case' } = countOptions;
|
|
79
|
-
if (syntax !== 'case' && syntax !== 'filter') {
|
|
80
|
-
throw new TypeError(`tableHelper.count(): syntax is 'case' or 'filter', got "${String(syntax)}"`);
|
|
81
|
-
}
|
|
82
179
|
const column = columnName ? this.column(columnName) : undefined;
|
|
83
180
|
if (distinct && !column) {
|
|
84
181
|
throw new TypeError("tableHelper.count({ distinct: true }) needs a column, e.g. count('email', { distinct: true })");
|
|
85
182
|
}
|
|
86
183
|
// `COUNT(*)`, not `COUNT("alias".*)`, which only PostgreSQL accepts;
|
|
87
184
|
// a CASE can't return `*`: without a column, it counts 1 per matching row
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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}"`);
|
|
218
|
+
}
|
|
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');
|
|
92
237
|
}
|
|
93
|
-
|
|
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})`;
|
|
94
258
|
},
|
|
95
259
|
/** Returns `CAST(expression AS type)`. */
|
|
96
260
|
cast(expression, type) {
|
|
@@ -119,6 +283,37 @@ const tableHelper = (alias, options = {}) => ({
|
|
|
119
283
|
*/
|
|
120
284
|
jsonPath(_columnName, jsonOperator, ...fieldNames) {
|
|
121
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})`;
|
|
122
317
|
}
|
|
123
318
|
});
|
|
124
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;
|