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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -3
  3. package/dist/decorators/calculated-column.decorator.d.ts +30 -0
  4. package/dist/decorators/calculated-column.decorator.js +82 -0
  5. package/dist/decorators/index.d.ts +1 -0
  6. package/dist/decorators/index.js +17 -0
  7. package/dist/extensions/calculated-column.extension.d.ts +33 -0
  8. package/dist/extensions/calculated-column.extension.js +33 -0
  9. package/dist/extensions/cursor-pagination.extension.d.ts +2 -1
  10. package/dist/extensions/cursor-pagination.extension.js +4 -3
  11. package/dist/extensions/get-many-with-totals.extension.d.ts +9 -1
  12. package/dist/extensions/get-many-with-totals.extension.js +158 -10
  13. package/dist/extensions/index.d.ts +7 -0
  14. package/dist/extensions/index.js +7 -0
  15. package/dist/extensions/join-lateral.extension.d.ts +19 -0
  16. package/dist/extensions/join-lateral.extension.js +50 -0
  17. package/dist/extensions/join-unnest.extension.d.ts +19 -0
  18. package/dist/extensions/join-unnest.extension.js +40 -0
  19. package/dist/extensions/order.extension.js +18 -7
  20. package/dist/extensions/query-returning.extension.d.ts +29 -0
  21. package/dist/extensions/query-returning.extension.js +50 -0
  22. package/dist/extensions/typed/index.d.ts +3 -0
  23. package/dist/extensions/typed/index.js +3 -2
  24. package/dist/extensions/typed/typed-distinct/typed-distinct.extension.d.ts +3 -2
  25. package/dist/extensions/typed/typed-group-by/typed-group-by.extension.d.ts +24 -0
  26. package/dist/extensions/typed/typed-group-by/typed-group-by.extension.js +31 -0
  27. package/dist/extensions/typed/typed-having/typed-having.extension.d.ts +36 -0
  28. package/dist/extensions/typed/typed-having/typed-having.extension.js +42 -0
  29. package/dist/extensions/typed/typed-joins/typed-joins.extension.d.ts +5 -4
  30. package/dist/extensions/typed/typed-order-by/typed-order-by-array-position.extension.d.ts +3 -2
  31. package/dist/extensions/typed/typed-order-by/typed-order-by-array-position.extension.js +10 -2
  32. package/dist/extensions/typed/typed-order-by/typed-order-by.extension.d.ts +3 -2
  33. package/dist/extensions/typed/typed-order-by/typed-order-by.extension.js +3 -2
  34. package/dist/extensions/typed/typed-root.extension.d.ts +25 -0
  35. package/dist/extensions/typed/typed-root.extension.js +13 -0
  36. package/dist/extensions/typed/typed-select/typed-select.extension.d.ts +5 -4
  37. package/dist/extensions/typed/typed-select/typed-select.extension.js +3 -2
  38. package/dist/extensions/typed/typed-where/typed-period-filter.extension.d.ts +2 -1
  39. package/dist/extensions/typed/typed-where/typed-where-array.extension.d.ts +10 -9
  40. package/dist/extensions/typed/typed-where/typed-where-array.extension.js +32 -0
  41. package/dist/extensions/typed/typed-where/typed-where-ilike.extension.d.ts +7 -6
  42. package/dist/extensions/typed/typed-where/typed-where-is-any.extension.d.ts +7 -6
  43. package/dist/extensions/typed/typed-where/typed-where-is-distinct-from.extension.d.ts +7 -6
  44. package/dist/extensions/typed/typed-where/typed-where-is-in.extension.d.ts +7 -6
  45. package/dist/extensions/typed/typed-where/typed-where-json.extension.d.ts +7 -6
  46. package/dist/extensions/typed/typed-where/typed-where-lower.extension.d.ts +7 -6
  47. package/dist/extensions/typed/typed-where/typed-where-similar.extension.d.ts +5 -4
  48. package/dist/extensions/typed/typed-where/typed-where.extension.d.ts +4 -3
  49. package/dist/extensions/union.extension.d.ts +22 -0
  50. package/dist/extensions/union.extension.js +52 -0
  51. package/dist/extensions/where-exists.extension.d.ts +50 -0
  52. package/dist/extensions/where-exists.extension.js +64 -0
  53. package/dist/extensions/where-relation-exists.extension.d.ts +38 -0
  54. package/dist/extensions/where-relation-exists.extension.js +95 -0
  55. package/dist/index.d.ts +2 -0
  56. package/dist/index.js +1 -0
  57. package/dist/types/extensions/cursor-pagination.types.d.ts +5 -0
  58. package/dist/types/extensions/get-many-with-totals.types.d.ts +19 -0
  59. package/dist/types/extensions/index.d.ts +5 -0
  60. package/dist/types/extensions/index.js +5 -0
  61. package/dist/types/extensions/join-lateral.types.d.ts +6 -0
  62. package/dist/types/extensions/join-lateral.types.js +2 -0
  63. package/dist/types/extensions/period.types.d.ts +8 -0
  64. package/dist/types/extensions/query-returning.types.d.ts +11 -0
  65. package/dist/types/extensions/query-returning.types.js +2 -0
  66. package/dist/types/extensions/typed-group-by.types.d.ts +8 -0
  67. package/dist/types/extensions/typed-group-by.types.js +2 -0
  68. package/dist/types/extensions/typed-having.types.d.ts +2 -0
  69. package/dist/types/extensions/typed-having.types.js +2 -0
  70. package/dist/types/extensions/union.types.d.ts +12 -0
  71. package/dist/types/extensions/union.types.js +2 -0
  72. package/dist/types/extensions/where.types.d.ts +1 -0
  73. package/dist/types/modules/typed-root.types.d.ts +13 -0
  74. package/dist/types/modules/typed-root.types.js +2 -0
  75. package/dist/utils/calculated-column.utils.d.ts +21 -0
  76. package/dist/utils/calculated-column.utils.js +63 -0
  77. package/dist/utils/parameter.utils.d.ts +7 -0
  78. package/dist/utils/parameter.utils.js +21 -0
  79. package/dist/utils/period.utils.js +7 -2
  80. package/dist/utils/prototype.utils.d.ts +5 -1
  81. package/dist/utils/prototype.utils.js +4 -1
  82. package/dist/utils/proxy-key.utils.d.ts +3 -3
  83. package/dist/utils/proxy-key.utils.js +7 -7
  84. package/dist/utils/query-builder.utils.d.ts +8 -0
  85. package/dist/utils/query-builder.utils.js +30 -2
  86. package/dist/utils/table.utils.d.ts +112 -2
  87. package/dist/utils/table.utils.js +206 -11
  88. package/dist/utils/time-zone.utils.d.ts +8 -0
  89. package/dist/utils/time-zone.utils.js +102 -0
  90. package/dist/utils/where.utils.d.ts +2 -2
  91. package/dist/utils/where.utils.js +23 -2
  92. package/package.json +2 -2
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
- - [ ] Virtual column decorator and its query-builder methods (`addSelectVirtual`, `orderByVirtual`, `getManyWithVirtual`, `getOneWithVirtual`)
63
- - [ ] Add `withVirtualColumns` option to `findManyWithTotals` method to use `getManyWithVirtual` instead of `getMany`
64
- - [ ] Add `isVirtual` option to `OrderParam` to indicate usage of `orderByVirtual` method
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
- if (!column) {
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,179 @@ 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
- async function getManyWithTotals(builder, paginationFilter, options) {
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
- const requests = [null, []];
23
- if (!disableTotalsCalculation) {
24
- requests[0] = await builder.getCount();
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
- if (loadAll) {
27
- requests[1] = await builder.getMany();
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
- requests[1] = await builder
31
- .applyPaginationFilter(paginationFilter, paginationOptions)
32
- .getMany();
109
+ hasMore =
110
+ total !== null && offset !== null ? offset + rows.length < total : null;
33
111
  }
34
- const [total, list] = requests;
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
+ if (!a.fulfilled) {
134
+ throw a.reason;
135
+ }
136
+ if (!b.fulfilled) {
137
+ throw b.reason;
138
+ }
139
+ return [a.value, b.value];
140
+ }
141
+ // Promise.allSettled is ES2020, past the ES2019 target of the build
142
+ function settle(promise) {
143
+ return promise.then((value) => ({ fulfilled: true, value }), (reason) => ({ fulfilled: false, reason }));
144
+ }
145
+ function getQueryRunner(builder) {
146
+ return builder.queryRunner;
147
+ }
148
+ /**
149
+ * Counts the rows of a raw query without its pagination and ORDER BY. The query goes into a
150
+ * subquery, so the count is that of its result rows whatever it groups or filters. A
151
+ * `countExpression` replaces the select list instead, without GROUP BY, HAVING and DISTINCT.
152
+ * Builds the count query before its first `await`, so the builder may change afterwards.
153
+ */
154
+ async function countRawRows(builder, countExpression) {
155
+ var _a;
156
+ const unpaged = cloneForCount(builder)
157
+ .orderBy()
158
+ .offset(undefined)
159
+ .limit(undefined)
160
+ .skip(undefined)
161
+ .take(undefined)
162
+ .setOption('disable-global-order');
163
+ let counting;
164
+ if (countExpression) {
165
+ // the expression counts on its own: DISTINCT ON next to an aggregate is an error
166
+ const counter = unpaged.select(countExpression, 'count').groupBy().distinct(false).distinctOn([]);
167
+ counter.expressionMap.havings = [];
168
+ counting = counter.getRawOne();
169
+ }
170
+ else {
171
+ // on the builder's query runner, if it has one: inside its transaction
172
+ const counter = builder.connection
173
+ .createQueryBuilder(getQueryRunner(builder))
174
+ .select('COUNT(*)', 'count')
175
+ .from(`(${unpaged.getQuery()})`, 'counted')
176
+ .setParameters(unpaged.getParameters());
177
+ // TypeORM 0.3's `$1` / `?` parameters; 1.x has none
178
+ const { nativeParameters } = unpaged.expressionMap;
179
+ if (nativeParameters) {
180
+ counter.setNativeParameters(nativeParameters);
181
+ }
182
+ const { cache, cacheId, cacheDuration } = unpaged.expressionMap;
183
+ Object.assign(counter.expressionMap, { cache, cacheId, cacheDuration });
184
+ counting = counter.getRawOne();
185
+ }
186
+ // COUNT is a bigint on PostgreSQL, which the driver hands over as a string
187
+ const row = await counting;
188
+ return Number((_a = row === null || row === void 0 ? void 0 : row.count) !== null && _a !== void 0 ? _a : 0);
189
+ }
@@ -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';
@@ -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
+ }