ts-prorm-orm 1.2.1 → 1.2.3

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 CHANGED
@@ -5,6 +5,46 @@ 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.3]
9
+
10
+ ### Fixed
11
+
12
+ - **`raw: true` was accepted and ignored.** Rows came back as full model
13
+ instances carrying `save`/`update`/`destroy` and internal bookkeeping fields
14
+ — slower than asked for, and wrong for grouped aggregates that correspond to
15
+ no model instance. It now returns plain row objects.
16
+ - `normalizeOperatorKeys` is exported from the package root, alongside the
17
+ `operatorToWhereKey` and `getOperatorString` helpers it belongs with.
18
+
19
+ ## [1.2.2]
20
+
21
+ ### Fixed
22
+
23
+ - **`HAVING` silently dropped every operator.** The having builders compared
24
+ operator keys against stringified symbol names (`'Symbol(gt)'`), which
25
+ `Object.keys()` cannot produce and which the `$gt` string form never matched
26
+ either — so `having: { n: { [Op.gt]: 1 } }` became `HAVING n = 1` and returned
27
+ wrong rows without erroring. HAVING now reuses the WHERE builder, which
28
+ already handled both forms, across sqlite, mysql, mariadb, postgres and
29
+ cockroachdb.
30
+ - **`where: { '$assoc.column$': v }` was silently ignored.** Every where-builder
31
+ skips keys starting with `$` as unrecognized logical operators, so the
32
+ condition was dropped and the query returned every parent. These now filter
33
+ the parent rows via the association's include; naming an alias that isn't
34
+ included raises `AssociationError` instead of being discarded.
35
+ - **Aggregates ignored paranoid filtering and scopes**, so `sum`/`min`/`max`
36
+ could include soft-deleted rows that `findAll` excluded. They also rebuilt
37
+ their WHERE by regex-matching `/WHERE (.+)$/` out of a rendered SELECT, which
38
+ breaks on any condition containing that word. All four now share one
39
+ implementation honouring `where`, paranoid, scopes and filtering includes, and
40
+ report unknown attribute names.
41
+
42
+ ### Added
43
+
44
+ - **`avg()`** — the one standard aggregate that was missing.
45
+ - `exports` now permits `ts-prorm-orm/package.json`, which build tooling and
46
+ version checks commonly read.
47
+
8
48
  ## [1.2.1]
9
49
 
