typeorm-extensions 1.3.4 → 2.0.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 (101) hide show
  1. package/README.md +14 -10
  2. package/dist/constants/index.d.ts +1 -1
  3. package/dist/constants/index.js +2 -2
  4. package/dist/extensions/cursor-pagination.extension.d.ts +18 -0
  5. package/dist/extensions/cursor-pagination.extension.js +108 -0
  6. package/dist/extensions/index.d.ts +7 -0
  7. package/dist/extensions/index.js +7 -0
  8. package/dist/extensions/order-by-array-position.extension.d.ts +24 -0
  9. package/dist/extensions/order-by-array-position.extension.js +18 -0
  10. package/dist/extensions/order.extension.d.ts +3 -2
  11. package/dist/extensions/order.extension.js +16 -13
  12. package/dist/extensions/period-filter.extension.d.ts +29 -0
  13. package/dist/extensions/period-filter.extension.js +23 -0
  14. package/dist/extensions/typed/index.d.ts +10 -0
  15. package/dist/extensions/typed/index.js +10 -0
  16. package/dist/extensions/typed/typed-distinct/typed-distinct.extension.js +2 -2
  17. package/dist/extensions/typed/typed-joins/typed-joins.extension.js +2 -1
  18. package/dist/extensions/typed/typed-order-by/typed-order-by-array-position.extension.d.ts +20 -0
  19. package/dist/extensions/typed/typed-order-by/typed-order-by-array-position.extension.js +21 -0
  20. package/dist/extensions/typed/typed-order-by/typed-order-by.extension.d.ts +4 -3
  21. package/dist/extensions/typed/typed-order-by/typed-order-by.extension.js +7 -6
  22. package/dist/extensions/typed/typed-returning/typed-returning.extension.d.ts +27 -0
  23. package/dist/extensions/typed/typed-returning/typed-returning.extension.js +38 -0
  24. package/dist/extensions/typed/typed-select/typed-select.extension.d.ts +6 -6
  25. package/dist/extensions/typed/typed-select/typed-select.extension.js +11 -8
  26. package/dist/extensions/typed/typed-where/typed-period-filter.extension.d.ts +31 -0
  27. package/dist/extensions/typed/typed-where/typed-period-filter.extension.js +25 -0
  28. package/dist/extensions/typed/typed-where/typed-where-array.extension.d.ts +68 -0
  29. package/dist/extensions/typed/typed-where/typed-where-array.extension.js +41 -0
  30. package/dist/extensions/typed/typed-where/typed-where-ilike.extension.d.ts +62 -0
  31. package/dist/extensions/typed/typed-where/typed-where-ilike.extension.js +84 -0
  32. package/dist/extensions/typed/typed-where/typed-where-is-any.extension.d.ts +144 -0
  33. package/dist/extensions/typed/typed-where/typed-where-is-any.extension.js +37 -0
  34. package/dist/extensions/typed/typed-where/typed-where-is-distinct-from.extension.d.ts +53 -0
  35. package/dist/extensions/typed/typed-where/typed-where-is-distinct-from.extension.js +38 -0
  36. package/dist/extensions/typed/typed-where/typed-where-is-in.extension.d.ts +25 -26
  37. package/dist/extensions/typed/typed-where/typed-where-is-in.extension.js +16 -23
  38. package/dist/extensions/typed/typed-where/typed-where-json.extension.d.ts +53 -0
  39. package/dist/extensions/typed/typed-where/typed-where-json.extension.js +39 -0
  40. package/dist/extensions/typed/typed-where/typed-where-lower.extension.d.ts +59 -0
  41. package/dist/extensions/typed/typed-where/typed-where-lower.extension.js +86 -0
  42. package/dist/extensions/typed/typed-where/typed-where-similar.extension.d.ts +54 -0
  43. package/dist/extensions/typed/typed-where/typed-where-similar.extension.js +73 -0
  44. package/dist/extensions/typed/typed-where/typed-where.extension.d.ts +13 -13
  45. package/dist/extensions/typed/typed-where/typed-where.extension.js +4 -12
  46. package/dist/extensions/where-array.extension.d.ts +70 -0
  47. package/dist/extensions/where-array.extension.js +47 -0
  48. package/dist/extensions/where-is-any.extension.d.ts +142 -0
  49. package/dist/extensions/where-is-any.extension.js +44 -0
  50. package/dist/extensions/where-is-distinct-from.extension.d.ts +56 -0
  51. package/dist/extensions/where-is-distinct-from.extension.js +55 -0
  52. package/dist/extensions/where-json.extension.d.ts +55 -0
  53. package/dist/extensions/where-json.extension.js +64 -0
  54. package/dist/helpers.d.ts +2 -0
  55. package/dist/helpers.js +23 -0
  56. package/dist/index.d.ts +2 -0
  57. package/dist/index.js +4 -0
  58. package/dist/types/extensions/cursor-pagination.types.d.ts +16 -0
  59. package/dist/types/extensions/cursor-pagination.types.js +2 -0
  60. package/dist/types/extensions/get-many-with-totals.types.d.ts +3 -1
  61. package/dist/types/extensions/index.d.ts +4 -0
  62. package/dist/types/extensions/index.js +4 -0
  63. package/dist/types/extensions/order.types.d.ts +1 -1
  64. package/dist/types/extensions/period.types.d.ts +15 -0
  65. package/dist/types/extensions/period.types.js +2 -0
  66. package/dist/types/extensions/search.types.d.ts +68 -0
  67. package/dist/types/extensions/search.types.js +2 -0
  68. package/dist/types/extensions/typed-order-by.types.d.ts +3 -0
  69. package/dist/types/extensions/typed-order-by.types.js +2 -0
  70. package/dist/types/extensions/typed-where.types.d.ts +4 -4
  71. package/dist/types/extensions/where.types.d.ts +1 -0
  72. package/dist/types/interfaces/cursor-pagination.interface.d.ts +16 -0
  73. package/dist/types/interfaces/cursor-pagination.interface.js +2 -0
  74. package/dist/types/interfaces/index.d.ts +2 -0
  75. package/dist/types/interfaces/index.js +2 -0
  76. package/dist/types/interfaces/period.interface.d.ts +12 -0
  77. package/dist/types/interfaces/period.interface.js +2 -0
  78. package/dist/types/modules/proxy-callback.types.d.ts +26 -3
  79. package/dist/types/util-types/index.d.ts +3 -1
  80. package/dist/utils/cursor.utils.d.ts +31 -0
  81. package/dist/utils/cursor.utils.js +130 -0
  82. package/dist/utils/order.utils.d.ts +15 -0
  83. package/dist/utils/order.utils.js +45 -0
  84. package/dist/utils/period.utils.d.ts +9 -0
  85. package/dist/utils/period.utils.js +82 -0
  86. package/dist/utils/proxy-key.utils.d.ts +13 -2
  87. package/dist/utils/proxy-key.utils.js +32 -14
  88. package/dist/utils/query-builder.utils.d.ts +46 -0
  89. package/dist/utils/query-builder.utils.js +208 -0
  90. package/dist/utils/search.utils.d.ts +27 -0
  91. package/dist/utils/search.utils.js +83 -0
  92. package/dist/utils/sql.utils.d.ts +27 -1
  93. package/dist/utils/sql.utils.js +87 -9
  94. package/dist/utils/table.utils.d.ts +86 -0
  95. package/dist/utils/table.utils.js +135 -0
  96. package/dist/utils/where.utils.d.ts +19 -2
  97. package/dist/utils/where.utils.js +68 -14
  98. package/helpers/package.json +7 -0
  99. package/package.json +3 -3
  100. package/dist/utils/identifier.utils.d.ts +0 -15
  101. package/dist/utils/identifier.utils.js +0 -45
