@avytrix/nestjs-drizzle-crud 3.8.1 → 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
 
package/CHANGELOG.md CHANGED
@@ -11,6 +11,58 @@ 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
+
14
66
  ## [3.8.1] - 2026-11-04
15
67
 
16
68
  Fixes from the 3.8.0 audit (D-01 to D-11). Three of them close gaps in tenant isolation: a scope
@@ -34,10 +86,13 @@ and nested children that skipped the key check. No public API or override point
34
86
  write is a 404 entry and the batch writes nothing, instead of being stamped again or
35
87
  hard-deleted. `massForceDelete` still removes a row in either state.
36
88
  - `massCreate` loads `relations` and `counts` inside its transaction again. A load that fails
37
- rolls the rows back, so a retry no longer stores them twice. The other mass writes, which give
38
- the same rows when retried, still load after the commit.
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.
39
92
  - `update` of a missing row with a value the database refuses (`update(999, { views: "abc" })`)
40
- is a 404, as it was before 3.8.0, instead of a 400.
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.
41
96
 
42
97
  ### Fixed
43
98
 
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[];
@@ -186,7 +189,11 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
186
189
  sync(id: any, relation: string, relatedIds: unknown[], options?: SqlOperationOptions): Promise<T>;
187
190
  private changeLinks;
188
191
  private valueKey;
189
- private uniqueValues;
192
+ private distinct;
193
+ private matchKeys;
194
+ private keyInput;
195
+ private mysqlKeyCast;
196
+ private pgKeyType;
190
197
  private tableName;
191
198
  protected prepareUpdate(data: UpdateDto, options?: SqlOperationOptions): Record<string, any>;
192
199
  update(id: any, data: UpdateDto, options?: SqlOperationOptions): Promise<T>;
@@ -219,6 +226,7 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
219
226
  massRestore(ids: any[], options?: SqlOperationOptions): Promise<T[]>;
220
227
  massDelete(ids: any[], options?: SqlOperationOptions): Promise<boolean>;
221
228
  massForceDelete(ids: any[], options?: SqlOperationOptions): Promise<boolean>;
229
+ private holdsSql;
222
230
  private uniqueIds;
223
231
  private fingerprint;
224
232
  private assertAllExist;