10
50
  ### 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 || (typeof having === 'object' && Object.keys(having).length === 0)) {
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
- // Use the condition builder with Op operator support
3804
- const sql = this.buildCondition(having, values);
3805
- return { sql, values };
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 || (typeof having === 'object' && Object.keys(having).length === 0)) {
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
- // Use the condition builder with Op operator support
2879
- const sql = this.buildCondition(having, values);
2880
- return { sql, values };
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 || (typeof having === 'object' && Object.keys(having).length === 0)) {
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
- // Use the condition builder with Op operator support
3375
- const sql = this.buildCondition(having, values);
3376
- return { sql, values };
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 || (typeof having === 'object' && Object.keys(having).length === 0)) {
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
- // Use the condition builder with Op operator support
4145
- const sql = this.buildCondition(having, values);
4146
- return { sql, values };
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 || (typeof having === 'object' && Object.keys(having).length === 0)) {
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
- // Use the condition builder with Op operator support
2947
- const sql = this.buildCondition(having, values);
2948
- return { sql, values };
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/index.d.ts CHANGED
@@ -163,7 +163,7 @@ export declare const GEOGRAPHY: ((...args: any[]) => import("./models").GEOGRAPH
163
163
  };
164
164
  export { Op } from './models/operators';
165
165
  export type { Operator } from './models/operators';
166
- export { Op as Operators, Operators as OpAliases, getOperatorString, operatorToWhereKey, where, and, or, not, eq, ne, gt, gte, lt, lte, like, notLike, iLike, notILike, startsWith, notStartsWith, endsWith, notEndsWith, substring, notSubstring, match, regexp, notRegexp, iRegexp, notIRegexp, in as in, inOp, notIn, between, notBetween, isNull, isNotNull, contains, overlap, asc, desc, random, isOrderExpression, } from './operators';
166
+ export { Op as Operators, Operators as OpAliases, getOperatorString, operatorToWhereKey, normalizeOperatorKeys, where, and, or, not, eq, ne, gt, gte, lt, lte, like, notLike, iLike, notILike, startsWith, notStartsWith, endsWith, notEndsWith, substring, notSubstring, match, regexp, notRegexp, iRegexp, notIRegexp, in as in, inOp, notIn, between, notBetween, isNull, isNotNull, contains, overlap, asc, desc, random, isOrderExpression, } from './operators';
167
167
  export type { OperatorSymbol, WhereConditionOptions, OrderExpression } from './operators';
168
168
  export * from './operators/index';
169
169
  export { Transaction } from './types';
package/dist/index.js CHANGED
@@ -41,12 +41,12 @@ var __importStar = (this && this.__importStar) || (function () {
41
41
  })();
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.VARBINARY = exports.BINARY = exports.BLOB = exports.JSONB = exports.JSON = exports.UUIDV4 = exports.UUID = exports.TIME = exports.NOW = exports.DATEONLY = exports.DATE = exports.BOOLEAN = exports.DECIMAL = exports.DOUBLE = exports.FLOAT = exports.BIGINT = exports.INTEGER = exports.TEXT = exports.CHAR = exports.VARCHAR = exports.STRING = exports.BelongsToMany = exports.BelongsTo = exports.HasMany = exports.HasOne = exports.clearMetadata = exports.getModelAttributes = exports.getAttributeMetadata = exports.getModelMetadata = exports.Comment = exports.Unique = exports.Default = exports.NotNull = exports.AutoIncrement = exports.PrimaryKey = exports.AllowNull = exports.Column = exports.Attribute = exports.Table = exports.Model = exports.AbstractDataType = exports.DataTypes = exports.createConnectionManager = exports.ConnectionManager = exports.StreamIterator = exports.BatchTransform = exports.FilterTransform = exports.MapTransform = exports.QueryTypes = exports.Prorm = void 0;
44
- exports.BaseDialect = exports.ReplicaManager = exports.Transaction = exports.isOrderExpression = exports.random = exports.desc = exports.asc = exports.overlap = exports.contains = exports.isNotNull = exports.isNull = exports.notBetween = exports.between = exports.notIn = exports.inOp = exports.in = exports.notIRegexp = exports.iRegexp = exports.notRegexp = exports.regexp = exports.match = exports.notSubstring = exports.substring = exports.notEndsWith = exports.endsWith = exports.notStartsWith = exports.startsWith = exports.notILike = exports.iLike = exports.notLike = exports.like = exports.lte = exports.lt = exports.gte = exports.gt = exports.ne = exports.eq = exports.not = exports.or = exports.and = exports.where = exports.operatorToWhereKey = exports.getOperatorString = exports.OpAliases = exports.Operators = exports.Op = exports.GEOGRAPHY = exports.GEOMETRY = exports.ARRAY = exports.ENUM = void 0;
45
- exports.lowerCase = exports.upperCase = exports.spliceStr = exports.formatDefaultValue = exports.toDefaultValue = exports.removeTicks = exports.addTicks = exports.escapeLike = exports.pad = exports.truncate = exports.coalesce = exports.isEmpty = exports.formatWhere = exports.escapeId = exports.escapeString = exports.formatValue = exports.formatValues = exports.isValidUUID = exports.generateShortUUID = exports.generateUUID = exports.omit = exports.pick = exports.merge = exports.cloneDeep = exports.singularize = exports.pluralize = exports.snakeCase = exports.camelize = exports.kebabToCamel = exports.camelToKebab = exports.snakeToCamel = exports.camelToSnake = exports.ModelManager = exports.ScopeManager = exports.createPool = exports.PooledConnection = exports.DatabaseConnectionPool = exports.ConnectionPool = exports.createMSSQLDialect = exports.MSSQLDialect = exports.createOracleDialect = exports.OracleDialect = exports.SQLiteDialect = exports.registerGraphDialects = exports.GRAPH_DIALECTS = exports.DgraphGraphDialect = exports.GremlinGraphDialect = exports.Neo4jGraphDialect = exports.BaseGraphDialect = exports.DialectRegistry = void 0;
46
- exports.ModelDiagram = exports.removeGeneratedModel = exports.listModels = exports.setOutputDir = exports.getOutputDir = exports.configureCLI = exports.generateModels = exports.model = exports.CLI = exports.utils = exports.isEmptyResultError = exports.isConstraintError = exports.isConnectionError = exports.isValidationError = exports.isPrormError = exports.ErrorCodes = exports.EmptyResultError = exports.EagerLoadError = exports.AssociationError = exports.BulkRecordError = exports.ResourceLockedError = exports.TimeoutError = exports.ExclusionConstraintError = exports.ForeignKeyConstraintError = exports.UniqueConstraintError = exports.ValidationError = exports.DatabaseError = exports.ConnectionError = exports.PrormError = exports.setDefaultLogger = exports.getDefaultLogger = exports.createSilentLogger = exports.createLogger = exports.LogLevelStrings = exports.LogLevel = exports.Logger = exports.createQueryInterface = exports.CreateQueryInterface = exports.stringUtils = exports.regex = exports.truncateStr = exports.padEnd = exports.padStart = exports.trimEnd = exports.trimStart = exports.rtrim = exports.ltrim = exports.trim = exports.capitalizeWords = exports.capitalize = void 0;
47
- exports.jsonExtract = exports.fullTextRank = exports.fullTextMatch = exports.currentTimestamp = exports.currentDate = exports.dateDiff = exports.dateSub = exports.dateAdd = exports.dateTrunc = exports.caseOf = exports.caseWhen = exports.SimpleCaseBuilder = exports.CaseBuilder = exports.arrayAgg = exports.stringAgg = exports.max = exports.min = exports.avg = exports.countDistinct = exports.count = exports.IsolationLevelEnum = exports.getConstantsForDialect = exports.SQLITE = exports.POSTGRES = exports.MARIADB = exports.MYSQL = exports.SQL = exports.createSQLiteAdvanced = exports.SQLiteAdvanced = exports.PRIVILEGE_LEVELS = exports.UserManager = exports.DatabaseSecurityDecorator = exports.Database = exports.buildCreateFullTextIndexSQL = exports.buildDropPolicySQL = exports.buildCreatePolicySQL = exports.buildEnableRowLevelSecuritySQL = exports.buildDropTriggerSQL = exports.buildCreateTriggerStatements = exports.buildDropSequenceSQL = exports.buildCreateSequenceSQL = exports.UnsupportedSchemaObjectError = exports.buildOptionsClause = exports.KNOWN_FDWS = exports.ForeignDataManager = exports.generateERDiagramFromFile = exports.generateModelDiagramFromFile = exports.generateERDiagram = exports.generateModelDiagram = exports.ERDiagram = void 0;
48
- exports.sqlRepeat = exports.sqlNow = exports.sqlFormatDate = exports.sqlJsonHasKey = exports.sqlJsonContains = exports.sqlRtrim = exports.sqlLtrim = exports.sqlTrim = exports.sqlSubstring = exports.sqlTruncate = exports.sqlRandom = exports.NullsOrder = exports.windowFn = exports.maxOver = exports.minOver = exports.countOver = exports.avgOver = exports.sumOver = exports.nthValue = exports.lastValue = exports.firstValue = exports.lead = exports.lag = exports.ntile = exports.cumeDist = exports.percentRank = exports.denseRank = exports.rank = exports.rowNumber = exports.WindowFunctionBuilder = exports.split = exports.rpad = exports.lpad = exports.replace = exports.length = exports.lower = exports.upper = exports.concat = exports.least = exports.greatest = exports.mod = exports.sign = exports.abs = exports.sqrt = exports.power = exports.floor = exports.ceil = exports.round = exports.jsonTypeOf = exports.jsonKeys = void 0;
49
- exports.sqlSum = exports.sqlReverse = void 0;
44
+ exports.ReplicaManager = exports.Transaction = exports.isOrderExpression = exports.random = exports.desc = exports.asc = exports.overlap = exports.contains = exports.isNotNull = exports.isNull = exports.notBetween = exports.between = exports.notIn = exports.inOp = exports.in = exports.notIRegexp = exports.iRegexp = exports.notRegexp = exports.regexp = exports.match = exports.notSubstring = exports.substring = exports.notEndsWith = exports.endsWith = exports.notStartsWith = exports.startsWith = exports.notILike = exports.iLike = exports.notLike = exports.like = exports.lte = exports.lt = exports.gte = exports.gt = exports.ne = exports.eq = exports.not = exports.or = exports.and = exports.where = exports.normalizeOperatorKeys = exports.operatorToWhereKey = exports.getOperatorString = exports.OpAliases = exports.Operators = exports.Op = exports.GEOGRAPHY = exports.GEOMETRY = exports.ARRAY = exports.ENUM = void 0;
45
+ exports.upperCase = exports.spliceStr = exports.formatDefaultValue = exports.toDefaultValue = exports.removeTicks = exports.addTicks = exports.escapeLike = exports.pad = exports.truncate = exports.coalesce = exports.isEmpty = exports.formatWhere = exports.escapeId = exports.escapeString = exports.formatValue = exports.formatValues = exports.isValidUUID = exports.generateShortUUID = exports.generateUUID = exports.omit = exports.pick = exports.merge = exports.cloneDeep = exports.singularize = exports.pluralize = exports.snakeCase = exports.camelize = exports.kebabToCamel = exports.camelToKebab = exports.snakeToCamel = exports.camelToSnake = exports.ModelManager = exports.ScopeManager = exports.createPool = exports.PooledConnection = exports.DatabaseConnectionPool = exports.ConnectionPool = exports.createMSSQLDialect = exports.MSSQLDialect = exports.createOracleDialect = exports.OracleDialect = exports.SQLiteDialect = exports.registerGraphDialects = exports.GRAPH_DIALECTS = exports.DgraphGraphDialect = exports.GremlinGraphDialect = exports.Neo4jGraphDialect = exports.BaseGraphDialect = exports.DialectRegistry = exports.BaseDialect = void 0;
46
+ exports.removeGeneratedModel = exports.listModels = exports.setOutputDir = exports.getOutputDir = exports.configureCLI = exports.generateModels = exports.model = exports.CLI = exports.utils = exports.isEmptyResultError = exports.isConstraintError = exports.isConnectionError = exports.isValidationError = exports.isPrormError = exports.ErrorCodes = exports.EmptyResultError = exports.EagerLoadError = exports.AssociationError = exports.BulkRecordError = exports.ResourceLockedError = exports.TimeoutError = exports.ExclusionConstraintError = exports.ForeignKeyConstraintError = exports.UniqueConstraintError = exports.ValidationError = exports.DatabaseError = exports.ConnectionError = exports.PrormError = exports.setDefaultLogger = exports.getDefaultLogger = exports.createSilentLogger = exports.createLogger = exports.LogLevelStrings = exports.LogLevel = exports.Logger = exports.createQueryInterface = exports.CreateQueryInterface = exports.stringUtils = exports.regex = exports.truncateStr = exports.padEnd = exports.padStart = exports.trimEnd = exports.trimStart = exports.rtrim = exports.ltrim = exports.trim = exports.capitalizeWords = exports.capitalize = exports.lowerCase = void 0;
47
+ exports.fullTextRank = exports.fullTextMatch = exports.currentTimestamp = exports.currentDate = exports.dateDiff = exports.dateSub = exports.dateAdd = exports.dateTrunc = exports.caseOf = exports.caseWhen = exports.SimpleCaseBuilder = exports.CaseBuilder = exports.arrayAgg = exports.stringAgg = exports.max = exports.min = exports.avg = exports.countDistinct = exports.count = exports.IsolationLevelEnum = exports.getConstantsForDialect = exports.SQLITE = exports.POSTGRES = exports.MARIADB = exports.MYSQL = exports.SQL = exports.createSQLiteAdvanced = exports.SQLiteAdvanced = exports.PRIVILEGE_LEVELS = exports.UserManager = exports.DatabaseSecurityDecorator = exports.Database = exports.buildCreateFullTextIndexSQL = exports.buildDropPolicySQL = exports.buildCreatePolicySQL = exports.buildEnableRowLevelSecuritySQL = exports.buildDropTriggerSQL = exports.buildCreateTriggerStatements = exports.buildDropSequenceSQL = exports.buildCreateSequenceSQL = exports.UnsupportedSchemaObjectError = exports.buildOptionsClause = exports.KNOWN_FDWS = exports.ForeignDataManager = exports.generateERDiagramFromFile = exports.generateModelDiagramFromFile = exports.generateERDiagram = exports.generateModelDiagram = exports.ERDiagram = exports.ModelDiagram = void 0;
48
+ exports.sqlNow = exports.sqlFormatDate = exports.sqlJsonHasKey = exports.sqlJsonContains = exports.sqlRtrim = exports.sqlLtrim = exports.sqlTrim = exports.sqlSubstring = exports.sqlTruncate = exports.sqlRandom = exports.NullsOrder = exports.windowFn = exports.maxOver = exports.minOver = exports.countOver = exports.avgOver = exports.sumOver = exports.nthValue = exports.lastValue = exports.firstValue = exports.lead = exports.lag = exports.ntile = exports.cumeDist = exports.percentRank = exports.denseRank = exports.rank = exports.rowNumber = exports.WindowFunctionBuilder = exports.split = exports.rpad = exports.lpad = exports.replace = exports.length = exports.lower = exports.upper = exports.concat = exports.least = exports.greatest = exports.mod = exports.sign = exports.abs = exports.sqrt = exports.power = exports.floor = exports.ceil = exports.round = exports.jsonTypeOf = exports.jsonKeys = exports.jsonExtract = void 0;
49
+ exports.sqlSum = exports.sqlReverse = exports.sqlRepeat = void 0;
50
50
  // Re-export Prorm class and query types
51
51
  var prorm_1 = require("./prorm");
52
52
  Object.defineProperty(exports, "Prorm", { enumerable: true, get: function () { return prorm_1.Prorm; } });
@@ -137,6 +137,7 @@ Object.defineProperty(exports, "Operators", { enumerable: true, get: function ()
137
137
  Object.defineProperty(exports, "OpAliases", { enumerable: true, get: function () { return operators_2.Operators; } });
138
138
  Object.defineProperty(exports, "getOperatorString", { enumerable: true, get: function () { return operators_2.getOperatorString; } });
139
139
  Object.defineProperty(exports, "operatorToWhereKey", { enumerable: true, get: function () { return operators_2.operatorToWhereKey; } });
140
+ Object.defineProperty(exports, "normalizeOperatorKeys", { enumerable: true, get: function () { return operators_2.normalizeOperatorKeys; } });
140
141
  // Core where helpers
141
142
  Object.defineProperty(exports, "where", { enumerable: true, get: function () { return operators_2.where; } });
142
143
  Object.defineProperty(exports, "and", { enumerable: true, get: function () { return operators_2.and; } });
@@ -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("..").LiteralValue;
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("..").ExtractPart) => import("./operators").ExtractExpression;
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;
@@ -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,
@@ -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
- having: findOptions.having,
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,
@@ -2966,12 +3022,18 @@ class Prorm extends events_1.EventEmitter {
2966
3022
  model: modelName,
2967
3023
  });
2968
3024
  }