@@ -1,19 +1,81 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createUniqueParameterName = exports.stringToSQLValue = exports.stringToSQLIdentifier = exports.wrapWithBackQoutes = exports.wrapWithDoubleQoutes = void 0;
3
+ exports.assertArrayType = exports.createUniqueParameterName = exports.stringToSQLValue = exports.stringToSQLIdentifier = exports.quoteIdentifierPath = exports.quoteIdentifier = exports.getDriverType = exports.isSqliteLikeDriver = exports.isMysqlLikeDriver = exports.wrapWithBackQoutes = exports.wrapWithDoubleQoutes = void 0;
4
4
  const common_utils_1 = require("./common.utils");
5
5
  const wrapWithDoubleQoutes = (str) => str.includes('"') ? str : `"${str}"`;
6
6
  exports.wrapWithDoubleQoutes = wrapWithDoubleQoutes;
7
7
  const wrapWithBackQoutes = (str) => str.includes('`') ? str : '`' + str + '`';
8
8
  exports.wrapWithBackQoutes = wrapWithBackQoutes;
9
- const stringToSQLIdentifier = (str, driver) => str.includes('.')
10
- ? str
11
- .split('.')
12
- .map((val) => (driver === null || driver === void 0 ? void 0 : driver.includes('mysql'))
13
- ? (0, exports.wrapWithBackQoutes)(val)
14
- : (0, exports.wrapWithDoubleQoutes)(val))
15
- .join('.')
16
- : (0, exports.wrapWithDoubleQoutes)(str);
9
+ const BACKTICK = '`';
10
+ // one identifier: unquoted (user, firstName) or already quoted ("user", `user`, "a""b")
11
+ const UNQUOTED_IDENTIFIER = '[\\p{L}_][\\p{L}\\p{N}_$]*';
12
+ const DOUBLE_QUOTED_IDENTIFIER = '"(?:[^"]|"")+"';
13
+ const BACK_QUOTED_IDENTIFIER = `${BACKTICK}(?:[^${BACKTICK}]|${BACKTICK}${BACKTICK})+${BACKTICK}`;
14
+ const IDENTIFIER = `(?:${UNQUOTED_IDENTIFIER}|${DOUBLE_QUOTED_IDENTIFIER}|${BACK_QUOTED_IDENTIFIER})`;
15
+ // `id`, `user.id`, `"user"."id"`, `schema.table.column`, optionally ending with `.*`
16
+ const IDENTIFIER_PATH = new RegExp(`^${IDENTIFIER}(?:\\.${IDENTIFIER})*(?:\\.\\*)?$`, 'u');
17
+ const IDENTIFIER_PATH_SEGMENT = new RegExp(`${IDENTIFIER}|\\*`, 'gu');
18
+ /** MySQL, MariaDB and Aurora MySQL: backtick identifiers, `<=>` null-safe comparison. */
19
+ const isMysqlLikeDriver = (driver) => driver === 'mysql' || driver === 'mariadb' || driver === 'aurora-mysql';
20
+ exports.isMysqlLikeDriver = isMysqlLikeDriver;
21
+ // `string[]`: TypeORM 1.0 removed the `sqlite` driver from `DatabaseType`, 0.3.x still has it
22
+ const SQLITE_LIKE_DRIVERS = [
23
+ 'sqlite',
24
+ 'better-sqlite3',
25
+ 'sqljs',
26
+ 'capacitor',
27
+ 'cordova',
28
+ 'expo',
29
+ 'nativescript',
30
+ 'react-native',
31
+ ];
32
+ /** SQLite drivers: `IS` / `IS NOT` are null-safe comparisons. */
33
+ const isSqliteLikeDriver = (driver) => driver !== undefined && SQLITE_LIKE_DRIVERS.includes(driver);
34
+ exports.isSqliteLikeDriver = isSqliteLikeDriver;
35
+ /** Database type of the query builder's connection, e.g. `'postgres'`; undefined without one. */
36
+ function getDriverType(builder) {
37
+ var _a, _b, _c;
38
+ return (_c = (_b = (_a = builder.connection) === null || _a === void 0 ? void 0 : _a.driver) === null || _b === void 0 ? void 0 : _b.options) === null || _c === void 0 ? void 0 : _c.type;
39
+ }
40
+ exports.getDriverType = getDriverType;
41
+ /**
42
+ * Quotes one identifier, whatever characters it has: `first-name` → `"first-name"`, a `"` inside is
43
+ * doubled. An already quoted identifier is kept. For names known to be identifiers (columns, aliases).
44
+ */
45
+ const quoteIdentifier = (name, driver) => {
46
+ const mysql = (0, exports.isMysqlLikeDriver)(driver);
47
+ const quote = mysql ? BACKTICK : '"';
48
+ if (name.length >= 2 && name.startsWith(quote) && name.endsWith(quote)) {
49
+ return name;
50
+ }
51
+ return `${quote}${name.split(quote).join(quote + quote)}${quote}`;
52
+ };
53
+ exports.quoteIdentifier = quoteIdentifier;
54
+ /**
55
+ * Quotes an alias or a table: as a path when it is one (`schema.table` → `"schema"."table"`),
56
+ * otherwise as one identifier (`user-1` → `"user-1"`).
57
+ */
58
+ const quoteIdentifierPath = (path, driver) => IDENTIFIER_PATH.test(path) ? (0, exports.stringToSQLIdentifier)(path, driver) : (0, exports.quoteIdentifier)(path, driver);
59
+ exports.quoteIdentifierPath = quoteIdentifierPath;
60
+ /**
61
+ * Quotes an identifier path: `user.id` → `"user"."id"`, `user.*` → `"user".*`.
62
+ * Already quoted segments are kept. Anything that isn't a path of identifiers
63
+ * (`COALESCE(a.x, b.x)`, `LOWER(u.email)`, `u.id::text`, `*`) is returned as is.
64
+ */
65
+ const stringToSQLIdentifier = (str, driver) => {
66
+ var _a;
67
+ if (!IDENTIFIER_PATH.test(str)) {
68
+ return str;
69
+ }
70
+ const quote = (0, exports.isMysqlLikeDriver)(driver)
71
+ ? exports.wrapWithBackQoutes
72
+ : exports.wrapWithDoubleQoutes;
73
+ return ((_a = str.match(IDENTIFIER_PATH_SEGMENT)) !== null && _a !== void 0 ? _a : [])
74
+ .map((segment) => segment === '*' || segment.startsWith('"') || segment.startsWith(BACKTICK)
75
+ ? segment
76
+ : quote(segment))
77
+ .join('.');
78
+ };
17
79
  exports.stringToSQLIdentifier = stringToSQLIdentifier;
