@imqueue/pg-sequelize 4.2.0 → 4.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
@@ -4,6 +4,39 @@ Notable changes to `@imqueue/pg-sequelize`.
4
4
 
5
5
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [4.2.1] - 2026-08-18
8
+
9
+ ### Fixed
10
+
11
+ - **`query.createEntity` leaked a pooled connection on every failed insert.**
12
+ When no transaction was passed it opened one, and finished it only on the happy
13
+ path — there was no `rollback` anywhere in the helper. A rejected `save()` (a
14
+ unique or foreign-key violation, a NOT NULL, an invalid enum) unwound past the
15
+ commit and left the transaction open.
16
+
17
+ In sequelize 6 a pooled connection is bound to the `Transaction` and only
18
+ `commit()` or `rollback()` hands it back, so nothing released it: `pool.max`
19
+ failed inserts — 5 by default — exhausted the pool, after which every query in
20
+ the process failed with `SequelizeConnectionAcquireTimeoutError` until it was
21
+ restarted. On the database side the backend sat in
22
+ `idle in transaction (aborted)` indefinitely.
23
+
24
+ Ownership is now all-or-nothing, decided by a single flag used for both the
25
+ commit and the rollback so the two cannot select different sets of calls. Only
26
+ the call that opened the transaction finishes it; a caller-supplied transaction
27
+ and the nested relation creates that inherit it are untouched, exactly as
28
+ before. A rollback that fails is swallowed so it cannot replace the error that
29
+ caused it.
30
+
31
+ Anyone who cannot upgrade can own the lifecycle from outside, which closes the
32
+ leak with no dependency change:
33
+
34
+ ```typescript
35
+ const entity = await Model.sequelize!.transaction(transaction =>
36
+ createEntity<T, I>(Model, data, fields, transaction),
37
+ );
38
+ ```
39
+
7
40
  ## [4.2.0] - 2026-08-01
8
41
 
9
42
  The package is renamed from `@imqueue/sequelize` to `@imqueue/pg-sequelize`. No
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @imqueue/pg-sequelize
2
2
 