2969
- // Wrap each row with instance methods
3025
+ // Wrap each row with instance methods.
3026
+ //
3027
+ // `raw: true` asks for plain rows and was accepted but ignored - every
3028
+ // row came back as a full instance carrying save/update/destroy and the
3029
+ // internal bookkeeping fields, which is both slower than asked for and
3030
+ // surprising when the result is a grouped aggregate that maps to no
3031
+ // model instance at all.
2970
3032
  const modelRef = this;
2971
3033
  const modelHooksRef = options.hooks || {};
2972
- const wrappedRows = result.rows.map((row) => {
2973
- return _wrapInstance(row, modelRef, modelHooksRef);
2974
- });
3034
+ const wrappedRows = (findOptions.raw === true
3035
+ ? result.rows.map((row) => ({ ...row }))
3036
+ : result.rows.map((row) => _wrapInstance(row, modelRef, modelHooksRef)));
2975
3037
  // Eager-load associations. The implementation is shared with the
2976
3038
  // class-based Model API (see models/eager-load.ts) - it used to be
2977
3039
  // duplicated here and in models/model.ts, and the two copies had
@@ -3849,72 +3911,20 @@ class Prorm extends events_1.EventEmitter {
3849
3911
  });
3850
3912
  return association;
3851
3913
  },