18
80
  const stringToSQLValue = (str) => `'${str}'`;
19
81
  exports.stringToSQLValue = stringToSQLValue;
@@ -22,3 +84,19 @@ const createUniqueParameterName = (prefix) => {
22
84
  return `${prefix || 'param'}_${uniqueCode}`;
23
85
  };
24
86
  exports.createUniqueParameterName = createUniqueParameterName;
87
+ // a type name, optionally schema-qualified, multi-word (`character varying`), with a
88
+ // modifier (`varchar(255)`) and array brackets: `uuid[]`, `public.order_status[]`, `"MyEnum"[]`
89
+ const TYPE_NAME_PART = '(?:[A-Za-z_][\\w$]*|"(?:[^"]|"")+")';
90
+ const ARRAY_TYPE_PATTERN = new RegExp(`^${TYPE_NAME_PART}(?:\\.${TYPE_NAME_PART})*(?: ${TYPE_NAME_PART})*` +
91
+ `(?:\\(\\d+(?:, ?\\d+)?\\))?(?:\\[\\d*\\])*$`);
92
+ /**
93
+ * Returns `arrayType` if it is a type name (`uuid[]`, `public.order_status[]`, `varchar(36)[]`),
94
+ * throws a `TypeError` otherwise: the type is inserted into the SQL as is.
95
+ */
96
+ function assertArrayType(arrayType) {
97
+ if (typeof arrayType !== 'string' || !ARRAY_TYPE_PATTERN.test(arrayType)) {
98
+ throw new TypeError(`Invalid array type "${arrayType}": expected a type name like "uuid[]"`);
99
+ }
100
+ return arrayType;
101
+ }
102
+ exports.assertArrayType = assertArrayType;
@@ -0,0 +1,86 @@
1
+ import { ObjectLiteral } from "typeorm";
2
+ export type TableHelperOptions = {
3
+ useQuotes?: boolean;
4
+ };
5
+ type JsonFieldTuple<T, Depth extends 0[] = []> = Depth['length'] extends 4 ? [string, ...string[]] : {
6
+ [K in keyof T & string]: [NonNullable<T[K]>] extends [Record<string, unknown>] ? [K, ...JsonFieldTuple<NonNullable<T[K]>, [...Depth, 0]>] | [K] : [K];
7
+ }[keyof T & string];
8
+ export type TableHelperCountOptions = {
9
+ /** `COUNT(DISTINCT column)`: count the different values of the column. Needs a column. */
10
+ distinct?: boolean;
11
+ /** Count only the rows that match this condition, e.g. `t.where('status', "= 'active'")`. */
12
+ filter?: string;
13
+ /**
14
+ * How `filter` is written:
15
+ * - `'case'` (default): `COUNT(CASE WHEN filter THEN column END)`, in every database;
16
+ * - `'filter'`: `COUNT(column) FILTER (WHERE filter)`, PostgreSQL and SQLite 3.30+ only, a few percent faster on PostgreSQL.
17
+ */
18
+ syntax?: 'case' | 'filter';
19
+ };
20
+ export type JsonFieldRest<V> = [NonNullable<V>] extends [Record<string, unknown>] ? JsonFieldTuple<NonNullable<V>> : string[];
21
+ /**
22
+ * Creates a type-safe SQL helper for building column references, WHERE expressions,
23
+ * JOIN clauses, and JSONB accessors for a given entity type.
24
+ *
25
+ * @example
26
+ * const t = tableHelper<UserEntity>('user');
27
+ * t.column('email') // → "user"."email"
28
+ * t.where('id', '= :id') // → "user"."id" = :id
29
+ * t.jsonColumn('metadata', '->>', 'role') // → "user"."metadata" ->> 'role'
30
+ */
31
+ export declare const tableHelper: <T extends ObjectLiteral>(alias?: string, options?: TableHelperOptions) => {
32
+ alias: string | undefined;
33
+ /**
34
+ * Returns a quoted SQL column identifier, e.g. `"alias"."column"`. The column is always quoted as one
35
+ * identifier, so names with other characters work too: `"user"."first-name"`.
36
+ */
37
+ column(columnName: keyof T, alias?: string): string;
38
+ /** Returns a column identifier optionally followed by an expression, e.g. `"user"."id" = :id`. */
39
+ where(columnName: keyof T, expression?: string): string;
40
+ /** Returns an unquoted join path, e.g. `user.images`. Needs the helper's alias, or `alias`. */
41
+ join(relation: keyof T, alias?: string): string;
42
+ /**
43
+ * Returns a column with a JOIN ON expression, e.g. `"user"."id" = "images"."userId"`.
44
+ * The expression is inserted as is: build its columns with another `tableHelper`
45
+ * (`t.joinOn('id', '= ' + images.column('userId'))`), since unquoted camelCase names
46
+ * like `images.userId` are folded to lowercase by PostgreSQL unless TypeORM rewrites them.
47
+ */
48
+ joinOn(thisColumn: keyof T, expression: string): string;
49
+ /** Returns `"alias"."column" IS NULL`. */
50
+ isNull(columnName: keyof T): string;
51
+ /** Returns `"alias"."column" IS NOT NULL`. */
52
+ isNotNull(columnName: keyof T): string;
53
+ /**
54
+ * Returns `CAST(COUNT("alias"."column") AS integer)`: the rows where the column isn't `NULL`.
55
+ * Without a column it is `COUNT(*)`, all the rows of the result, in every database. To count the rows
56
+ * of a table joined with LEFT JOIN, count one of its non-null columns: `images.count('id')`.
57
+ * @example
58
+ * t.count('email', { distinct: true }) // CAST(COUNT(DISTINCT "user"."email") AS integer)
59
+ * t.count(undefined, { filter: t.where('status', "= 'active'") })
60
+ * // CAST(COUNT(CASE WHEN "user"."status" = 'active' THEN 1 END) AS integer)
61
+ * t.count(undefined, { filter: t.where('status', "= 'active'"), syntax: 'filter' })
62
+ * // CAST(COUNT(*) FILTER (WHERE "user"."status" = 'active') AS integer)
63
+ */
64
+ count(columnName?: keyof T | undefined, countOptions?: TableHelperCountOptions): string;
65
+ /** Returns `CAST(expression AS type)`. */
66
+ cast(expression: string, type: string): string;
67
+ /**
68
+ * Returns a full JSONB column access expression.
69
+ * @example
70
+ * t.jsonColumn('metadata', '->>', 'role') // → "user"."metadata" ->> 'role'
71
+ * t.jsonColumn('metadata', '->>', 'settings', 'theme') // → "user"."metadata" -> 'settings' ->> 'theme'
72
+ */
73
+ jsonColumn<K extends keyof T>(columnName: K, jsonOperator: '->>' | '->', ...fieldNames: JsonFieldRest<T[K]>): string;
74
+ /**
75
+ * Returns only the JSONB operator fragment (without the column).
76
+ * Useful inside `whereTyped` conditions strings.
77
+ * @example
78
+ * t.jsonPath('metadata', '->>', 'role') // → ->> 'role'
79
+ * t.jsonPath('metadata', '->>', 'settings', 'theme') // → -> 'settings' ->> 'theme'
80
+ *
81
+ * // Combined with whereTyped:
82
+ * qb.whereTyped(u => u.metadata, `${t.jsonPath('metadata', '->>', 'role')} =`, 'admin')
83
+ */
84
+ jsonPath<K_1 extends keyof T>(_columnName: K_1, jsonOperator: '->>' | '->', ...fieldNames: JsonFieldRest<T[K_1]>): string;
85
+ };
86
+ export {};
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tableHelper = void 0;
4
+ const sql_utils_1 = require("./sql.utils");
5
+ /** Wraps a JSON key in single quotes, doubling any quote inside it (standard SQL string literal). */
6
+ function toJsonKeyLiteral(fieldName) {
7
+ return `'${String(fieldName).replace(/'/g, "''")}'`;
8
+ }
9
+ function buildJsonPath(fieldNames, jsonOperator) {
10
+ let result = '';
11
+ for (let i = 0; i < fieldNames.length; i++) {
12
+ const op = i === fieldNames.length - 1 ? jsonOperator : '->';
13
+ result += `${result ? ' ' : ''}${op} ${toJsonKeyLiteral(fieldNames[i])}`;
14
+ }
15
+ return result;
16
+ }
17
+ /**
18
+ * Creates a type-safe SQL helper for building column references, WHERE expressions,
19
+ * JOIN clauses, and JSONB accessors for a given entity type.
20
+ *
21
+ * @example
22
+ * const t = tableHelper<UserEntity>('user');
23
+ * t.column('email') // → "user"."email"
24
+ * t.where('id', '= :id') // → "user"."id" = :id
25
+ * t.jsonColumn('metadata', '->>', 'role') // → "user"."metadata" ->> 'role'
26
+ */
27
+ const tableHelper = (alias, options = {}) => ({
28
+ alias,
29
+ /**
30
+ * Returns a quoted SQL column identifier, e.g. `"alias"."column"`. The column is always quoted as one
31
+ * identifier, so names with other characters work too: `"user"."first-name"`.
32
+ */
33
+ column(columnName, alias) {
34
+ const { useQuotes = true } = options;
35
+ const tableAlias = alias !== null && alias !== void 0 ? alias : this.alias;
36
+ const column = String(columnName);
37
+ if (!useQuotes) {
38
+ return [tableAlias, column].filter(Boolean).join('.');
39
+ }
40
+ return [tableAlias ? (0, sql_utils_1.quoteIdentifierPath)(tableAlias) : undefined, (0, sql_utils_1.quoteIdentifier)(column)]
41
+ .filter(Boolean)
42
+ .join('.');
43
+ },
44
+ /** Returns a column identifier optionally followed by an expression, e.g. `"user"."id" = :id`. */
45
+ where(columnName, expression) {
46
+ const column = this.column(columnName);
47
+ return [column, expression].filter(Boolean).join(' ');
48
+ },
49
+ /** Returns an unquoted join path, e.g. `user.images`. Needs the helper's alias, or `alias`. */
50
+ join(relation, alias) {
51
+ const tableAlias = alias !== null && alias !== void 0 ? alias : this.alias;
52
+ if (!tableAlias) {
53
+ throw new TypeError(`tableHelper().join('${String(relation)}') needs an alias: tableHelper('user').join('${String(relation)}')`);
54
+ }
55
+ return `${tableAlias}.${String(relation)}`;
56
+ },
57
+ /**
58
+ * Returns a column with a JOIN ON expression, e.g. `"user"."id" = "images"."userId"`.
59
+ * The expression is inserted as is: build its columns with another `tableHelper`
60
+ * (`t.joinOn('id', '= ' + images.column('userId'))`), since unquoted camelCase names
61
+ * like `images.userId` are folded to lowercase by PostgreSQL unless TypeORM rewrites them.
62
+ */
63
+ joinOn(thisColumn, expression) {
64
+ return this.where(thisColumn, expression);
65
+ },
66
+ /** Returns `"alias"."column" IS NULL`. */
67
+ isNull(columnName) {
68
+ return this.where(columnName, 'IS NULL');
69
+ },
70
+ /** Returns `"alias"."column" IS NOT NULL`. */
71
+ isNotNull(columnName) {
72
+ return this.where(columnName, 'IS NOT NULL');
73
+ },
74
+ /**
75
+ * Returns `CAST(COUNT("alias"."column") AS integer)`: the rows where the column isn't `NULL`.
76
+ * Without a column it is `COUNT(*)`, all the rows of the result, in every database. To count the rows
77
+ * of a table joined with LEFT JOIN, count one of its non-null columns: `images.count('id')`.
78
+ * @example
79
+ * t.count('email', { distinct: true }) // CAST(COUNT(DISTINCT "user"."email") AS integer)
80
+ * t.count(undefined, { filter: t.where('status', "= 'active'") })
81
+ * // CAST(COUNT(CASE WHEN "user"."status" = 'active' THEN 1 END) AS integer)
82
+ * t.count(undefined, { filter: t.where('status', "= 'active'"), syntax: 'filter' })
83
+ * // CAST(COUNT(*) FILTER (WHERE "user"."status" = 'active') AS integer)
84
+ */
85
+ count(columnName, countOptions = {}) {
86
+ if (typeof countOptions !== 'object' || countOptions === null) {
87
+ throw new TypeError('tableHelper.count(): the second argument is an options object, e.g. { distinct: true } or { filter: "…" }');
88
+ }
89
+ const { distinct = false, filter, syntax = 'case' } = countOptions;
90
+ if (syntax !== 'case' && syntax !== 'filter') {
91
+ throw new TypeError(`tableHelper.count(): syntax is 'case' or 'filter', got "${String(syntax)}"`);
92
+ }
93
+ const column = columnName ? this.column(columnName) : undefined;
94
+ if (distinct && !column) {
95
+ throw new TypeError("tableHelper.count({ distinct: true }) needs a column, e.g. count('email', { distinct: true })");
96
+ }
97
+ // `COUNT(*)`, not `COUNT("alias".*)`, which only PostgreSQL accepts;
98
+ // a CASE can't return `*`: without a column, it counts 1 per matching row
99
+ const counted = filter && syntax === 'case' ? `CASE WHEN ${filter} THEN ${column !== null && column !== void 0 ? column : '1'} END` : column !== null && column !== void 0 ? column : '*';
100
+ let expression = `COUNT(${distinct ? 'DISTINCT ' : ''}${counted})`;
101
+ if (filter && syntax === 'filter') {
102
+ expression += ` FILTER (WHERE ${filter})`;
103
+ }
104
+ return this.cast(expression, 'integer');
105
+ },
106
+ /** Returns `CAST(expression AS type)`. */
107
+ cast(expression, type) {
108
+ return `CAST(${expression} AS ${type})`;
109
+ },
110
+ /**
111
+ * Returns a full JSONB column access expression.
112
+ * @example
113
+ * t.jsonColumn('metadata', '->>', 'role') // → "user"."metadata" ->> 'role'
114
+ * t.jsonColumn('metadata', '->>', 'settings', 'theme') // → "user"."metadata" -> 'settings' ->> 'theme'
115
+ */
116
+ jsonColumn(columnName, jsonOperator, ...fieldNames) {
117
+ const column = this.column(columnName);
118
+ const path = buildJsonPath(fieldNames, jsonOperator);
119
+ return `${column} ${path}`;
120
+ },
121
+ /**
122
+ * Returns only the JSONB operator fragment (without the column).
123
+ * Useful inside `whereTyped` conditions strings.
124
+ * @example
125
+ * t.jsonPath('metadata', '->>', 'role') // → ->> 'role'
126
+ * t.jsonPath('metadata', '->>', 'settings', 'theme') // → -> 'settings' ->> 'theme'
127
+ *
128
+ * // Combined with whereTyped:
129
+ * qb.whereTyped(u => u.metadata, `${t.jsonPath('metadata', '->>', 'role')} =`, 'admin')
130
+ */
131
+ jsonPath(_columnName, jsonOperator, ...fieldNames) {
132
+ return buildJsonPath(fieldNames, jsonOperator);
133
+ }
134
+ });
135
+ exports.tableHelper = tableHelper;
@@ -1,6 +1,23 @@
1
- import type { WhereExpressionBuilder } from 'typeorm';
1
+ import type { ObjectLiteral, WhereExpressionBuilder } from 'typeorm';
2
2
  import type { WhereMethods } from '../types/extensions/where.types';
