@avytrix/nestjs-drizzle-crud 3.7.1 → 3.8.0
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 +13 -4
- package/CHANGELOG.md +148 -1
- package/README.md +24 -6
- package/dist/core/abstract/sql-base-crud.service.d.ts +43 -5
- package/dist/core/abstract/sql-base-crud.service.js +795 -460
- package/dist/core/abstract/sql-base-crud.service.js.map +1 -1
- package/dist/core/utils/query-parser.js +8 -9
- package/dist/core/utils/query-parser.js.map +1 -1
- package/dist/modules/drizzle-connection.js +1 -0
- package/dist/modules/drizzle-connection.js.map +1 -1
- package/dist/modules/drizzle-crud.module.js +4 -0
- package/dist/modules/drizzle-crud.module.js.map +1 -1
- package/package.json +1 -1
package/AGENT.md
CHANGED
|
@@ -189,7 +189,10 @@ Operators inside an object value: `eq`, `gt`, `gte`, `lt`, `lte`, `neq`, `like`,
|
|
|
189
189
|
`sql.caseSensitive`), `iStartsWith`, `iEndsWith`, `iContains` (always
|
|
190
190
|
case-insensitive), and on PostgreSQL only `arrayContains`, `arrayOverlaps` and
|
|
191
191
|
`jsonContains`. Any other operator is a 400. A bare string is an exact match,
|
|
192
|
-
not a pattern, and `%` in it is a literal character.
|
|
192
|
+
not a pattern, and `%` in it is a literal character. On a text column it is
|
|
193
|
+
`lower(col) = lower($1)` in `findAll` and `count` (index `lower(col)`), but a
|
|
194
|
+
plain, case-sensitive `=` in `findOne`. https://crud.avytrix.dev/docs/indexes/
|
|
195
|
+
lists the index each query shape needs; the package creates none. `isNull` and `isNotNull`
|
|
193
196
|
take `true` / `false` or the strings `"true"` / `"false"`.
|
|
194
197
|
|
|
195
198
|
OR and NOT need no raw Drizzle. `and`, `or` and `not` group filter objects, nest,
|
|
@@ -390,8 +393,10 @@ consumer
|
|
|
390
393
|
The scope is ANDed into every read, count and write (bulk, upsert, soft delete,
|
|
391
394
|
restore, force delete, attach included) and written onto every created row, so
|
|
392
395
|
another tenant's id is a 404. A missing context value is a 403, never an
|
|
393
|
-
unscoped query.
|
|
394
|
-
|
|
396
|
+
unscoped query. A belongs-to key a write sets must point at a row of the
|
|
397
|
+
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
|
|
399
|
+
by hand, and use `skipScope: true` only in trusted server code.
|
|
395
400
|
|
|
396
401
|
# Optimistic locking
|
|
397
402
|
|
|
@@ -434,7 +439,8 @@ never reaches the caller as a 500: a unique violation is a 409, a value that is
|
|
|
434
439
|
too long, fails a check, has the wrong type or is not a valid date is a 400 (on
|
|
435
440
|
reads too), a foreign key pointing at a row that does not exist is a 400, and a
|
|
436
441
|
delete blocked by dependent rows is a 409. The same holds on MySQL (strict mode
|
|
437
|
-
errors included) and SQLite (constraint errors
|
|
442
|
+
errors included) and SQLite (constraint errors, and the `STRICT` and
|
|
443
|
+
`SQLITE_MISMATCH` type errors).
|
|
438
444
|
|
|
439
445
|
---
|
|
440
446
|
|
|
@@ -453,6 +459,9 @@ whole set (plus one `SELECT` that checks the ids), so never loop `create` or
|
|
|
453
459
|
`update` over an array yourself. The 400 from `BulkOperationException` lists
|
|
454
460
|
only the failed items: `errors: [{ index | id, status, message }]`.
|
|
455
461
|
`bulkMode: 'perItem'` runs one single-row call per item when a hook needs it.
|
|
462
|
+
One statement may bind at most 65,535 values on PostgreSQL and MySQL, 32,766 on
|
|
463
|
+
SQLite and 100 on Cloudflare D1; past that the call is a 400 before it runs, so
|
|
464
|
+
split very large batches into chunks.
|
|
456
465
|
|
|
457
466
|
To insert or update by a unique key, use
|
|
458
467
|
`upsert(dto, { conflictTarget: 'email' })` or `massUpsert(rows, ...)` rather
|
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,152 @@ 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.0] - 2026-11-03
|
|
15
|
+
|
|
16
|
+
Performance fixes from the 3.7.1 performance report. Calls return the same rows, in the same
|
|
17
|
+
order, with the same errors, in fewer statements and shorter transactions. The one visible
|
|
18
|
+
difference is under Changed: a row soft-deleted between the guard read and the write is now a 404
|
|
19
|
+
instead of a write. Inside `SqlBaseCrudService` the six mass writes share one skeleton, and so
|
|
20
|
+
do `softDelete`, `restore` and `delete`; no public API, override point or message changed.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Full-text search takes a column that already is a `tsvector`, such as a generated, stored
|
|
25
|
+
column, and uses it as is instead of wrapping it in `to_tsvector`, so a GIN index on the column
|
|
26
|
+
serves `fullTextSearch()` and `search.mode: "fullText"`. Before, such a column failed with
|
|
27
|
+
`function to_tsvector(unknown, tsvector) does not exist`. Next to text columns it is
|
|
28
|
+
`coalesce(col, ''::tsvector)`; text columns keep the 3.7.2 vector, bare for one column and
|
|
29
|
+
coalesced for several. On 200,000 rows a search on the stored column with its index took
|
|
30
|
+
0.6 ms, against 600 to 900 ms computing `to_tsvector` per row without one. The full-text page
|
|
31
|
+
shows the index for each shape.
|
|
32
|
+
- A docs page, [Database indexes for fast Drizzle queries](https://crud.avytrix.dev/docs/indexes/),
|
|
33
|
+
lists the index each query shape needs: `lower(col)` (or `citext`, or `caseSensitive: true`) for
|
|
34
|
+
the default case-insensitive equality, the sort columns plus the primary key for cursor pages,
|
|
35
|
+
GIN for full-text search, `pg_trgm` for `contains` and `search`, foreign keys and both join
|
|
36
|
+
table columns for relations, partial indexes on `deleted_at IS NULL`, and the tenant column
|
|
37
|
+
first. It says when to use `withCount: false` or the cursor on a large table. The filtering
|
|
38
|
+
page now spells out that `findOne` compares strings with a plain, case-sensitive `=` while
|
|
39
|
+
`findAll` and `count` use `lower()` by default (unchanged; a default change waits for a major),
|
|
40
|
+
and the hooks page says where after hooks run: after the commit for a single write, inside
|
|
41
|
+
the transaction for a mass write.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- A row soft-deleted between the guard read and the write is no longer written: `update` and
|
|
46
|
+
`softDelete` only touch a live row, `restore` only a row in the trash, and `delete` no longer
|
|
47
|
+
hard-deletes a row that was soft-deleted meanwhile (`forceDelete` still does). Such a call is a
|
|
48
|
+
404 instead of a write, a `false` return or a "Failed to update entity" error, and one whose
|
|
49
|
+
row vanished with an expected version is a 404 instead of a 409.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- Keyset pages seek instead of filtering. `findByCursor` adds a bound on the first sort column
|
|
54
|
+
(`col <= $1` descending, `col >= $1` ascending) ahead of the OR expansion, drops the `IS NULL`
|
|
55
|
+
branch for NOT NULL and primary key columns (nullable and relation columns keep it), and on
|
|
56
|
+
PostgreSQL compares `(a, id) > ($1, $2)` as one row value when every column is NOT NULL and
|
|
57
|
+
sorts the same way. With an index on the sort columns and the primary key, page 9,000 of
|
|
58
|
+
200,000 rows went from 24.4 ms to 0.6 ms (`created_at` descending) and from 50.3 ms to 0.5 ms
|
|
59
|
+
(`views` ascending) on PGlite. A sort column the row already carries is no longer selected a
|
|
60
|
+
second time for the cursor. Pages, their order and cursors are unchanged.
|
|
61
|
+
- Single-row writes skip the read first. On PostgreSQL and SQLite, `update`, `softDelete`,
|
|
62
|
+
`restore`, `delete` and `forceDelete` send one statement whose `WHERE` holds the whole guard
|
|
63
|
+
(primary key, tenant scope, expected version, soft delete state) and read the row only when
|
|
64
|
+
nothing came back, to answer with the same 404 or 409 as before. The read still runs first
|
|
65
|
+
when something a caller can observe sits between it and the write (an overridden `before*`
|
|
66
|
+
hook, `validateUpdate` or `mapUpdateDtoToEntity`, or a before event), so hooks and events see
|
|
67
|
+
exactly what they saw before, and on MySQL. A missing row is still a 404 ahead of a bad body.
|
|
68
|
+
- Mass writes send no savepoint on the happy path when they open their own transaction:
|
|
69
|
+
`massUpdate`, `massSoftDelete`, `massRestore` and `massDelete` take 4 round trips instead of 8
|
|
70
|
+
(`BEGIN`, `SELECT`, the set statement, `COMMIT`), `massCreate` and `massUpsert` 3 instead of 5.
|
|
71
|
+
When the set statement fails, the transaction rolls back and the items are replayed in a fresh
|
|
72
|
+
transaction that is rolled back too, so `BulkOperationException` names the same items with the
|
|
73
|
+
same statuses and messages. Inside a caller's transaction, or once a before event handed the
|
|
74
|
+
transaction to a subscriber, the savepoints stay, so a failed item does not abort the caller's
|
|
75
|
+
PostgreSQL transaction.
|
|
76
|
+
- `massUpdate` sends one `UPDATE` per distinct SET when `beforeUpdate` returns different data
|
|
77
|
+
per id, instead of one `UPDATE` and a savepoint pair per id: a hook with two results over 50
|
|
78
|
+
ids is 5 round trips instead of 155. Rows still come back in the input order, after hooks run
|
|
79
|
+
in that order, and a failure still names every failing id.
|
|
80
|
+
- On MySQL, `massCreate` of rows with auto-increment keys reads the inserted rows back with one
|
|
81
|
+
`SELECT ... WHERE id IN (...)` instead of one per row. The inserts stay one per row: MySQL only
|
|
82
|
+
reports the first id of a multi-row `INSERT`, and InnoDB does not promise the others are
|
|
83
|
+
consecutive.
|
|
84
|
+
- Relation loaders and relation counts split a parent key list that would not fit in one
|
|
85
|
+
statement. `loadBelongsTo`, `loadHasMany`, `loadManyToMany` and `loadCounts` send the keys in
|
|
86
|
+
chunks of 64,535 on PostgreSQL and MySQL, 31,766 on SQLite and 90 on Cloudflare D1, one query per
|
|
87
|
+
chunk, so a nested page whose level holds 70,000 keys loads in two queries instead of failing
|
|
88
|
+
with a 400 (3.7.2) or a 500 (before). Below that, it is one query per relation as before. The
|
|
89
|
+
chunk size is the new protected `relationChunkSize(db)`.
|
|
90
|
+
- Fewer statements in a few calls. `exists(id, options)` is one `SELECT` and no longer loads
|
|
91
|
+
`options.relations` or `options.counts` it throws away (4 statements to 1 with two relations and
|
|
92
|
+
a count); they are still checked against the allowlists. A nested create skips the "already
|
|
93
|
+
linked?" read for its many-to-many ids. `attach`, `sync` and `detach` read the row once and load
|
|
94
|
+
the relation onto it, instead of reading it before and after (8, 9 and 6 round trips to 7, 8 and
|
|
95
|
+
5). A mass write that opened its own transaction loads `options.relations` and `options.counts`
|
|
96
|
+
after it commits, so it holds its row locks for less time; with a count filter, or in a caller's
|
|
97
|
+
transaction, they still load inside. The returned rows are the same.
|
|
98
|
+
- A `scope` function runs once per call instead of once per statement and relation: 5 times for
|
|
99
|
+
a `findAll` with three relations and a count, 7 for a `massUpdate`, now 1 each.
|
|
100
|
+
|
|
101
|
+
## [3.7.2] - 2026-11-03
|
|
102
|
+
|
|
103
|
+
Fixes from the 3.7.1 audit. The batch limit now counts the statement the database receives.
|
|
104
|
+
|
|
105
|
+
### Changed
|
|
106
|
+
|
|
107
|
+
- The bound parameter limit is checked per statement, against the database's own limit:
|
|
108
|
+
65,535 on PostgreSQL and MySQL, 32,766 on SQLite and libSQL, 100 on Cloudflare D1. The service
|
|
109
|
+
counts the parameters of every statement as Drizzle builds it, so two OR'd `IN` lists, the
|
|
110
|
+
version checks of a `massUpdate` with `expectedVersions`, the children of a nested create and
|
|
111
|
+
the targets of `attach` and `sync` all count, and a statement past the limit is a 400 that
|
|
112
|
+
names its count and the limit before it runs. Before, each input list was checked against
|
|
113
|
+
60,000 on its own: some of those calls still ended in a 500, a `massCreate` that would have fit
|
|
114
|
+
on PostgreSQL was refused (it counted four extra values per row), and on SQLite the cap sat
|
|
115
|
+
above the database's limit. The message changes from "Too many values in one call (n); the
|
|
116
|
+
limit is 60000" to "Too many values in one statement: n bound parameters, and PostgreSQL takes
|
|
117
|
+
at most 65535". `bindLimit(db)` is protected, for a server built with another limit.
|
|
118
|
+
`MAX_BATCH_PARAMETERS`, `assertBatchSize` and `insertParameters` stay for compatibility but are
|
|
119
|
+
deprecated and no longer used.
|
|
120
|
+
- On a scoped entity, `create`, `update`, `upsert` and the mass writes check every belongs-to key
|
|
121
|
+
they set against the caller's tenant when the related table has the scope columns, the way
|
|
122
|
+
`attach` checks its targets: one `SELECT` per relation for the whole call. A key to another
|
|
123
|
+
tenant's row is the same 400 `ForeignKeyViolationException` as a key to a missing row, so the
|
|
124
|
+
answer no longer tells them apart, and a missing row is refused even without a database
|
|
125
|
+
foreign key. Calls with `skipScope: true` and related tables without the scope columns are
|
|
126
|
+
not checked.
|
|
127
|
+
- On SQLite, `contains`, `startsWith` and `endsWith` with `sql.caseSensitive: true` compile to
|
|
128
|
+
`GLOB` and match case exactly. They used `LIKE`, which ignores ASCII case. The default
|
|
129
|
+
(case-insensitive) and the `like` operator are unchanged.
|
|
130
|
+
- On MySQL, `connection.idleTimeout` also sets mysql2's `maxIdle` to one below the pool size.
|
|
131
|
+
mysql2 only reaps idle connections when `maxIdle` is below `connectionLimit`, so the timeout
|
|
132
|
+
had no effect before.
|
|
133
|
+
- `forRootAsync` with more than one of `useFactory`, `useClass` and `useExisting` throws at
|
|
134
|
+
startup and names them. Before, `useFactory` won and the class was ignored.
|
|
135
|
+
|
|
136
|
+
### Fixed
|
|
137
|
+
|
|
138
|
+
- On SQLite, a `STRICT` table refusing a value of the wrong type (extended code 3091, which
|
|
139
|
+
libSQL reports as `UNKNOWN_SQLITE_ERROR_3091`) and `SQLITE_MISMATCH` (`"abc"` for an
|
|
140
|
+
`INTEGER PRIMARY KEY`) are a 400 `ValidationFailedException`, not a 500. The dialect contract
|
|
141
|
+
checks the same mapping on every dialect.
|
|
142
|
+
- Filters inside `options.counts` get the same 32 level depth cap as a `findAll` filter. A very
|
|
143
|
+
deep one was a 500 (stack overflow).
|
|
144
|
+
- The filter depth check no longer overflows the call stack on an `IN` list of a few hundred
|
|
145
|
+
thousand items.
|
|
146
|
+
- A batch so large that Drizzle's own SQL builder overflows the call stack is the same 400 as
|
|
147
|
+
any other statement past the limit.
|
|
148
|
+
- Full-text search over several columns no longer drops rows that have `NULL` in one of them.
|
|
149
|
+
The vector was `to_tsvector(cfg, a) || to_tsvector(cfg, b)`, and `x || NULL` is `NULL`, so
|
|
150
|
+
such a row never matched, in `fullTextSearch()` and in `search.mode: "fullText"` alike. Each
|
|
151
|
+
column is now wrapped in `coalesce(column, '')` when there is more than one. A GIN index on the
|
|
152
|
+
old multi-column expression no longer matches the query; recreate it with the `coalesce` (the
|
|
153
|
+
full-text search page has the exact `CREATE INDEX`). A single column is unchanged.
|
|
154
|
+
|
|
155
|
+
### Docs
|
|
156
|
+
|
|
157
|
+
- `AGENTS.md` names the package `@avytrix/nestjs-drizzle-crud`, and says that PGlite, the engine
|
|
158
|
+
of the unit and e2e specs, returns no rows above 32,767 bound parameters.
|
|
159
|
+
|
|
14
160
|
## [3.7.1] - 2026-11-02
|
|
15
161
|
|
|
16
162
|
### Fixed
|
|
@@ -276,7 +422,8 @@ app.
|
|
|
276
422
|
only operator on MySQL is a 400.
|
|
277
423
|
- **Keyset pagination.** `findByCursor(filters, { cursor, limit, sortBy, count }, options)` returns
|
|
278
424
|
`{ data, limit, nextCursor, total? }`. The cursor is base64url JSON of the last row's sort values
|
|
279
|
-
plus the primary key, so pages stay stable under inserts and
|
|
425
|
+
plus the primary key, so pages stay stable under inserts and, with an index on the sort columns
|
|
426
|
+
(since 3.8.0), deep pages cost the same as the
|
|
280
427
|
first. It works with every sort, filter, relation and `select`, keeps timestamp microseconds and
|
|
281
428
|
follows the database's NULL ordering. The count only runs with `count: true`. A cursor that is not
|
|
282
429
|
valid, or was made for another sort, is a 400.
|
package/README.md
CHANGED
|
@@ -287,7 +287,7 @@ export class UsersController {
|
|
|
287
287
|
|---|---|---|
|
|
288
288
|
| `dialect` | `'postgresql' \| 'mysql' \| 'sqlite'` | **Required.** Database dialect. `'sqlite'` covers SQLite, libSQL and Cloudflare D1. |
|
|
289
289
|
| `connectionString` | `string` | Connection string. The module builds the connection (PostgreSQL with `postgres`, MySQL with `mysql2`). |
|
|
290
|
-
| `connection` | `{ poolSize?, idleTimeout?, ssl? }` | Pool size, idle timeout (seconds) and TLS for the connection built from `connectionString`. |
|
|
290
|
+
| `connection` | `{ poolSize?, idleTimeout?, ssl? }` | Pool size, idle timeout (seconds) and TLS for the connection built from `connectionString`. On MySQL an idle timeout also sets mysql2's `maxIdle` one below the pool size, so idle connections really close. |
|
|
291
291
|
| `db` | `Drizzle instance` | Alternatively, pass a Drizzle instance you built yourself (any dialect). |
|
|
292
292
|
| `schema` | `Record<string, unknown>` | Drizzle schema, used when building from `connectionString`. |
|
|
293
293
|
| `defaults.softDelete` | `boolean` | Enable soft delete for all entities (default `true`). |
|
|
@@ -334,7 +334,8 @@ DrizzleCrudModule.forRootAsync({
|
|
|
334
334
|
```
|
|
335
335
|
|
|
336
336
|
`forRootAsync` also takes `useClass` or `useExisting` with a class that
|
|
337
|
-
implements `DrizzleCrudOptionsFactory
|
|
337
|
+
implements `DrizzleCrudOptionsFactory`. Give exactly one of the three; more than
|
|
338
|
+
one is a startup error:
|
|
338
339
|
|
|
339
340
|
```typescript
|
|
340
341
|
@Injectable()
|
|
@@ -542,7 +543,7 @@ await service.findAll({
|
|
|
542
543
|
| `in` / `notIn` | `IN (...)` / `NOT IN (...)` |
|
|
543
544
|
| `between` | `BETWEEN a AND b`, `[from, to]` |
|
|
544
545
|
| `isNull` / `isNotNull` | `true` / `"true"` gives `IS NULL` / `IS NOT NULL`, `false` / `"false"` the opposite |
|
|
545
|
-
| `startsWith` / `endsWith` / `contains` | escaped `LIKE`, case-insensitive unless `sql.caseSensitive` |
|
|
546
|
+
| `startsWith` / `endsWith` / `contains` | escaped `LIKE`, case-insensitive unless `sql.caseSensitive` (then `GLOB` on SQLite, whose `LIKE` ignores case) |
|
|
546
547
|
| `iStartsWith` / `iEndsWith` / `iContains` | escaped `ILIKE` (`LIKE` on MySQL), always case-insensitive |
|
|
547
548
|
| `arrayContains` / `arrayOverlaps` | PostgreSQL arrays, `@>` / `&&` |
|
|
548
549
|
| `jsonContains` | PostgreSQL `jsonb`, `@>` |
|
|
@@ -562,6 +563,12 @@ instead of returning an arbitrary row.
|
|
|
562
563
|
> `like` / `ilike` operators and supply your own wildcards, or `startsWith` /
|
|
563
564
|
> `endsWith` / `contains`, which escape the value for you.
|
|
564
565
|
|
|
566
|
+
`findOne` is the exception: it compiles a string to a plain `=`, so it is
|
|
567
|
+
case-sensitive whatever `sql.caseSensitive` says. A plain index on the column
|
|
568
|
+
cannot serve `lower(column) = lower($1)`; create one on `lower(column)`. The
|
|
569
|
+
[indexes page](https://crud.avytrix.dev/docs/indexes/) lists the index each
|
|
570
|
+
filter, sort, search and relation needs.
|
|
571
|
+
|
|
565
572
|
---
|
|
566
573
|
|
|
567
574
|
## Query strings: parseQuery
|
|
@@ -653,7 +660,9 @@ in `forRoot` to apply a `created_at` fallback to every entity that doesn't defin
|
|
|
653
660
|
## Cursor pagination
|
|
654
661
|
|
|
655
662
|
`findByCursor` pages with `WHERE (sort columns) > (last row)` instead of
|
|
656
|
-
`OFFSET`,
|
|
663
|
+
`OFFSET`, starting with a bound on the first sort column, so with an index on
|
|
664
|
+
the sort columns plus the primary key (`(created_at DESC, id)` below) deep
|
|
665
|
+
pages stay as fast as the first, and rows inserted earlier do not shift the
|
|
657
666
|
next page:
|
|
658
667
|
|
|
659
668
|
```typescript
|
|
@@ -930,7 +939,9 @@ Each bulk method is one statement for the whole set (plus one `SELECT` that
|
|
|
930
939
|
checks the ids exist), inside a single transaction: `massUpdate` over 100 ids is
|
|
931
940
|
two queries. If any item fails, a `BulkOperationException` (400) is thrown and
|
|
932
941
|
the transaction rolls back. The failing items are found by replaying the batch
|
|
933
|
-
|
|
942
|
+
in a transaction (or, inside your own transaction, a savepoint) that is rolled
|
|
943
|
+
back, and the response body lists only them. The happy path sends no savepoint
|
|
944
|
+
unless it runs inside your transaction:
|
|
934
945
|
|
|
935
946
|
```json
|
|
936
947
|
{ "statusCode": 400, "error": "Bad Request",
|
|
@@ -940,6 +951,11 @@ inside a savepoint that is rolled back, and the response body lists only them:
|
|
|
940
951
|
|
|
941
952
|
`bulkMode: 'perItem'` runs the single-row method per item instead, as in 3.3.
|
|
942
953
|
|
|
954
|
+
One statement may bind at most 65,535 values on PostgreSQL and MySQL, 32,766 on
|
|
955
|
+
SQLite and 100 on Cloudflare D1. The service counts the real statement (every
|
|
956
|
+
column, version check and OR'd list included) and answers one past the limit
|
|
957
|
+
with a 400 that names both numbers, before it runs. Split larger batches.
|
|
958
|
+
|
|
943
959
|
---
|
|
944
960
|
|
|
945
961
|
## Upsert
|
|
@@ -1044,7 +1060,9 @@ await docs.findAll({}, {}, { context: { tenantId: 7 } });
|
|
|
1044
1060
|
|
|
1045
1061
|
A missing context value is a 403 (`MissingScopeException`), never an unscoped
|
|
1046
1062
|
query. A resolver that returns `null`, or `skipScope: true`, runs unscoped on
|
|
1047
|
-
purpose.
|
|
1063
|
+
purpose. A belongs-to key that a write sets must point at a row of the same
|
|
1064
|
+
tenant when the related table has the scope columns: another tenant's row is the
|
|
1065
|
+
same 400 `ForeignKeyViolationException` as a missing one.
|
|
1048
1066
|
|
|
1049
1067
|
---
|
|
1050
1068
|
|
|
@@ -26,6 +26,8 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
26
26
|
protected checkReadOptions(options?: SqlOperationOptions): void;
|
|
27
27
|
protected rowScope(options?: SqlOperationOptions): any[];
|
|
28
28
|
protected resolveScope(options?: SqlOperationOptions): CrudScopeValues | null;
|
|
29
|
+
private withScopeMemo;
|
|
30
|
+
private checkScope;
|
|
29
31
|
protected scopeConditions(options?: SqlOperationOptions, table?: any): any[];
|
|
30
32
|
protected relatedRowScope(target: any, options?: SqlOperationOptions): any[];
|
|
31
33
|
protected relatedScopeConditions(target: any, options?: SqlOperationOptions): any[];
|
|
@@ -35,6 +37,14 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
35
37
|
protected filterWritable(entity: Record<string, any>, operation: "create" | "update", options?: SqlOperationOptions): Record<string, any>;
|
|
36
38
|
protected keyOf(value: unknown): string;
|
|
37
39
|
constructor(config: SqlCrudConfig);
|
|
40
|
+
private readonly boundDbs;
|
|
41
|
+
protected dbFor(transaction?: any): any;
|
|
42
|
+
protected withBindLimit(db: any): any;
|
|
43
|
+
protected bindLimit(db: any): {
|
|
44
|
+
limit: number;
|
|
45
|
+
name: string;
|
|
46
|
+
};
|
|
47
|
+
protected isD1(db: any): boolean;
|
|
38
48
|
private validateConfiguration;
|
|
39
49
|
private validateColumns;
|
|
40
50
|
private validateRelations;
|
|
@@ -53,6 +63,7 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
53
63
|
protected beforeRestore(_id: any): Promise<void>;
|
|
54
64
|
protected afterRestore(_entity: T): Promise<void>;
|
|
55
65
|
protected relationType(relation: RelationConfig): RelationType;
|
|
66
|
+
private relationNamed;
|
|
56
67
|
protected isCollection(relation: RelationConfig): boolean;
|
|
57
68
|
protected eagerRelations(relations: string[]): string[];
|
|
58
69
|
protected relationTree(paths: string[], configured?: RelationsConfig): RelationNode[];
|
|
@@ -70,7 +81,12 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
70
81
|
protected loadRelations(db: any, hosts: any[], nodes: RelationNode[], select?: SelectTree, hostTable?: any, options?: SqlOperationOptions): Promise<void>;
|
|
71
82
|
protected loadBelongsTo(db: any, hosts: any[], node: RelationNode, select: SelectTree, options?: SqlOperationOptions): Promise<void>;
|
|
72
83
|
protected loadHasMany(db: any, hosts: any[], node: RelationNode, select: SelectTree, hostTable?: any, options?: SqlOperationOptions): Promise<void>;
|
|
84
|
+
private loaderKeys;
|
|
85
|
+
private loadCollection;
|
|
86
|
+
private selectThrough;
|
|
73
87
|
protected loadManyToMany(db: any, hosts: any[], node: RelationNode, select: SelectTree, hostTable?: any, options?: SqlOperationOptions): Promise<void>;
|
|
88
|
+
protected relationChunkSize(db: any): number;
|
|
89
|
+
private inChunks;
|
|
74
90
|
private groupCollection;
|
|
75
91
|
protected applyJoins(query: any, relationNames: string[], options?: SqlOperationOptions): any;
|
|
76
92
|
protected normalizeRelations(rows: any[], eager: string[]): any[];
|
|
@@ -103,6 +119,7 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
103
119
|
vector: any;
|
|
104
120
|
query: any;
|
|
105
121
|
};
|
|
122
|
+
private isTsVector;
|
|
106
123
|
protected buildSearchCondition(search?: SqlOperationOptions["search"]): any;
|
|
107
124
|
protected caseInsensitiveLike(column: any, pattern: string): any;
|
|
108
125
|
protected likePattern(column: any, pattern: string): any;
|
|
@@ -110,14 +127,20 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
110
127
|
idKind(): "int" | "bigint" | "uuid" | "string";
|
|
111
128
|
parseId(raw: unknown): Promise<unknown>;
|
|
112
129
|
private findForGuard;
|
|
130
|
+
private guardRow;
|
|
131
|
+
private skipsGuard;
|
|
132
|
+
private guardOnError;
|
|
113
133
|
protected assertVersion(id: any, actual: unknown, expected: unknown): void;
|
|
114
134
|
protected writeWhere(ids: any[], options?: SqlOperationOptions, table?: any): any;
|
|
115
135
|
protected withVersionBump(set: Record<string, any>): Record<string, any>;
|
|
116
136
|
find(id: any, options?: SqlOperationOptions): Promise<T | null>;
|
|
117
137
|
findOne(where: Partial<T>, options?: SqlOperationOptions): Promise<T | null>;
|
|
118
138
|
private findFirst;
|
|
139
|
+
private readFirst;
|
|
140
|
+
private hydrate;
|
|
119
141
|
private planList;
|
|
120
142
|
private listConditions;
|
|
143
|
+
private parseCount;
|
|
121
144
|
private buildCountQuery;
|
|
122
145
|
findAll(filters: FilterDto | undefined, pagination: PaginationOptions & {
|
|
123
146
|
withCount: false;
|
|
@@ -130,6 +153,10 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
130
153
|
protected cursorField(column: any): any;
|
|
131
154
|
private compareCursor;
|
|
132
155
|
protected keysetCondition(specs: SortSpec[], values: unknown[]): any;
|
|
156
|
+
private keysetBound;
|
|
157
|
+
private keysetRowComparison;
|
|
158
|
+
private keysetNullable;
|
|
159
|
+
private nullsFirst;
|
|
133
160
|
private afterCursor;
|
|
134
161
|
protected emitEvent(phase: "before" | "after", operation: CrudWriteOperation, bulk: boolean, payload: {
|
|
135
162
|
ids?: unknown[];
|
|
@@ -139,6 +166,8 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
139
166
|
}, options?: SqlOperationOptions): Promise<void>;
|
|
140
167
|
private idsOf;
|
|
141
168
|
protected loadWriteRelations(db: any, rows: T[], options?: SqlOperationOptions): Promise<void>;
|
|
169
|
+
private loadsAfterCommit;
|
|
170
|
+
private writeThenLoad;
|
|
142
171
|
private expectedVersionOf;
|
|
143
172
|
protected prepareCreate(data: CreateDto, options?: SqlOperationOptions): Promise<Record<string, any>>;
|
|
144
173
|
private insertOne;
|
|
@@ -146,6 +175,10 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
146
175
|
private takeNested;
|
|
147
176
|
private insertNested;
|
|
148
177
|
private relatedRow;
|
|
178
|
+
private keysOutsideScope;
|
|
179
|
+
private assertBatchKeysInScope;
|
|
180
|
+
private assertKeysInScope;
|
|
181
|
+
private missingParentException;
|
|
149
182
|
private linkTargets;
|
|
150
183
|
attach(id: any, relation: string, relatedIds: unknown[], options?: SqlOperationOptions): Promise<T>;
|
|
151
184
|
detach(id: any, relation: string, relatedIds?: unknown[], options?: SqlOperationOptions): Promise<T>;
|
|
@@ -159,6 +192,8 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
159
192
|
private updateWhereIn;
|
|
160
193
|
private fetchByIds;
|
|
161
194
|
private inIdOrder;
|
|
195
|
+
private guardedWrite;
|
|
196
|
+
private staleOrFalse;
|
|
162
197
|
softDelete(id: any, options?: SqlOperationOptions): Promise<boolean>;
|
|
163
198
|
private softDeleteSet;
|
|
164
199
|
restore(id: any, options?: SqlOperationOptions): Promise<T>;
|
|
@@ -166,14 +201,18 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
166
201
|
forceDelete(id: any, options?: SqlOperationOptions): Promise<boolean>;
|
|
167
202
|
upsert(data: CreateDto, upsert?: UpsertOptions, options?: SqlOperationOptions): Promise<T>;
|
|
168
203
|
massUpsert(data: CreateDto[], upsert?: UpsertOptions, options?: SqlOperationOptions): Promise<T[]>;
|
|
204
|
+
private upsertSet;
|
|
169
205
|
private conflictKeys;
|
|
170
206
|
private conflictValue;
|
|
171
207
|
private upsertRows;
|
|
172
208
|
private excludedValue;
|
|
209
|
+
private massWrite;
|
|
173
210
|
massCreate(data: CreateDto[], options?: SqlOperationOptions): Promise<T[]>;
|
|
174
211
|
private insertMany;
|
|
212
|
+
private readBackCreated;
|
|
175
213
|
private assertAllWritten;
|
|
176
214
|
massUpdate(ids: any[], data: UpdateDto, options?: SqlOperationOptions): Promise<T[]>;
|
|
215
|
+
private updateSets;
|
|
177
216
|
private runKeyed;
|
|
178
217
|
massSoftDelete(ids: any[], options?: SqlOperationOptions): Promise<boolean>;
|
|
179
218
|
massRestore(ids: any[], options?: SqlOperationOptions): Promise<T[]>;
|
|
@@ -189,12 +228,9 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
189
228
|
id?: any;
|
|
190
229
|
run: (sp: any) => Promise<unknown>;
|
|
191
230
|
}>, operation: "write" | "delete"): Promise<R>;
|
|
231
|
+
private settleSet;
|
|
192
232
|
private diagnose;
|
|
193
|
-
private
|
|
194
|
-
private massUpdatePerItem;
|
|
195
|
-
private massSoftDeletePerItem;
|
|
196
|
-
private massRestorePerItem;
|
|
197
|
-
private massDeletePerItem;
|
|
233
|
+
private eachInSavepoint;
|
|
198
234
|
protected savepoint<R>(tx: any, run: (sp: any) => Promise<R>): Promise<R>;
|
|
199
235
|
exists(id: any, options?: SqlOperationOptions): Promise<boolean>;
|
|
200
236
|
count(filters?: FilterDto, options?: SqlOperationOptions): Promise<number>;
|
|
@@ -203,6 +239,7 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
203
239
|
protected countNames(counts?: RelationCounts): string[];
|
|
204
240
|
protected withCountKeys(base: string[], counts?: RelationCounts): string[];
|
|
205
241
|
protected loadCounts(db: any, rows: any[], counts: RelationCounts | undefined, options?: SqlOperationOptions): Promise<void>;
|
|
242
|
+
private countPlan;
|
|
206
243
|
private countQuery;
|
|
207
244
|
protected pushColumnCondition(conditions: any[], column: any, value: any, caseInsensitive?: boolean): void;
|
|
208
245
|
protected buildCollectionFilter(relation: RelationConfig, value: Record<string, any>, db?: any, name?: string, options?: SqlOperationOptions): any | undefined;
|
|
@@ -229,6 +266,7 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
229
266
|
private patternCondition;
|
|
230
267
|
protected applyComplexFilter(conditions: any[], column: any, filterObj: any): void;
|
|
231
268
|
protected executeSqlTransaction<R>(operation: (tx: any) => Promise<R>, existingTransaction?: any): Promise<R>;
|
|
269
|
+
private readonly ownedTransactions;
|
|
232
270
|
protected assertTransactions(db: any): void;
|
|
233
271
|
protected getEntityName(): string;
|
|
234
272
|
fullTextSearch(searchTerm: string, searchColumns: string[], pagination?: PaginationOptions, options?: FullTextSearchOptions<FilterDto>): Promise<{
|