3
- [![Build Status](https://img.shields.io/github/actions/workflow/status/imqueue/pg-sequelize/build.yml)](https://github.com/imqueue/pg-sequelize)
3
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/imqueue/pg-sequelize/build.yml)](https://github.com/imqueue/pg-sequelize/actions/workflows/build.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@imqueue/pg-sequelize)](https://www.npmjs.com/package/@imqueue/pg-sequelize)
4
5
  [![Known Vulnerabilities](https://snyk.io/test/github/imqueue/pg-sequelize/badge.svg?targetFile=package.json)](https://snyk.io/test/github/imqueue/pg-sequelize?targetFile=package.json)
5
6
  [![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://github.com/imqueue/pg-sequelize/blob/master/LICENSE)
6
7
 
@@ -86,7 +87,8 @@ Every exported symbol carries its own documentation, so an editor is the fastest
86
87
  reference. The same content is published, symbol by symbol, as the
87
88
  [API reference](https://imqueue.org/api/pg-sequelize/latest/) — searchable, and linkable
88
89
  when you need to point someone at one thing. The wider ecosystem documentation is at
89
- [imqueue.org](https://imqueue.org/docs/).
90
+ [imqueue.org](https://imqueue.org/docs/). Current version, licence and Node floor
91
+ for every package: [imqueue.org/status.json](https://imqueue.org/status.json).
90
92
 
91
93
  ## License
92
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imqueue/pg-sequelize",
3
- "version": "4.2.0",
3
+ "version": "4.2.2",
4
4
  "description": "Sequelize/Postgres toolkit for @imqueue microservices — turns a query described as data (filters, paging, ordering, fields) into one efficient statement, plus database views as models and the Postgres index options Sequelize cannot express",
5
5
  "keywords": [
6
6
  "imqueue",
@@ -46,7 +46,7 @@
46
46
  "author": "imqueue.com <support@imqueue.com>",
47
47
  "license": "GPL-3.0-only",
48
48
  "dependencies": {
49
- "@imqueue/rpc": "^3.4.1",
49
+ "@imqueue/rpc": "^3.7.1",
50
50
  "pg": "^8.22.0",
51
51
  "reflect-metadata": "^0.2.2",
52
52
  "sequelize": "^6.37.8",
@@ -505,6 +505,18 @@ export var query;
505
505
  * Recursively creates entity and all it's relations from a given input
506
506
  * using a given model.
507
507
  *
508
+ * @remarks
509
+ * Ownership of the transaction is all-or-nothing: the call that opened one
510
+ * both commits it on success and rolls it back on failure, and every other
511
+ * call leaves it entirely alone. It used to commit without rolling back, so
512
+ * a rejected `save()` — a unique or foreign-key violation, a NOT NULL, an
513
+ * invalid enum — unwound past the commit and left the transaction open. In
514
+ * sequelize 6 a pooled connection is bound to the `Transaction` and only
515
+ * `commit()` or `rollback()` returns it, so nothing ever handed it back:
516
+ * `pool.max` failed inserts (5 by default) exhausted the pool and every
517
+ * later query failed with `SequelizeConnectionAcquireTimeoutError` until
518
+ * the process restarted.
519
+ *
508
520
  * @param model - Model to create.
509
521
  * @param input - One entity, or several.
510
522
  * @param fields - Requested fields map, deciding what comes back.
@@ -520,39 +532,59 @@ export var query;
520
532
  // the synchronous require(esm) path used by CommonJS consumers
521
533
  // cannot evaluate (bindings would stay undefined)
522
534
  const { database } = await import('../index.js');
535
+ // One flag for both ends of the lifecycle, so the commit and the
536
+ // rollback below cannot select different sets of calls: this call opened
537
+ // the transaction (`createEntity` passes doCommit=false when the caller
538
+ // supplied one) and is not a nested relation create (those inherit the
539
+ // caller's transaction and carry a `parent`).
540
+ const ownsTransaction = !transaction && !parent && doCommit;
523
541
  transaction =
524
542
  transaction ||
525
543
  (await database().transaction({
526
544
  autocommit: false,
527
545
  }));
528
- // todo: this could be optimized through bulk operations
529
- if (isArray(input) && parentProperty && parent) {
530
- parent.appendChild(parentProperty, await Promise.all(input.map(inputItem => doCreateEntity(model, inputItem, fields, transaction, parentProperty, parent, true, doCommit))));
531
- return parent;
532
- }
533
- if (fields) {
534
- primaryKeys(model).forEach(name => !fields[name] && (fields[name] = false));
535
- }
536
- const fieldNames = Object.keys(input);
537
- const relationArgs = prepareInput(input, filtered(model.associations, fieldNames), model, fields, transaction, parent);
538
- const entity = new model(input);
539
- await entity.save({
540
- transaction,
541
- returning: fields
542
- ? filtered(model.rawAttributes, Object.keys(fields), model)
543
- : true,
544
- });
545
- if (!noAppend && parentProperty && parent) {
546
- parent.appendChild(parentProperty, entity);
546
+ try {
547
+ // todo: this could be optimized through bulk operations
548
+ if (isArray(input) && parentProperty && parent) {
549
+ parent.appendChild(parentProperty, await Promise.all(input.map(inputItem => doCreateEntity(model, inputItem, fields, transaction, parentProperty, parent, true, doCommit))));
550
+ return parent;
551
+ }
552
+ if (fields) {
553
+ primaryKeys(model).forEach(name => !fields[name] && (fields[name] = false));
554
+ }
555
+ const fieldNames = Object.keys(input);
556
+ const relationArgs = prepareInput(input, filtered(model.associations, fieldNames), model, fields, transaction, parent);
557
+ const entity = new model(input);
558
+ await entity.save({
559
+ transaction,
560
+ returning: fields
561
+ ? filtered(model.rawAttributes, Object.keys(fields), model)
562
+ : true,
563
+ });
564
+ if (!noAppend && parentProperty && parent) {
565
+ parent.appendChild(parentProperty, entity);
566
+ }
567
+ await Promise.all(relationArgs.map(async (args) => {
568
+ args.push(entity);
569
+ await doCreateEntity(...args);
570
+ }));
571
+ if (ownsTransaction) {
572
+ await transaction.commit();
573
+ }
574
+ return entity;
547
575
  }
548
- await Promise.all(relationArgs.map(async (args) => {
549
- args.push(entity);
550
- await doCreateEntity(...args);
551
- }));
552
- if (!parent && doCommit) {
553
- await transaction.commit();
576
+ catch (err) {
577
+ if (ownsTransaction) {
578
+ // swallowed deliberately: a rollback that fails must not
579
+ // replace the error that caused it, and every path out of
580
+ // rollback() has dealt with the connection anyway — released on
581
+ // success, destroyed by forceCleanup() when the ROLLBACK itself
582
+ // errors, and already gone on the two guard clauses that throw
583
+ // before touching it
584
+ await transaction.rollback().catch(() => { });
585
+ }
586
+ throw err;
554
587
  }
555
- return entity;
556
588
  }
557
589
  /**
558
590
  * The counting counterpart of {@link query.autoQuery}, for the same fields and filter.