3
3
  export declare function getValuePlaceholder<T>(value: T, paramName?: string): string;
4
- export declare function attachWhere<QB extends WhereExpressionBuilder>(builder: QB, method: WhereMethods, field: string, conditions: string, value?: unknown, options?: {
4
+ /**
5
+ * Whether `params` is a `{ name: value }` params object (a plain object or a class instance such
6
+ * as a DTO), rather than the value itself (a primitive, an array, a `Date`, a `Buffer`).
7
+ */
8
+ export declare function isParamsObject(params: unknown): params is Record<string, unknown>;
9
+ /**
10
+ * Checks whether `conditions` already references a key of `params` as a TypeORM
11
+ * named parameter (`:key` or `:...key`). Matches parameters the same way TypeORM does,
12
+ * so `::type` casts and colons inside literals don't count unless they name a `params` key.
13
+ */
14
+ export declare function hasParamPlaceholder(conditions: string, params?: unknown): boolean;
15
+ export declare function attachWhere<QB extends WhereExpressionBuilder>(builder: QB, method: WhereMethods, field: string, conditions: string, params?: unknown, options?: {
5
16
  addValue?: boolean;
6
17
  }): QB;
18
+ /**
19
+ * Adds an AND condition that narrows the whole WHERE clause. TypeORM's `andWhere` doesn't group
20
+ * the conditions before it, so after `.where(a).orWhere(b)` it gives `a OR b AND c`, i.e.
21
+ * `a OR (b AND c)`. Existing conditions with an OR are wrapped first: `(a OR b) AND c`.
22
+ */
23
+ export declare function andWhereNarrowing<QB extends WhereExpressionBuilder>(builder: QB, condition: string, parameters?: ObjectLiteral): QB;
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.attachWhere = exports.getValuePlaceholder = void 0;
3
+ exports.andWhereNarrowing = exports.attachWhere = exports.hasParamPlaceholder = exports.isParamsObject = exports.getValuePlaceholder = void 0;
4
4
  const sql_utils_1 = require("./sql.utils");
5
+ const query_builder_utils_1 = require("./query-builder.utils");
5
6
  function getValuePlaceholder(value, paramName) {
6
7
  if (value === undefined || value === null || paramName === undefined) {
7
8
  return 'NULL';
@@ -12,30 +13,83 @@ function getValuePlaceholder(value, paramName) {
12
13
  return `:${paramName}`;
13
14
  }
14
15
  exports.getValuePlaceholder = getValuePlaceholder;
15
- function attachWhere(builder, method, field, conditions, value, options) {
16
- const { addValue = true } = options || {};
16
+ const isDate = (value) => Object.prototype.toString.call(value) === '[object Date]';
17
+ /**
18
+ * Objects that are parameter values themselves, not `{ name: value }` params objects:
19
+ * `Date`, and binary data (`Buffer`, typed arrays, `ArrayBuffer`).
20
+ */
21
+ const isValueObject = (value) => isDate(value) || ArrayBuffer.isView(value) || value instanceof ArrayBuffer;
22
+ /**
23
+ * Whether `params` is a `{ name: value }` params object (a plain object or a class instance such
24
+ * as a DTO), rather than the value itself (a primitive, an array, a `Date`, a `Buffer`).
25
+ */
26
+ function isParamsObject(params) {
27
+ return (params !== null &&
28
+ typeof params === 'object' &&
29
+ !Array.isArray(params) &&
30
+ !isValueObject(params));
31
+ }
32
+ exports.isParamsObject = isParamsObject;
33
+ /**
34
+ * Checks whether `conditions` already references a key of `params` as a TypeORM
35
+ * named parameter (`:key` or `:...key`). Matches parameters the same way TypeORM does,
36
+ * so `::type` casts and colons inside literals don't count unless they name a `params` key.
37
+ */
38
+ function hasParamPlaceholder(conditions, params) {
39
+ if (!isParamsObject(params)) {
40
+ return false;
41
+ }
42
+ const placeholderRegex = /:(?:\.\.\.)?([A-Za-z0-9_.]+)/g;
43
+ let match;
44
+ while ((match = placeholderRegex.exec(conditions)) !== null) {
45
+ if (Object.prototype.hasOwnProperty.call(params, match[1])) {
46
+ return true;
47
+ }
48
+ }
49
+ return false;
50
+ }
51
+ exports.hasParamPlaceholder = hasParamPlaceholder;
52
+ function attachWhere(builder, method, field, conditions, params, options) {
53
+ let { addValue } = options || {};
17
54
  if (method && typeof field === 'string' && typeof conditions === 'string') {
18
- field = (0, sql_utils_1.stringToSQLIdentifier)(field);
19
- if (addValue) {
20
- // is object (not primitive, null or array)
21
- const firstValue = value !== null && typeof value === 'object' && !Array.isArray(value)
22
- ? Object.values(value)[0]
23
- : value;
55
+ field = (0, query_builder_utils_1.toFieldSql)(builder, field);
56
+ const hasParams = typeof params !== 'undefined';
57
+ // `addValue` defaults to `true` unless `conditions` already references one of the `params` keys
58
+ addValue = addValue !== null && addValue !== void 0 ? addValue : !hasParamPlaceholder(conditions, params);
59
+ if (addValue && hasParams) {
60
+ // pick first value from params object or pass as is (a Date or a Buffer is a value too)
61
+ const firstValue = isParamsObject(params) ? Object.values(params)[0] : params;
24
62
  // 0, false, '' are valid values like any other
25
- // null is a valid value, but undefined and NaN are not
63
+ // null, undefined, NaN and invalid dates are equal to NULL
26
64
  const valueIsValid = firstValue !== undefined &&
27
- (typeof firstValue === 'number' ? !isNaN(firstValue) : true);
65
+ (typeof firstValue === 'number' ? !isNaN(firstValue) : true) &&
66
+ (isDate(firstValue) ? !isNaN(firstValue.getTime()) : true);
28
67
  const paramName = valueIsValid
29
68
  ? (0, sql_utils_1.createUniqueParameterName)(method)
30
69
  : undefined;
31
- const params = paramName && valueIsValid ? { [paramName]: firstValue } : undefined;
70
+ const queryParams = paramName && valueIsValid ? { [paramName]: firstValue } : undefined;
32
71
  const valuePlaceholder = getValuePlaceholder(firstValue, paramName);
33
- return builder[method](`${field} ${conditions} ${valuePlaceholder}`, params);
72
+ return builder[method](`${field} ${conditions} ${valuePlaceholder}`, queryParams);
34
73
  }
35
74
  else {
36
- return builder[method](`${field} ${conditions}`, value);
75
+ return builder[method](`${field} ${conditions}`, params);
37
76
  }
38
77
  }
39
78
  return builder;
40
79
  }
41
80
  exports.attachWhere = attachWhere;
81
+ /**
82
+ * Adds an AND condition that narrows the whole WHERE clause. TypeORM's `andWhere` doesn't group
83
+ * the conditions before it, so after `.where(a).orWhere(b)` it gives `a OR b AND c`, i.e.
84
+ * `a OR (b AND c)`. Existing conditions with an OR are wrapped first: `(a OR b) AND c`.
85
+ */
86
+ function andWhereNarrowing(builder, condition, parameters) {
87
+ const expressionMap = builder.expressionMap;
88
+ const wheres = expressionMap === null || expressionMap === void 0 ? void 0 : expressionMap.wheres;
89
+ if (wheres === null || wheres === void 0 ? void 0 : wheres.some((where) => where.type === 'or')) {
90
+ // a nested clause list is rendered in parentheses
91
+ expressionMap.wheres = [{ type: 'simple', condition: wheres }];
92
+ }
93
+ return builder.andWhere(condition, parameters);
94
+ }
95
+ exports.andWhereNarrowing = andWhereNarrowing;
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "typeorm-extensions/helpers",
3
+ "private": true,
4
+ "main": "../dist/helpers.js",
5
+ "types": "../dist/helpers.d.ts",
6
+ "sideEffects": false
7
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "typeorm-extensions",
3
- "version": "1.3.4",
3
+ "version": "2.0.0",
4
4
  "description": "typeorm commonly used extensions",
5
5
  "main": "./dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
- "files": ["dist", "LICENSE", "README.md", "package.json"],
7
+ "files": ["dist", "helpers", "LICENSE", "README.md", "package.json"],
8
8
  "type": "commonjs",
9
9
  "homepage": "https://koenigstag.github.io/typeorm-extensions/",
10
10
  "repository": {
@@ -25,7 +25,7 @@
25
25
  "author": "koenigstag",
26
26
  "license": "ISC",
27
27
  "peerDependencies": {
28
- "typeorm": "^0.3.12"
28
+ "typeorm": "^0.3.12 || ^1.0.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@typescript-eslint/eslint-plugin": "^7.1.1",
@@ -1,15 +0,0 @@
1
- export declare class Identifier {
2
- readonly field: string;
3
- readonly alias?: string | undefined;
4
- private readonly useDoubleQuotes?;
5
- private readonly useAlias;
6
- constructor(field: string, alias?: string | undefined, useDoubleQuotes?: boolean | undefined);
7
- fieldAsAlias(): string;
8
- fieldIdentifier(): string;
9
- get aliasSql(): string | undefined;
10
- get fieldSql(): string;
11
- private calcUseAlias;
12
- static fieldToSql(field: string, useDoubleQuotes?: boolean): string;
13
- static aliasToSql(alias: string | undefined, useDoubleQuotes?: boolean): string | undefined;
14
- static toIdentifier(value: string, useDoubleQuotes?: boolean): string;
15
- }
@@ -1,45 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Identifier = void 0;
4
- const sql_utils_1 = require("./sql.utils");
5
- class Identifier {
6
- constructor(field, alias, useDoubleQuotes) {
7
- this.field = field;
8
- this.alias = alias;
9
- this.useDoubleQuotes = useDoubleQuotes;
10
- this.useAlias = this.calcUseAlias();
11
- }
12
- fieldAsAlias() {
13
- if (this.useAlias) {
14
- return `${this.fieldSql} AS ${this.aliasSql}`;
15
- }
16
- return this.fieldSql;
17
- }
18
- fieldIdentifier() {
19
- if (this.useAlias) {
20
- return `${this.aliasSql}.${this.fieldSql}`;
21
- }
22
- return this.fieldSql;
23
- }
24
- get aliasSql() {
25
- return Identifier.aliasToSql(this.alias, this.useDoubleQuotes);
26
- }
27
- get fieldSql() {
28
- return Identifier.fieldToSql(this.field, this.useDoubleQuotes);
29
- }
30
- /* PRIVATE */
31
- calcUseAlias() {
32
- return !!this.alias && !this.field.includes('.');
33
- }
34
- /* STATIC */
35
- static fieldToSql(field, useDoubleQuotes) {
36
- return this.toIdentifier(field, useDoubleQuotes);
37
- }
38
- static aliasToSql(alias, useDoubleQuotes) {
39
- return alias ? this.toIdentifier(alias, useDoubleQuotes) : undefined;
40
- }
41
- static toIdentifier(value, useDoubleQuotes) {
42
- return useDoubleQuotes ? (0, sql_utils_1.stringToSQLIdentifier)(value) : value;
43
- }
44
- }
45
- exports.Identifier = Identifier;