typeorm-extensions 2.1.0 → 2.2.0-rc.1
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 +159 -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 +17 -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 +3 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 koenigstag
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -59,6 +59,9 @@ For more information, please refer to the [documentation](https://koenigstag.git
|
|
|
59
59
|
## ToDo list
|
|
60
60
|
|
|
61
61
|
- [x] Add tests for pagination and order extensions
|
|
62
|
-
- [
|
|
63
|
-
- [
|
|
64
|
-
|
|
62
|
+
- [x] Calculated columns: the `@CalculatedColumn` decorator and `addSelectCalculated`. `getMany`, `getOne` and `getManyWithTotals` fill them as they are, so they need no variants of their own
|
|
63
|
+
- [x] Order by a calculated column: `orderByCalculated`, and `applyOrder` orders a `@CalculatedColumn` field by its value
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ColumnType, ValueTransformer } from 'typeorm';
|
|
2
|
+
export type CalculatedColumnOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* The type the selected value is converted from, as for `@Column`: `'boolean'`, `'jsonb'`, `'timestamptz'`.
|
|
5
|
+
* Defaults to the property's TypeScript type with `emitDecoratorMetadata`: `Number`, `String`, `Boolean`, `Date`.
|
|
6
|
+
* `Number` turns numeric strings (a PostgreSQL `COUNT`, an `AVG`) into numbers, decimals kept; `Boolean`
|
|
7
|
+
* reads `0` / `1`, `'0'` / `'1'` and `t` / `f` as well.
|
|
8
|
+
*/
|
|
9
|
+
type?: ColumnType;
|
|
10
|
+
/** Converts the selected value, e.g. JSON text from SQLite. Only `from` is used. */
|
|
11
|
+
transformer?: ValueTransformer | ValueTransformer[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A property that a query fills with an SQL expression of its own, through `addSelectCalculated`:
|
|
15
|
+
* a count, a latest date, a flag. It is no column of the table: schema sync leaves it out, `save`,
|
|
16
|
+
* `insert` and `update` never write it, and an entity loaded without `addSelectCalculated` doesn't have it.
|
|
17
|
+
*
|
|
18
|
+
* Unlike TypeORM's `@VirtualColumn`, whose one SQL query runs whenever the entity is loaded, each
|
|
19
|
+
* query chooses whether to compute it, and how.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* \@CalculatedColumn()
|
|
23
|
+
* imagesCount?: number;
|
|
24
|
+
*
|
|
25
|
+
* userRepository
|
|
26
|
+
* .createQueryBuilder('user')
|
|
27
|
+
* .addSelectCalculated('(SELECT COUNT(*) FROM "images" "i" WHERE "i"."userId" = "user"."id")', 'imagesCount')
|
|
28
|
+
* .getMany(); // [{ id: 1, …, imagesCount: 2 }]
|
|
29
|
+
*/
|
|
30
|
+
export declare function CalculatedColumn(typeOrOptions?: ColumnType | CalculatedColumnOptions): PropertyDecorator;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CalculatedColumn = void 0;
|
|
4
|
+
const typeorm_1 = require("typeorm");
|
|
5
|
+
/**
|
|
6
|
+
* A property that a query fills with an SQL expression of its own, through `addSelectCalculated`:
|
|
7
|
+
* a count, a latest date, a flag. It is no column of the table: schema sync leaves it out, `save`,
|
|
8
|
+
* `insert` and `update` never write it, and an entity loaded without `addSelectCalculated` doesn't have it.
|
|
9
|
+
*
|
|
10
|
+
* Unlike TypeORM's `@VirtualColumn`, whose one SQL query runs whenever the entity is loaded, each
|
|
11
|
+
* query chooses whether to compute it, and how.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* \@CalculatedColumn()
|
|
15
|
+
* imagesCount?: number;
|
|
16
|
+
*
|
|
17
|
+
* userRepository
|
|
18
|
+
* .createQueryBuilder('user')
|
|
19
|
+
* .addSelectCalculated('(SELECT COUNT(*) FROM "images" "i" WHERE "i"."userId" = "user"."id")', 'imagesCount')
|
|
20
|
+
* .getMany(); // [{ id: 1, …, imagesCount: 2 }]
|
|
21
|
+
*/
|
|
22
|
+
function CalculatedColumn(typeOrOptions) {
|
|
23
|
+
return (object, propertyName) => {
|
|
24
|
+
var _a, _b;
|
|
25
|
+
const options = typeof typeOrOptions === 'object' ? typeOrOptions : { type: typeOrOptions };
|
|
26
|
+
const reflectedType = typeof Reflect === 'object' && typeof Reflect.getMetadata === 'function'
|
|
27
|
+
? Reflect.getMetadata('design:type', object, propertyName)
|
|
28
|
+
: undefined;
|
|
29
|
+
// a union type (`string | null`) is reflected as Object, which no driver converts from
|
|
30
|
+
const type = (_a = options.type) !== null && _a !== void 0 ? _a : (reflectedType !== Object ? reflectedType : undefined);
|
|
31
|
+
if (!type) {
|
|
32
|
+
throw new TypeError(`@CalculatedColumn() on ${object.constructor.name}.${String(propertyName)} needs a type: ` +
|
|
33
|
+
`@CalculatedColumn('varchar'), or a property type of number, string, boolean or Date`);
|
|
34
|
+
}
|
|
35
|
+
const conversion = ownConversions.get(type);
|
|
36
|
+
const transformers = [(_b = options.transformer) !== null && _b !== void 0 ? _b : []].flat();
|
|
37
|
+
// the mode of TypeORM's @VirtualColumn: not a column of the table, never inserted or updated;
|
|
38
|
+
// with no `query` and `select: false` TypeORM never selects it by itself
|
|
39
|
+
(0, typeorm_1.getMetadataArgsStorage)().columns.push({
|
|
40
|
+
target: object.constructor,
|
|
41
|
+
propertyName: String(propertyName),
|
|
42
|
+
mode: 'virtual-property',
|
|
43
|
+
options: {
|
|
44
|
+
// the drivers parseInt a Number (AVG 150.5 → 150) and read MySQL's '0' as true, so these
|
|
45
|
+
// types are converted here; String is a type no driver converts. TypeORM applies `from` last to first
|
|
46
|
+
type: conversion ? String : type,
|
|
47
|
+
select: false,
|
|
48
|
+
insert: false,
|
|
49
|
+
update: false,
|
|
50
|
+
transformer: conversion ? [...transformers, conversion] : options.transformer,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
exports.CalculatedColumn = CalculatedColumn;
|
|
56
|
+
function toNumber(value) {
|
|
57
|
+
if (typeof value === 'bigint') {
|
|
58
|
+
return Number(value);
|
|
59
|
+
}
|
|
60
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
const number = Number(value);
|
|
64
|
+
return Number.isNaN(number) ? value : number;
|
|
65
|
+
}
|
|
66
|
+
function toBoolean(value) {
|
|
67
|
+
if (value === null || value === undefined) {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
if (typeof value === 'string') {
|
|
71
|
+
return !['', '0', 'f', 'false'].includes(value.trim().toLowerCase());
|
|
72
|
+
}
|
|
73
|
+
return Boolean(value);
|
|
74
|
+
}
|
|
75
|
+
const numberConversion = { from: toNumber, to: (value) => value };
|
|
76
|
+
const booleanConversion = { from: toBoolean, to: (value) => value };
|
|
77
|
+
const ownConversions = new Map([
|
|
78
|
+
[Number, numberConversion],
|
|
79
|
+
[Boolean, booleanConversion],
|
|
80
|
+
['boolean', booleanConversion],
|
|
81
|
+
['bool', booleanConversion],
|
|
82
|
+
]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './calculated-column.decorator';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./calculated-column.decorator"), exports);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
|
|
2
|
+
/** `addSelect`'s selection: an SQL expression, or a factory that builds a subquery on the builder it is given. */
|
|
3
|
+
export type CalculatedSelection<SubEntity extends ObjectLiteral = ObjectLiteral> = string | ((qb: SelectQueryBuilder<ObjectLiteral>) => SelectQueryBuilder<SubEntity>);
|
|
4
|
+
declare module 'typeorm/query-builder/SelectQueryBuilder' {
|
|
5
|
+
interface SelectQueryBuilder<Entity extends ObjectLiteral> {
|
|
6
|
+
/**
|
|
7
|
+
* Selects an SQL expression into a `@CalculatedColumn` property of the main alias's entity:
|
|
8
|
+
* `getMany()` / `getOne()` put the value into the entities, converted by the column's type.
|
|
9
|
+
* The selection is `addSelect`'s: an expression or a subquery factory.
|
|
10
|
+
*
|
|
11
|
+
* The value is read when the entity is selected as a whole (the alias, not some of its columns).
|
|
12
|
+
* Throws a `TypeError` if the property isn't a `@CalculatedColumn`.
|
|
13
|
+
*/
|
|
14
|
+
addSelectCalculated<K extends keyof Entity & string, SubEntity extends ObjectLiteral = ObjectLiteral>(selection: CalculatedSelection<SubEntity>, property: K): this;
|
|
15
|
+
/** Selects into a `@CalculatedColumn` property of the entity of another alias, e.g. a joined one. */
|
|
16
|
+
addSelectCalculated<SubEntity extends ObjectLiteral = ObjectLiteral>(selection: CalculatedSelection<SubEntity>, property: string, alias: string): this;
|
|
17
|
+
/**
|
|
18
|
+
* Sets ORDER BY the value of a `@CalculatedColumn` property of the main alias's entity, selected
|
|
19
|
+
* with `addSelectCalculated` first. Replaces the previous ORDER BY, like `orderBy`.
|
|
20
|
+
*
|
|
21
|
+
* The value is selected a second time under a generated alias (`calculated_…`) and ordered by it,
|
|
22
|
+
* which also works with `take` / `skip` and joins; it shows up in `getRawMany()` results.
|
|
23
|
+
* Throws a `TypeError` if the property isn't a selected `@CalculatedColumn`.
|
|
24
|
+
*/
|
|
25
|
+
orderByCalculated<K extends keyof Entity & string>(property: K, order?: 'ASC' | 'DESC', nulls?: 'NULLS FIRST' | 'NULLS LAST'): this;
|
|
26
|
+
/** Sets ORDER BY a `@CalculatedColumn` property of the entity of another alias, e.g. a joined one. */
|
|
27
|
+
orderByCalculated(property: string, order: 'ASC' | 'DESC' | undefined, nulls: 'NULLS FIRST' | 'NULLS LAST' | undefined, alias: string): this;
|
|
28
|
+
/** Adds ORDER BY the value of a `@CalculatedColumn` property, like `addOrderBy` (see `orderByCalculated`). */
|
|
29
|
+
addOrderByCalculated<K extends keyof Entity & string>(property: K, order?: 'ASC' | 'DESC', nulls?: 'NULLS FIRST' | 'NULLS LAST'): this;
|
|
30
|
+
/** Adds ORDER BY a `@CalculatedColumn` property of the entity of another alias. */
|
|
31
|
+
addOrderByCalculated(property: string, order: 'ASC' | 'DESC' | undefined, nulls: 'NULLS FIRST' | 'NULLS LAST' | undefined, alias: string): this;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const typeorm_1 = require("typeorm");
|
|
4
|
+
const prototype_utils_1 = require("../utils/prototype.utils");
|
|
5
|
+
const calculated_column_utils_1 = require("../utils/calculated-column.utils");
|
|
6
|
+
// patching
|
|
7
|
+
const extension = {
|
|
8
|
+
prototype: {
|
|
9
|
+
addSelectCalculated(selection, property, alias) {
|
|
10
|
+
return addSelectCalculated(this, selection, property, alias);
|
|
11
|
+
},
|
|
12
|
+
orderByCalculated(property, order, nulls, alias) {
|
|
13
|
+
return attachOrderByCalculated(this, 'orderBy', property, order, nulls, alias);
|
|
14
|
+
},
|
|
15
|
+
addOrderByCalculated(property, order, nulls, alias) {
|
|
16
|
+
return attachOrderByCalculated(this, 'addOrderBy', property, order, nulls, alias);
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
(0, prototype_utils_1.patchPrototype)(typeorm_1.SelectQueryBuilder, extension);
|
|
21
|
+
// implementation
|
|
22
|
+
function addSelectCalculated(builder, selection, property, aliasName) {
|
|
23
|
+
const { key } = (0, calculated_column_utils_1.resolveCalculatedColumn)(builder, property, aliasName, 'addSelectCalculated');
|
|
24
|
+
// two calls: addSelect's overloads take a string or a factory, not their union
|
|
25
|
+
if (typeof selection === 'function') {
|
|
26
|
+
return builder.addSelect(selection, key);
|
|
27
|
+
}
|
|
28
|
+
return builder.addSelect(selection, key);
|
|
29
|
+
}
|
|
30
|
+
function attachOrderByCalculated(builder, method, property, order, nulls, aliasName) {
|
|
31
|
+
const orderAlias = (0, calculated_column_utils_1.selectCalculatedForOrder)(builder, property, aliasName, `${method}Calculated`);
|
|
32
|
+
return builder[method](orderAlias, order, nulls);
|
|
33
|
+
}
|
|
@@ -9,7 +9,8 @@ declare module 'typeorm/query-builder/SelectQueryBuilder' {
|
|
|
9
9
|
* deep they are, and rows inserted meanwhile don't shift them.
|
|
10
10
|
*
|
|
11
11
|
* Orders the query by `options.orderBy` (the primary key is appended), replacing its ORDER BY,
|
|
12
|
-
* loads `pageSize + 1` rows to know whether there is a next page, and returns `nextCursor
|
|
12
|
+
* loads `pageSize + 1` rows to know whether there is a next page, and returns `nextCursor`
|
|
13
|
+
* together with `hasMore`.
|
|
13
14
|
* The query builder itself is not changed. Throws `InvalidCursorError` for a cursor that can't
|
|
14
15
|
* be decoded or was made for another order.
|
|
15
16
|
*/
|
|
@@ -29,7 +29,8 @@ function getCursorKeys(builder, orderBy) {
|
|
|
29
29
|
const metadata = mainAlias.metadata;
|
|
30
30
|
const keys = (orderBy !== null && orderBy !== void 0 ? orderBy : []).map(({ field, direction }) => {
|
|
31
31
|
const column = metadata.findColumnWithPropertyPath(String(field));
|
|
32
|
-
|
|
32
|
+
// a @CalculatedColumn is in the metadata, but not in the table
|
|
33
|
+
if (!column || column.isVirtualProperty) {
|
|
33
34
|
throw new TypeError(`getManyWithCursor: "${String(field)}" is not a column of ${metadata.name}`);
|
|
34
35
|
}
|
|
35
36
|
return { column, direction: (0, order_utils_1.toSqlOrderDirection)(direction) };
|
|
@@ -89,7 +90,7 @@ async function getManyWithCursor(builder, cursorFilter, options) {
|
|
|
89
90
|
const { entities, raw } = await query.getRawAndEntities();
|
|
90
91
|
const list = entities.slice(0, pageSize);
|
|
91
92
|
if (entities.length <= pageSize) {
|
|
92
|
-
return { list, nextCursor: null };
|
|
93
|
+
return { list, nextCursor: null, hasMore: false };
|
|
93
94
|
}
|
|
94
95
|
// with joined collections there are several raw rows per entity: find one of the last entity's
|
|
95
96
|
const last = list[list.length - 1];
|
|
@@ -104,5 +105,5 @@ async function getManyWithCursor(builder, cursorFilter, options) {
|
|
|
104
105
|
}
|
|
105
106
|
return value;
|
|
106
107
|
});
|
|
107
|
-
return { list, nextCursor: (0, cursor_utils_1.encodeCursor)(fields, values) };
|
|
108
|
+
return { list, nextCursor: (0, cursor_utils_1.encodeCursor)(fields, values), hasMore: true };
|
|
108
109
|
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import './pagination.extension';
|
|
2
|
+
import { ObjectLiteral } from 'typeorm';
|
|
2
3
|
import { PaginationFilter } from '../types/interfaces/pagination.interface';
|
|
3
|
-
import type { GetManyWithTotalsOptions, ListWithTotals } from '../types/extensions';
|
|
4
|
+
import type { GetManyWithTotalsOptions, GetRawManyWithTotalsOptions, ListWithTotals } from '../types/extensions';
|
|
4
5
|
declare module 'typeorm/query-builder/SelectQueryBuilder' {
|
|
5
6
|
interface SelectQueryBuilder<Entity> {
|
|
6
7
|
getManyWithTotals(paginationFilter: PaginationFilter, options?: GetManyWithTotalsOptions): Promise<ListWithTotals<Entity>>;
|
|
8
|
+
/**
|
|
9
|
+
* `getManyWithTotals` for raw rows: the page comes from `getRawMany()`, paged with LIMIT / OFFSET.
|
|
10
|
+
* `total` is the number of rows the query returns without pagination, counted as a subquery,
|
|
11
|
+
* `SELECT COUNT(*) FROM (query)`, so GROUP BY, HAVING and DISTINCT count the way they page.
|
|
12
|
+
* `options.countExpression` counts with an expression of the query instead.
|
|
13
|
+
*/
|
|
14
|
+
getRawManyWithTotals<Row = ObjectLiteral>(paginationFilter: PaginationFilter, options?: GetRawManyWithTotalsOptions): Promise<ListWithTotals<Row>>;
|
|
7
15
|
}
|
|
8
16
|
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
require("./pagination.extension");
|
|
4
4
|
const SelectQueryBuilder_1 = require("typeorm/query-builder/SelectQueryBuilder");
|
|
5
5
|
const pagination_utils_1 = require("../utils/pagination.utils");
|
|
6
|
+
const constants_1 = require("../constants");
|
|
6
7
|
const prototype_utils_1 = require("../utils/prototype.utils");
|
|
7
8
|
// patching
|
|
8
9
|
const extension = {
|
|
@@ -10,32 +11,180 @@ const extension = {
|
|
|
10
11
|
getManyWithTotals(paginationFilter, options) {
|
|
11
12
|
return getManyWithTotals(this, paginationFilter, options);
|
|
12
13
|
},
|
|
14
|
+
getRawManyWithTotals(paginationFilter, options) {
|
|
15
|
+
return getRawManyWithTotals(this, paginationFilter, options);
|
|
16
|
+
},
|
|
13
17
|
},
|
|
14
18
|
};
|
|
15
19
|
(0, prototype_utils_1.patchPrototype)(SelectQueryBuilder_1.SelectQueryBuilder, extension);
|
|
16
20
|
// implementation
|
|
17
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Whether the count and the page query may run at the same time. A builder bound to a query
|
|
23
|
+
* runner — inside a transaction, or from a transactional manager — runs them one after another:
|
|
24
|
+
* there is one connection, on which the driver would serialise them anyway, and on `pg` that queue
|
|
25
|
+
* is deprecated. Without one, each query obtains a runner: its own pooled connection, or on SQLite
|
|
26
|
+
* the driver's single shared runner.
|
|
27
|
+
*
|
|
28
|
+
* `useTransaction` is not consulted. With a pool each query opens a transaction on its own
|
|
29
|
+
* connection, whether the two run together or one after another. On SQLite the two share the
|
|
30
|
+
* runner, so the one that starts second finds a transaction active and does not open its own.
|
|
31
|
+
*/
|
|
32
|
+
function canCountConcurrently(builder) {
|
|
33
|
+
const queryBuilder = builder;
|
|
34
|
+
return !queryBuilder.queryRunner;
|
|
35
|
+
}
|
|
36
|
+
function getManyWithTotals(builder, paginationFilter, options) {
|
|
37
|
+
return readWithTotals(builder, paginationFilter, options, {
|
|
38
|
+
rows: (query) => query.getMany(),
|
|
39
|
+
// on a clone: getCount writes to the expression map the page query reads
|
|
40
|
+
count: (query) => cloneForCount(query).getCount(),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** A clone with a cache id of its own: under the page query's id each would read the other's cached rows. */
|
|
44
|
+
function cloneForCount(query) {
|
|
45
|
+
const clone = query.clone();
|
|
46
|
+
if (clone.expressionMap.cacheId) {
|
|
47
|
+
clone.expressionMap.cacheId = `${clone.expressionMap.cacheId}-count`;
|
|
48
|
+
}
|
|
49
|
+
return clone;
|
|
50
|
+
}
|
|
51
|
+
function getRawManyWithTotals(builder, paginationFilter, options) {
|
|
52
|
+
const { countExpression, ...totalsOptions } = options !== null && options !== void 0 ? options : {};
|
|
53
|
+
// take / skip page entities; raw rows are paged with LIMIT / OFFSET
|
|
54
|
+
return readWithTotals(builder, paginationFilter, { ...totalsOptions, useTakeAndSkip: false }, {
|
|
55
|
+
rows: (query) => query.getRawMany(),
|
|
56
|
+
count: (query) => countRawRows(query, countExpression),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function readWithTotals(builder, paginationFilter, options, read) {
|
|
60
|
+
var _a;
|
|
18
61
|
const { disableTotalsCalculation = false, loadAll = false, ...paginationOptions } = options || {};
|
|
19
62
|
const { limit, offset } = loadAll
|
|
20
63
|
? { limit: null, offset: null }
|
|
21
64
|
: (0, pagination_utils_1.getLimitAndOffset)(paginationFilter);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
65
|
+
// without a total, one row past the page is what tells whether more rows follow
|
|
66
|
+
const probeForMore = disableTotalsCalculation && !loadAll && limit !== null;
|
|
67
|
+
const readPage = () => {
|
|
68
|
+
if (loadAll) {
|
|
69
|
+
return read.rows(builder);
|
|
70
|
+
}
|
|
71
|
+
const paged = builder.applyPaginationFilter(paginationFilter, paginationOptions);
|
|
72
|
+
if (probeForMore) {
|
|
73
|
+
const probeLimit = limit + 1;
|
|
74
|
+
const { useTakeAndSkip = constants_1.defaultUseTakeAndSkip } = paginationOptions;
|
|
75
|
+
// replaces the limit applyPaginationFilter just set, keeping its offset
|
|
76
|
+
if (useTakeAndSkip) {
|
|
77
|
+
paged.take(probeLimit);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
paged.limit(probeLimit);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return read.rows(paged);
|
|
84
|
+
};
|
|
85
|
+
let total = null;
|
|
86
|
+
let rows;
|
|
87
|
+
if (disableTotalsCalculation) {
|
|
88
|
+
rows = await readPage();
|
|
89
|
+
}
|
|
90
|
+
else if (canCountConcurrently(builder)) {
|
|
91
|
+
// the count clones the builder before readPage mutates it, so it has to start first: the two
|
|
92
|
+
// queries would otherwise share one expression map, which each of them writes to
|
|
93
|
+
const counting = read.count(builder);
|
|
94
|
+
// a page that throws before its query starts (a limit TypeORM rejects) waits for the count too
|
|
95
|
+
const reading = new Promise((resolve) => resolve(readPage()));
|
|
96
|
+
[total, rows] = await bothOrFirstError(counting, reading);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
total = await read.count(builder);
|
|
100
|
+
rows = await readPage();
|
|
25
101
|
}
|
|
26
|
-
|
|
27
|
-
|
|
102
|
+
let list = rows;
|
|
103
|
+
let hasMore;
|
|
104
|
+
if (probeForMore) {
|
|
105
|
+
hasMore = rows.length > limit;
|
|
106
|
+
list = hasMore ? rows.slice(0, limit) : rows;
|
|
28
107
|
}
|
|
29
108
|
else {
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
.getMany();
|
|
109
|
+
hasMore =
|
|
110
|
+
total !== null && offset !== null ? offset + rows.length < total : null;
|
|
33
111
|
}
|
|
34
|
-
|
|
112
|
+
// allOrderBys, not orderBys: it is what the SQL is built from, so without an explicit order it
|
|
113
|
+
// falls back to the `orderBy` of @Entity, and respects the `disable-global-order` option
|
|
114
|
+
const orderBy = Object.entries((_a = builder.expressionMap.allOrderBys) !== null && _a !== void 0 ? _a : {}).map(([selector, value]) => typeof value === 'string'
|
|
115
|
+
? { selector, order: value }
|
|
116
|
+
: { selector, order: value.order, nulls: value.nulls });
|
|
35
117
|
return {
|
|
36
118
|
list,
|
|
37
119
|
total,
|
|
38
120
|
limit,
|
|
39
121
|
offset,
|
|
122
|
+
hasMore,
|
|
123
|
+
orderBy,
|
|
40
124
|
};
|
|
41
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Both results, or the first error once both queries have settled. `Promise.all` alone would reject as soon as
|
|
128
|
+
* one fails, while the other still runs on its own connection: the call would return with a query in flight,
|
|
129
|
+
* whose outcome nobody reads. When both fail, the count's error is thrown, being the first query started.
|
|
130
|
+
*/
|
|
131
|
+
async function bothOrFirstError(first, second) {
|
|
132
|
+
const [a, b] = await Promise.all([settle(first), settle(second)]);
|
|
133
|
+
// `=== false`: without strictNullChecks `!fulfilled` doesn't narrow the union
|
|
134
|
+
if (a.fulfilled === false) {
|
|
135
|
+
throw a.reason;
|
|
136
|
+
}
|
|
137
|
+
if (b.fulfilled === false) {
|
|
138
|
+
throw b.reason;
|
|
139
|
+
}
|
|
140
|
+
return [a.value, b.value];
|
|
141
|
+
}
|
|
142
|
+
// Promise.allSettled is ES2020, past the ES2019 target of the build
|
|
143
|
+
function settle(promise) {
|
|
144
|
+
return promise.then((value) => ({ fulfilled: true, value }), (reason) => ({ fulfilled: false, reason }));
|
|
145
|
+
}
|
|
146
|
+
function getQueryRunner(builder) {
|
|
147
|
+
return builder.queryRunner;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Counts the rows of a raw query without its pagination and ORDER BY. The query goes into a
|
|
151
|
+
* subquery, so the count is that of its result rows whatever it groups or filters. A
|
|
152
|
+
* `countExpression` replaces the select list instead, without GROUP BY, HAVING and DISTINCT.
|
|
153
|
+
* Builds the count query before its first `await`, so the builder may change afterwards.
|
|
154
|
+
*/
|
|
155
|
+
async function countRawRows(builder, countExpression) {
|
|
156
|
+
var _a;
|
|
157
|
+
const unpaged = cloneForCount(builder)
|
|
158
|
+
.orderBy()
|
|
159
|
+
.offset(undefined)
|
|
160
|
+
.limit(undefined)
|
|
161
|
+
.skip(undefined)
|
|
162
|
+
.take(undefined)
|
|
163
|
+
.setOption('disable-global-order');
|
|
164
|
+
let counting;
|
|
165
|
+
if (countExpression) {
|
|
166
|
+
// the expression counts on its own: DISTINCT ON next to an aggregate is an error
|
|
167
|
+
const counter = unpaged.select(countExpression, 'count').groupBy().distinct(false).distinctOn([]);
|
|
168
|
+
counter.expressionMap.havings = [];
|
|
169
|
+
counting = counter.getRawOne();
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// on the builder's query runner, if it has one: inside its transaction
|
|
173
|
+
const counter = builder.connection
|
|
174
|
+
.createQueryBuilder(getQueryRunner(builder))
|
|
175
|
+
.select('COUNT(*)', 'count')
|
|
176
|
+
.from(`(${unpaged.getQuery()})`, 'counted')
|
|
177
|
+
.setParameters(unpaged.getParameters());
|
|
178
|
+
// TypeORM 0.3's `$1` / `?` parameters; 1.x has none
|
|
179
|
+
const { nativeParameters } = unpaged.expressionMap;
|
|
180
|
+
if (nativeParameters) {
|
|
181
|
+
counter.setNativeParameters(nativeParameters);
|
|
182
|
+
}
|
|
183
|
+
const { cache, cacheId, cacheDuration } = unpaged.expressionMap;
|
|
184
|
+
Object.assign(counter.expressionMap, { cache, cacheId, cacheDuration });
|
|
185
|
+
counting = counter.getRawOne();
|
|
186
|
+
}
|
|
187
|
+
// COUNT is a bigint on PostgreSQL, which the driver hands over as a string
|
|
188
|
+
const row = await counting;
|
|
189
|
+
return Number((_a = row === null || row === void 0 ? void 0 : row.count) !== null && _a !== void 0 ? _a : 0);
|
|
190
|
+
}
|
|
@@ -10,3 +10,10 @@ export * from './period-filter.extension';
|
|
|
10
10
|
export * from './cursor-pagination.extension';
|
|
11
11
|
export * from './order-by-array-position.extension';
|
|
12
12
|
export * from './typed';
|
|
13
|
+
export * from './union.extension';
|
|
14
|
+
export * from './calculated-column.extension';
|
|
15
|
+
export * from './join-unnest.extension';
|
|
16
|
+
export * from './join-lateral.extension';
|
|
17
|
+
export * from './where-exists.extension';
|
|
18
|
+
export * from './where-relation-exists.extension';
|
|
19
|
+
export * from './query-returning.extension';
|
package/dist/extensions/index.js
CHANGED
|
@@ -26,3 +26,10 @@ __exportStar(require("./period-filter.extension"), exports);
|
|
|
26
26
|
__exportStar(require("./cursor-pagination.extension"), exports);
|
|
27
27
|
__exportStar(require("./order-by-array-position.extension"), exports);
|
|
28
28
|
__exportStar(require("./typed"), exports);
|
|
29
|
+
__exportStar(require("./union.extension"), exports);
|
|
30
|
+
__exportStar(require("./calculated-column.extension"), exports);
|
|
31
|
+
__exportStar(require("./join-unnest.extension"), exports);
|
|
32
|
+
__exportStar(require("./join-lateral.extension"), exports);
|
|
33
|
+
__exportStar(require("./where-exists.extension"), exports);
|
|
34
|
+
__exportStar(require("./where-relation-exists.extension"), exports);
|
|
35
|
+
__exportStar(require("./query-returning.extension"), exports);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ObjectLiteral } from 'typeorm';
|
|
2
|
+
import type { LateralSubQuery } from '../types/extensions/join-lateral.types';
|
|
3
|
+
export interface JoinLateralMethods {
|
|
4
|
+
/**
|
|
5
|
+
* `LEFT JOIN LATERAL (subQuery) "alias" ON condition`: the subquery runs for each row and can use
|
|
6
|
+
* the columns of the tables joined before it, e.g. the latest ticket of each user. Rows without a
|
|
7
|
+
* match stay, with `NULL` columns. The condition defaults to `TRUE`.
|
|
8
|
+
*
|
|
9
|
+
* PostgreSQL, CockroachDB, MySQL 8.0.14+ and Oracle have LATERAL; SQLite, SQL Server and MariaDB don't.
|
|
10
|
+
* The subquery's columns are raw: select them with `addSelect('"alias"."column"', 'name')`.
|
|
11
|
+
*/
|
|
12
|
+
leftJoinLateral<SubEntity extends ObjectLiteral = ObjectLiteral>(subQuery: LateralSubQuery<SubEntity>, alias: string, condition?: string, parameters?: ObjectLiteral): this;
|
|
13
|
+
/** `INNER JOIN LATERAL`: as `leftJoinLateral`, but rows without a match are dropped. */
|
|
14
|
+
innerJoinLateral<SubEntity extends ObjectLiteral = ObjectLiteral>(subQuery: LateralSubQuery<SubEntity>, alias: string, condition?: string, parameters?: ObjectLiteral): this;
|
|
15
|
+
}
|
|
16
|
+
declare module 'typeorm/query-builder/SelectQueryBuilder' {
|
|
17
|
+
interface SelectQueryBuilder<Entity extends ObjectLiteral> extends JoinLateralMethods {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const typeorm_1 = require("typeorm");
|
|
4
|
+
const parameter_utils_1 = require("../utils/parameter.utils");
|
|
5
|
+
const prototype_utils_1 = require("../utils/prototype.utils");
|
|
6
|
+
/* eslint-enable @typescript-eslint/no-empty-interface, @typescript-eslint/no-unused-vars */
|
|
7
|
+
// patching
|
|
8
|
+
const extension = {
|
|
9
|
+
prototype: {
|
|
10
|
+
leftJoinLateral(subQuery, alias, condition, parameters) {
|
|
11
|
+
return joinLateral(this, 'leftJoin', subQuery, alias, condition, parameters);
|
|
12
|
+
},
|
|
13
|
+
innerJoinLateral(subQuery, alias, condition, parameters) {
|
|
14
|
+
return joinLateral(this, 'innerJoin', subQuery, alias, condition, parameters);
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
(0, prototype_utils_1.patchPrototype)(typeorm_1.SelectQueryBuilder, extension);
|
|
19
|
+
function joinLateral(builder, method, subQuery, alias, condition = 'TRUE', parameters) {
|
|
20
|
+
const lateral = toLateralSql(builder, subQuery);
|
|
21
|
+
// TypeORM puts the getQuery() of a join's subquery factory into the SQL as it is, and adds its
|
|
22
|
+
// getParameters(): handing it the LATERAL keyword with the subquery is its own subquery-join path
|
|
23
|
+
const factory = () => ({
|
|
24
|
+
getQuery: () => `LATERAL ${lateral.sql}`,
|
|
25
|
+
getParameters: () => lateral.parameters,
|
|
26
|
+
});
|
|
27
|
+
return builder[method](factory, alias, condition, parameters);
|
|
28
|
+
}
|
|
29
|
+
function toLateralSql(builder, subQuery) {
|
|
30
|
+
if (typeof subQuery === 'string') {
|
|
31
|
+
// raw SQL: its placeholders are the query's own parameters, set with `parameters` or setParameter
|
|
32
|
+
return { sql: `(${subQuery})`, parameters: {} };
|
|
33
|
+
}
|
|
34
|
+
const subBuilder = typeof subQuery === 'function' ? subQuery(detachedSubQuery(builder)) : subQuery;
|
|
35
|
+
const query = subBuilder.getQuery();
|
|
36
|
+
// a builder from subQuery() wraps itself in parentheses, any other doesn't
|
|
37
|
+
const wrapped = subBuilder.expressionMap.subQuery ? query : `(${query})`;
|
|
38
|
+
const [sql, parameters] = (0, parameter_utils_1.withUniqueParameterNames)(wrapped, subBuilder.getParameters());
|
|
39
|
+
return { sql, parameters };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A subquery builder for a factory, as TypeORM's `subQuery()` but without a parent: TypeORM's would
|
|
43
|
+
* set the factory's parameters on the outer query under their own names, before they can be renamed.
|
|
44
|
+
*/
|
|
45
|
+
function detachedSubQuery(builder) {
|
|
46
|
+
const queryRunner = builder.queryRunner;
|
|
47
|
+
const subQuery = builder.connection.createQueryBuilder(queryRunner);
|
|
48
|
+
subQuery.expressionMap.subQuery = true;
|
|
49
|
+
return subQuery;
|
|
50
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ObjectLiteral } from 'typeorm';
|
|
2
|
+
import { ArgsType } from '../types/util-types';
|
|
3
|
+
export interface JoinUnnestMethods {
|
|
4
|
+
/**
|
|
5
|
+
* Keeps the rows whose `field` is one of `array`, joining the array as a set of rows:
|
|
6
|
+
* `INNER JOIN (SELECT DISTINCT unnest(CAST(:array AS uuid[])) AS "value") "unnest_x7k2q"
|
|
7
|
+
* ON "user"."id" = "unnest_x7k2q"."value"`. The same rows as `whereIsAny(field, array)`; for a
|
|
8
|
+
* large array the planner can join from it instead of testing every row against it.
|
|
9
|
+
*
|
|
10
|
+
* `arrayType` (`'uuid[]'`) defaults to the array of the column type, when `field` names an entity
|
|
11
|
+
* column; otherwise, and for enum columns, it is required. An empty array matches no row.
|
|
12
|
+
* PostgreSQL and CockroachDB.
|
|
13
|
+
*/
|
|
14
|
+
innerJoinUnnest(field: string, array: ArgsType | null | undefined, arrayType?: string): this;
|
|
15
|
+
}
|
|
16
|
+
declare module 'typeorm/query-builder/SelectQueryBuilder' {
|
|
17
|
+
interface SelectQueryBuilder<Entity extends ObjectLiteral> extends JoinUnnestMethods {
|
|
18
|
+
}
|
|
19
|
+
}
|