@avytrix/nestjs-drizzle-crud 3.8.0 → 3.8.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/AGENT.md
CHANGED
|
@@ -151,6 +151,8 @@ Those are legacy manual patterns and they break registration.
|
|
|
151
151
|
`withCount: false` it skips the count, `total` is `null` and `hasMore` is set.
|
|
152
152
|
`delete` and `softDelete` return a boolean. `update` and `restore` return the entity and
|
|
153
153
|
throw `EntityNotFoundException` when the row is missing.
|
|
154
|
+
Writes turn an ISO string or a number for a date-mode `timestamp` / `date` /
|
|
155
|
+
`datetime` column into a `Date`; a value that is not a date is a 400 naming the column.
|
|
154
156
|
|
|
155
157
|
Every method takes `options`: `{ transaction, relations, select, search, hooks, lock, forNoKeyUpdate, withDeleted, onlyDeleted, skipAllowlists, bulkMode, counts, context, skipScope, skipEvents, expectedVersion, expectedVersions }`.
|
|
156
158
|
|
|
@@ -395,7 +397,9 @@ restore, force delete, attach included) and written onto every created row, so
|
|
|
395
397
|
another tenant's id is a 404. A missing context value is a 403, never an
|
|
396
398
|
unscoped query. A belongs-to key a write sets must point at a row of the
|
|
397
399
|
caller's tenant when the related table has the scope columns; another tenant's
|
|
398
|
-
row is the same 400 as a missing one.
|
|
400
|
+
row is the same 400 as a missing one. Nested create children get the same check
|
|
401
|
+
for the keys their nested relation declares in its own `relations`; declare
|
|
402
|
+
every belongs-to a child can set. Do not add `tenant_id` to filters or DTOs
|
|
399
403
|
by hand, and use `skipScope: true` only in trusted server code.
|
|
400
404
|
|
|
401
405
|
# Optimistic locking
|
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,114 @@ restore, `ER_DUP_ENTRY` mapping, dialect locking) are tracked separately and unc
|
|
|
11
11
|
3.3.1, 3.4.0, 3.5.0 and 3.5.2 were tagged but never published to npm. Their changes reached npm
|
|
12
12
|
in 3.5.1 (3.3.1 to 3.5.0) and 3.6.0 (3.5.2). The unscoped `nestjs-drizzle-crud` stops at 3.3.0.
|
|
13
13
|
|
|
14
|
+
## [3.8.2] - 2026-11-04
|
|
15
|
+
|
|
16
|
+
Fixes from the 3.8.1 audit (G-01 to G-08). The tenant key check now lets the database compare keys,
|
|
17
|
+
so it follows each column's collation, and a malformed key or an ISO date string in a write body is
|
|
18
|
+
no longer a 500. No public API or override point changed, and no operation sends more statements
|
|
19
|
+
than in 3.8.1.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- The tenant key check, `attach` and `sync` leave the key comparison to the database. They join the
|
|
24
|
+
keys they were sent, as an inline `VALUES` list, against the related table in the same scoped
|
|
25
|
+
`SELECT`, so the column's type and collation decide which key matched. A `citext` or SQLite
|
|
26
|
+
`NOCASE` key of the caller's own tenant in another case is accepted again (3.8.1 refused it with
|
|
27
|
+
a 400), and on MySQL a key under a `_bin` collation that differs from another tenant's only in
|
|
28
|
+
case is refused. PostgreSQL `text` still compares exactly, and a uuid matches in any case. On
|
|
29
|
+
MySQL an integer or decimal key is compared in its column's type, so a `BIGINT` past 2^53 is
|
|
30
|
+
matched exactly. `attach` and `sync` link each target by its key as stored, so `"ann@x.io"` and
|
|
31
|
+
`"Ann@X.io"` on a `citext` column give one link.
|
|
32
|
+
- `massUpdate` and `massUpsert` load `relations` and `counts` inside their transaction when a value
|
|
33
|
+
holds a Drizzle `sql` expression. A load that fails then rolls the write back, so a retry no
|
|
34
|
+
longer adds `views + 1` twice.
|
|
35
|
+
- A `RangeError` thrown while a statement's values are mapped (a custom type's own check, say) is
|
|
36
|
+
no longer reported as "Too many values in one statement". Only the call stack overflow of an
|
|
37
|
+
oversized parameter list is.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- A belongs-to key the column's type cannot hold (`"nope"` for a uuid) is a 400 in `massCreate`,
|
|
42
|
+
`massUpdate` and `massUpsert`, and in `update` inside a caller's transaction. It was a raw
|
|
43
|
+
`22P02` or `25P02`, a 500.
|
|
44
|
+
- Every write (`create`, `update`, `upsert`, the mass writes and nested rows) turns an ISO date
|
|
45
|
+
string or a number for a date-mode `timestamp`, `date` or `datetime` column into a `Date`, as
|
|
46
|
+
filters already did. It was a 500 `TypeError` (`value.toISOString is not a function`). A value
|
|
47
|
+
that is not a date, an Invalid Date included, is a 400 that names the column, in a write and in a
|
|
48
|
+
filter.
|
|
49
|
+
- `massUpdate` groups SETs by their raw values: a hook that returns `null` for one id and an
|
|
50
|
+
Invalid Date for another no longer gets both into one `UPDATE`. A `Date` and its ISO string are
|
|
51
|
+
two values, and byte arrays compare by their bytes.
|
|
52
|
+
- A `massUpdate` whose SETs all hold the same `sql` expression object, as in
|
|
53
|
+
``massUpdate(ids, { n: sql`n + 1` })``, sends one `UPDATE` instead of one per id.
|
|
54
|
+
- The per-call scope memo ends with the public call that started it. Options a hook kept and passed
|
|
55
|
+
to a later call reused the old memo, which grew with each call and resolved a context object
|
|
56
|
+
mutated in place to its old tenant.
|
|
57
|
+
|
|
58
|
+
### Docs
|
|
59
|
+
|
|
60
|
+
- The 3.8.1 entry now says the 404 ahead of a refused value holds outside a caller's transaction,
|
|
61
|
+
and that a mass write with an SQL value was not safe to retry. The bulk operations page says when
|
|
62
|
+
such a write loads its relations and that one shared `sql` object is one `UPDATE`.
|
|
63
|
+
- The multi-tenancy page says the database compares the keys. The CRUD operations page, the README
|
|
64
|
+
and `AGENT.md` say date strings are parsed on writes.
|
|
65
|
+
|
|
66
|
+
## [3.8.1] - 2026-11-04
|
|
67
|
+
|
|
68
|
+
Fixes from the 3.8.0 audit (D-01 to D-11). Three of them close gaps in tenant isolation: a scope
|
|
69
|
+
handed from one service to another, a batch key that differs from another tenant's only in case,
|
|
70
|
+
and nested children that skipped the key check. No public API or override point changed.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- Text keys compare exactly in the tenant key check, `attach` and `sync`: `"US"` and `"us"` are
|
|
75
|
+
two keys, as they are two rows on PostgreSQL and SQLite. Before, every key was lowercased, so a
|
|
76
|
+
batch holding both could point a row at another tenant's `"us"` and get a 201 where `"us"`
|
|
77
|
+
alone was a 400. A uuid column still ignores case, and so does MySQL, to follow its default
|
|
78
|
+
collations, which match `"us"` to a stored `"US"` and cannot hold both as distinct keys.
|
|
79
|
+
- On a scoped entity, a nested create checks the belongs-to keys of the rows it writes (a
|
|
80
|
+
belongs-to parent, has-many children, new many-to-many targets) against the caller's tenant,
|
|
81
|
+
using the nested relation's own `relations`. A child key to another tenant's row is the same
|
|
82
|
+
400 `ForeignKeyViolationException` as a key to a missing row, and nothing is written. It was a
|
|
83
|
+
201. A child key that no nested relation declares is still left to the database's foreign key.
|
|
84
|
+
- `massSoftDelete` and `massDelete` put `deleted_at IS NULL` in the `WHERE` of their write, as the
|
|
85
|
+
single-row writes have since 3.8.0. A row soft-deleted between the existence check and the
|
|
86
|
+
write is a 404 entry and the batch writes nothing, instead of being stamped again or
|
|
87
|
+
hard-deleted. `massForceDelete` still removes a row in either state.
|
|
88
|
+
- `massCreate` loads `relations` and `counts` inside its transaction again. A load that fails
|
|
89
|
+
rolls the rows back, so a retry no longer stores them twice. The other mass writes still load
|
|
90
|
+
after the commit, where a retry gives the same rows, except for a write whose values hold an SQL
|
|
91
|
+
expression (``massUpdate(ids, { views: sql`views + 1` })``), which 3.8.2 loads inside too.
|
|
92
|
+
- `update` of a missing row with a value the database refuses (`update(999, { views: "abc" })`)
|
|
93
|
+
is a 404, as it was before 3.8.0, instead of a 400, outside a caller's transaction. Inside one
|
|
94
|
+
(`options.transaction`) the failed statement has aborted the transaction on PostgreSQL, so the
|
|
95
|
+
answer stays the 400, which is the same for every tenant.
|
|
96
|
+
|
|
97
|
+
### Fixed
|
|
98
|
+
|
|
99
|
+
- The per-call scope memo is keyed by service and by the context it was resolved for. Options
|
|
100
|
+
handed from one service to another, the natural way for a subclass override to share a
|
|
101
|
+
transaction, carried the first service's scope: a service whose scope is `null` let a scoped
|
|
102
|
+
one read every tenant's rows, and a second service scoped by another column stored `NULL` in
|
|
103
|
+
it. Each service still runs its scope function once per call.
|
|
104
|
+
- An override of `emitEvent` or `prepareUpdate` keeps the guard read ahead of the write, like an
|
|
105
|
+
override of a `before*` hook. An `emitEvent` override saw the before event for another
|
|
106
|
+
tenant's id before the 404.
|
|
107
|
+
- `massUpdate` with a `beforeUpdate` hook that returns `NaN` for one id and `null` for another no
|
|
108
|
+
longer writes `NaN` to both, and a Drizzle `sql` expression as a value (`views + 1`) works as
|
|
109
|
+
it does in `update` instead of failing with a circular `TypeError` (a 500).
|
|
110
|
+
- The tenant key check sends its `SELECT`s, one per relation, together instead of one after the
|
|
111
|
+
other.
|
|
112
|
+
|
|
113
|
+
### Docs
|
|
114
|
+
|
|
115
|
+
- The multi-tenancy FAQ answered "No" to "Are eager-loaded relations filtered by tenant?", against
|
|
116
|
+
the section on the same page. It now says yes, when the related table has the scope columns.
|
|
117
|
+
The multi-tenancy and relations-on-writes pages say which nested keys are checked, and the bulk
|
|
118
|
+
operations page says when a mass write loads its relations.
|
|
119
|
+
- `AGENTS.md`, `CONTRIBUTING.md` and `apps/docs/AGENTS.md` point at the changelog page where it
|
|
120
|
+
lives now, `(support)/changelog.mdx`.
|
|
121
|
+
|
|
14
122
|
## [3.8.0] - 2026-11-03
|
|
15
123
|
|
|
16
124
|
Performance fixes from the 3.7.1 performance report. Calls return the same rows, in the same
|
package/README.md
CHANGED
|
@@ -445,6 +445,10 @@ export class UsersService extends SqlBaseCrudService<User, CreateUserDto, Update
|
|
|
445
445
|
| `detach(id, relation, ids?, options?)` | `Promise<T>` | Unlink some, or all when `ids` is left out. |
|
|
446
446
|
| `sync(id, relation, ids, options?)` | `Promise<T>` | Make the links exactly `ids`. |
|
|
447
447
|
|
|
448
|
+
Every write turns an ISO string or a number for a date-mode `timestamp`, `date`
|
|
449
|
+
or `datetime` column into a `Date`, so a JSON body needs no transforming DTO for
|
|
450
|
+
it. A value that is not a date is a 400 that names the column.
|
|
451
|
+
|
|
448
452
|
### Bulk (run inside a transaction)
|
|
449
453
|
|
|
450
454
|
| Method | Returns |
|
|
@@ -21,12 +21,15 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
21
21
|
protected primaryKeyOf(table: any): string;
|
|
22
22
|
protected isTextColumn(column: any): boolean;
|
|
23
23
|
protected coerceFilterValue(column: any, value: any): any;
|
|
24
|
+
private toDate;
|
|
25
|
+
private coerceDates;
|
|
24
26
|
private static readonly ALLOWLIST_LABELS;
|
|
25
27
|
protected assertAllowed(list: keyof CrudAllowlists, path: string, options?: SqlOperationOptions): void;
|
|
26
28
|
protected checkReadOptions(options?: SqlOperationOptions): void;
|
|
27
29
|
protected rowScope(options?: SqlOperationOptions): any[];
|
|
28
30
|
protected resolveScope(options?: SqlOperationOptions): CrudScopeValues | null;
|
|
29
31
|
private withScopeMemo;
|
|
32
|
+
private scopedCall;
|
|
30
33
|
private checkScope;
|
|
31
34
|
protected scopeConditions(options?: SqlOperationOptions, table?: any): any[];
|
|
32
35
|
protected relatedRowScope(target: any, options?: SqlOperationOptions): any[];
|
|
@@ -178,6 +181,7 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
178
181
|
private keysOutsideScope;
|
|
179
182
|
private assertBatchKeysInScope;
|
|
180
183
|
private assertKeysInScope;
|
|
184
|
+
private assertNestedKeysInScope;
|
|
181
185
|
private missingParentException;
|
|
182
186
|
private linkTargets;
|
|
183
187
|
attach(id: any, relation: string, relatedIds: unknown[], options?: SqlOperationOptions): Promise<T>;
|
|
@@ -185,7 +189,11 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
185
189
|
sync(id: any, relation: string, relatedIds: unknown[], options?: SqlOperationOptions): Promise<T>;
|
|
186
190
|
private changeLinks;
|
|
187
191
|
private valueKey;
|
|
188
|
-
private
|
|
192
|
+
private distinct;
|
|
193
|
+
private matchKeys;
|
|
194
|
+
private keyInput;
|
|
195
|
+
private mysqlKeyCast;
|
|
196
|
+
private pgKeyType;
|
|
189
197
|
private tableName;
|
|
190
198
|
protected prepareUpdate(data: UpdateDto, options?: SqlOperationOptions): Record<string, any>;
|
|
191
199
|
update(id: any, data: UpdateDto, options?: SqlOperationOptions): Promise<T>;
|
|
@@ -218,6 +226,7 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
218
226
|
massRestore(ids: any[], options?: SqlOperationOptions): Promise<T[]>;
|
|
219
227
|
massDelete(ids: any[], options?: SqlOperationOptions): Promise<boolean>;
|
|
220
228
|
massForceDelete(ids: any[], options?: SqlOperationOptions): Promise<boolean>;
|
|
229
|
+
private holdsSql;
|
|
221
230
|
private uniqueIds;
|
|
222
231
|
private fingerprint;
|
|
223
232
|
private assertAllExist;
|