@avytrix/nestjs-drizzle-crud 3.6.0 → 3.7.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 +17 -9
- package/CHANGELOG.md +47 -1
- package/README.md +56 -20
- package/dist/core/abstract/sql-base-crud.service.d.ts +13 -2
- package/dist/core/abstract/sql-base-crud.service.js +147 -41
- package/dist/core/abstract/sql-base-crud.service.js.map +1 -1
- package/dist/core/interfaces/drizzle-crud-config.interface.d.ts +16 -0
- package/dist/core/interfaces/sql-crud-config.interface.d.ts +9 -1
- package/dist/core/types/sql.types.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/modules/drizzle-connection.d.ts +3 -1
- package/dist/modules/drizzle-connection.js +49 -2
- package/dist/modules/drizzle-connection.js.map +1 -1
- package/dist/modules/drizzle-crud.module.d.ts +2 -6
- package/dist/modules/drizzle-crud.module.js +28 -6
- package/dist/modules/drizzle-crud.module.js.map +1 -1
- package/package.json +10 -2
package/AGENT.md
CHANGED
|
@@ -60,11 +60,16 @@ pnpm add @avytrix/nestjs-drizzle-crud
|
|
|
60
60
|
pnpm add @nestjs/common @nestjs/core drizzle-orm reflect-metadata # peers, always
|
|
61
61
|
pnpm add postgres # PostgreSQL
|
|
62
62
|
pnpm add mysql2 # MySQL
|
|
63
|
+
pnpm add @libsql/client # SQLite / libSQL
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
`postgres`
|
|
66
|
-
the connection from a `connectionString`
|
|
67
|
-
`
|
|
66
|
+
`postgres` and `mysql2` are optional peers. You only need them when you let the
|
|
67
|
+
module build the connection from a `connectionString` (PostgreSQL or MySQL;
|
|
68
|
+
`connection: { poolSize, idleTimeout, ssl }` tunes it). If you pass your own
|
|
69
|
+
`db`, no driver peer is required. SQLite (`dialect: 'sqlite'`, also libSQL and
|
|
70
|
+
Cloudflare D1) always takes your own `db`. On D1 and synchronous SQLite drivers
|
|
71
|
+
the mass writes, nested creates and attach / detach / sync throw a
|
|
72
|
+
`TransactionException`; use libSQL when you need them.
|
|
68
73
|
|
|
69
74
|
Requires Node >= 20 and `emitDecoratorMetadata` / `experimentalDecorators` in
|
|
70
75
|
`tsconfig.json`, which the Nest CLI already sets.
|
|
@@ -87,7 +92,8 @@ DrizzleCrudModule.forRootAsync({
|
|
|
87
92
|
```
|
|
88
93
|
|
|
89
94
|
Use `forRoot` for a static config and `forRootAsync` when the value comes from
|
|
90
|
-
`ConfigService` or the environment
|
|
95
|
+
`ConfigService` or the environment (`useFactory`, or `useClass` / `useExisting`
|
|
96
|
+
with a class that implements `DrizzleCrudOptionsFactory`). Provide exactly one of `connectionString`
|
|
91
97
|
or `db`. The module is global, so feature modules do not import it again for
|
|
92
98
|
the connection.
|
|
93
99
|
|
|
@@ -138,7 +144,7 @@ Those are legacy manual patterns and they break registration.
|
|
|
138
144
|
| OpenAPI | `@ApiCrudList(source)`, `@ApiCrudQuery(source)` (needs `@nestjs/swagger`) |
|
|
139
145
|
| Bulk | `massCreate`, `massUpdate`, `massUpsert`, `massSoftDelete`, `massRestore`, `massDelete`, `massForceDelete` |
|
|
140
146
|
| Query string | `parseQuery(query, options)`, or the standalone `parseCrudQuery(query, table, options)` |
|
|
141
|
-
| Search | `fullTextSearch(term, columns, pagination)`, PostgreSQL only |
|
|
147
|
+
| Search | `fullTextSearch(term, columns, pagination, { filters, relations, select, language, queryMode })`, PostgreSQL only |
|
|
142
148
|
| Transaction | `executeSqlTransaction(cb)` |
|
|
143
149
|
|
|
144
150
|
`findAll` returns `{ data, total, page, limit }`, not a bare array; with
|
|
@@ -427,7 +433,8 @@ Driver error codes are translated at the service boundary, so bad client input
|
|
|
427
433
|
never reaches the caller as a 500: a unique violation is a 409, a value that is
|
|
428
434
|
too long, fails a check, has the wrong type or is not a valid date is a 400 (on
|
|
429
435
|
reads too), a foreign key pointing at a row that does not exist is a 400, and a
|
|
430
|
-
delete blocked by dependent rows is a 409.
|
|
436
|
+
delete blocked by dependent rows is a 409. The same holds on MySQL (strict mode
|
|
437
|
+
errors included) and SQLite (constraint errors).
|
|
431
438
|
|
|
432
439
|
---
|
|
433
440
|
|
|
@@ -452,9 +459,10 @@ To insert or update by a unique key, use
|
|
|
452
459
|
than a find followed by a create.
|
|
453
460
|
|
|
454
461
|
Take a row lock with `options.lock: 'update' | 'share'` only inside a
|
|
455
|
-
transaction, and prefer `forNoKeyUpdate` when the key is not changing.
|
|
456
|
-
covers the service's own table only (`FOR UPDATE OF
|
|
457
|
-
an eager belongs-to.
|
|
462
|
+
transaction, and prefer `forNoKeyUpdate` when the key is not changing. On
|
|
463
|
+
PostgreSQL the lock covers the service's own table only (`FOR UPDATE OF
|
|
464
|
+
<table>`), so it works with an eager belongs-to. MySQL gets `FOR UPDATE` or
|
|
465
|
+
`FOR SHARE`; on SQLite a lock is a 400.
|
|
458
466
|
|
|
459
467
|
---
|
|
460
468
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to `@avytrix/nestjs-drizzle-crud` (published as `nestjs-drizzle-crud` up to 3.
|
|
3
|
+
All notable changes to `@avytrix/nestjs-drizzle-crud` (published as `nestjs-drizzle-crud` up to 3.3.0) are documented here.
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
@@ -8,6 +8,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
Scope note: the 3.0.x line is PostgreSQL-first. MySQL-specific paths (non-`RETURNING`
|
|
9
9
|
restore, `ER_DUP_ENTRY` mapping, dialect locking) are tracked separately and unchanged here.
|
|
10
10
|
|
|
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
|
+
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
|
+
|
|
14
|
+
## [3.7.0] - 2026-11-02
|
|
15
|
+
|
|
16
|
+
SQLite support, MySQL connections built for you, and full-text search that works like `findAll`.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `dialect: "sqlite"` for SQLite, libSQL (Turso) and Cloudflare D1. Pass the Drizzle instance as
|
|
21
|
+
`db`. Writes use `RETURNING`, `upsert` uses `ON CONFLICT DO UPDATE` with the tenant guard, and
|
|
22
|
+
keyset pages follow SQLite's NULL order. `LIKE` gets `ESCAPE '\'`, so `%`, `_` and `\` in a
|
|
23
|
+
search term match literally. Constraint errors map like the other dialects: `UNIQUE` 409,
|
|
24
|
+
`FOREIGN KEY` 400 on a write and 409 on a delete, `NOT NULL` and `CHECK` 400. Full-text search,
|
|
25
|
+
row locks and the array and JSON operators are a 400.
|
|
26
|
+
- On Cloudflare D1 and synchronous SQLite drivers (better-sqlite3, bun:sqlite, sql.js), the calls
|
|
27
|
+
that need an interactive transaction (mass writes, nested creates, `attach`, `detach`, `sync`)
|
|
28
|
+
throw a `TransactionException` that says why, before any statement runs. `TransactionException`
|
|
29
|
+
is no longer deprecated.
|
|
30
|
+
- `connectionString` works with `dialect: "mysql"`: the module builds a `mysql2` pool, wraps it
|
|
31
|
+
with `drizzle-orm/mysql2` and ends it on shutdown. `mysql2` is a new optional peer, loaded only
|
|
32
|
+
on that path.
|
|
33
|
+
- `connection: { poolSize, idleTimeout, ssl }` for the connection the module builds from
|
|
34
|
+
`connectionString`, on PostgreSQL (postgres-js) and MySQL (mysql2).
|
|
35
|
+
- `forRootAsync` takes `useClass` and `useExisting`, with a class that implements
|
|
36
|
+
`DrizzleCrudOptionsFactory` (`createDrizzleCrudOptions()`), next to `useFactory`.
|
|
37
|
+
- `fullTextSearch()` takes `options.filters`, and honours `options.relations`, `options.select`
|
|
38
|
+
and `options.counts`, the same as `findAll`. Before, they were ignored.
|
|
39
|
+
- `language` (the text search configuration, default `"english"`) and `queryMode` (`"plain"`, the
|
|
40
|
+
default, or `"websearch"` for `websearch_to_tsquery`) on `fullTextSearch()` options and on
|
|
41
|
+
`search` with `mode: "fullText"`. A language that is not a plain identifier is a 400.
|
|
42
|
+
- New types: `DrizzleConnectionOptions`, `DrizzleCrudAsyncOptions`, `DrizzleCrudOptionsFactory`,
|
|
43
|
+
`FullTextOptions`, `FullTextQueryMode` and `FullTextSearchOptions`.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- MySQL: `lock` and `forNoKeyUpdate` add `FOR UPDATE` or `FOR SHARE` (`forNoKeyUpdate` becomes
|
|
48
|
+
`FOR UPDATE`). They used to be accepted and silently ignored.
|
|
49
|
+
- MySQL strict mode data errors (a missing required value, a value too long, out of range, of
|
|
50
|
+
the wrong type, a bad date, a failed check) are a 400 that names the column, instead of a 500.
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
|
|
54
|
+
- The default `sql.useReturning` is `true` for every dialect except MySQL, so SQLite gets it.
|
|
55
|
+
PostgreSQL and MySQL keep the values they had.
|
|
56
|
+
|
|
11
57
|
## [3.6.0] - 2026-11-02
|
|
12
58
|
|
|
13
59
|
Fixes from the second audit. The first one is a security fix: upgrade if you use `scope`.
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
[](https://github.com/sponsors/avytrix)
|
|
7
7
|
|
|
8
|
-
**nestjs-drizzle-crud is a NestJS + Drizzle ORM CRUD library: extend one base class and every table gets type-safe find, create, update, delete, filtering, pagination, relations and soft delete, on PostgreSQL or
|
|
8
|
+
**nestjs-drizzle-crud is a NestJS + Drizzle ORM CRUD library: extend one base class and every table gets type-safe find, create, update, delete, filtering, pagination, relations and soft delete, on PostgreSQL, MySQL or SQLite.**
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
pnpm add @avytrix/nestjs-drizzle-crud drizzle-orm
|
|
@@ -94,7 +94,7 @@ sponsors are what make keeping it free sustainable.
|
|
|
94
94
|
- 🚀 **CRUD**: `find`, `findOne`, `findAll`, `create`, `update`, `delete`, and more
|
|
95
95
|
- 🧩 **Configure once**: `forRoot()` owns the connection; services only declare a table
|
|
96
96
|
- ⚡ **Type-safe**: generics over your entity, create/update DTOs and filter types
|
|
97
|
-
- 🗃️ **PostgreSQL &
|
|
97
|
+
- 🗃️ **PostgreSQL, MySQL & SQLite**: dialect-aware (`RETURNING` vs `insertId`), libSQL and Cloudflare D1 included
|
|
98
98
|
- 🔄 **Soft delete**: opt-in, with `restore`, a trash view (`onlyDeleted`) and `forceDelete`
|
|
99
99
|
- 📦 **Bulk operations**: one statement per mass create/update/delete, inside a transaction, plus `upsert`
|
|
100
100
|
- 🔍 **Filtering**: comparison, `in`/`notIn`, `between`, escaped `contains`/`startsWith`, array and JSON operators, `or`/`not` groups
|
|
@@ -136,23 +136,23 @@ pnpm add @nestjs/common @nestjs/core drizzle-orm reflect-metadata
|
|
|
136
136
|
# PostgreSQL (also required if you use `connectionString` with dialect 'postgresql')
|
|
137
137
|
pnpm add postgres
|
|
138
138
|
|
|
139
|
-
# MySQL
|
|
139
|
+
# MySQL (also required if you use `connectionString` with dialect 'mysql')
|
|
140
140
|
pnpm add mysql2
|
|
141
141
|
|
|
142
|
+
# SQLite / libSQL (you build the Drizzle instance and pass it as `db`)
|
|
143
|
+
pnpm add @libsql/client
|
|
144
|
+
|
|
142
145
|
# Only for ApiCrudQuery / ApiCrudList (OpenAPI docs)
|
|
143
146
|
pnpm add @nestjs/swagger
|
|
144
147
|
```
|
|
145
148
|
|
|
146
149
|
> Using npm or yarn? Swap `pnpm add` for `npm install` / `yarn add`.
|
|
147
150
|
|
|
148
|
-
> **pnpm note:** `postgres`
|
|
149
|
-
>
|
|
150
|
-
> `connectionString` make sure
|
|
151
|
-
>
|
|
152
|
-
|
|
153
|
-
> `postgres` is an **optional** peer dependency. It's only needed when you let the
|
|
154
|
-
> module build the connection from a `connectionString` for the `postgresql` dialect.
|
|
155
|
-
> If you pass a pre-built `db` instead, you don't need it.
|
|
151
|
+
> **pnpm note:** `postgres` and `mysql2` are optional peer dependencies. pnpm
|
|
152
|
+
> enforces peer deps strictly, so if you build the connection from a
|
|
153
|
+
> `connectionString` make sure the driver for your dialect is installed in your app.
|
|
154
|
+
> If you pass a pre-built `db` instead, you don't need either. SQLite always takes a
|
|
155
|
+
> `db` you build.
|
|
156
156
|
|
|
157
157
|
---
|
|
158
158
|
|
|
@@ -285,15 +285,16 @@ export class UsersController {
|
|
|
285
285
|
|
|
286
286
|
| Field | Type | Description |
|
|
287
287
|
|---|---|---|
|
|
288
|
-
| `dialect` | `'postgresql' \| 'mysql'` | **Required.** Database dialect. |
|
|
289
|
-
| `connectionString` | `string` | Connection string. The module builds the connection (PostgreSQL
|
|
288
|
+
| `dialect` | `'postgresql' \| 'mysql' \| 'sqlite'` | **Required.** Database dialect. `'sqlite'` covers SQLite, libSQL and Cloudflare D1. |
|
|
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
291
|
| `db` | `Drizzle instance` | Alternatively, pass a Drizzle instance you built yourself (any dialect). |
|
|
291
292
|
| `schema` | `Record<string, unknown>` | Drizzle schema, used when building from `connectionString`. |
|
|
292
293
|
| `defaults.softDelete` | `boolean` | Enable soft delete for all entities (default `true`). |
|
|
293
294
|
| `defaults.timestamps` | `boolean` | Auto-manage `created_at`/`updated_at` for all entities (default `true`). |
|
|
294
295
|
| `defaults.pagination` | `{ defaultLimit?, maxLimit? }` | Pagination defaults (default `{ 20, 100 }`). Either key alone keeps the other's default. |
|
|
295
296
|
| `defaults.sortOrder` | `'asc' \| 'desc'` | Opt-in default sort: entities **without** an explicit `defaultSort` fall back to ordering by their `created_at` column in this direction. Omit it and `findAll` orders by the primary key only. |
|
|
296
|
-
| `sql` | `{ caseSensitive?, useReturning? }` | Dialect tuning. `useReturning` defaults to `true` for PostgreSQL, `false` for MySQL. `jsonSupport` and `enableFullTextSearch` are deprecated and do nothing. |
|
|
297
|
+
| `sql` | `{ caseSensitive?, useReturning? }` | Dialect tuning. `useReturning` defaults to `true` for PostgreSQL and SQLite, `false` for MySQL. `jsonSupport` and `enableFullTextSearch` are deprecated and do nothing. |
|
|
297
298
|
| `hooks` | `{ enableGlobalHooks?, subscribers? }` | Module-level write events on the `CrudEventBus`. See [Events](#events-and-global-hooks). |
|
|
298
299
|
|
|
299
300
|
> **Provide exactly one of `connectionString` or `db`.** If your tables have no
|
|
@@ -306,7 +307,7 @@ export class UsersController {
|
|
|
306
307
|
> table does not have stops the app at startup, and the message names the key
|
|
307
308
|
> to use.
|
|
308
309
|
|
|
309
|
-
**Build the connection yourself (any dialect,
|
|
310
|
+
**Build the connection yourself (any dialect, required for SQLite):**
|
|
310
311
|
|
|
311
312
|
```typescript
|
|
312
313
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
@@ -332,6 +333,27 @@ DrizzleCrudModule.forRootAsync({
|
|
|
332
333
|
});
|
|
333
334
|
```
|
|
334
335
|
|
|
336
|
+
`forRootAsync` also takes `useClass` or `useExisting` with a class that
|
|
337
|
+
implements `DrizzleCrudOptionsFactory`:
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
@Injectable()
|
|
341
|
+
class CrudConfig implements DrizzleCrudOptionsFactory {
|
|
342
|
+
constructor(private readonly cfg: ConfigService) {}
|
|
343
|
+
createDrizzleCrudOptions(): DrizzleCrudConfig {
|
|
344
|
+
return { dialect: 'mysql', connectionString: this.cfg.get('DATABASE_URL') };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
DrizzleCrudModule.forRootAsync({ useClass: CrudConfig });
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
On SQLite (`dialect: 'sqlite'`), mass writes, nested creates and attach / detach
|
|
352
|
+
/ sync need an async transaction: they work on libSQL and throw a
|
|
353
|
+
`TransactionException` on Cloudflare D1 and synchronous drivers such as
|
|
354
|
+
better-sqlite3. Full-text search, row locks and the array and JSON operators
|
|
355
|
+
are a 400 there.
|
|
356
|
+
|
|
335
357
|
### `DrizzleCrudModule.forFeature(entities)`
|
|
336
358
|
|
|
337
359
|
Registers one or more services and binds each to its table. Per-entity overrides
|
|
@@ -1111,7 +1133,19 @@ const { data, total } = await service.fullTextSearch(
|
|
|
1111
1133
|
```
|
|
1112
1134
|
|
|
1113
1135
|
Builds `to_tsvector(...) @@ plainto_tsquery(...)` across the given columns and
|
|
1114
|
-
orders by `ts_rank`. Throws if the dialect is not `postgresql`.
|
|
1136
|
+
orders by `ts_rank`. Throws a 400 if the dialect is not `postgresql`. The options
|
|
1137
|
+
take `filters`, `relations`, `select` and `counts` as on `findAll`, plus
|
|
1138
|
+
`language` (default `'english'`) and `queryMode: 'websearch'` for
|
|
1139
|
+
`websearch_to_tsquery`:
|
|
1140
|
+
|
|
1141
|
+
```typescript
|
|
1142
|
+
await service.fullTextSearch('"zero downtime" -kubernetes', ['title', 'body'], {}, {
|
|
1143
|
+
filters: { status: 'published' },
|
|
1144
|
+
relations: ['author'],
|
|
1145
|
+
language: 'english',
|
|
1146
|
+
queryMode: 'websearch',
|
|
1147
|
+
});
|
|
1148
|
+
```
|
|
1115
1149
|
|
|
1116
1150
|
For paginated list endpoints, prefer `findAll(..., ..., { search })` so search is
|
|
1117
1151
|
combined with filters and the count query. Use `fullTextSearch` directly when
|
|
@@ -1211,7 +1245,7 @@ read-only and never touches your database. Full setup:
|
|
|
1211
1245
|
|
|
1212
1246
|
Use these facts instead of guessing.
|
|
1213
1247
|
|
|
1214
|
-
**Package:** `nestjs-drizzle-crud` · **Peers:** `@nestjs/common`, `@nestjs/core`, `drizzle-orm`, `reflect-metadata`; optional `postgres` (PG) / `@nestjs/swagger` (for `ApiCrudList`)
|
|
1248
|
+
**Package:** `nestjs-drizzle-crud` · **Peers:** `@nestjs/common`, `@nestjs/core`, `drizzle-orm`, `reflect-metadata`; optional `postgres` (PG `connectionString`) / `mysql2` (MySQL `connectionString`) / `@nestjs/swagger` (for `ApiCrudList`). SQLite drivers are yours, passed as `db`.
|
|
1215
1249
|
|
|
1216
1250
|
**Setup is two steps and no per-service connection wiring:**
|
|
1217
1251
|
1. `DrizzleCrudModule.forRoot({ dialect, connectionString | db, schema, defaults })` once in `AppModule`.
|
|
@@ -1242,15 +1276,17 @@ export class XService extends SqlBaseCrudService<X, CreateXDto, UpdateXDto, XFil
|
|
|
1242
1276
|
- Relations: declare in forFeature `config.relations` and eager-load via `options.relations: ['relName']`, where a dotted path nests (`['states', 'states.cities']`, `['state.country']`; the parent path is pulled in automatically). Belongs-to is `{ table, localKey, references? }`, LEFT JOINed into the main query, resolving to an object or `null`. Has-many is `{ table, type: 'hasMany', foreignKey, parentKey?, orderBy? }`. Many-to-many is `{ table, type: 'manyToMany', through, throughLocalKey, throughForeignKey, references?, parentKey?, orderBy? }`. Nested relations live in a `relations` map on the relation. Every collection and every nested level costs exactly one extra query for the whole result set, never one per row. Filter via `findAll({ relName: { col: value } })` with the same operators; collection filters compile to `parentKey IN (SELECT ... WHERE ...)`, so `total` stays correct. Filters go through relation paths at any depth (`{ state: { country: { code: 'IN' } } }`), and collections take `some` / `none` / `every` (`{ posts: { none: { status: 'draft' } } }`), compiled to subqueries.
|
|
1243
1277
|
- `options.select` narrows the projection and takes dotted relation paths at any depth: `['id', 'name', 'region.code', 'states.cities.name']`. Unknown names are ignored; an all-dotted list leaves the base row complete; the parent key, the child foreign key and any nested key are re-added automatically.
|
|
1244
1278
|
- Primary keys: `primaryKey` (default `'id'`) per entity in forFeature config (`primaryKeyType` is deprecated and does nothing). UUID works on Postgres via RETURNING; with uuid keys, route params are strings, so don't use `ParseIntPipe`.
|
|
1245
|
-
- Full-text search is PostgreSQL-only.
|
|
1279
|
+
- Full-text search is PostgreSQL-only; it takes `filters`, `relations`, `select`, `language` and `queryMode` in its options.
|
|
1280
|
+
- SQLite (`dialect: 'sqlite'`): pass `db`. Bulk writes need libSQL; D1 and sync drivers throw `TransactionException`. Locks, full-text and array/JSON operators are a 400.
|
|
1281
|
+
- Exports (3.7): `DrizzleConnectionOptions`, `DrizzleCrudAsyncOptions`, `DrizzleCrudOptionsFactory`, `FullTextOptions`, `FullTextQueryMode`, `FullTextSearchOptions`.
|
|
1246
1282
|
- Exports (3.5): `createCrudController`, `CrudContext`, `CrudEventBus`, `ApiCrudQuery`, `ApiCrudList`, `MissingScopeException`, `StaleVersionException`, and the types `CrudControllerOptions`, `CrudEvent`, `CrudSubscriber`, `CrudScope`, `CollectionFilter`, `CountByRow`, `CrudPage`, `UncountedCrudPage`, `RelationCounts`.
|
|
1247
|
-
- Exports: `SqlBaseCrudService`, `DrizzleCrudModule`, `DRIZZLE_DB`, `DRIZZLE_CRUD_CONFIG`, `TestCrudFactory`, exceptions (`EntityNotFoundException`, `DuplicateEntityException`, `ForeignKeyViolationException`, `EntityInUseException`, `ValidationFailedException`, `BulkOperationException`, plus the deprecated `DatabaseConnectionException`
|
|
1283
|
+
- Exports: `SqlBaseCrudService`, `DrizzleCrudModule`, `DRIZZLE_DB`, `DRIZZLE_CRUD_CONFIG`, `TestCrudFactory`, exceptions (`EntityNotFoundException`, `DuplicateEntityException`, `ForeignKeyViolationException`, `EntityInUseException`, `ValidationFailedException`, `BulkOperationException`, `TransactionException`, plus the deprecated `DatabaseConnectionException`), and types (`SqlCrudConfig`, `SqlOperationOptions`, `DrizzleCrudConfig`, `CrudFeature`, `CrudFeatureConfig`, `PaginationOptions`, `OperationOptions`, `SqlSearchOptions`, `SqlSearchMode`, `BulkOperationResult`, `BulkOperationItemError`, `SqlDialect`, `PrimaryKeyType`, `CrudFilter`, `FilterOperators`, `SortInput`, `CursorPaginationOptions`, `CursorPage`, `CrudAllowlists`, `UpsertOptions`, `BulkMode`, `ParsedCrudQuery`), and `parseCrudQuery`.
|
|
1248
1284
|
|
|
1249
1285
|
### `SqlCrudConfig`
|
|
1250
1286
|
|
|
1251
1287
|
```typescript
|
|
1252
1288
|
interface SqlCrudConfig {
|
|
1253
|
-
dialect: 'postgresql' | 'mysql';
|
|
1289
|
+
dialect: 'postgresql' | 'mysql' | 'sqlite';
|
|
1254
1290
|
db: any; // Drizzle instance (injected by forFeature)
|
|
1255
1291
|
table: any; // Drizzle table (set by forFeature)
|
|
1256
1292
|
primaryKey: string; // default 'id'
|
|
@@ -3,7 +3,7 @@ import type { ICrudService, PaginationOptions } from "../interfaces/crud-service
|
|
|
3
3
|
import type { CrudWriteOperation } from "../interfaces/events.interface";
|
|
4
4
|
import type { CountByRow, CrudAllowlists, CrudPage, CursorPage, CursorPaginationOptions, ParseCrudQueryOptions, ParsedCrudQuery, RelationCounts, SortInput, UncountedCrudPage, UpsertOptions } from "../interfaces/query.interface";
|
|
5
5
|
import type { CrudScopeValues } from "../interfaces/scope.interface";
|
|
6
|
-
import type { RelationConfig, RelationNode, RelationsConfig, RelationType, SelectTree, SqlCrudConfig, SqlOperationOptions } from "../interfaces/sql-crud-config.interface";
|
|
6
|
+
import type { FullTextOptions, FullTextSearchOptions, RelationConfig, RelationNode, RelationsConfig, RelationType, SelectTree, SqlCrudConfig, SqlOperationOptions } from "../interfaces/sql-crud-config.interface";
|
|
7
7
|
import type { SortOrder } from "../types/sql.types";
|
|
8
8
|
interface SortSpec {
|
|
9
9
|
key: string;
|
|
@@ -85,8 +85,12 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
85
85
|
offset: number;
|
|
86
86
|
};
|
|
87
87
|
protected isUniqueViolation(error: any): boolean;
|
|
88
|
+
protected sqliteConstraint(error: any): string | undefined;
|
|
88
89
|
protected toDuplicateException(error: any): DuplicateEntityException;
|
|
89
90
|
private static readonly DATA_ERRORS;
|
|
91
|
+
private static readonly MYSQL_DATA_ERRORS;
|
|
92
|
+
private static readonly SQLITE_DATA_ERRORS;
|
|
93
|
+
private dataErrorMessage;
|
|
90
94
|
protected isDataException(error: any): boolean;
|
|
91
95
|
protected toDataException(error: any): ValidationFailedException;
|
|
92
96
|
protected toClientError(error: any, operation?: "read" | "write" | "delete"): unknown;
|
|
@@ -94,8 +98,14 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
94
98
|
protected isForeignKeyViolation(error: any): boolean;
|
|
95
99
|
protected toForeignKeyException(error: any): ForeignKeyViolationException;
|
|
96
100
|
protected applyLock(query: any, options?: SqlOperationOptions): any;
|
|
101
|
+
protected textSearchConfig(language?: unknown): any;
|
|
102
|
+
protected fullTextParts(columns: any[], term: string, options?: FullTextOptions): {
|
|
103
|
+
vector: any;
|
|
104
|
+
query: any;
|
|
105
|
+
};
|
|
97
106
|
protected buildSearchCondition(search?: SqlOperationOptions["search"]): any;
|
|
98
107
|
protected caseInsensitiveLike(column: any, pattern: string): any;
|
|
108
|
+
protected likePattern(column: any, pattern: string): any;
|
|
99
109
|
parseQuery(query: Record<string, unknown> | null | undefined, options?: ParseCrudQueryOptions): ParsedCrudQuery;
|
|
100
110
|
idKind(): "int" | "bigint" | "uuid" | "string";
|
|
101
111
|
parseId(raw: unknown): Promise<unknown>;
|
|
@@ -219,8 +229,9 @@ export declare abstract class SqlBaseCrudService<T extends Record<string, any>,
|
|
|
219
229
|
private patternCondition;
|
|
220
230
|
protected applyComplexFilter(conditions: any[], column: any, filterObj: any): void;
|
|
221
231
|
protected executeSqlTransaction<R>(operation: (tx: any) => Promise<R>, existingTransaction?: any): Promise<R>;
|
|
232
|
+
protected assertTransactions(db: any): void;
|
|
222
233
|
protected getEntityName(): string;
|
|
223
|
-
fullTextSearch(searchTerm: string, searchColumns: string[], pagination?: PaginationOptions, options?:
|
|
234
|
+
fullTextSearch(searchTerm: string, searchColumns: string[], pagination?: PaginationOptions, options?: FullTextSearchOptions<FilterDto>): Promise<{
|
|
224
235
|
data: T[];
|
|
225
236
|
total: number;
|
|
226
237
|
page: number;
|
|
@@ -66,6 +66,10 @@ class SqlBaseCrudService {
|
|
|
66
66
|
return result.rowsAffected > 0;
|
|
67
67
|
if (typeof result.affectedRows === "number")
|
|
68
68
|
return result.affectedRows > 0;
|
|
69
|
+
if (typeof result.changes === "number")
|
|
70
|
+
return result.changes > 0;
|
|
71
|
+
if (typeof result.meta?.changes === "number")
|
|
72
|
+
return result.meta.changes > 0;
|
|
69
73
|
return false;
|
|
70
74
|
}
|
|
71
75
|
buildWhereConditionsFromPartial(where) {
|
|
@@ -806,13 +810,32 @@ class SqlBaseCrudService {
|
|
|
806
810
|
isUniqueViolation(error) {
|
|
807
811
|
const code = error?.code ?? error?.cause?.code;
|
|
808
812
|
const errno = error?.errno ?? error?.cause?.errno;
|
|
809
|
-
|
|
813
|
+
const sqlite = this.sqliteConstraint(error);
|
|
814
|
+
return (code === "23505" ||
|
|
815
|
+
code === "ER_DUP_ENTRY" ||
|
|
816
|
+
errno === 1062 ||
|
|
817
|
+
sqlite === "UNIQUE" ||
|
|
818
|
+
sqlite === "PRIMARYKEY");
|
|
819
|
+
}
|
|
820
|
+
sqliteConstraint(error) {
|
|
821
|
+
if (this.config.dialect !== "sqlite")
|
|
822
|
+
return undefined;
|
|
823
|
+
for (const e of [error, error?.cause]) {
|
|
824
|
+
const code = e?.extendedCode ?? e?.code;
|
|
825
|
+
if (typeof code === "string" && code.startsWith("SQLITE_CONSTRAINT_")) {
|
|
826
|
+
return code.slice("SQLITE_CONSTRAINT_".length);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const message = `${error?.cause?.message ?? ""} ${error?.message ?? ""}`;
|
|
830
|
+
const m = message.match(/(UNIQUE|PRIMARY KEY|FOREIGN KEY|NOT NULL|CHECK) constraint failed/);
|
|
831
|
+
return m?.[1]?.replace(" ", "");
|
|
810
832
|
}
|
|
811
833
|
toDuplicateException(error) {
|
|
812
834
|
const detail = error?.detail ?? error?.cause?.detail ?? "";
|
|
813
835
|
const message = error?.cause?.message ?? error?.message ?? "";
|
|
814
836
|
const m = detail.match(/Key \((?<field>.+?)\)=\((?<value>.+?)\)/) ??
|
|
815
|
-
message.match(/Duplicate entry '(?<value>.*)' for key '(?:[^']*\.)?(?<field>[^']+)'/)
|
|
837
|
+
message.match(/Duplicate entry '(?<value>.*)' for key '(?:[^']*\.)?(?<field>[^']+)'/) ??
|
|
838
|
+
message.match(/UNIQUE constraint failed: (?:[^.,\s]+\.)?(?<field>[^,\s]+)/);
|
|
816
839
|
return new crud_exceptions_1.DuplicateEntityException(this.getEntityName(), m?.groups?.field ?? "unique field", m?.groups?.value ?? "");
|
|
817
840
|
}
|
|
818
841
|
static DATA_ERRORS = {
|
|
@@ -825,18 +848,53 @@ class SqlBaseCrudService {
|
|
|
825
848
|
"22008": "a date or time value is out of range",
|
|
826
849
|
"21000": "the same key appears more than once in one upsert",
|
|
827
850
|
};
|
|
828
|
-
|
|
851
|
+
static MYSQL_DATA_ERRORS = {
|
|
852
|
+
1048: "a required value is missing",
|
|
853
|
+
1364: "a required value is missing",
|
|
854
|
+
1406: "a value is too long for its column",
|
|
855
|
+
3819: "a value violates a column check constraint",
|
|
856
|
+
1264: "a numeric value is out of range",
|
|
857
|
+
1265: "a value does not match its column type",
|
|
858
|
+
1366: "a value does not match its column type",
|
|
859
|
+
1292: "a date or time value has an invalid format",
|
|
860
|
+
};
|
|
861
|
+
static SQLITE_DATA_ERRORS = {
|
|
862
|
+
NOTNULL: "a required value is missing",
|
|
863
|
+
CHECK: "a value violates a column check constraint",
|
|
864
|
+
};
|
|
865
|
+
dataErrorMessage(error) {
|
|
829
866
|
const code = error?.code ?? error?.cause?.code;
|
|
830
|
-
|
|
867
|
+
if (Object.hasOwn(SqlBaseCrudService.DATA_ERRORS, code ?? "")) {
|
|
868
|
+
return SqlBaseCrudService.DATA_ERRORS[code];
|
|
869
|
+
}
|
|
870
|
+
if (this.config.dialect === "mysql") {
|
|
871
|
+
const errno = error?.errno ?? error?.cause?.errno;
|
|
872
|
+
return Object.hasOwn(SqlBaseCrudService.MYSQL_DATA_ERRORS, errno ?? "")
|
|
873
|
+
? SqlBaseCrudService.MYSQL_DATA_ERRORS[errno]
|
|
874
|
+
: undefined;
|
|
875
|
+
}
|
|
876
|
+
const sqlite = this.sqliteConstraint(error) ?? "";
|
|
877
|
+
return Object.hasOwn(SqlBaseCrudService.SQLITE_DATA_ERRORS, sqlite)
|
|
878
|
+
? SqlBaseCrudService.SQLITE_DATA_ERRORS[sqlite]
|
|
879
|
+
: undefined;
|
|
880
|
+
}
|
|
881
|
+
isDataException(error) {
|
|
882
|
+
return this.dataErrorMessage(error) !== undefined;
|
|
831
883
|
}
|
|
832
884
|
toDataException(error) {
|
|
833
|
-
const
|
|
834
|
-
const message = SqlBaseCrudService.DATA_ERRORS[code] ?? "invalid value";
|
|
885
|
+
const message = this.dataErrorMessage(error) ?? "invalid value";
|
|
835
886
|
const cause = error?.cause ?? error;
|
|
887
|
+
const text = String(cause?.message ?? "");
|
|
836
888
|
const column = error?.column ??
|
|
837
889
|
cause?.column ??
|
|
838
890
|
error?.column_name ??
|
|
839
|
-
cause?.column_name
|
|
891
|
+
cause?.column_name ??
|
|
892
|
+
(this.config.dialect === "mysql"
|
|
893
|
+
? text.match(/column '([^']+)'/i)?.[1]
|
|
894
|
+
: undefined) ??
|
|
895
|
+
(this.config.dialect === "sqlite"
|
|
896
|
+
? text.match(/NOT NULL constraint failed: (?:[^.\s]+\.)?(\S+)/)?.[1]
|
|
897
|
+
: undefined);
|
|
840
898
|
return new crud_exceptions_1.ValidationFailedException(column ? `${message}: ${column}` : message);
|
|
841
899
|
}
|
|
842
900
|
toClientError(error, operation = "write") {
|
|
@@ -862,7 +920,10 @@ class SqlBaseCrudService {
|
|
|
862
920
|
isForeignKeyViolation(error) {
|
|
863
921
|
const code = error?.code ?? error?.cause?.code;
|
|
864
922
|
const errno = error?.errno ?? error?.cause?.errno;
|
|
865
|
-
return code === "23503" ||
|
|
923
|
+
return (code === "23503" ||
|
|
924
|
+
errno === 1451 ||
|
|
925
|
+
errno === 1452 ||
|
|
926
|
+
this.sqliteConstraint(error) === "FOREIGNKEY");
|
|
866
927
|
}
|
|
867
928
|
toForeignKeyException(error) {
|
|
868
929
|
const detail = error?.detail ?? error?.cause?.detail ?? "";
|
|
@@ -870,14 +931,37 @@ class SqlBaseCrudService {
|
|
|
870
931
|
return new crud_exceptions_1.ForeignKeyViolationException(this.getEntityName(), m?.groups?.field ?? "reference");
|
|
871
932
|
}
|
|
872
933
|
applyLock(query, options) {
|
|
873
|
-
|
|
934
|
+
const strength = options?.forNoKeyUpdate
|
|
935
|
+
? "no key update"
|
|
936
|
+
: options?.lock && options.lock !== "none"
|
|
937
|
+
? options.lock
|
|
938
|
+
: undefined;
|
|
939
|
+
if (!strength)
|
|
874
940
|
return query;
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
if (
|
|
879
|
-
return query.for(
|
|
880
|
-
|
|
941
|
+
if (this.config.dialect === "postgresql") {
|
|
942
|
+
return query.for(strength, { of: this.config.table });
|
|
943
|
+
}
|
|
944
|
+
if (this.config.dialect === "mysql") {
|
|
945
|
+
return query.for(strength === "share" ? "share" : "update");
|
|
946
|
+
}
|
|
947
|
+
throw new common_1.BadRequestException(`Row locks (lock, forNoKeyUpdate) are not supported on ${this.config.dialect}`);
|
|
948
|
+
}
|
|
949
|
+
textSearchConfig(language = "english") {
|
|
950
|
+
if (typeof language !== "string" ||
|
|
951
|
+
!/^[A-Za-z_][A-Za-z0-9_]{0,62}(\.[A-Za-z_][A-Za-z0-9_]{0,62})?$/.test(language)) {
|
|
952
|
+
throw new common_1.BadRequestException(`Invalid full-text search language: ${String(language)}`);
|
|
953
|
+
}
|
|
954
|
+
return drizzle_orm_1.sql.raw(`'${language}'`);
|
|
955
|
+
}
|
|
956
|
+
fullTextParts(columns, term, options = {}) {
|
|
957
|
+
const config = this.textSearchConfig(options.language);
|
|
958
|
+
const mode = options.queryMode ?? "plain";
|
|
959
|
+
if (mode !== "plain" && mode !== "websearch") {
|
|
960
|
+
throw new common_1.BadRequestException(`Invalid full-text query mode: ${String(mode)} (expected 'plain' or 'websearch')`);
|
|
961
|
+
}
|
|
962
|
+
const parse = drizzle_orm_1.sql.raw(mode === "websearch" ? "websearch_to_tsquery" : "plainto_tsquery");
|
|
963
|
+
const vector = drizzle_orm_1.sql.join(columns.map((column) => (0, drizzle_orm_1.sql) `to_tsvector(${config}, ${column})`), (0, drizzle_orm_1.sql) ` || `);
|
|
964
|
+
return { vector, query: (0, drizzle_orm_1.sql) `${parse}(${config}, ${term})` };
|
|
881
965
|
}
|
|
882
966
|
buildSearchCondition(search) {
|
|
883
967
|
if (!search?.term || !search.columns?.length)
|
|
@@ -894,18 +978,22 @@ class SqlBaseCrudService {
|
|
|
894
978
|
if (this.config.dialect !== "postgresql") {
|
|
895
979
|
throw new common_1.BadRequestException("Full-text search only supported in PostgreSQL");
|
|
896
980
|
}
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
return (0, drizzle_orm_1.sql) `${tsVector} @@ ${tsQuery}`;
|
|
981
|
+
const { vector, query } = this.fullTextParts(columns, term, search);
|
|
982
|
+
return (0, drizzle_orm_1.sql) `${vector} @@ ${query}`;
|
|
900
983
|
}
|
|
901
984
|
const pattern = `%${term.replace(/[\\%_]/g, "\\$&")}%`;
|
|
902
985
|
const conditions = columns.map((column) => this.caseInsensitiveLike(column, pattern));
|
|
903
986
|
return conditions.length === 1 ? conditions[0] : (0, drizzle_orm_1.or)(...conditions);
|
|
904
987
|
}
|
|
905
988
|
caseInsensitiveLike(column, pattern) {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
989
|
+
if (this.config.dialect === "postgresql")
|
|
990
|
+
return (0, drizzle_orm_1.ilike)(column, pattern);
|
|
991
|
+
return this.likePattern(column, pattern);
|
|
992
|
+
}
|
|
993
|
+
likePattern(column, pattern) {
|
|
994
|
+
return this.config.dialect === "sqlite"
|
|
995
|
+
? (0, drizzle_orm_1.sql) `${column} like ${pattern} escape '\\'`
|
|
996
|
+
: (0, drizzle_orm_1.like)(column, pattern);
|
|
909
997
|
}
|
|
910
998
|
parseQuery(query, options = {}) {
|
|
911
999
|
const config = this.config;
|
|
@@ -1211,7 +1299,7 @@ class SqlBaseCrudService {
|
|
|
1211
1299
|
return branches.length > 0 ? (0, drizzle_orm_1.or)(...branches) : (0, drizzle_orm_1.sql) `false`;
|
|
1212
1300
|
}
|
|
1213
1301
|
afterCursor(spec, value) {
|
|
1214
|
-
const postgres = this.config.dialect
|
|
1302
|
+
const postgres = this.config.dialect === "postgresql";
|
|
1215
1303
|
const nullsFirst = postgres ? spec.order === "desc" : spec.order === "asc";
|
|
1216
1304
|
if (value === null) {
|
|
1217
1305
|
return nullsFirst ? (0, drizzle_orm_1.isNotNull)(spec.column) : undefined;
|
|
@@ -2751,7 +2839,7 @@ class SqlBaseCrudService {
|
|
|
2751
2839
|
: `%${escaped}%`;
|
|
2752
2840
|
return insensitive || this.config.sql?.caseSensitive === false
|
|
2753
2841
|
? this.caseInsensitiveLike(column, pattern)
|
|
2754
|
-
:
|
|
2842
|
+
: this.likePattern(column, pattern);
|
|
2755
2843
|
}
|
|
2756
2844
|
applyComplexFilter(conditions, column, filterObj) {
|
|
2757
2845
|
const coerce = (value) => this.coerceFilterValue(column, value);
|
|
@@ -2783,7 +2871,7 @@ class SqlBaseCrudService {
|
|
|
2783
2871
|
conditions.push((0, drizzle_orm_1.ne)(column, coerce(raw)));
|
|
2784
2872
|
break;
|
|
2785
2873
|
case "like":
|
|
2786
|
-
conditions.push(
|
|
2874
|
+
conditions.push(this.likePattern(column, raw));
|
|
2787
2875
|
break;
|
|
2788
2876
|
case "ilike":
|
|
2789
2877
|
conditions.push(this.caseInsensitiveLike(column, raw));
|
|
@@ -2841,10 +2929,22 @@ class SqlBaseCrudService {
|
|
|
2841
2929
|
}
|
|
2842
2930
|
}
|
|
2843
2931
|
async executeSqlTransaction(operation, existingTransaction) {
|
|
2932
|
+
this.assertTransactions(existingTransaction ?? this.config.db);
|
|
2844
2933
|
if (existingTransaction)
|
|
2845
2934
|
return operation(existingTransaction);
|
|
2846
2935
|
return this.config.db.transaction(operation);
|
|
2847
2936
|
}
|
|
2937
|
+
assertTransactions(db) {
|
|
2938
|
+
if (this.config.dialect !== "sqlite")
|
|
2939
|
+
return;
|
|
2940
|
+
const kind = db?.constructor?.[drizzle_orm_1.entityKind];
|
|
2941
|
+
if (kind === "D1Database" || kind === "D1Transaction") {
|
|
2942
|
+
throw new crud_exceptions_1.TransactionException("Cloudflare D1 has no interactive transactions, so mass writes, nested writes and attach/detach/sync are not available on it. Use the single-row methods, or db.batch() yourself.");
|
|
2943
|
+
}
|
|
2944
|
+
if (db?.resultKind === "sync") {
|
|
2945
|
+
throw new crud_exceptions_1.TransactionException("This SQLite driver is synchronous (better-sqlite3, bun:sqlite, sql.js), and its transactions cannot wait for async work, so mass writes, nested writes and attach/detach/sync are not available on it. Use an async driver such as libSQL (@libsql/client).");
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2848
2948
|
getEntityName() {
|
|
2849
2949
|
try {
|
|
2850
2950
|
return (0, drizzle_orm_1.getTableName)(this.config.table) || "Entity";
|
|
@@ -2868,31 +2968,37 @@ class SqlBaseCrudService {
|
|
|
2868
2968
|
this.assertAllowed("searchableColumns", name, options);
|
|
2869
2969
|
return column;
|
|
2870
2970
|
});
|
|
2871
|
-
const {
|
|
2872
|
-
const
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
const whereExpr = (0, drizzle_orm_1.and)(matchExpr, ...this.rowScope(options));
|
|
2971
|
+
const { filters, language, queryMode, ...readOptions } = options ?? {};
|
|
2972
|
+
const { vector, query } = this.fullTextParts(columns, searchTerm, {
|
|
2973
|
+
language,
|
|
2974
|
+
queryMode,
|
|
2975
|
+
});
|
|
2877
2976
|
const { page, limit, offset } = this.resolvePagination(pagination?.page, pagination?.limit);
|
|
2977
|
+
const plan = this.planList(filters, undefined, "asc", readOptions);
|
|
2978
|
+
const conditions = [(0, drizzle_orm_1.sql) `${vector} @@ ${query}`, ...plan.conditions];
|
|
2979
|
+
const joins = Array.from(new Set([
|
|
2980
|
+
...plan.joined.map((node) => node.name),
|
|
2981
|
+
...plan.filterRelations,
|
|
2982
|
+
]));
|
|
2878
2983
|
const pk = this.columnOf(this.config.table, this.config.primaryKey);
|
|
2879
|
-
const orderBy = [(0, drizzle_orm_1.sql) `ts_rank(${
|
|
2984
|
+
const orderBy = [(0, drizzle_orm_1.sql) `ts_rank(${vector}, ${query}) DESC`];
|
|
2880
2985
|
if (pk)
|
|
2881
2986
|
orderBy.push((0, drizzle_orm_1.asc)(pk));
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
.
|
|
2885
|
-
.where(whereExpr)
|
|
2987
|
+
let dataQuery = this.startSelect(plan.db, plan.baseSelect, plan.joined, joins.length > 0, plan.selection);
|
|
2988
|
+
dataQuery = this.applyJoins(dataQuery, joins, readOptions)
|
|
2989
|
+
.where((0, drizzle_orm_1.and)(...conditions))
|
|
2886
2990
|
.orderBy(...orderBy)
|
|
2887
2991
|
.limit(limit)
|
|
2888
2992
|
.offset(offset);
|
|
2889
|
-
const countQuery = db
|
|
2890
|
-
|
|
2891
|
-
.from(this.config.table)
|
|
2892
|
-
.where(whereExpr);
|
|
2893
|
-
const [data, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
|
|
2993
|
+
const countQuery = this.buildCountQuery(plan.db, conditions, plan.filterRelations, readOptions);
|
|
2994
|
+
const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
|
|
2894
2995
|
const total = parseInt(totalResult[0]?.count?.toString() || "0", 10);
|
|
2895
|
-
|
|
2996
|
+
const rows = this.normalizeRelations(fetched, plan.joined.map((node) => node.name));
|
|
2997
|
+
await Promise.all([
|
|
2998
|
+
this.loadRelations(plan.db, rows, plan.nodes, plan.selection, undefined, readOptions),
|
|
2999
|
+
this.loadCounts(plan.db, rows, readOptions.counts, readOptions),
|
|
3000
|
+
]);
|
|
3001
|
+
return { data: rows, total, page, limit };
|
|
2896
3002
|
}
|
|
2897
3003
|
}
|
|
2898
3004
|
exports.SqlBaseCrudService = SqlBaseCrudService;
|