3852
- // Additional methods required by ModelStatic
3853
- async max(attribute, aggOptions) {
3854
- if (!self.dialect)
3855
- throw new Error('Database not connected');
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;
3914
+ // Single-value aggregates. All four share one implementation that
3915
+ // honors where, paranoid filtering, scopes and filtering includes.
3916
+ async max(attribute, aggOptions = {}) {
3917
+ return self._aggregate(model, modelName, 'MAX', attribute, this._mergeScopes(aggOptions));
3874
3918
  },
3875
- async min(attribute, aggOptions) {
3876
- if (!self.dialect)
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;
3919
+ async min(attribute, aggOptions = {}) {
3920
+ return self._aggregate(model, modelName, 'MIN', attribute, this._mergeScopes(aggOptions));
3896
3921
  },
3897
- async sum(attribute, aggOptions) {
3898
- if (!self.dialect)
3899
- throw new Error('Database not connected');
3900
- const dialect = self.dialect;
3901
- const escapedAttr = dialect.escapeId(attribute);
3902
- const escapedTable = dialect.escapeId(this.tableName);
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;
3922
+ async sum(attribute, aggOptions = {}) {
3923
+ return (await self._aggregate(model, modelName, 'SUM', attribute, this._mergeScopes(aggOptions)));
3924
+ },
3925
+ /** Arithmetic mean of a column. Returns null when no rows match. */
3926
+ async avg(attribute, aggOptions = {}) {
3927
+ return self._aggregate(model, modelName, 'AVG', attribute, this._mergeScopes(aggOptions));
3918
3928
  },
3919
3929
  async upsert(values, upsertOptions) {
3920
3930
  if (!self.dialect) {
@@ -6392,6 +6402,67 @@ $$ LANGUAGE plpgsql;`;
6392
6402
  const raw = rows[0].count ?? rows[0].COUNT ?? Object.values(rows[0])[0];
6393
6403
  return Number(raw) || 0;
6394
6404
  }
6405
+ /**
6406
+ * Run a single-value aggregate (MAX/MIN/SUM/AVG) for a model.
6407
+ *
6408
+ * `max`/`min`/`sum` each carried their own copy of this, built the WHERE by
6409
+ * rendering a throwaway SELECT and pulling the clause back out with
6410
+ * `/WHERE (.+)$/` — which breaks on any condition containing the word WHERE
6411
+ * (a subquery, or a string value) — and none of them honored paranoid
6412
+ * filtering, scopes, or filtering includes, so an aggregate could silently
6413
+ * include soft-deleted rows that `findAll` excluded.
6414
+ */
6415
+ async _aggregate(model, modelName, aggregate, attribute, options = {}) {
6416
+ if (!this.dialect) {
6417
+ throw new Error('Database not connected');
6418
+ }
6419
+ const dialect = this.dialect;
6420
+ if (!model.rawAttributes?.[attribute]) {
6421
+ const known = Object.keys(model.rawAttributes || {}).join(', ');
6422
+ throw new Error(`${modelName} has no attribute '${attribute}' to ${aggregate.toLowerCase()}.` +
6423
+ (known ? ` Known attributes: ${known}.` : ''));
6424
+ }
6425
+ const modelOptions = model.options;
6426
+ const deletedAt = modelOptions?.deletedAt || 'deletedAt';
6427
+ let where = options.where;
6428
+ if (modelOptions?.paranoid === true &&
6429
+ options.paranoid !== false &&
6430
+ model.rawAttributes?.[deletedAt]) {
6431
+ where = { ...where, [deletedAt]: { $isNull: true } };
6432
+ }
6433
+ // Filtering includes restrict the aggregated set, as they do for findAll.
6434
+ if (options.include && options.include.length > 0) {
6435
+ for (const rawInclude of options.include) {
6436
+ const filter = await (0, eager_load_1.resolveRequiredIncludeFilter)(this.eagerLoadContext(), model, modelName, rawInclude);
6437
+ if (!filter)
6438
+ continue;
6439
+ if (filter.values.length === 0)
6440
+ return aggregate === 'SUM' ? 0 : null;
6441
+ const existing = where?.[filter.parentAttr];
6442
+ where = {
6443
+ ...where,
6444
+ [filter.parentAttr]: existing
6445
+ ? { $and: [existing, { $in: filter.values }] }
6446
+ : { $in: filter.values },
6447
+ };
6448
+ }
6449
+ }
6450
+ const qualified = model.schema
6451
+ ? `${dialect.escapeId(model.schema)}.${dialect.escapeId(model.tableName)}`
6452
+ : dialect.escapeId(model.tableName);
6453
+ let sql = `SELECT ${aggregate}(${dialect.escapeId(attribute)}) AS ${dialect.escapeId('__agg')} FROM ${qualified}`;
6454
+ let values = [];
6455
+ if (where && Object.keys(where).length > 0) {
6456
+ const built = dialect.buildWhereClause((0, operators_1.normalizeOperatorKeys)(where));
6457
+ sql += ` WHERE ${built.sql}`;
6458
+ values = built.values;
6459
+ }
6460
+ const result = await dialect.query(dialect.replaceReplacements(sql, values));
6461
+ const val = result.rows?.[0]?.__agg;
6462
+ if (val === null || val === undefined)
6463
+ return aggregate === 'SUM' ? 0 : null;
6464
+ return Number(val);
6465
+ }
6395
6466
  /**
6396
6467
  * The view of this instance that the shared eager-loader needs.
6397
6468
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-prorm-orm",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
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",