ts-prorm-orm 1.2.0 → 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 CHANGED
@@ -5,6 +5,47 @@ 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
+
37
+ ## [1.2.1]
38
+
39
+ ### Fixed
40
+
41
+ - `EagerLoadError` and `UnsupportedSchemaObjectError` were not exported from the
42
+ package root, so callers could not catch by name the errors that `include` and
43
+ the schema-object methods actually throw. Both are now exported, along with the
44
+ schema-object SQL builders and their option types (the two whose names already
45
+ existed in `./types` are aliased as `CreateSequenceOptions` and
46
+ `CreatePolicyOptions` rather than shadowing them). A test now pins the public
47
+ error surface.
48
+
8
49
  ## [1.2.0]
9
50
 
10
51
  ### Fixed — relations
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
@@ -189,7 +189,7 @@ export * from './migrations';
189
189
  export * from './schema';
190
190
  export { Logger, LogLevel, LogLevelStrings, createLogger, createSilentLogger, getDefaultLogger, setDefaultLogger, } from './logging';
191
191
  export type { LoggerOptions, LogEntry, LoggerEvents } from './logging';
192
- export { PrormError, ConnectionError, DatabaseError, ValidationError, UniqueConstraintError, ForeignKeyConstraintError, ExclusionConstraintError, TimeoutError, ResourceLockedError, BulkRecordError, AssociationError, EmptyResultError, ErrorCodes, isPrormError, isValidationError, isConnectionError, isConstraintError, isEmptyResultError, } from './errors';
192
+ export { PrormError, ConnectionError, DatabaseError, ValidationError, UniqueConstraintError, ForeignKeyConstraintError, ExclusionConstraintError, TimeoutError, ResourceLockedError, BulkRecordError, AssociationError, EagerLoadError, EmptyResultError, ErrorCodes, isPrormError, isValidationError, isConnectionError, isConstraintError, isEmptyResultError, } from './errors';
193
193
  export * from './errors/utils';
194
194
  export * from './utils/index';
195
195
  import * as Utils from './utils/index';
@@ -201,6 +201,8 @@ export * from './query-optimizers';
201
201
  export { ForeignDataManager, KNOWN_FDWS, buildOptionsClause } from './foreign-data';
202
202
  export type { ForeignServerOptions, AlterForeignServerOptions, UserMappingOptions, ForeignTableOptions, ForeignTableColumnDef, ImportForeignSchemaOptions, ServerInfo, } from './foreign-data';
203
203
  export * from './external-fields';
204
+ export { UnsupportedSchemaObjectError, buildCreateSequenceSQL, buildDropSequenceSQL, buildCreateTriggerStatements, buildDropTriggerSQL, buildEnableRowLevelSecuritySQL, buildCreatePolicySQL, buildDropPolicySQL, buildCreateFullTextIndexSQL, } from './schema-objects';
205
+ export type { FullTextIndexOptions, TriggerDefinition, SequenceOptions as CreateSequenceOptions, PolicyOptions as CreatePolicyOptions, } from './schema-objects';
204
206
  export * from './decorators';
205
207
  export { Database } from './decorators/collate';
206
208
  export { Database as DatabaseSecurityDecorator } from './compliance/security-decorator';
package/dist/index.js CHANGED
@@ -43,9 +43,10 @@ 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
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
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.ERDiagram = 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.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.mod = exports.sign = exports.abs = exports.sqrt = exports.power = exports.floor = exports.ceil = exports.round = exports.jsonTypeOf = exports.jsonKeys = 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.buildOptionsClause = exports.KNOWN_FDWS = exports.ForeignDataManager = exports.generateERDiagramFromFile = exports.generateModelDiagramFromFile = exports.generateERDiagram = exports.generateModelDiagram = void 0;
48
- exports.sqlSum = exports.sqlReverse = 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 = 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;
49
50
  // Re-export Prorm class and query types
50
51
  var prorm_1 = require("./prorm");
51
52
  Object.defineProperty(exports, "Prorm", { enumerable: true, get: function () { return prorm_1.Prorm; } });
