@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 +33 -0
- package/README.md +4 -2
- package/package.json +2 -2
- package/src/helpers/query.js +58 -26
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
|
-
[](https://github.com/imqueue/pg-sequelize)
|
|
3
|
+
[](https://github.com/imqueue/pg-sequelize/actions/workflows/build.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@imqueue/pg-sequelize)
|
|
4
5
|
[](https://snyk.io/test/github/imqueue/pg-sequelize?targetFile=package.json)
|
|
5
6
|
[](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.
|
|
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.
|
|
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",
|
package/src/helpers/query.js
CHANGED
|
@@ -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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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.
|