@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. Do not add `tenant_id` to filters or DTOs
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 uniqueValues;
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;