@@ -312,6 +313,7 @@ Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function
312
313
  Object.defineProperty(exports, "ResourceLockedError", { enumerable: true, get: function () { return errors_1.ResourceLockedError; } });
313
314
  Object.defineProperty(exports, "BulkRecordError", { enumerable: true, get: function () { return errors_1.BulkRecordError; } });
314
315
  Object.defineProperty(exports, "AssociationError", { enumerable: true, get: function () { return errors_1.AssociationError; } });
316
+ Object.defineProperty(exports, "EagerLoadError", { enumerable: true, get: function () { return errors_1.EagerLoadError; } });
315
317
  Object.defineProperty(exports, "EmptyResultError", { enumerable: true, get: function () { return errors_1.EmptyResultError; } });
316
318
  Object.defineProperty(exports, "ErrorCodes", { enumerable: true, get: function () { return errors_1.ErrorCodes; } });
317
319
  Object.defineProperty(exports, "isPrormError", { enumerable: true, get: function () { return errors_1.isPrormError; } });
@@ -352,6 +354,21 @@ Object.defineProperty(exports, "KNOWN_FDWS", { enumerable: true, get: function (
352
354
  Object.defineProperty(exports, "buildOptionsClause", { enumerable: true, get: function () { return foreign_data_1.buildOptionsClause; } });
353
355
  // Export external fields (@ExternalField + store registry)
354
356
  __exportStar(require("./external-fields"), exports);
357
+ // Export schema-object builders and their options, so callers can catch
358
+ // UnsupportedSchemaObjectError and type the QueryInterface arguments.
359
+ // `SequenceOptions` and `PolicyOptions` already exist in './types' with
360
+ // different shapes, so the schema-object ones are aliased rather than
361
+ // shadowing names existing consumers may already import.
362
+ var schema_objects_1 = require("./schema-objects");
363
+ Object.defineProperty(exports, "UnsupportedSchemaObjectError", { enumerable: true, get: function () { return schema_objects_1.UnsupportedSchemaObjectError; } });
364
+ Object.defineProperty(exports, "buildCreateSequenceSQL", { enumerable: true, get: function () { return schema_objects_1.buildCreateSequenceSQL; } });
365
+ Object.defineProperty(exports, "buildDropSequenceSQL", { enumerable: true, get: function () { return schema_objects_1.buildDropSequenceSQL; } });
366
+ Object.defineProperty(exports, "buildCreateTriggerStatements", { enumerable: true, get: function () { return schema_objects_1.buildCreateTriggerStatements; } });
367
+ Object.defineProperty(exports, "buildDropTriggerSQL", { enumerable: true, get: function () { return schema_objects_1.buildDropTriggerSQL; } });
368
+ Object.defineProperty(exports, "buildEnableRowLevelSecuritySQL", { enumerable: true, get: function () { return schema_objects_1.buildEnableRowLevelSecuritySQL; } });
369
+ Object.defineProperty(exports, "buildCreatePolicySQL", { enumerable: true, get: function () { return schema_objects_1.buildCreatePolicySQL; } });
370
+ Object.defineProperty(exports, "buildDropPolicySQL", { enumerable: true, get: function () { return schema_objects_1.buildDropPolicySQL; } });
371
+ Object.defineProperty(exports, "buildCreateFullTextIndexSQL", { enumerable: true, get: function () { return schema_objects_1.buildCreateFullTextIndexSQL; } });
355
372
  // Export decorators / annotations
356
373
  __exportStar(require("./decorators"), exports);
357
374
  // './decorators' (collate.ts) and './compliance' (security-decorator.ts) both export a
@@ -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,
@@ -3849,72 +3905,20 @@ class Prorm extends events_1.EventEmitter {
3849
3905
  });
3850
3906
  return association;
3851
3907
  },
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;
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
- 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;
3913
+ async min(attribute, aggOptions = {}) {
3914
+ return self._aggregate(model, modelName, 'MIN', attribute, this._mergeScopes(aggOptions));
3896
3915
  },
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;
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.0",
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",