ts-prorm-orm 1.2.1 → 1.2.2
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/CHANGELOG.md +29 -0
- package/README.md +3 -0
- package/dist/dialects/cockroachdb/index.js +10 -4
- package/dist/dialects/mariadb/index.js +10 -4
- package/dist/dialects/mysql/index.js +10 -4
- package/dist/dialects/postgres/index.js +10 -4
- package/dist/dialects/sqlite/index.js +10 -4
- package/dist/models/model.d.ts +2 -2
- package/dist/models/model.js +4 -3
- package/dist/operators.d.ts +11 -0
- package/dist/operators.js +35 -0
- package/dist/prorm.d.ts +11 -0
- package/dist/prorm.js +130 -65
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.2]
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`HAVING` silently dropped every operator.** The having builders compared
|
|
13
|
+
operator keys against stringified symbol names (`'Symbol(gt)'`), which
|
|
14
|
+
`Object.keys()` cannot produce and which the `$gt` string form never matched
|
|
15
|
+
either — so `having: { n: { [Op.gt]: 1 } }` became `HAVING n = 1` and returned
|
|
16
|
+
wrong rows without erroring. HAVING now reuses the WHERE builder, which
|
|
17
|
+
already handled both forms, across sqlite, mysql, mariadb, postgres and
|
|
18
|
+
cockroachdb.
|
|
19
|
+
- **`where: { '$assoc.column$': v }` was silently ignored.** Every where-builder
|
|
20
|
+
skips keys starting with `$` as unrecognized logical operators, so the
|
|
21
|
+
condition was dropped and the query returned every parent. These now filter
|
|
22
|
+
the parent rows via the association's include; naming an alias that isn't
|
|
23
|
+
included raises `AssociationError` instead of being discarded.
|
|
24
|
+
- **Aggregates ignored paranoid filtering and scopes**, so `sum`/`min`/`max`
|
|
25
|
+
could include soft-deleted rows that `findAll` excluded. They also rebuilt
|
|
26
|
+
their WHERE by regex-matching `/WHERE (.+)$/` out of a rendered SELECT, which
|
|
27
|
+
breaks on any condition containing that word. All four now share one
|
|
28
|
+
implementation honouring `where`, paranoid, scopes and filtering includes, and
|
|
29
|
+
report unknown attribute names.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **`avg()`** — the one standard aggregate that was missing.
|
|
34
|
+
- `exports` now permits `ts-prorm-orm/package.json`, which build tooling and
|
|
35
|
+
version checks commonly read.
|
|
36
|
+
|
|
8
37
|
## [1.2.1]
|
|
9
38
|
|
|
10
39
|
### Fixed
|
package/README.md
CHANGED
|
@@ -29,6 +29,9 @@ A TypeScript ORM supporting multiple database dialects with full TypeScript supp
|
|
|
29
29
|
- **Validation**: Built-in and custom validators
|
|
30
30
|
- **Stored Procedures**: Create and call stored procedures (across supported dialects)
|
|
31
31
|
- **Triggers**: Database triggers with FOR EACH ROW and WHEN clauses (across supported dialects)
|
|
32
|
+
- **Aggregates**: `count`, `sum`, `avg`, `min`, `max` — all issuing real SQL
|
|
33
|
+
aggregates and honouring `where`, scopes, soft-deletes and relation filters;
|
|
34
|
+
`group` + `having` for grouped results
|
|
32
35
|
- **Sequences**: `createSequence()` on PostgreSQL, MariaDB, Oracle, MSSQL, Db2 and
|
|
33
36
|
friends. SQLite and MySQL have no sequences and report that explicitly rather
|
|
34
37
|
than emitting SQL the driver rejects
|
|
@@ -3797,12 +3797,18 @@ class CockroachDBDialect {
|
|
|
3797
3797
|
*/
|
|
3798
3798
|
buildHavingClause(having) {
|
|
3799
3799
|
const values = [];
|
|
3800
|
-
if (!having ||
|
|
3800
|
+
if (!having ||
|
|
3801
|
+
(typeof having === 'object' &&
|
|
3802
|
+
Object.keys(having).length === 0 &&
|
|
3803
|
+
Object.getOwnPropertySymbols(having).length === 0)) {
|
|
3801
3804
|
return { sql: '', values };
|
|
3802
3805
|
}
|
|
3803
|
-
//
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
+
// HAVING shares WHERE's expression grammar, so reuse that builder rather
|
|
3807
|
+
// than a second dispatch. `buildCondition` compared operator keys against
|
|
3808
|
+
// stringified symbol names ('Symbol(gt)'), which Object.keys() can never
|
|
3809
|
+
// produce and which the `$gt` string form never matches either - so every
|
|
3810
|
+
// operator silently degraded to equality and HAVING returned wrong rows.
|
|
3811
|
+
return this.buildWhereClause(having);
|
|
3806
3812
|
}
|
|
3807
3813
|
/**
|
|
3808
3814
|
* Build the expression used after `AS OF SYSTEM TIME` for CockroachDB
|
|
@@ -2872,12 +2872,18 @@ class MariaDBDialect {
|
|
|
2872
2872
|
*/
|
|
2873
2873
|
buildHavingClause(having) {
|
|
2874
2874
|
const values = [];
|
|
2875
|
-
if (!having ||
|
|
2875
|
+
if (!having ||
|
|
2876
|
+
(typeof having === 'object' &&
|
|
2877
|
+
Object.keys(having).length === 0 &&
|
|
2878
|
+
Object.getOwnPropertySymbols(having).length === 0)) {
|
|
2876
2879
|
return { sql: '', values };
|
|
2877
2880
|
}
|
|
2878
|
-
//
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
+
// HAVING shares WHERE's expression grammar, so reuse that builder rather
|
|
2882
|
+
// than a second dispatch. `buildCondition` compared operator keys against
|
|
2883
|
+
// stringified symbol names ('Symbol(gt)'), which Object.keys() can never
|
|
2884
|
+
// produce and which the `$gt` string form never matches either - so every
|
|
2885
|
+
// operator silently degraded to equality and HAVING returned wrong rows.
|
|
2886
|
+
return this.buildWhereClause(having);
|
|
2881
2887
|
}
|
|
2882
2888
|
/**
|
|
2883
2889
|
* Build a SELECT query with support for Common Table Expressions (CTEs)
|
|
@@ -3368,12 +3368,18 @@ class MySQLDialect {
|
|
|
3368
3368
|
*/
|
|
3369
3369
|
buildHavingClause(having) {
|
|
3370
3370
|
const values = [];
|
|
3371
|
-
if (!having ||
|
|
3371
|
+
if (!having ||
|
|
3372
|
+
(typeof having === 'object' &&
|
|
3373
|
+
Object.keys(having).length === 0 &&
|
|
3374
|
+
Object.getOwnPropertySymbols(having).length === 0)) {
|
|
3372
3375
|
return { sql: '', values };
|
|
3373
3376
|
}
|
|
3374
|
-
//
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
+
// HAVING shares WHERE's expression grammar, so reuse that builder rather
|
|
3378
|
+
// than a second dispatch. `buildCondition` compared operator keys against
|
|
3379
|
+
// stringified symbol names ('Symbol(gt)'), which Object.keys() can never
|
|
3380
|
+
// produce and which the `$gt` string form never matches either - so every
|
|
3381
|
+
// operator silently degraded to equality and HAVING returned wrong rows.
|
|
3382
|
+
return this.buildWhereClause(having);
|
|
3377
3383
|
}
|
|
3378
3384
|
/**
|
|
3379
3385
|
* Build a `WITH [RECURSIVE] name (cols) AS (query), ...` prefix for the
|
|
@@ -4138,12 +4138,18 @@ class PostgresDialect {
|
|
|
4138
4138
|
*/
|
|
4139
4139
|
buildHavingClause(having) {
|
|
4140
4140
|
const values = [];
|
|
4141
|
-
if (!having ||
|
|
4141
|
+
if (!having ||
|
|
4142
|
+
(typeof having === 'object' &&
|
|
4143
|
+
Object.keys(having).length === 0 &&
|
|
4144
|
+
Object.getOwnPropertySymbols(having).length === 0)) {
|
|
4142
4145
|
return { sql: '', values };
|
|
4143
4146
|
}
|
|
4144
|
-
//
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
+
// HAVING shares WHERE's expression grammar, so reuse that builder rather
|
|
4148
|
+
// than a second dispatch. `buildCondition` compared operator keys against
|
|
4149
|
+
// stringified symbol names ('Symbol(gt)'), which Object.keys() can never
|
|
4150
|
+
// produce and which the `$gt` string form never matches either - so every
|
|
4151
|
+
// operator silently degraded to equality and HAVING returned wrong rows.
|
|
4152
|
+
return this.buildWhereClause(having);
|
|
4147
4153
|
}
|
|
4148
4154
|
/**
|
|
4149
4155
|
* Build a SELECT query with PostgreSQL-specific features
|
|
@@ -2940,12 +2940,18 @@ class SQLiteDialect {
|
|
|
2940
2940
|
*/
|
|
2941
2941
|
buildHavingClause(having) {
|
|
2942
2942
|
const values = [];
|
|
2943
|
-
if (!having ||
|
|
2943
|
+
if (!having ||
|
|
2944
|
+
(typeof having === 'object' &&
|
|
2945
|
+
Object.keys(having).length === 0 &&
|
|
2946
|
+
Object.getOwnPropertySymbols(having).length === 0)) {
|
|
2944
2947
|
return { sql: '', values };
|
|
2945
2948
|
}
|
|
2946
|
-
//
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
+
// HAVING shares WHERE's expression grammar, so reuse that builder rather
|
|
2950
|
+
// than a second dispatch. `buildCondition` compared operator keys against
|
|
2951
|
+
// stringified symbol names ('Symbol(gt)'), which Object.keys() can never
|
|
2952
|
+
// produce and which the `$gt` string form never matches either - so every
|
|
2953
|
+
// operator silently degraded to equality and HAVING returned wrong rows.
|
|
2954
|
+
return this.buildWhereClause(having);
|
|
2949
2955
|
}
|
|
2950
2956
|
/**
|
|
2951
2957
|
* Build a `WITH [RECURSIVE] name [(cols)] AS (query), ...` clause from
|
package/dist/models/model.d.ts
CHANGED
|
@@ -763,9 +763,9 @@ declare const _default: {
|
|
|
763
763
|
path: string;
|
|
764
764
|
value?: any;
|
|
765
765
|
}) => Record<string, any>;
|
|
766
|
-
readonly literal: (value: string) => import("
|
|
766
|
+
readonly literal: (value: string) => import("../operators").LiteralValue;
|
|
767
767
|
readonly cast: (value: any, type: string) => import("./operators").CastExpression;
|
|
768
|
-
readonly extract: (field: any, part: import("
|
|
768
|
+
readonly extract: (field: any, part: import("../operators").ExtractPart) => import("./operators").ExtractExpression;
|
|
769
769
|
readonly conv: (value: any, from: string | null | undefined, to: string) => import("./operators").ConvExpression;
|
|
770
770
|
readonly where: (column: {
|
|
771
771
|
$col: string;
|
package/dist/models/model.js
CHANGED
|
@@ -41,6 +41,7 @@ exports.ModelValidator = exports.ModelInstance = exports.Model = void 0;
|
|
|
41
41
|
exports.createModel = createModel;
|
|
42
42
|
const eager_load_1 = require("./eager-load");
|
|
43
43
|
const operators_1 = require("./operators");
|
|
44
|
+
const operators_2 = require("../operators");
|
|
44
45
|
const scopes_1 = require("./scopes");
|
|
45
46
|
const associations_1 = require("./associations");
|
|
46
47
|
const validators_1 = require("../validators");
|
|
@@ -2869,7 +2870,7 @@ function createModel(prorm, modelName, attributes, options = {}) {
|
|
|
2869
2870
|
limit: findOptions.limit,
|
|
2870
2871
|
offset: findOptions.offset,
|
|
2871
2872
|
group: findOptions.group,
|
|
2872
|
-
having: findOptions.having,
|
|
2873
|
+
having: (0, operators_2.normalizeOperatorKeys)(findOptions.having),
|
|
2873
2874
|
raw: findOptions.raw,
|
|
2874
2875
|
subQuery: subquery,
|
|
2875
2876
|
duplicate: hasDuplicateColumns,
|
|
@@ -2947,7 +2948,7 @@ function createModel(prorm, modelName, attributes, options = {}) {
|
|
|
2947
2948
|
limit: findOptions.limit,
|
|
2948
2949
|
offset: findOptions.offset,
|
|
2949
2950
|
group: findOptions.group,
|
|
2950
|
-
having: findOptions.having,
|
|
2951
|
+
having: (0, operators_2.normalizeOperatorKeys)(findOptions.having),
|
|
2951
2952
|
subQuery: findOptions.subQuery,
|
|
2952
2953
|
raw: findOptions.raw,
|
|
2953
2954
|
distinct: findOptions.distinct,
|
|
@@ -3048,7 +3049,7 @@ function createModel(prorm, modelName, attributes, options = {}) {
|
|
|
3048
3049
|
where: findOptions.where,
|
|
3049
3050
|
include: findOptions.include,
|
|
3050
3051
|
group: findOptions.group,
|
|
3051
|
-
having: findOptions.having,
|
|
3052
|
+
having: (0, operators_2.normalizeOperatorKeys)(findOptions.having),
|
|
3052
3053
|
raw: findOptions.raw,
|
|
3053
3054
|
distinct: findOptions.distinct,
|
|
3054
3055
|
col: findOptions.col,
|
package/dist/operators.d.ts
CHANGED
|
@@ -1716,6 +1716,17 @@ export interface WhereConditionOptions {
|
|
|
1716
1716
|
/**
|
|
1717
1717
|
* Convert operator symbol to where clause key
|
|
1718
1718
|
*/
|
|
1719
|
+
/**
|
|
1720
|
+
* Recursively rewrite `Op.*` symbol keys into their `$string` equivalents.
|
|
1721
|
+
*
|
|
1722
|
+
* `Op.gt` and friends are Symbols, and most builders walk conditions with
|
|
1723
|
+
* `Object.entries()`, which skips symbol keys entirely. The WHERE path
|
|
1724
|
+
* normalizes them per-dialect; HAVING did not, so
|
|
1725
|
+
* `having: { n: { [Op.gt]: 1 } }` lost the operator and silently degraded to
|
|
1726
|
+
* equality - returning wrong rows rather than an error. Normalizing centrally
|
|
1727
|
+
* means every consumer sees the string form.
|
|
1728
|
+
*/
|
|
1729
|
+
export declare function normalizeOperatorKeys<T>(value: T): T;
|
|
1719
1730
|
export declare function operatorToWhereKey(op: OperatorSymbol): string;
|
|
1720
1731
|
/**
|
|
1721
1732
|
* Type for where condition object (plain object with field conditions)
|
package/dist/operators.js
CHANGED
|
@@ -5,6 +5,7 @@ exports.isColumnReference = isColumnReference;
|
|
|
5
5
|
exports.isColOperator = isColOperator;
|
|
6
6
|
exports.isOperator = isOperator;
|
|
7
7
|
exports.getOperatorString = getOperatorString;
|
|
8
|
+
exports.normalizeOperatorKeys = normalizeOperatorKeys;
|
|
8
9
|
exports.operatorToWhereKey = operatorToWhereKey;
|
|
9
10
|
exports.where = where;
|
|
10
11
|
exports.and = and;
|
|
@@ -1134,6 +1135,40 @@ function getOperatorString(op) {
|
|
|
1134
1135
|
/**
|
|
1135
1136
|
* Convert operator symbol to where clause key
|
|
1136
1137
|
*/
|
|
1138
|
+
/**
|
|
1139
|
+
* Recursively rewrite `Op.*` symbol keys into their `$string` equivalents.
|
|
1140
|
+
*
|
|
1141
|
+
* `Op.gt` and friends are Symbols, and most builders walk conditions with
|
|
1142
|
+
* `Object.entries()`, which skips symbol keys entirely. The WHERE path
|
|
1143
|
+
* normalizes them per-dialect; HAVING did not, so
|
|
1144
|
+
* `having: { n: { [Op.gt]: 1 } }` lost the operator and silently degraded to
|
|
1145
|
+
* equality - returning wrong rows rather than an error. Normalizing centrally
|
|
1146
|
+
* means every consumer sees the string form.
|
|
1147
|
+
*/
|
|
1148
|
+
function normalizeOperatorKeys(value) {
|
|
1149
|
+
if (Array.isArray(value)) {
|
|
1150
|
+
return value.map((v) => normalizeOperatorKeys(v));
|
|
1151
|
+
}
|
|
1152
|
+
if (value === null || typeof value !== 'object')
|
|
1153
|
+
return value;
|
|
1154
|
+
// Leave class instances (Date, Buffer, literal/fn expressions) alone.
|
|
1155
|
+
if (value instanceof Date ||
|
|
1156
|
+
Buffer.isBuffer(value) ||
|
|
1157
|
+
value.__type !== undefined) {
|
|
1158
|
+
return value;
|
|
1159
|
+
}
|
|
1160
|
+
const src = value;
|
|
1161
|
+
const out = {};
|
|
1162
|
+
for (const [k, v] of Object.entries(src)) {
|
|
1163
|
+
out[k] = normalizeOperatorKeys(v);
|
|
1164
|
+
}
|
|
1165
|
+
for (const sym of Object.getOwnPropertySymbols(src)) {
|
|
1166
|
+
const strKey = operatorToWhereKey(sym);
|
|
1167
|
+
if (strKey)
|
|
1168
|
+
out[strKey] = normalizeOperatorKeys(src[sym]);
|
|
1169
|
+
}
|
|
1170
|
+
return out;
|
|
1171
|
+
}
|
|
1137
1172
|
function operatorToWhereKey(op) {
|
|
1138
1173
|
if (op === exports.Op.and)
|
|
1139
1174
|
return '$and';
|
package/dist/prorm.d.ts
CHANGED
|
@@ -1672,6 +1672,17 @@ export declare class Prorm extends EventEmitter {
|
|
|
1672
1672
|
* - `group` returns the number of groups
|
|
1673
1673
|
*/
|
|
1674
1674
|
private _countRows;
|
|
1675
|
+
/**
|
|
1676
|
+
* Run a single-value aggregate (MAX/MIN/SUM/AVG) for a model.
|
|
1677
|
+
*
|
|
1678
|
+
* `max`/`min`/`sum` each carried their own copy of this, built the WHERE by
|
|
1679
|
+
* rendering a throwaway SELECT and pulling the clause back out with
|
|
1680
|
+
* `/WHERE (.+)$/` — which breaks on any condition containing the word WHERE
|
|
1681
|
+
* (a subquery, or a string value) — and none of them honored paranoid
|
|
1682
|
+
* filtering, scopes, or filtering includes, so an aggregate could silently
|
|
1683
|
+
* include soft-deleted rows that `findAll` excluded.
|
|
1684
|
+
*/
|
|
1685
|
+
private _aggregate;
|
|
1675
1686
|
/**
|
|
1676
1687
|
* The view of this instance that the shared eager-loader needs.
|
|
1677
1688
|
*/
|
package/dist/prorm.js
CHANGED
|
@@ -461,6 +461,38 @@ function resolveModelPrimaryKeyAttr(m) {
|
|
|
461
461
|
}
|
|
462
462
|
return 'id';
|
|
463
463
|
}
|
|
464
|
+
/**
|
|
465
|
+
* Split `$alias.column$` keys out of a where clause.
|
|
466
|
+
*
|
|
467
|
+
* This syntax names a column on an included association. Every where-builder
|
|
468
|
+
* skips keys beginning with `$` as unrecognized logical operators, so these
|
|
469
|
+
* conditions were dropped without a word and the query came back unfiltered.
|
|
470
|
+
* Pulling them out here lets each be applied to its association's include,
|
|
471
|
+
* which does filter the parent rows.
|
|
472
|
+
*/
|
|
473
|
+
function extractAssociationConditions(where) {
|
|
474
|
+
const conditions = [];
|
|
475
|
+
if (!where || typeof where !== 'object')
|
|
476
|
+
return { rest: where, conditions };
|
|
477
|
+
const rest = {};
|
|
478
|
+
for (const [key, value] of Object.entries(where)) {
|
|
479
|
+
const match = /^\$(.+)\$$/.exec(key);
|
|
480
|
+
if (match) {
|
|
481
|
+
const path = match[1];
|
|
482
|
+
const dot = path.lastIndexOf('.');
|
|
483
|
+
if (dot > 0) {
|
|
484
|
+
conditions.push({
|
|
485
|
+
alias: path.slice(0, dot),
|
|
486
|
+
column: path.slice(dot + 1),
|
|
487
|
+
value,
|
|
488
|
+
});
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
rest[key] = value;
|
|
493
|
+
}
|
|
494
|
+
return { rest, conditions };
|
|
495
|
+
}
|
|
464
496
|
/**
|
|
465
497
|
* Derive a model's table name from its name and options.
|
|
466
498
|
*
|
|
@@ -2876,6 +2908,27 @@ class Prorm extends events_1.EventEmitter {
|
|
|
2876
2908
|
// queries returned every parent regardless. Resolve the matching parent
|
|
2877
2909
|
// keys first and constrain the main query with them, so the restriction
|
|
2878
2910
|
// is applied before LIMIT/OFFSET rather than after.
|
|
2911
|
+
// `where: { '$posts.title$': 'x' }` names a column on an included
|
|
2912
|
+
// association. The where-builders skip any key starting with `$` as an
|
|
2913
|
+
// unrecognized logical operator, so these conditions were silently
|
|
2914
|
+
// dropped and the query returned every parent unfiltered. Move each one
|
|
2915
|
+
// onto its association's include, where it filters parents.
|
|
2916
|
+
const crossTableWhere = extractAssociationConditions(where);
|
|
2917
|
+
if (crossTableWhere.conditions.length > 0) {
|
|
2918
|
+
where = crossTableWhere.rest;
|
|
2919
|
+
const includeList = (findOptions.include || []).slice();
|
|
2920
|
+
for (const { alias, column, value } of crossTableWhere.conditions) {
|
|
2921
|
+
const target = includeList.find((inc) => (inc?.as ?? inc?.model?.name) === alias);
|
|
2922
|
+
if (target) {
|
|
2923
|
+
target.where = { ...(target.where || {}), [column]: value };
|
|
2924
|
+
}
|
|
2925
|
+
else {
|
|
2926
|
+
throw new errors_1.AssociationError(`where key '$${alias}.${column}$' refers to '${alias}', which is not ` +
|
|
2927
|
+
`included in this query. Add \`include: [{ model: ..., as: '${alias}' }]\`.`, { association: alias });
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
findOptions = { ...findOptions, include: includeList };
|
|
2931
|
+
}
|
|
2879
2932
|
if (findOptions.include && findOptions.include.length > 0) {
|
|
2880
2933
|
for (const rawInclude of findOptions.include) {
|
|
2881
2934
|
const filter = await (0, eager_load_1.resolveRequiredIncludeFilter)(self.eagerLoadContext(), model, modelName, rawInclude);
|
|
@@ -2915,7 +2968,10 @@ class Prorm extends events_1.EventEmitter {
|
|
|
2915
2968
|
limit: findOptions.limit,
|
|
2916
2969
|
offset: findOptions.offset,
|
|
2917
2970
|
group: findOptions.group,
|
|
2918
|
-
|
|
2971
|
+
// Normalize Op.* symbol keys: the HAVING builders walk conditions
|
|
2972
|
+
// with Object.entries(), which skips symbols, so an unnormalized
|
|
2973
|
+
// `{ [Op.gt]: 1 }` silently degraded to `= 1`.
|
|
2974
|
+
having: (0, operators_1.normalizeOperatorKeys)(findOptions.having),
|
|
2919
2975
|
raw: findOptions.raw,
|
|
2920
2976
|
subQuery: findOptions.subquery,
|
|
2921
2977
|
benchmark: findOptions.benchmark,
|
|
@@ -3849,72 +3905,20 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3849
3905
|
});
|
|
3850
3906
|
return association;
|
|
3851
3907
|
},
|
|
3852
|
-
//
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
const dialect = self.dialect;
|
|
3857
|
-
const escapedAttr = dialect.escapeId(attribute);
|
|
3858
|
-
const escapedTable = dialect.escapeId(this.tableName);
|
|
3859
|
-
let sql = `SELECT MAX(${escapedAttr}) AS __agg FROM ${escapedTable}`;
|
|
3860
|
-
if (aggOptions?.where) {
|
|
3861
|
-
const whereQuery = dialect.buildSelectQuery({
|
|
3862
|
-
tableName: this.tableName,
|
|
3863
|
-
where: aggOptions.where,
|
|
3864
|
-
});
|
|
3865
|
-
const whereMatch = dialect
|
|
3866
|
-
.replaceReplacements(whereQuery.sql, whereQuery.values)
|
|
3867
|
-
.match(/WHERE (.+)$/i);
|
|
3868
|
-
if (whereMatch)
|
|
3869
|
-
sql += ` WHERE ${whereMatch[1]}`;
|
|
3870
|
-
}
|
|
3871
|
-
const result = await dialect.query(sql);
|
|
3872
|
-
const val = result.rows[0]?.__agg;
|
|
3873
|
-
return val !== null && val !== undefined ? Number(val) : null;
|
|
3908
|
+
// Single-value aggregates. All four share one implementation that
|
|
3909
|
+
// honors where, paranoid filtering, scopes and filtering includes.
|
|
3910
|
+
async max(attribute, aggOptions = {}) {
|
|
3911
|
+
return self._aggregate(model, modelName, 'MAX', attribute, this._mergeScopes(aggOptions));
|
|
3874
3912
|
},
|
|
3875
|
-
async min(attribute, aggOptions) {
|
|
3876
|
-
|
|
3877
|
-
throw new Error('Database not connected');
|
|
3878
|
-
const dialect = self.dialect;
|
|
3879
|
-
const escapedAttr = dialect.escapeId(attribute);
|
|
3880
|
-
const escapedTable = dialect.escapeId(this.tableName);
|
|
3881
|
-
let sql = `SELECT MIN(${escapedAttr}) AS __agg FROM ${escapedTable}`;
|
|
3882
|
-
if (aggOptions?.where) {
|
|
3883
|
-
const whereQuery = dialect.buildSelectQuery({
|
|
3884
|
-
tableName: this.tableName,
|
|
3885
|
-
where: aggOptions.where,
|
|
3886
|
-
});
|
|
3887
|
-
const whereMatch = dialect
|
|
3888
|
-
.replaceReplacements(whereQuery.sql, whereQuery.values)
|
|
3889
|
-
.match(/WHERE (.+)$/i);
|
|
3890
|
-
if (whereMatch)
|
|
3891
|
-
sql += ` WHERE ${whereMatch[1]}`;
|
|
3892
|
-
}
|
|
3893
|
-
const result = await dialect.query(sql);
|
|
3894
|
-
const val = result.rows[0]?.__agg;
|
|
3895
|
-
return val !== null && val !== undefined ? Number(val) : null;
|
|
3913
|
+
async min(attribute, aggOptions = {}) {
|
|
3914
|
+
return self._aggregate(model, modelName, 'MIN', attribute, this._mergeScopes(aggOptions));
|
|
3896
3915
|
},
|
|
3897
|
-
async sum(attribute, aggOptions) {
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
let sql = `SELECT SUM(${escapedAttr}) AS __agg FROM ${escapedTable}`;
|
|
3904
|
-
if (aggOptions?.where) {
|
|
3905
|
-
const whereQuery = dialect.buildSelectQuery({
|
|
3906
|
-
tableName: this.tableName,
|
|
3907
|
-
where: aggOptions.where,
|
|
3908
|
-
});
|
|
3909
|
-
const whereMatch = dialect
|
|
3910
|
-
.replaceReplacements(whereQuery.sql, whereQuery.values)
|
|
3911
|
-
.match(/WHERE (.+)$/i);
|
|
3912
|
-
if (whereMatch)
|
|
3913
|
-
sql += ` WHERE ${whereMatch[1]}`;
|
|
3914
|
-
}
|
|
3915
|
-
const result = await dialect.query(sql);
|
|
3916
|
-
const val = result.rows[0]?.__agg;
|
|
3917
|
-
return val !== null && val !== undefined ? Number(val) : 0;
|
|
3916
|
+
async sum(attribute, aggOptions = {}) {
|
|
3917
|
+
return (await self._aggregate(model, modelName, 'SUM', attribute, this._mergeScopes(aggOptions)));
|
|
3918
|
+
},
|
|
3919
|
+
/** Arithmetic mean of a column. Returns null when no rows match. */
|
|
3920
|
+
async avg(attribute, aggOptions = {}) {
|
|
3921
|
+
return self._aggregate(model, modelName, 'AVG', attribute, this._mergeScopes(aggOptions));
|
|
3918
3922
|
},
|
|
3919
3923
|
async upsert(values, upsertOptions) {
|
|
3920
3924
|
if (!self.dialect) {
|
|
@@ -6392,6 +6396,67 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6392
6396
|
const raw = rows[0].count ?? rows[0].COUNT ?? Object.values(rows[0])[0];
|
|
6393
6397
|
return Number(raw) || 0;
|
|
6394
6398
|
}
|
|
6399
|
+
/**
|
|
6400
|
+
* Run a single-value aggregate (MAX/MIN/SUM/AVG) for a model.
|
|
6401
|
+
*
|
|
6402
|
+
* `max`/`min`/`sum` each carried their own copy of this, built the WHERE by
|
|
6403
|
+
* rendering a throwaway SELECT and pulling the clause back out with
|
|
6404
|
+
* `/WHERE (.+)$/` — which breaks on any condition containing the word WHERE
|
|
6405
|
+
* (a subquery, or a string value) — and none of them honored paranoid
|
|
6406
|
+
* filtering, scopes, or filtering includes, so an aggregate could silently
|
|
6407
|
+
* include soft-deleted rows that `findAll` excluded.
|
|
6408
|
+
*/
|
|
6409
|
+
async _aggregate(model, modelName, aggregate, attribute, options = {}) {
|
|
6410
|
+
if (!this.dialect) {
|
|
6411
|
+
throw new Error('Database not connected');
|
|
6412
|
+
}
|
|
6413
|
+
const dialect = this.dialect;
|
|
6414
|
+
if (!model.rawAttributes?.[attribute]) {
|
|
6415
|
+
const known = Object.keys(model.rawAttributes || {}).join(', ');
|
|
6416
|
+
throw new Error(`${modelName} has no attribute '${attribute}' to ${aggregate.toLowerCase()}.` +
|
|
6417
|
+
(known ? ` Known attributes: ${known}.` : ''));
|
|
6418
|
+
}
|
|
6419
|
+
const modelOptions = model.options;
|
|
6420
|
+
const deletedAt = modelOptions?.deletedAt || 'deletedAt';
|
|
6421
|
+
let where = options.where;
|
|
6422
|
+
if (modelOptions?.paranoid === true &&
|
|
6423
|
+
options.paranoid !== false &&
|
|
6424
|
+
model.rawAttributes?.[deletedAt]) {
|
|
6425
|
+
where = { ...where, [deletedAt]: { $isNull: true } };
|
|
6426
|
+
}
|
|
6427
|
+
// Filtering includes restrict the aggregated set, as they do for findAll.
|
|
6428
|
+
if (options.include && options.include.length > 0) {
|
|
6429
|
+
for (const rawInclude of options.include) {
|
|
6430
|
+
const filter = await (0, eager_load_1.resolveRequiredIncludeFilter)(this.eagerLoadContext(), model, modelName, rawInclude);
|
|
6431
|
+
if (!filter)
|
|
6432
|
+
continue;
|
|
6433
|
+
if (filter.values.length === 0)
|
|
6434
|
+
return aggregate === 'SUM' ? 0 : null;
|
|
6435
|
+
const existing = where?.[filter.parentAttr];
|
|
6436
|
+
where = {
|
|
6437
|
+
...where,
|
|
6438
|
+
[filter.parentAttr]: existing
|
|
6439
|
+
? { $and: [existing, { $in: filter.values }] }
|
|
6440
|
+
: { $in: filter.values },
|
|
6441
|
+
};
|
|
6442
|
+
}
|
|
6443
|
+
}
|
|
6444
|
+
const qualified = model.schema
|
|
6445
|
+
? `${dialect.escapeId(model.schema)}.${dialect.escapeId(model.tableName)}`
|
|
6446
|
+
: dialect.escapeId(model.tableName);
|
|
6447
|
+
let sql = `SELECT ${aggregate}(${dialect.escapeId(attribute)}) AS ${dialect.escapeId('__agg')} FROM ${qualified}`;
|
|
6448
|
+
let values = [];
|
|
6449
|
+
if (where && Object.keys(where).length > 0) {
|
|
6450
|
+
const built = dialect.buildWhereClause((0, operators_1.normalizeOperatorKeys)(where));
|
|
6451
|
+
sql += ` WHERE ${built.sql}`;
|
|
6452
|
+
values = built.values;
|
|
6453
|
+
}
|
|
6454
|
+
const result = await dialect.query(dialect.replaceReplacements(sql, values));
|
|
6455
|
+
const val = result.rows?.[0]?.__agg;
|
|
6456
|
+
if (val === null || val === undefined)
|
|
6457
|
+
return aggregate === 'SUM' ? 0 : null;
|
|
6458
|
+
return Number(val);
|
|
6459
|
+
}
|
|
6395
6460
|
/**
|
|
6396
6461
|
* The view of this instance that the shared eager-loader needs.
|
|
6397
6462
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-prorm-orm",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"author": "Malcolm Stone",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"import": "./dist/index.js",
|
|
29
29
|
"require": "./dist/index.js",
|
|
30
30
|
"types": "./dist/index.d.ts"
|
|
31
|
-
}
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
32
33
|
},
|
|
33
34
|
"scripts": {
|
|
34
35
|
"build": "tsc",
|