typeorm-extensions 2.2.0-rc.7 → 2.2.0-rc.8

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 CHANGED
@@ -5,62 +5,101 @@
5
5
  [![Issues](https://img.shields.io/github/issues-raw/koenigstag/typeorm-extensions.svg?maxAge=25000)](https://github.com/koenigstag/typeorm-extensions/issues)
6
6
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/koenigstag/typeorm-extensions/pulls)
7
7
 
8
- ## Description
8
+ **Type-safe extensions for the TypeORM QueryBuilder.** Write selects, joins, conditions and ordering as property callbacks the compiler checks against your entities, page through results with totals or cursors, and build the raw SQL you still need with helpers that quote it for the database.
9
9
 
10
- TypeORM Extensions is a library that provides additional functionality and extensions for the TypeORM QueryBuilder. It aims to simplify common tasks and enhance the capabilities of TypeORM.
10
+ ```typescript
11
+ import 'typeorm-extensions';
11
12
 
12
- ## Features
13
+ const { list, total, hasMore } = await dataSource
14
+ .getRepository(UserEntity)
15
+ .createQueryBuilder('user')
16
+ .leftJoinTyped(user => user.images, 'images')
17
+ .whereTyped(user => user.createdAt, '>', since)
18
+ // a key inside a JSON column: "user"."metadata" ->> 'role' = :param
19
+ .andWhereTyped(user => user.metadata.role, '=', 'admin')
20
+ .orderByTyped(user => user.images.url, 'ASC', 'NULLS LAST')
21
+ .getManyWithTotals({ page: 1, pageSize: 20 });
22
+ ```
13
23
 
14
- - Simplicity: applying pagination and ordering to queries is as simple as calling a method.
15
- - Type Safety: the library is written in TypeScript and provides type-safe QueryBuilder methods based on Entity metadata.
16
- - Flexibility: the library is designed to be flexible and can be used with any TypeORM entity.
24
+ Try it in the [playground](https://koenigstag.github.io/typeorm-extensions/playground/): an editor and a database in the browser.
17
25
 
18
- ## Installation
26
+ ## Why
19
27
 
20
- To install `typeorm-extensions`:
28
+ The QueryBuilder takes columns as strings. A renamed property, a typo or a half-quoted alias compiles and then fails at runtime:
21
29
 
22
- Install using npm, yarn or pnpm:
23
- ```bash
24
- npm install typeorm-extensions
30
+ ```typescript
31
+ .where('user.createdAt > :since', { since }) // still compiles after createdAt is renamed
32
+ .andWhere('order."userId" = :userId', { userId }) // PostgreSQL: syntax error at or near "order"
25
33
  ```
26
34
 
27
- ## Usage
35
+ With the extensions a column is a property of the entity type, so the compiler catches the rename, and the SQL is built and quoted for you:
28
36
 
29
37
  ```typescript
30
- import 'typeorm-extensions'; // Importing the library root extends the QueryBuilder with all extensions
38
+ .whereTyped(user => user.createdAt, '>', since)
31
39
 
32
- // Or init specific extension: import 'typeorm-extensions/dist/extensions/pagination.extension';
40
+ const orders = tableHelper(OrderEntity, 'order');
41
+ .andWhere(`${orders.userId} = :userId`, { userId }) // "order"."userId" = :userId
42
+ ```
33
43
 
34
- const query = myDataSource
35
- .getRepository(UserEntity)
36
- .createQueryBuilder('users')
37
- // Join relation defined in entity model
38
- .leftJoinTyped(user => user.images, 'images')
39
- // Select type-safe properties
40
- .selectTyped(user => [
41
- user.id,
42
- user.name,
43
- user.email,
44
- user.images.url,
45
- ])
46
- .whereTyped(user => user.name, 'ILIKE', 'John')
47
- // Paths into JSON columns: "users"."metadata" ->> 'role' = :param
48
- .andWhereTyped(user => user.metadata.role, '=', 'admin')
49
- // Order by type-safe own and relation properties
50
- .orderByTyped(user => user.images.url, 'ASC', 'NULLS LAST')
51
- // Use pagination as simple as that
52
- .applyPaginationFilter({ page: 1, pageSize: 10 }, { useTakeAndSkip: true });
44
+ ## Installation
45
+
46
+ ```bash
47
+ npm install typeorm-extensions
53
48
  ```
54
49
 
50
+ `typeorm` 0.3.12 or later, or 1.x, is a peer dependency. Release candidates of the next version are published under the `next` tag: `npm install typeorm-extensions@next`.
51
+
52
+ ## Features
53
+
54
+ **Typed queries** ([docs](https://koenigstag.github.io/typeorm-extensions/docs/api/typed/))
55
+ - `selectTyped`, `whereTyped` (keys inside JSON columns too; `strictParams` checks the parameter against the column type), `orderByTyped`, `groupByTyped` / `havingTyped`, `distinctOnTyped`, `returningTyped`
56
+ - typed joins and map joins (`leftJoinTyped`, `innerJoinAndMapOneTyped`, …), `typed<Root>()` for the aliases a query joins, `whereRelationExists` built from the relation metadata
57
+
58
+ **Pagination** ([docs](https://koenigstag.github.io/typeorm-extensions/docs/api/pagination/))
59
+ - `getManyWithTotals` and `getRawManyWithTotals`: the rows of a page, the total, `page` / `pageSize`, `hasMore` and the order it was read in
60
+ - `getManyWithCursor`: keyset pagination with an opaque cursor
61
+ - `applyPaginationFilter` for a page of any query
62
+
63
+ **Filters and ordering** ([docs](https://koenigstag.github.io/typeorm-extensions/docs/api/where/))
64
+ - `whereIsIn` / `whereIsNotIn` that handle empty arrays, `whereIsAny`, null-safe `whereIsDistinctFrom`
65
+ - case-insensitive search with escaped wildcards, `applyPeriodFilter` in a time zone
66
+ - `applyOrder` from a client's order filter, `orderByArrayPosition`
67
+
68
+ **Subqueries and joins**
69
+ - `whereExists` / `whereNotExists`, with the subquery's parameters renamed so they never clash with the outer query's
70
+ - `leftJoinLateral` / `innerJoinLateral`, `innerJoinUnnest`, `fromUnion` and CTE unions
71
+
72
+ **SQL helpers** ([docs](https://koenigstag.github.io/typeorm-extensions/docs/api/utils/tableHelper/))
73
+ - `tableHelper`: quoted column references, conditions, counts, sums, JSON objects and paths, time zones. Given the entity class it writes the database name of a renamed column
74
+ - the reserved qualifier guard: catches SQL like `order."userId"` before PostgreSQL rejects it
75
+
76
+ **Columns** ([docs](https://koenigstag.github.io/typeorm-extensions/docs/api/columns/generatedColumn/))
77
+ - `@CalculatedColumn` with `addSelectCalculated` / `orderByCalculated`: a value each query computes
78
+ - `@GeneratedColumn`: a column the database generates, which TypeORM reads and never writes
79
+
80
+ ## Entry points
81
+
82
+ | Import | Gives |
83
+ |---|---|
84
+ | `import 'typeorm-extensions'` | every QueryBuilder extension, and all exports |
85
+ | `import 'typeorm-extensions/dist/extensions/pagination.extension'` | one extension |
86
+ | `import { tableHelper, GeneratedColumn } from 'typeorm-extensions/helpers'` | the helpers, with no side effects |
87
+
88
+ ## Compatibility
89
+
90
+ TypeORM 0.3.12 and later, and 1.x. CI runs the test suite on SQLite with TypeORM 0.3.12, the latest 0.3 and 1.x, and on MySQL 8.4. Features that only work on some databases (array and JSON operators, `DISTINCT ON`, `orderByArrayPosition`, …) say so in their docs.
91
+
55
92
  ## Documentation
56
93
 
57
- For more information, please refer to the [documentation](https://koenigstag.github.io/typeorm-extensions).
94
+ The [documentation](https://koenigstag.github.io/typeorm-extensions/) has a page for every method, with its signature, options and examples, and the [playground](https://koenigstag.github.io/typeorm-extensions/playground/) runs them.
58
95
 
59
96
  ## ToDo list
60
97
 
61
98
  - [x] Add tests for pagination and order extensions
62
99
  - [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
100
  - [x] Order by a calculated column: `orderByCalculated`, and `applyOrder` orders a `@CalculatedColumn` field by its value
101
+ - [x] Generated columns: the `@GeneratedColumn` decorator, a `@Column` that TypeORM reads and never writes
102
+ - [x] `tableHelper(Entity, alias)` writes the database names of renamed columns
64
103
 
65
104
  ## License
66
105
 
@@ -0,0 +1,17 @@
1
+ import { ColumnOptions, ColumnType } from 'typeorm';
2
+ /** The options of `@Column`, without those that would let TypeORM write the column. */
3
+ export type GeneratedColumnOptions = Omit<ColumnOptions, 'insert' | 'update' | 'readonly'>;
4
+ /**
5
+ * A column of the table that the database computes: `GENERATED ALWAYS AS (…) STORED`. TypeORM reads it like any
6
+ * column and never writes it. `asExpression` alone doesn't stop TypeORM (0.3 and 1.x) from sending the column in
7
+ * `save`, `insert` and `update`, which the database rejects for a generated column.
8
+ *
9
+ * Pass `asExpression` and `generatedType: 'STORED'` when TypeORM creates the schema (synchronize, generated
10
+ * migrations); a column created by a hand-written migration needs neither.
11
+ *
12
+ * @example
13
+ * \@GeneratedColumn({ type: 'integer', asExpression: 'price * quantity', generatedType: 'STORED' })
14
+ * total: number;
15
+ */
16
+ export declare function GeneratedColumn(options?: GeneratedColumnOptions): PropertyDecorator;
17
+ export declare function GeneratedColumn(type: ColumnType, options?: GeneratedColumnOptions): PropertyDecorator;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GeneratedColumn = void 0;
4
+ const typeorm_1 = require("typeorm");
5
+ function GeneratedColumn(typeOrOptions, options = {}) {
6
+ const columnOptions = typeof typeOrOptions === 'object'
7
+ ? { ...typeOrOptions }
8
+ : typeOrOptions === undefined
9
+ ? { ...options }
10
+ : { ...options, type: typeOrOptions };
11
+ // TypeORM 0.3 applies `readonly` after `update`: `readonly: false` from an untyped caller would turn writes back on
12
+ delete columnOptions.readonly;
13
+ return (0, typeorm_1.Column)({ ...columnOptions, insert: false, update: false });
14
+ }
15
+ exports.GeneratedColumn = GeneratedColumn;
@@ -1 +1,2 @@
1
1
  export * from './calculated-column.decorator';
2
+ export * from './generated-column.decorator';
@@ -15,3 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./calculated-column.decorator"), exports);
18
+ __exportStar(require("./generated-column.decorator"), exports);
@@ -7,6 +7,7 @@ const prototype_utils_1 = require("../utils/prototype.utils");
7
7
  const order_utils_1 = require("../utils/order.utils");
8
8
  const where_utils_1 = require("../utils/where.utils");
9
9
  const sql_utils_1 = require("../utils/sql.utils");
10
+ const query_builder_utils_1 = require("../utils/query-builder.utils");
10
11
  const cursor_utils_1 = require("../utils/cursor.utils");
11
12
  var cursor_utils_2 = require("../utils/cursor.utils");
12
13
  Object.defineProperty(exports, "InvalidCursorError", { enumerable: true, get: function () { return cursor_utils_2.InvalidCursorError; } });
@@ -68,6 +69,7 @@ async function getManyWithCursor(builder, cursorFilter, options) {
68
69
  }
69
70
  query.orderBy();
70
71
  keys.forEach((key) => query.addOrderBy(`${alias}.${key.column.propertyPath}`, key.direction));
72
+ const appliedOrderBy = (0, query_builder_utils_1.getAppliedOrderBy)(query);
71
73
  // the key values of the last row are read from the raw result: dates and times as text, so the
72
74
  // cursor keeps the database's precision (PostgreSQL timestamps have microseconds, a JS Date has not)
73
75
  const keyAliases = keys.map((key, i) => {
@@ -90,7 +92,7 @@ async function getManyWithCursor(builder, cursorFilter, options) {
90
92
  const { entities, raw } = await query.getRawAndEntities();
91
93
  const list = entities.slice(0, pageSize);
92
94
  if (entities.length <= pageSize) {
93
- return { list, nextCursor: null, hasMore: false };
95
+ return { list, nextCursor: null, hasMore: false, pageSize, orderBy: appliedOrderBy };
94
96
  }
95
97
  // with joined collections there are several raw rows per entity: find one of the last entity's
96
98
  const last = list[list.length - 1];
@@ -105,5 +107,5 @@ async function getManyWithCursor(builder, cursorFilter, options) {
105
107
  }
106
108
  return value;
107
109
  });
108
- return { list, nextCursor: (0, cursor_utils_1.encodeCursor)(fields, values), hasMore: true };
110
+ return { list, nextCursor: (0, cursor_utils_1.encodeCursor)(fields, values), hasMore: true, pageSize, orderBy: appliedOrderBy };
109
111
  }
@@ -58,10 +58,9 @@ function getRawManyWithTotals(builder, paginationFilter, options) {
58
58
  });
59
59
  }
60
60
  async function readWithTotals(builder, paginationFilter, options, read) {
61
- var _a;
62
61
  const { disableTotalsCalculation = false, loadAll = false, ...paginationOptions } = options || {};
63
- const { limit, offset } = loadAll
64
- ? { limit: null, offset: null }
62
+ const { limit, offset, page, pageSize } = loadAll
63
+ ? { limit: null, offset: null, page: null, pageSize: null }
65
64
  : (0, pagination_utils_1.getLimitAndOffset)(paginationFilter);
66
65
  // without a total, one row past the page is what tells whether more rows follow
67
66
  const probeForMore = disableTotalsCalculation && !loadAll && limit !== null;
@@ -110,21 +109,15 @@ async function readWithTotals(builder, paginationFilter, options, read) {
110
109
  hasMore =
111
110
  total !== null && offset !== null ? offset + rows.length < total : null;
112
111
  }
113
- // allOrderBys, not orderBys: it is what the SQL is built from, so without an explicit order it
114
- // falls back to the `orderBy` of @Entity, and respects the `disable-global-order` option
115
- const orderBy = Object.entries((_a = builder.expressionMap.allOrderBys) !== null && _a !== void 0 ? _a : {}).map(([key, value]) => {
116
- const selector = (0, query_builder_utils_1.resolveDeferredKeys)(builder, key);
117
- return typeof value === 'string'
118
- ? { selector, order: value }
119
- : { selector, order: value.order, nulls: value.nulls };
120
- });
121
112
  return {
122
113
  list,
123
114
  total,
115
+ page,
116
+ pageSize,
124
117
  limit,
125
118
  offset,
126
119
  hasMore,
127
- orderBy,
120
+ orderBy: (0, query_builder_utils_1.getAppliedOrderBy)(builder),
128
121
  };
129
122
  }
130
123
  /**
package/dist/helpers.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './utils/table.utils';
2
+ export * from './decorators/generated-column.decorator';
2
3
  export { escapeLikePattern } from './utils/search.utils';
3
4
  export { COCKROACHDB_RESERVED_KEY_WORDS, findBareReservedQualifiers, POSTGRES_RESERVED_KEY_WORDS, } from './utils/reserved-qualifier.utils';
4
5
  export type { ReservedQualifier } from './utils/reserved-qualifier.utils';
package/dist/helpers.js CHANGED
@@ -16,9 +16,10 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.ReservedQualifierError = exports.enableReservedQualifierGuard = exports.disableReservedQualifierGuard = exports.POSTGRES_RESERVED_KEY_WORDS = exports.findBareReservedQualifiers = exports.COCKROACHDB_RESERVED_KEY_WORDS = exports.escapeLikePattern = void 0;
18
18
  // Side-effect-free entry point: `typeorm-extensions/helpers`.
19
- // Exports helpers (tableHelper, escapeLikePattern, the reserved qualifier guard) only and patches nothing on import;
19
+ // Exports helpers (tableHelper, escapeLikePattern, the reserved qualifier guard, @GeneratedColumn) only and patches nothing on import;
20
20
  // importing the package root (`typeorm-extensions`) installs every QueryBuilder extension.
21
21
  __exportStar(require("./utils/table.utils"), exports);
22
+ __exportStar(require("./decorators/generated-column.decorator"), exports);
22
23
  var search_utils_1 = require("./utils/search.utils");
23
24
  Object.defineProperty(exports, "escapeLikePattern", { enumerable: true, get: function () { return search_utils_1.escapeLikePattern; } });
24
25
  var reserved_qualifier_utils_1 = require("./utils/reserved-qualifier.utils");
@@ -1,4 +1,5 @@
1
1
  import type { CursorOrderParam } from '../interfaces/cursor-pagination.interface';
2
+ import type { OrderType } from '../interfaces/ordering.interface';
2
3
  export type GetManyWithCursorOptions<Entity> = {
3
4
  /**
4
5
  * Columns that order the rows, e.g. `[{ field: 'createdAt', direction: 'DESC' }]`.
@@ -18,4 +19,8 @@ export interface ListWithCursor<Entity> {
18
19
  * keyset query loads `pageSize + 1` rows, so it needs no total. Equivalent to `nextCursor !== null`.
19
20
  */
20
21
  hasMore: boolean;
22
+ /** The page size used: the `pageSize` given, rounded down, or `10` when it was missing or below 1. */
23
+ pageSize: number;
24
+ /** The ORDER BY the page was read in: the `orderBy` columns, then the primary key columns not among them. */
25
+ orderBy: OrderType[];
21
26
  }
@@ -18,6 +18,10 @@ export type GetRawManyWithTotalsOptions = Omit<GetManyWithTotalsOptions, 'useTak
18
18
  export interface ListWithTotals<Entity> {
19
19
  list: Entity[];
20
20
  total: number | null;
21
+ /** The page read, 1-based: the `page` given, or `1` when it was missing or below 1. `null` with `loadAll`. */
22
+ page: number | null;
23
+ /** The page size used: the `pageSize` given, or `10` when it was missing or below 1. `null` with `loadAll`. */
24
+ pageSize: number | null;
21
25
  limit: number | null;
22
26
  offset: number | null;
23
27
  /**
@@ -2,4 +2,6 @@ import { PaginationFilter } from "../types/interfaces/pagination.interface";
2
2
  export declare const getLimitAndOffset: ({ page, pageSize }: PaginationFilter) => {
3
3
  limit: number;
4
4
  offset: number;
5
+ page: number;
6
+ pageSize: number;
5
7
  };
@@ -11,6 +11,6 @@ const getLimitAndOffset = ({ page, pageSize }) => {
11
11
  }
12
12
  const offset = (page >= 1 ? page - 1 : 0) * pageSize;
13
13
  const limit = pageSize;
14
- return { limit, offset };
14
+ return { limit, offset, page, pageSize };
15
15
  };
16
16
  exports.getLimitAndOffset = getLimitAndOffset;
@@ -1,6 +1,8 @@
1
+ import { QueryBuilder } from 'typeorm';
1
2
  import type { ObjectLiteral, WhereExpressionBuilder } from 'typeorm';
2
3
  import type { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
3
4
  import type { KeyProxyCallback } from '../types/modules/proxy-callback.types';
5
+ import type { OrderType } from '../types/interfaces/ordering.interface';
4
6
  import { JsonPathSqlOptions } from './json-path.utils';
5
7
  /** Replaces placeholders of `deferColumnKey` in SQL built by `builder`, through the aliases it has by then. */
6
8
  export declare function resolveDeferredKeys(builder: WhereExpressionBuilder, statement: string): string;
@@ -60,3 +62,9 @@ export declare function getTypedSingleColumnKey<Type extends ObjectLiteral>(buil
60
62
  * Strings are kept as is.
61
63
  */
62
64
  export declare function getTypedSelectionKey<Type extends ObjectLiteral>(builder: WhereExpressionBuilder, selection: KeyProxyCallback<Type> | string, useQuotes?: boolean, columnsFor?: string): string | string[];
65
+ /**
66
+ * The ORDER BY a query is built with, in the order it was applied. `allOrderBys`, not `orderBys`: it is what the SQL
67
+ * is built from, so without an explicit order it falls back to the `orderBy` of @Entity, and respects the
68
+ * `disable-global-order` option.
69
+ */
70
+ export declare function getAppliedOrderBy<Entity extends ObjectLiteral>(builder: QueryBuilder<Entity>): OrderType[];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getTypedSelectionKey = exports.getTypedSingleColumnKey = exports.isTypedJsonPath = exports.getTypedColumnKey = exports.toFieldSql = exports.findFieldColumn = exports.getColumnDatabaseName = exports.resolveDeferredKeys = void 0;
3
+ exports.getAppliedOrderBy = exports.getTypedSelectionKey = exports.getTypedSingleColumnKey = exports.isTypedJsonPath = exports.getTypedColumnKey = exports.toFieldSql = exports.findFieldColumn = exports.getColumnDatabaseName = exports.resolveDeferredKeys = void 0;
4
4
  const typeorm_1 = require("typeorm");
5
5
  const QueryExpressionMap_1 = require("typeorm/query-builder/QueryExpressionMap");
6
6
  const proxy_key_utils_1 = require("./proxy-key.utils");
@@ -442,3 +442,18 @@ function getTypedSelectionKey(builder, selection, useQuotes, columnsFor) {
442
442
  return Array.isArray(keyHolder) ? keyHolder.map(toKey) : toKey(keyHolder);
443
443
  }
444
444
  exports.getTypedSelectionKey = getTypedSelectionKey;
445
+ /**
446
+ * The ORDER BY a query is built with, in the order it was applied. `allOrderBys`, not `orderBys`: it is what the SQL
447
+ * is built from, so without an explicit order it falls back to the `orderBy` of @Entity, and respects the
448
+ * `disable-global-order` option.
449
+ */
450
+ function getAppliedOrderBy(builder) {
451
+ var _a;
452
+ return Object.entries((_a = builder.expressionMap.allOrderBys) !== null && _a !== void 0 ? _a : {}).map(([key, value]) => {
453
+ const selector = resolveDeferredKeys(builder, key);
454
+ return typeof value === 'string'
455
+ ? { selector, order: value }
456
+ : { selector, order: value.order, nulls: value.nulls };
457
+ });
458
+ }
459
+ exports.getAppliedOrderBy = getAppliedOrderBy;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typeorm-extensions",
3
- "version": "2.2.0-rc.7",
3
+ "version": "2.2.0-rc.8",
4
4
  "description": "typeorm commonly used extensions",
5
5
  "main": "./dist/index.js",
6
6
  "types": "dist/index.d.ts",