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