@venizia/ignis-docs 0.2.1-0 → 0.2.1-1
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/content/best-practices/architectural-patterns.md +3 -3
- package/content/best-practices/code-style-standards/naming-conventions.md +1 -1
- package/content/best-practices/contribution-workflow.md +2 -2
- package/content/best-practices/error-handling.md +94 -89
- package/content/best-practices/security-guidelines.md +5 -5
- package/content/extensions/components/api-reference.md +22 -21
- package/content/extensions/components/authentication/api.md +64 -28
- package/content/extensions/components/authentication/errors.md +19 -5
- package/content/extensions/components/authentication/index.md +19 -18
- package/content/extensions/components/authentication/usage.md +55 -30
- package/content/extensions/components/authorization/api.md +351 -84
- package/content/extensions/components/authorization/errors.md +51 -17
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +24 -16
- package/content/extensions/components/authorization/usage.md +45 -21
- package/content/extensions/components/health-check.md +15 -9
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +105 -54
- package/content/extensions/components/mail/errors.md +12 -10
- package/content/extensions/components/mail/index.md +32 -13
- package/content/extensions/components/mail/usage.md +26 -18
- package/content/extensions/components/request-tracker.md +18 -14
- package/content/extensions/components/socket-io/api.md +377 -882
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +72 -88
- package/content/extensions/components/socket-io/usage.md +107 -117
- package/content/extensions/components/static-asset/api.md +83 -31
- package/content/extensions/components/static-asset/errors.md +18 -7
- package/content/extensions/components/static-asset/index.md +18 -11
- package/content/extensions/components/static-asset/usage.md +11 -6
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +58 -27
- package/content/extensions/components/websocket/errors.md +3 -3
- package/content/extensions/components/websocket/index.md +8 -7
- package/content/extensions/components/websocket/usage.md +21 -8
- package/content/extensions/helpers/cron/index.md +8 -7
- package/content/extensions/helpers/crypto/index.md +16 -8
- package/content/extensions/helpers/crypto/reference.md +96 -24
- package/content/extensions/helpers/env/index.md +14 -10
- package/content/extensions/helpers/error/index.md +99 -23
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +23 -6
- package/content/extensions/helpers/inversion/reference.md +30 -22
- package/content/extensions/helpers/kafka/admin.md +3 -2
- package/content/extensions/helpers/kafka/compile-binary.md +69 -44
- package/content/extensions/helpers/kafka/consumer.md +24 -21
- package/content/extensions/helpers/kafka/examples.md +22 -234
- package/content/extensions/helpers/kafka/index.md +30 -62
- package/content/extensions/helpers/kafka/producer.md +31 -26
- package/content/extensions/helpers/kafka/schema-registry.md +19 -14
- package/content/extensions/helpers/logger/hf-logger.md +49 -22
- package/content/extensions/helpers/logger/index.md +37 -12
- package/content/extensions/helpers/logger/pino.md +31 -11
- package/content/extensions/helpers/logger/reference.md +243 -52
- package/content/extensions/helpers/network/api.md +65 -30
- package/content/extensions/helpers/network/index.md +32 -11
- package/content/extensions/helpers/queue/index.md +17 -6
- package/content/extensions/helpers/queue/reference.md +52 -25
- package/content/extensions/helpers/redis/index.md +29 -11
- package/content/extensions/helpers/redis/reference.md +85 -28
- package/content/extensions/helpers/secrets/index.md +82 -12
- package/content/extensions/helpers/socket-io/api.md +40 -21
- package/content/extensions/helpers/socket-io/index.md +26 -10
- package/content/extensions/helpers/storage/api.md +42 -24
- package/content/extensions/helpers/storage/index.md +10 -7
- package/content/extensions/helpers/types/index.md +20 -7
- package/content/extensions/helpers/types/reference.md +53 -14
- package/content/extensions/helpers/uid/index.md +166 -10
- package/content/extensions/helpers/websocket/api.md +52 -13
- package/content/extensions/helpers/websocket/index.md +7 -4
- package/content/extensions/helpers/worker-thread/index.md +9 -5
- package/content/extensions/helpers/worker-thread/reference.md +20 -20
- package/content/extensions/index.md +38 -39
- package/content/extensions/src-details/mcp-server.md +96 -548
- package/content/guides/core-concepts/persistent/datasources.md +2 -2
- package/content/guides/core-concepts/persistent/index.md +5 -1
- package/content/guides/core-concepts/persistent/pglite.md +218 -0
- package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
- package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
- package/content/guides/core-concepts/persistent/search-typesense.md +26 -14
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- package/content/guides/get-started/5-minute-quickstart.md +59 -206
- package/content/guides/get-started/philosophy.md +135 -670
- package/content/guides/get-started/setup.md +53 -74
- package/content/guides/migrations/redis-helpers-migration.md +4 -3
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +7 -6
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +63 -21
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +14 -14
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +32 -31
- package/content/references/base/datasources.md +6 -6
- package/content/references/base/dependency-injection.md +12 -11
- package/content/references/base/filter-system/application-usage.md +54 -30
- package/content/references/base/filter-system/array-operators.md +24 -46
- package/content/references/base/filter-system/comparison-operators.md +47 -67
- package/content/references/base/filter-system/default-filter.md +59 -53
- package/content/references/base/filter-system/fields-order-pagination.md +92 -146
- package/content/references/base/filter-system/index.md +25 -13
- package/content/references/base/filter-system/json-filtering.md +45 -184
- package/content/references/base/filter-system/list-operators.md +23 -53
- package/content/references/base/filter-system/logical-operators.md +63 -121
- package/content/references/base/filter-system/null-operators.md +34 -104
- package/content/references/base/filter-system/pattern-matching.md +40 -55
- package/content/references/base/filter-system/quick-reference.md +86 -198
- package/content/references/base/filter-system/range-operators.md +18 -46
- package/content/references/base/filter-system/tips.md +6 -6
- package/content/references/base/filter-system/use-cases.md +33 -15
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +11 -10
- package/content/references/base/models-reference.md +17 -17
- package/content/references/base/models.md +4 -3
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +224 -326
- package/content/references/base/repositories/index.md +19 -6
- package/content/references/base/repositories/mixins.md +5 -5
- package/content/references/base/repositories/relations.md +160 -293
- package/content/references/base/repositories/soft-deletable.md +16 -6
- package/content/references/base/secrets.md +17 -13
- package/content/references/base/services.md +6 -4
- package/content/references/configuration/environment-variables.md +31 -23
- package/content/references/configuration/index.md +6 -4
- package/content/references/index.md +1 -1
- package/content/references/utilities/duration.md +85 -0
- package/content/references/utilities/index.md +5 -1
- package/content/references/utilities/jsx-reference.md +11 -11
- package/content/references/utilities/jsx.md +2 -2
- package/content/references/utilities/module.md +78 -25
- package/content/references/utilities/request.md +2 -1
- package/content/references/utilities/retry.md +139 -0
- package/content/references/utilities/schema.md +2 -2
- package/content/references/utilities/statuses-reference.md +4 -4
- package/content/references/utilities/statuses.md +5 -5
- package/dist/mcp-server/index.js +0 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
- package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
- package/package.json +17 -16
|
@@ -6,24 +6,23 @@ difficulty: intermediate
|
|
|
6
6
|
|
|
7
7
|
# Advanced Repository Features
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Everything beyond basic CRUD - transactions, row-level locking, hidden-property exclusion, performance tuning, return-type inference, debugging, and the built-in safety guards. For the common tasks, start with the [Repositories overview](/references/base/repositories/).
|
|
10
10
|
|
|
11
11
|
**Files:**
|
|
12
12
|
|
|
13
|
-
- [`packages/core/src/base/repositories/core/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/repositories/core/abstract.ts) - engine-neutral `AbstractRepository`
|
|
14
|
-
- [`packages/core/src/base/repositories/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/repositories/common/types.ts) - `IExtraOptions`, `TLockOptions`, `TCount`, `TDataRange`
|
|
15
|
-
- [`packages/
|
|
16
|
-
- [`packages/core/src/connectors/postgres/repositories/core/
|
|
17
|
-
- [`packages/core/src/connectors/postgres/repositories/core/
|
|
18
|
-
- [`packages/core/src/connectors/postgres/repositories/core/
|
|
19
|
-
- [`packages/core/src/connectors/postgres/repositories/
|
|
20
|
-
- [`packages/core/src/connectors/postgres/
|
|
13
|
+
- [`packages/core-server/src/base/repositories/core/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/repositories/core/abstract.ts) - engine-neutral `AbstractRepository`
|
|
14
|
+
- [`packages/core-server/src/base/repositories/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/repositories/common/types.ts) - `IExtraOptions`, `TLockOptions`, `TCount`, `TDataRange`, `IReadRetryOptions`, `IWithReadRetry`, `TFindOptions`, `TFindOneOptions`, `TFindRangeOptions`, `TDataWithRange`
|
|
15
|
+
- [`packages/helpers/src/utilities/retry.utility.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/utilities/retry.utility.ts) - `executeWithRetryUntil`, the engine behind `options.retry`
|
|
16
|
+
- [`packages/core-server/src/connectors/postgres/repositories/core/base.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/postgres/repositories/core/base.ts) - `RelationalBaseRepository` - hidden-column exclusion, `buildQuery`, `resolveConnector`, lock validation
|
|
17
|
+
- [`packages/core-server/src/connectors/postgres/repositories/core/readable.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/postgres/repositories/core/readable.ts) - `ReadableRelationalRepository` - Core API vs. Query API selection, `shouldQueryRange`
|
|
18
|
+
- [`packages/core-server/src/connectors/postgres/repositories/core/persistable.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/postgres/repositories/core/persistable.ts) - `PersistableRelationalRepository` - create/update/delete, empty-where guard
|
|
19
|
+
- [`packages/core-server/src/connectors/postgres/repositories/core/soft-deletable.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/postgres/repositories/core/soft-deletable.ts) - `SoftDeletableRelationalRepository`
|
|
20
|
+
- [`packages/core-server/src/connectors/postgres/repositories/dialect/update.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/postgres/repositories/dialect/update.ts) - `UpdateBuilder` - nested JSON path updates
|
|
21
|
+
- [`packages/core-server/src/connectors/postgres/datasources/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/postgres/datasources/abstract.ts) - `beginTransaction()`, isolation levels
|
|
21
22
|
|
|
22
23
|
## Transactions
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
### Basic transaction
|
|
25
|
+
Operations on separate repositories only become one atomic unit when they share a transaction handle. Start one and pass it to every call that must succeed or fail together:
|
|
27
26
|
|
|
28
27
|
```typescript
|
|
29
28
|
const tx = await repository.beginTransaction();
|
|
@@ -48,12 +47,18 @@ try {
|
|
|
48
47
|
}
|
|
49
48
|
```
|
|
50
49
|
|
|
50
|
+
### The rules
|
|
51
|
+
|
|
52
|
+
- `beginTransaction()` delegates to `dataSource.beginTransaction()`.
|
|
53
|
+
- The returned `IDatabaseTransaction` exposes `isActive`, `commit()`, `rollback()`, `connector`, and `isolationLevel`.
|
|
54
|
+
- Pass the same `tx` as `options.transaction` on every call that belongs to the unit of work.
|
|
55
|
+
|
|
51
56
|
> [!WARNING] `rollback()` can throw
|
|
52
|
-
> A failed `COMMIT` or `ROLLBACK`
|
|
57
|
+
> A failed `COMMIT` or `ROLLBACK` throws rather than resolving as success - a poisoned connection is destroyed rather than returned to the pool. Because `rollback()` can throw and is normally called from a `catch`, nest it in its own `try...catch` if the rollback error matters. A `rollback()` called after the transaction already failed is a silent no-op. The `catch { await tx.rollback(); throw error; }` pattern in the basic transaction example is always safe. See [DataSources - Full Reference](/references/base/datasources-reference#transaction-support) for the full commit/rollback lifecycle.
|
|
53
58
|
|
|
54
59
|
### Isolation levels
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
Pass `isolationLevel` to control how the transaction sees concurrent changes:
|
|
57
62
|
|
|
58
63
|
```typescript
|
|
59
64
|
import { IsolationLevels } from '@venizia/ignis/postgres';
|
|
@@ -63,15 +68,17 @@ const tx = await repository.beginTransaction({
|
|
|
63
68
|
});
|
|
64
69
|
```
|
|
65
70
|
|
|
66
|
-
| Level |
|
|
67
|
-
|
|
68
|
-
| `IsolationLevels.READ_COMMITTED` | Default.
|
|
69
|
-
| `IsolationLevels.REPEATABLE_READ` | Consistent reads within transaction |
|
|
70
|
-
| `IsolationLevels.SERIALIZABLE` | Full isolation, prevents anomalies |
|
|
71
|
+
| Level | SQL | Use case |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `IsolationLevels.READ_COMMITTED` | `READ COMMITTED` | Default. Sees committed data only |
|
|
74
|
+
| `IsolationLevels.REPEATABLE_READ` | `REPEATABLE READ` | Consistent reads within the transaction |
|
|
75
|
+
| `IsolationLevels.SERIALIZABLE` | `SERIALIZABLE` | Full isolation, prevents anomalies |
|
|
71
76
|
|
|
72
77
|
A plain string literal (`'SERIALIZABLE'`) works too - `isolationLevel` is typed `TIsolationLevel`, a string union the const class's values satisfy.
|
|
73
78
|
|
|
74
|
-
###
|
|
79
|
+
### Raw SQL inside a transaction
|
|
80
|
+
|
|
81
|
+
A transaction also covers Drizzle's `sql` template for atomic column updates:
|
|
75
82
|
|
|
76
83
|
```typescript
|
|
77
84
|
async function transferFunds(fromId: string, toId: string, amount: number) {
|
|
@@ -90,11 +97,6 @@ async function transferFunds(fromId: string, toId: string, amount: number) {
|
|
|
90
97
|
options: { transaction: tx },
|
|
91
98
|
});
|
|
92
99
|
|
|
93
|
-
await transferRepository.create({
|
|
94
|
-
data: { fromId, toId, amount, status: 'completed' },
|
|
95
|
-
options: { transaction: tx },
|
|
96
|
-
});
|
|
97
|
-
|
|
98
100
|
await tx.commit();
|
|
99
101
|
} catch (error) {
|
|
100
102
|
await tx.rollback();
|
|
@@ -105,11 +107,7 @@ async function transferFunds(fromId: string, toId: string, amount: number) {
|
|
|
105
107
|
|
|
106
108
|
## Row-Level Locking
|
|
107
109
|
|
|
108
|
-
Acquire pessimistic locks on selected rows
|
|
109
|
-
|
|
110
|
-
### Basic usage
|
|
111
|
-
|
|
112
|
-
Pass `lock` in `options` alongside a `transaction`:
|
|
110
|
+
Acquire pessimistic locks on selected rows inside a transaction with PostgreSQL's `SELECT ... FOR UPDATE/SHARE`. Pass `lock` alongside `transaction`:
|
|
113
111
|
|
|
114
112
|
```typescript
|
|
115
113
|
const tx = await repository.beginTransaction();
|
|
@@ -118,10 +116,7 @@ try {
|
|
|
118
116
|
// Lock the row - other transactions will wait
|
|
119
117
|
const item = await repository.findOne({
|
|
120
118
|
filter: { where: { id: '123' } },
|
|
121
|
-
options: {
|
|
122
|
-
transaction: tx,
|
|
123
|
-
lock: { strength: 'update' },
|
|
124
|
-
},
|
|
119
|
+
options: { transaction: tx, lock: { strength: 'update' } },
|
|
125
120
|
});
|
|
126
121
|
|
|
127
122
|
// Safe to modify - no concurrent changes possible
|
|
@@ -138,22 +133,22 @@ try {
|
|
|
138
133
|
}
|
|
139
134
|
```
|
|
140
135
|
|
|
136
|
+
**Supported methods:** `find`, `findOne`, `findById` (delegates to `findOne`). Not `count`/`existsWith`.
|
|
137
|
+
|
|
141
138
|
### Lock strengths
|
|
142
139
|
|
|
143
|
-
Use the `LockStrengths` const class or string
|
|
140
|
+
Use the `LockStrengths` const class or an equivalent string literal:
|
|
144
141
|
|
|
145
142
|
```typescript
|
|
146
143
|
import { LockStrengths } from '@venizia/ignis';
|
|
147
144
|
|
|
148
|
-
// Using constant
|
|
149
145
|
lock: { strength: LockStrengths.UPDATE }
|
|
150
|
-
|
|
151
|
-
// Using string literal
|
|
146
|
+
// same as
|
|
152
147
|
lock: { strength: 'update' }
|
|
153
148
|
```
|
|
154
149
|
|
|
155
150
|
| Strength | SQL | Use case |
|
|
156
|
-
|
|
151
|
+
|---|---|---|
|
|
157
152
|
| `update` | `FOR UPDATE` | Exclusive lock for writes |
|
|
158
153
|
| `no key update` | `FOR NO KEY UPDATE` | Exclusive lock, allows concurrent `FOR KEY SHARE` |
|
|
159
154
|
| `share` | `FOR SHARE` | Shared read lock, prevents writes |
|
|
@@ -161,64 +156,103 @@ lock: { strength: 'update' }
|
|
|
161
156
|
|
|
162
157
|
### Wait behavior
|
|
163
158
|
|
|
164
|
-
Control what happens when
|
|
159
|
+
Control what happens when a targeted row is already locked via `lock.config`:
|
|
165
160
|
|
|
166
161
|
```typescript
|
|
167
162
|
// Skip locked rows (queue-style worker pattern)
|
|
168
163
|
const items = await repository.find({
|
|
169
164
|
filter: { where: { status: 'pending' }, limit: 10 },
|
|
170
|
-
options: {
|
|
171
|
-
transaction: tx,
|
|
172
|
-
lock: { strength: 'update', config: { skipLocked: true } },
|
|
173
|
-
},
|
|
165
|
+
options: { transaction: tx, lock: { strength: 'update', config: { skipLocked: true } } },
|
|
174
166
|
});
|
|
175
167
|
|
|
176
168
|
// Fail immediately instead of waiting
|
|
177
169
|
const item = await repository.findOne({
|
|
178
170
|
filter: { where: { id: '123' } },
|
|
179
|
-
options: {
|
|
180
|
-
transaction: tx,
|
|
181
|
-
lock: { strength: 'update', config: { noWait: true } },
|
|
182
|
-
},
|
|
171
|
+
options: { transaction: tx, lock: { strength: 'update', config: { noWait: true } } },
|
|
183
172
|
});
|
|
184
173
|
```
|
|
185
174
|
|
|
186
175
|
| Config | SQL | Behavior |
|
|
187
|
-
|
|
188
|
-
| *(none)* | `FOR UPDATE` | Wait until lock is released |
|
|
189
|
-
| `{ noWait: true }` | `FOR UPDATE NOWAIT` | Throw
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| *(none)* | `FOR UPDATE` | Wait until the lock is released |
|
|
178
|
+
| `{ noWait: true }` | `FOR UPDATE NOWAIT` | Throw immediately if locked |
|
|
190
179
|
| `{ skipLocked: true }` | `FOR UPDATE SKIP LOCKED` | Silently skip locked rows |
|
|
191
180
|
|
|
192
181
|
`noWait` and `skipLocked` are mutually exclusive at the type level (`TLockConfig`).
|
|
193
182
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
>
|
|
197
|
-
>
|
|
183
|
+
> [!WARNING] Requires a transaction, incompatible with `include`/`fields`
|
|
184
|
+
> Row-level locking needs a transaction and cannot combine with `include`/`fields` in the filter - both force the Drizzle Query API, which has no `.for()`.
|
|
185
|
+
>
|
|
186
|
+
> ```typescript
|
|
187
|
+
> // Error - no transaction
|
|
188
|
+
> await repository.findOne({
|
|
189
|
+
> filter: { where: { id: '123' } },
|
|
190
|
+
> options: { lock: { strength: 'update' } },
|
|
191
|
+
> });
|
|
192
|
+
>
|
|
193
|
+
> // Error - include forces the Query API
|
|
194
|
+
> await repository.findOne({
|
|
195
|
+
> filter: { where: { id: '123' }, include: [{ relation: 'posts' }] },
|
|
196
|
+
> options: { transaction: tx, lock: { strength: 'update' } },
|
|
197
|
+
> });
|
|
198
|
+
> ```
|
|
199
|
+
|
|
200
|
+
## Read Retry (Replica Lag)
|
|
201
|
+
|
|
202
|
+
Behind a replicated pool (e.g. PgDog), a read right after a write can land on a replica that has not caught up. The row looks missing or stale. Pass `retry` to re-read until it is fresh:
|
|
198
203
|
|
|
199
204
|
```typescript
|
|
200
|
-
//
|
|
201
|
-
await
|
|
202
|
-
|
|
203
|
-
options: {
|
|
205
|
+
// create -> read: default predicate ("result is non-null") is enough
|
|
206
|
+
const user = await userRepository.findById({
|
|
207
|
+
id,
|
|
208
|
+
options: { retry: { maxAttempts: 4 } },
|
|
204
209
|
});
|
|
205
210
|
|
|
206
|
-
//
|
|
207
|
-
await
|
|
208
|
-
|
|
209
|
-
options: {
|
|
211
|
+
// update -> read: tell retry what "fresh" means
|
|
212
|
+
const order = await orderRepository.findById({
|
|
213
|
+
id,
|
|
214
|
+
options: { retry: { until: result => result?.status === 'PAID' } },
|
|
210
215
|
});
|
|
211
216
|
```
|
|
212
217
|
|
|
213
|
-
|
|
218
|
+
Works the same on PostgreSQL and search (Typesense, Meilisearch) repositories.
|
|
214
219
|
|
|
215
|
-
|
|
220
|
+
### The rules
|
|
221
|
+
|
|
222
|
+
- Retry happens only when the read succeeded but `until(result)` says "not yet".
|
|
223
|
+
- A real database error is never retried. It throws immediately, same as without `retry`.
|
|
224
|
+
- Out of attempts? You get the last result as-is. No new error.
|
|
225
|
+
- Inside a transaction, retry is skipped - transactions already go to the primary. Locked reads (`lock`) require a transaction, so they never retry either.
|
|
226
|
+
- On [`SoftDeletableRepository.findById`](/references/base/repositories/soft-deletable#findbyid-with-isstrict), `isStrict: true` is checked only after the attempts run out. A strict read waits out replica lag before it throws `404`.
|
|
227
|
+
- Write verbs do not have this option. `retry` on a write is a compile error.
|
|
228
|
+
|
|
229
|
+
### Options
|
|
230
|
+
|
|
231
|
+
| Option | Type | Default | Meaning |
|
|
232
|
+
|--------|------|---------|---------|
|
|
233
|
+
| `maxAttempts` | `number` | `3` | Total reads, including the first. Below `1` throws. |
|
|
234
|
+
| `until` | `(result) => boolean` | per verb (below) | Return `true` to stop: "fresh enough". |
|
|
235
|
+
| `maxTotalMs` | `number` | unlimited | Stop starting new attempts after this much time. Never cuts a running read short. |
|
|
236
|
+
| `backoff` | `IRetryBackoffOptions` | 50ms up to 500ms, jittered | Wait between attempts. Details: [Retry Utility](/references/utilities/retry). |
|
|
237
|
+
| `signal` | `AbortSignal` | - | Cancel the loop. An abort rejects the call. |
|
|
238
|
+
|
|
239
|
+
`until` is typed per verb - the predicate sees exactly what the verb returns:
|
|
216
240
|
|
|
217
|
-
|
|
241
|
+
| Verb | `until` sees | Default: stops when |
|
|
242
|
+
|---|---|---|
|
|
243
|
+
| `findOne` / `findById` | `TNullable<R>` | result is not `null`/`undefined` |
|
|
244
|
+
| `find` | `Array<R>` | array is non-empty |
|
|
245
|
+
| `find` + `shouldQueryRange: true` | `{ data: R[]; range: ... }` | `data` is non-empty |
|
|
218
246
|
|
|
219
|
-
|
|
247
|
+
> [!WARNING] Empty is a normal answer for `find`
|
|
248
|
+
> `find`'s default predicate is "array is non-empty". A `find` that legitimately matches nothing will burn all attempts before returning `[]`. Where "no results" is normal, pass your own `until` - or do not use `retry` there.
|
|
220
249
|
|
|
221
|
-
|
|
250
|
+
> [!TIP]
|
|
251
|
+
> `retry` runs on `executeWithRetryUntil` from `@venizia/ignis-helpers` - use it directly for any non-repository polling. See [Retry Utility](/references/utilities/retry).
|
|
252
|
+
|
|
253
|
+
## Hidden Properties
|
|
254
|
+
|
|
255
|
+
Fields like `password` must never leave the database by accident. Declare them once on the model and every read path drops them for you:
|
|
222
256
|
|
|
223
257
|
```typescript
|
|
224
258
|
@model({
|
|
@@ -232,130 +266,72 @@ export class User extends BaseEntity<typeof User.schema> {
|
|
|
232
266
|
}
|
|
233
267
|
```
|
|
234
268
|
|
|
235
|
-
### Automatic exclusion
|
|
236
|
-
|
|
237
|
-
Hidden properties are excluded at the **SQL level** for maximum security:
|
|
238
|
-
|
|
239
269
|
```typescript
|
|
240
|
-
// Read operations exclude hidden properties
|
|
241
270
|
const user = await userRepository.findById({ id: '123' });
|
|
242
|
-
//
|
|
243
|
-
// Note: password, secret, apiKey are NOT included
|
|
244
|
-
|
|
245
|
-
// Write operations exclude hidden from RETURNING clause
|
|
246
|
-
const created = await userRepository.create({
|
|
247
|
-
data: { email: 'new@example.com', password: 'hashed_secret' },
|
|
248
|
-
});
|
|
249
|
-
// Result: { count: 1, data: { id: '456', email: 'new@example.com' } }
|
|
250
|
-
// Note: password stored in DB but not returned
|
|
271
|
+
// { id: '123', email: 'john@example.com', name: 'John' } - no password, secret, apiKey
|
|
251
272
|
```
|
|
252
273
|
|
|
253
|
-
###
|
|
274
|
+
### The rules
|
|
254
275
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
});
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
### Relations with hidden properties
|
|
265
|
-
|
|
266
|
-
Hidden properties are also excluded from included relations - see [Relations & Includes](./relations#hidden-properties-in-relations).
|
|
267
|
-
|
|
268
|
-
```typescript
|
|
269
|
-
const post = await postRepository.findOne({
|
|
270
|
-
filter: { include: [{ relation: 'author' }] },
|
|
271
|
-
});
|
|
272
|
-
// post.author will NOT include password, secret, etc.
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
### Accessing hidden data
|
|
276
|
-
|
|
277
|
-
When you need hidden fields (e.g., for authentication), bypass the repository via the `connector` getter:
|
|
278
|
-
|
|
279
|
-
```typescript
|
|
280
|
-
// Direct connector access - includes all fields
|
|
281
|
-
const connector = userRepository.connector;
|
|
282
|
-
const [fullUser] = await connector
|
|
283
|
-
.select()
|
|
284
|
-
.from(User.schema)
|
|
285
|
-
.where(eq(User.schema.email, 'john@example.com'));
|
|
286
|
-
// fullUser includes password, secret, apiKey
|
|
287
|
-
```
|
|
276
|
+
- Exclusion happens at the **SQL level** - hidden columns are never selected, not filtered out afterward.
|
|
277
|
+
- Read operations exclude hidden properties from the result.
|
|
278
|
+
- Write operations exclude hidden properties from the `RETURNING` clause - the value is still written, only not echoed back.
|
|
279
|
+
- You **can** filter `where` on a hidden property; you still cannot see it in the result.
|
|
280
|
+
- Hidden properties are also excluded from included relations - see [Relations & Includes](./relations#hidden-properties-in-relations).
|
|
281
|
+
- Need a hidden field anyway (e.g. to verify a password hash)? Use `repository.connector` to bypass the exclusion - see [Direct Connector Access](#direct-connector-access).
|
|
288
282
|
|
|
289
283
|
## Performance Optimization
|
|
290
284
|
|
|
291
285
|
### Core API for flat queries
|
|
292
286
|
|
|
293
|
-
|
|
287
|
+
A query with no `include`/`fields` runs on Drizzle's Core API, which is faster than the Query API a relation forces:
|
|
294
288
|
|
|
295
289
|
```typescript
|
|
296
290
|
// Automatically optimized - uses Core API
|
|
297
291
|
const users = await repository.find({
|
|
298
|
-
filter: {
|
|
299
|
-
where: { status: 'active' },
|
|
300
|
-
limit: 10,
|
|
301
|
-
order: ['createdAt DESC'],
|
|
302
|
-
},
|
|
292
|
+
filter: { where: { status: 'active' }, limit: 10, order: ['createdAt DESC'] },
|
|
303
293
|
});
|
|
304
|
-
//
|
|
294
|
+
// db.select().from(table).where(...).orderBy(...).limit(10)
|
|
305
295
|
|
|
306
|
-
//
|
|
296
|
+
// Has a relation - uses Query API
|
|
307
297
|
const usersWithPosts = await repository.find({
|
|
308
|
-
filter: {
|
|
309
|
-
where: { status: 'active' },
|
|
310
|
-
include: [{ relation: 'posts' }],
|
|
311
|
-
},
|
|
298
|
+
filter: { where: { status: 'active' }, include: [{ relation: 'posts' }] },
|
|
312
299
|
});
|
|
313
|
-
//
|
|
300
|
+
// db.query.tableName.findMany({ with: { posts: true }, ... })
|
|
314
301
|
```
|
|
315
302
|
|
|
316
303
|
| Filter options | API used | Performance |
|
|
317
|
-
|
|
304
|
+
|---|---|---|
|
|
318
305
|
| `where`, `limit`, `order`, `offset`/`skip` only | Core API | ~15-20% faster |
|
|
319
306
|
| Has `include` (relations) | Query API | Standard |
|
|
320
307
|
| Has `fields` selection | Query API | Standard |
|
|
321
308
|
|
|
322
|
-
### Always
|
|
309
|
+
### Always set a limit
|
|
323
310
|
|
|
324
|
-
|
|
311
|
+
An unbounded `find` can return millions of rows:
|
|
325
312
|
|
|
326
313
|
```typescript
|
|
327
|
-
//
|
|
328
|
-
await repository.find({
|
|
329
|
-
filter: { where: { status: 'active' }, limit: 100 },
|
|
330
|
-
});
|
|
314
|
+
// Bounded result set
|
|
315
|
+
await repository.find({ filter: { where: { status: 'active' }, limit: 100 } });
|
|
331
316
|
|
|
332
|
-
// Dangerous -
|
|
333
|
-
await repository.find({
|
|
334
|
-
filter: { where: { status: 'active' } },
|
|
335
|
-
});
|
|
317
|
+
// Dangerous - no limit in the filter
|
|
318
|
+
await repository.find({ filter: { where: { status: 'active' } } });
|
|
336
319
|
```
|
|
337
320
|
|
|
338
321
|
> [!NOTE]
|
|
339
|
-
> `find()` always applies a default limit when
|
|
322
|
+
> `find()` always applies a default limit when the filter has none. It uses the model's `@model({ settings: { defaultLimit } })` if declared, otherwise the global default of `10`. Pass an explicit `limit` to override either default. `findOne`/`findById` are unaffected - they force `limit: 1` on the Core API path regardless.
|
|
340
323
|
|
|
341
324
|
### Pagination with data range
|
|
342
325
|
|
|
343
|
-
|
|
326
|
+
Pass `shouldQueryRange` to get data and total count from a single call:
|
|
344
327
|
|
|
345
328
|
```typescript
|
|
346
329
|
const result = await userRepository.find({
|
|
347
|
-
filter: {
|
|
348
|
-
where: { status: 'active' },
|
|
349
|
-
limit: 20,
|
|
350
|
-
skip: 40,
|
|
351
|
-
order: ['createdAt DESC'],
|
|
352
|
-
},
|
|
330
|
+
filter: { where: { status: 'active' }, limit: 20, skip: 40, order: ['createdAt DESC'] },
|
|
353
331
|
options: { shouldQueryRange: true },
|
|
354
332
|
});
|
|
355
|
-
|
|
356
|
-
//
|
|
357
|
-
// range follows HTTP Content-Range standard (inclusive end index)
|
|
358
|
-
// Example: { data: [...20 users], range: { start: 40, end: 59, total: 150 } }
|
|
333
|
+
// { data: User[], range: { start: 40, end: 59, total: 150 } }
|
|
334
|
+
// range follows the HTTP Content-Range standard (inclusive end index)
|
|
359
335
|
```
|
|
360
336
|
|
|
361
337
|
- **Parallel by default.** `find` and `count` execute concurrently via `Promise.all`.
|
|
@@ -364,14 +340,14 @@ const result = await userRepository.find({
|
|
|
364
340
|
### WeakMap cache
|
|
365
341
|
|
|
366
342
|
- **Column metadata is cached per schema.** The filter builder caches table column metadata (`getCachedColumns`) to avoid repeated reflection.
|
|
367
|
-
- **Populated on first access.** The first access calls Drizzle's `getTableColumns()` and caches the result;
|
|
343
|
+
- **Populated on first access.** The first access calls Drizzle's `getTableColumns()` and caches the result; later queries read the `WeakMap` instead.
|
|
368
344
|
- **No configuration needed.** The cache is automatic.
|
|
369
345
|
|
|
370
346
|
## TypeScript Return Types
|
|
371
347
|
|
|
372
348
|
### shouldReturn inference
|
|
373
349
|
|
|
374
|
-
|
|
350
|
+
`shouldReturn` decides the result shape at the type level, no manual casting needed:
|
|
375
351
|
|
|
376
352
|
```typescript
|
|
377
353
|
// shouldReturn: false - TypeScript knows data is null
|
|
@@ -379,38 +355,24 @@ const result1 = await repository.create({
|
|
|
379
355
|
data: { name: 'John' },
|
|
380
356
|
options: { shouldReturn: false },
|
|
381
357
|
});
|
|
382
|
-
//
|
|
358
|
+
// Promise<{ count: number; data: undefined | null }>
|
|
383
359
|
|
|
384
360
|
// shouldReturn: true (default) - TypeScript knows data is the entity
|
|
385
|
-
const result2 = await repository.create({
|
|
386
|
-
|
|
387
|
-
options: { shouldReturn: true },
|
|
388
|
-
});
|
|
389
|
-
// Type: Promise<{ count: number; data: User }>
|
|
390
|
-
console.log(result2.data.name); // 'John' - fully typed!
|
|
391
|
-
|
|
392
|
-
// Array operations
|
|
393
|
-
const results = await repository.createAll({
|
|
394
|
-
data: [{ name: 'John' }, { name: 'Jane' }],
|
|
395
|
-
options: { shouldReturn: true },
|
|
396
|
-
});
|
|
397
|
-
// Type: Promise<{ count: number; data: User[] }>
|
|
361
|
+
const result2 = await repository.create({ data: { name: 'John' } });
|
|
362
|
+
console.log(result2.data.name); // 'John' - fully typed
|
|
398
363
|
```
|
|
399
364
|
|
|
365
|
+
The same inference applies to `createAll`, `updateById`, `updateAll`, `updateBy`, `deleteById`, `deleteAll`, `deleteBy`.
|
|
366
|
+
|
|
400
367
|
### Generic return types
|
|
401
368
|
|
|
402
|
-
|
|
369
|
+
Pass a type argument to widen the result for a query with relations:
|
|
403
370
|
|
|
404
371
|
```typescript
|
|
405
|
-
type UserWithPosts = User & {
|
|
406
|
-
posts: Post[];
|
|
407
|
-
};
|
|
372
|
+
type UserWithPosts = User & { posts: Post[] };
|
|
408
373
|
|
|
409
374
|
const user = await userRepository.findOne<UserWithPosts>({
|
|
410
|
-
filter: {
|
|
411
|
-
where: { id: '123' },
|
|
412
|
-
include: [{ relation: 'posts' }],
|
|
413
|
-
},
|
|
375
|
+
filter: { where: { id: '123' }, include: [{ relation: 'posts' }] },
|
|
414
376
|
});
|
|
415
377
|
|
|
416
378
|
if (user) {
|
|
@@ -418,149 +380,131 @@ if (user) {
|
|
|
418
380
|
}
|
|
419
381
|
```
|
|
420
382
|
|
|
421
|
-
**Supported
|
|
422
|
-
- `find<R>()`, `findOne<R>()`, `findById<R>()`
|
|
423
|
-
- `create<R>()`, `createAll<R>()`
|
|
424
|
-
- `updateById<R>()`, `updateAll<R>()`, `updateBy<R>()`
|
|
425
|
-
- `deleteById<R>()`, `deleteAll<R>()`, `deleteBy<R>()`
|
|
383
|
+
**Supported on:** `find<R>()`, `findOne<R>()`, `findById<R>()`, `create<R>()`, `createAll<R>()`, `updateById<R>()`, `updateAll<R>()`, `updateBy<R>()`, `deleteById<R>()`, `deleteAll<R>()`, `deleteBy<R>()`.
|
|
426
384
|
|
|
427
385
|
## Debugging
|
|
428
386
|
|
|
429
387
|
### Log option
|
|
430
388
|
|
|
431
|
-
|
|
389
|
+
Pass `log` to trace a single operation without turning on logging globally:
|
|
432
390
|
|
|
433
391
|
```typescript
|
|
434
392
|
await repository.create({
|
|
435
393
|
data: { name: 'John', email: 'john@example.com' },
|
|
436
394
|
options: { log: { use: true, level: 'debug' } },
|
|
437
395
|
});
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
// Available levels: 'debug', 'info', 'warn', 'error'
|
|
441
|
-
await repository.updateById({
|
|
442
|
-
id: '123',
|
|
443
|
-
data: { name: 'Jane' },
|
|
444
|
-
options: { log: { use: true, level: 'info' } },
|
|
445
|
-
});
|
|
396
|
+
// [_create] Executing with opts: { data: [...], options: {...} }
|
|
446
397
|
```
|
|
447
398
|
|
|
448
|
-
|
|
399
|
+
| Level | Meaning |
|
|
400
|
+
|---|---|
|
|
401
|
+
| `debug` | Verbose - opts and intermediate state |
|
|
402
|
+
| `info` | Default when `level` is omitted |
|
|
403
|
+
| `warn` | Notable but non-fatal conditions |
|
|
404
|
+
| `error` | Failure paths |
|
|
405
|
+
|
|
406
|
+
**Supported on:** `create`, `createAll`, `updateById`, `updateAll`, `updateBy`, `deleteById`, `deleteAll`, `deleteBy` - every write operation that goes through the internal `_create`, `_update`, or `_delete` methods.
|
|
449
407
|
|
|
450
408
|
### Query interface validation
|
|
451
409
|
|
|
452
|
-
The repository validates schema registration
|
|
410
|
+
The repository validates schema registration the first time it touches the Query API. A mismatch fails with a pointer to the problem instead of a raw Drizzle error:
|
|
453
411
|
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
// | Ensure the model's TABLE_NAME matches the schema registration key
|
|
412
|
+
```
|
|
413
|
+
Error: [UserRepository] Schema key mismatch
|
|
414
|
+
| Entity name 'User' not found in connector.query
|
|
415
|
+
| Available keys: [Configuration, Post]
|
|
416
|
+
| Ensure the model's TABLE_NAME matches the schema registration key
|
|
460
417
|
```
|
|
461
418
|
|
|
462
419
|
## Safety Features
|
|
463
420
|
|
|
464
421
|
### Empty where protection
|
|
465
422
|
|
|
466
|
-
`updateAll`/`updateBy`/`deleteAll`/`deleteBy` refuse an empty `where` unless `force: true` is passed
|
|
423
|
+
`updateAll`/`updateBy`/`deleteAll`/`deleteBy` refuse an empty `where` unless `force: true` is passed, to block accidental mass updates and deletes:
|
|
467
424
|
|
|
468
425
|
```typescript
|
|
469
|
-
// Throws
|
|
426
|
+
// Throws - empty where without force
|
|
470
427
|
await repository.deleteAll({ where: {} });
|
|
471
428
|
|
|
472
|
-
// Explicit force
|
|
473
|
-
await repository.deleteAll({
|
|
474
|
-
where: {},
|
|
475
|
-
options: { force: true },
|
|
476
|
-
});
|
|
429
|
+
// Explicit force - logs a warning, proceeds
|
|
430
|
+
await repository.deleteAll({ where: {}, options: { force: true } });
|
|
477
431
|
// Warning: [_delete] Entity: User | Performing delete with empty condition
|
|
478
432
|
```
|
|
479
433
|
|
|
480
434
|
| Scenario | `force: false` (default) | `force: true` |
|
|
481
|
-
|
|
482
|
-
| Empty `where` | Throws
|
|
435
|
+
|---|---|---|
|
|
436
|
+
| Empty `where` | Throws | Logs a warning, proceeds |
|
|
483
437
|
| Valid `where` | Executes normally | Executes normally |
|
|
484
438
|
|
|
485
439
|
> [!NOTE]
|
|
486
|
-
> `updateById` and `deleteById` always have a non-empty where (`{ id }`) so
|
|
440
|
+
> `updateById` and `deleteById` always have a non-empty where (`{ id }`), so this guard never applies to them.
|
|
487
441
|
|
|
488
442
|
### Transaction safety
|
|
489
443
|
|
|
490
|
-
`resolveConnector` validates transaction state before use:
|
|
444
|
+
`resolveConnector` validates transaction state before every use. A transaction already committed or rolled back fails fast instead of running against a dead connection:
|
|
491
445
|
|
|
492
|
-
```
|
|
493
|
-
|
|
494
|
-
// Error: [UserRepository][resolveConnector] Transaction is no longer active
|
|
446
|
+
```
|
|
447
|
+
Error: [UserRepository][resolveConnector] Transaction is no longer active
|
|
495
448
|
```
|
|
496
449
|
|
|
497
450
|
## Direct Connector Access
|
|
498
451
|
|
|
499
|
-
|
|
452
|
+
`repository.connector` is a getter (not a method) that resolves the datasource's Drizzle connector, for queries the repository API does not cover:
|
|
500
453
|
|
|
501
454
|
```typescript
|
|
502
455
|
const connector = repository.connector;
|
|
503
456
|
|
|
504
|
-
// Raw Drizzle query
|
|
505
457
|
const results = await connector
|
|
506
|
-
.select({
|
|
507
|
-
userId: userTable.id,
|
|
508
|
-
postCount: sql<number>`count(${postTable.id})`,
|
|
509
|
-
})
|
|
458
|
+
.select({ userId: userTable.id, postCount: sql<number>`count(${postTable.id})` })
|
|
510
459
|
.from(userTable)
|
|
511
460
|
.leftJoin(postTable, eq(userTable.id, postTable.authorId))
|
|
512
461
|
.groupBy(userTable.id)
|
|
513
462
|
.having(sql`count(${postTable.id}) > 5`);
|
|
514
|
-
|
|
515
|
-
// Use with caution - bypasses repository features like hidden properties
|
|
516
463
|
```
|
|
517
464
|
|
|
465
|
+
> [!WARNING]
|
|
466
|
+
> Queries through `connector` bypass repository features - hidden-property exclusion included. Use it with intent, not as a default escape hatch.
|
|
467
|
+
|
|
518
468
|
## Repository Class Hierarchy
|
|
519
469
|
|
|
520
|
-
The PostgreSQL connector's canonical names carry the engine in the class name
|
|
470
|
+
The PostgreSQL connector's canonical names carry the engine in the class name. The historical `*Repository` names remain as compatibility aliases re-exporting the exact same classes.
|
|
521
471
|
|
|
522
472
|
| Canonical class | Alias | Scope | Description |
|
|
523
473
|
|---|---|---|---|
|
|
524
|
-
| `AbstractRepository` | - | N/A | Engine-neutral abstract base (`src/base`), defines
|
|
474
|
+
| `AbstractRepository` | - | N/A | Engine-neutral abstract base (`src/base`), defines every method signature, lazy `dataSource`/`entity` resolution. Plain `BaseHelper` subclass, no mixin composition. |
|
|
525
475
|
| `RelationalBaseRepository` | `PostgresBaseRepository` | N/A | PostgreSQL connector base. Adds `FilterBuilder`/`UpdateBuilder`, hidden-column exclusion (`getHiddenProperties`/`getVisibleProperties`), default-filter application (`getDefaultFilter`/`applyDefaultFilter`) - the behavior formerly provided by the now-removed `FieldsVisibilityMixin`/`DefaultFilterMixin` (see [Repository Mixins](./mixins)). |
|
|
526
|
-
| `ReadableRelationalRepository` | `ReadableRepository` | `READ_ONLY` | Read-only operations (`find`, `findOne`, `findById`, `count`, `existsWith`). Write operations throw
|
|
476
|
+
| `ReadableRelationalRepository` | `ReadableRepository` | `READ_ONLY` | Read-only operations (`find`, `findOne`, `findById`, `count`, `existsWith`). Write operations throw. |
|
|
527
477
|
| `PersistableRelationalRepository` | `PersistableRepository` | `READ_WRITE` | Adds write operations (`create`, `update`, `delete`) with `UpdateBuilder`. |
|
|
528
478
|
| `DefaultRelationalRepository` | `DefaultCRUDRepository` | `READ_WRITE` | Extends `PersistableRelationalRepository` with no additional logic - **recommended default**. |
|
|
529
|
-
| `SoftDeletableRelationalRepository` | `SoftDeletableRepository` | `READ_WRITE` | Extends `DefaultRelationalRepository` with soft delete
|
|
530
|
-
|
|
531
|
-
Code samples throughout the docs use the alias names (`DefaultCRUDRepository`, `ReadableRepository`) since that is what `@venizia/ignis/postgres` code most commonly imports today.
|
|
479
|
+
| `SoftDeletableRelationalRepository` | `SoftDeletableRepository` | `READ_WRITE` | Extends `DefaultRelationalRepository` with soft delete and restore - see [SoftDeletableRepository](./soft-deletable). |
|
|
532
480
|
|
|
533
|
-
|
|
481
|
+
Code samples throughout the docs use the alias names (`DefaultCRUDRepository`, `ReadableRepository`), since that is what `@venizia/ignis/postgres` code most commonly imports today.
|
|
534
482
|
|
|
535
483
|
```typescript
|
|
536
484
|
@repository({ model: AuditLog, dataSource: PostgresDataSource })
|
|
537
485
|
export class AuditLogRepository extends ReadableRepository<typeof AuditLog.schema> {
|
|
538
486
|
// Only has: find, findOne, findById, count, existsWith
|
|
539
|
-
// Write operations throw "NOT ALLOWED" error
|
|
487
|
+
// Write operations throw a "NOT ALLOWED" error
|
|
540
488
|
}
|
|
541
489
|
```
|
|
542
490
|
|
|
543
491
|
### Alias methods
|
|
544
492
|
|
|
545
|
-
`AbstractRepository` provides two alias methods for convenience:
|
|
493
|
+
`AbstractRepository` provides two alias methods for convenience, both delegating directly and supporting the same `shouldReturn`/`force` options:
|
|
546
494
|
|
|
547
|
-
- `updateBy(opts)` -
|
|
548
|
-
- `deleteBy(opts)` -
|
|
549
|
-
|
|
550
|
-
Both accept the same parameters (`where`, `data`/`options`) and support `shouldReturn` and `force` options.
|
|
495
|
+
- `updateBy(opts)` - alias for `updateAll(opts)`.
|
|
496
|
+
- `deleteBy(opts)` - alias for `deleteAll(opts)`.
|
|
551
497
|
|
|
552
498
|
## Default Filter Bypass
|
|
553
499
|
|
|
554
|
-
|
|
500
|
+
A model's `defaultFilter` (e.g. soft-delete's `isDeleted = false`) applies to every query unless you opt out for an admin or maintenance path:
|
|
555
501
|
|
|
556
502
|
```typescript
|
|
557
503
|
// Normal query - default filter applies
|
|
558
|
-
await repository.find({
|
|
559
|
-
|
|
560
|
-
});
|
|
561
|
-
// WHERE isDeleted = false AND status = 'active' (if model has soft-delete default)
|
|
504
|
+
await repository.find({ filter: { where: { status: 'active' } } });
|
|
505
|
+
// WHERE isDeleted = false AND status = 'active' (if the model has a soft-delete default)
|
|
562
506
|
|
|
563
|
-
// Admin query - bypass default filter
|
|
507
|
+
// Admin query - bypass the default filter
|
|
564
508
|
await repository.find({
|
|
565
509
|
filter: { where: { status: 'active' } },
|
|
566
510
|
options: { shouldSkipDefaultFilter: true },
|
|
@@ -568,122 +512,67 @@ await repository.find({
|
|
|
568
512
|
// WHERE status = 'active' (includes deleted records)
|
|
569
513
|
```
|
|
570
514
|
|
|
571
|
-
**Supported on
|
|
515
|
+
**Supported on every operation:**
|
|
572
516
|
|
|
573
517
|
```typescript
|
|
574
|
-
// Read operations
|
|
575
518
|
await repository.find({ filter, options: { shouldSkipDefaultFilter: true } });
|
|
576
519
|
await repository.findOne({ filter, options: { shouldSkipDefaultFilter: true } });
|
|
577
520
|
await repository.count({ where, options: { shouldSkipDefaultFilter: true } });
|
|
578
|
-
|
|
579
|
-
// Write operations
|
|
580
521
|
await repository.updateAll({ where, data, options: { shouldSkipDefaultFilter: true } });
|
|
581
522
|
await repository.deleteAll({ where, options: { shouldSkipDefaultFilter: true, force: true } });
|
|
582
523
|
```
|
|
583
524
|
|
|
584
|
-
**Combined with transactions:**
|
|
585
|
-
|
|
586
|
-
```typescript
|
|
587
|
-
const tx = await repository.beginTransaction();
|
|
588
|
-
await repository.updateAll({
|
|
589
|
-
where: { status: 'archived' },
|
|
590
|
-
data: { isDeleted: true },
|
|
591
|
-
options: {
|
|
592
|
-
transaction: tx,
|
|
593
|
-
shouldSkipDefaultFilter: true,
|
|
594
|
-
},
|
|
595
|
-
});
|
|
596
|
-
await tx.commit();
|
|
597
|
-
```
|
|
598
|
-
|
|
599
525
|
> [!TIP]
|
|
600
|
-
>
|
|
526
|
+
> Combine it with a transaction for an atomic admin operation:
|
|
527
|
+
> ```typescript
|
|
528
|
+
> const tx = await repository.beginTransaction();
|
|
529
|
+
> await repository.updateAll({
|
|
530
|
+
> where: { status: 'archived' },
|
|
531
|
+
> data: { isDeleted: true },
|
|
532
|
+
> options: { transaction: tx, shouldSkipDefaultFilter: true },
|
|
533
|
+
> });
|
|
534
|
+
> await tx.commit();
|
|
535
|
+
> ```
|
|
536
|
+
> See [Default Filter](../filter-system/default-filter) for configuring model default filters.
|
|
601
537
|
|
|
602
538
|
## Nested JSON Updates
|
|
603
539
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
### Basic usage
|
|
607
|
-
|
|
608
|
-
Use dot notation keys to target nested properties:
|
|
540
|
+
`json`/`jsonb` columns update in place with dot-notation keys - `UpdateBuilder` compiles them into chained `jsonb_set` calls instead of overwriting the whole column:
|
|
609
541
|
|
|
610
542
|
```typescript
|
|
611
|
-
//
|
|
612
|
-
// Current value: { theme: 'light', notifications: { email: true } }
|
|
613
|
-
|
|
543
|
+
// 'metadata' is a jsonb column: { theme: 'light', notifications: { email: true } }
|
|
614
544
|
await repository.updateById({
|
|
615
545
|
id: '123',
|
|
616
546
|
data: {
|
|
617
|
-
|
|
618
|
-
'metadata.theme': 'dark',
|
|
547
|
+
status: 'active', // regular column
|
|
548
|
+
'metadata.theme': 'dark', // JSON path, any depth
|
|
549
|
+
'metadata.addresses[0].primary': true, // array element by index
|
|
619
550
|
},
|
|
620
551
|
});
|
|
621
|
-
|
|
622
|
-
// New value: { theme: 'dark', notifications: { email: true } }
|
|
552
|
+
// metadata becomes: { theme: 'dark', notifications: { email: true }, addresses: [{ primary: true }, ...] }
|
|
623
553
|
```
|
|
624
554
|
|
|
625
|
-
###
|
|
555
|
+
### The rules
|
|
626
556
|
|
|
627
|
-
- **Deep nesting:**
|
|
628
|
-
- **Array access:** update array
|
|
629
|
-
- **Auto-creation:**
|
|
630
|
-
- **Type safety:**
|
|
631
|
-
- **Multiple
|
|
632
|
-
- **Mixed updates:**
|
|
557
|
+
- **Deep nesting:** target a property at any depth (`settings.display.font.size`).
|
|
558
|
+
- **Array access:** update an array element by index (`tags[0]`).
|
|
559
|
+
- **Auto-creation:** missing intermediate keys are created automatically (`jsonb_set` with `create_missing = true`).
|
|
560
|
+
- **Type safety:** the target column must be `json`/`jsonb`. Any other column type throws.
|
|
561
|
+
- **Multiple paths on one column:** chained as nested `jsonb_set` calls in a single statement.
|
|
562
|
+
- **Mixed updates:** regular columns and JSON paths combine in the same `data` object, as in the basic usage example.
|
|
633
563
|
|
|
634
|
-
###
|
|
564
|
+
### Security and validation
|
|
635
565
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
await repository.updateById({
|
|
640
|
-
id: '123',
|
|
641
|
-
data: {
|
|
642
|
-
'metadata.settings.display.fontSize': 16,
|
|
643
|
-
'metadata.settings.display.showSidebar': true,
|
|
644
|
-
},
|
|
645
|
-
});
|
|
646
|
-
```
|
|
647
|
-
|
|
648
|
-
#### Array element updates
|
|
649
|
-
|
|
650
|
-
```typescript
|
|
651
|
-
await repository.updateById({
|
|
652
|
-
id: '123',
|
|
653
|
-
data: {
|
|
654
|
-
// Set the first address as primary
|
|
655
|
-
'metadata.addresses[0].primary': true,
|
|
656
|
-
},
|
|
657
|
-
});
|
|
658
|
-
```
|
|
659
|
-
|
|
660
|
-
#### Mixed updates (regular + JSON)
|
|
661
|
-
|
|
662
|
-
```typescript
|
|
663
|
-
await repository.updateById({
|
|
664
|
-
id: '123',
|
|
665
|
-
data: {
|
|
666
|
-
status: 'active', // Regular column
|
|
667
|
-
'metadata.lastLogin': now, // JSON path
|
|
668
|
-
'preferences.lang': 'en', // Another JSON path
|
|
669
|
-
},
|
|
670
|
-
});
|
|
671
|
-
```
|
|
672
|
-
|
|
673
|
-
### Security & validation
|
|
674
|
-
|
|
675
|
-
The framework validates JSON paths to prevent SQL injection:
|
|
676
|
-
|
|
677
|
-
- **Allowed characters:** path components must match `/^[a-zA-Z_][a-zA-Z0-9_-]*$|^\d+$/` (identifiers, kebab-case, or array indices).
|
|
678
|
-
- **Column type validation:** only `json` and `jsonb` columns are allowed. Other column types throw an error.
|
|
679
|
-
- **Values:** values are serialized to JSONB literals with proper escaping.
|
|
566
|
+
- **Allowed characters:** each path component must match `/^[a-zA-Z_][a-zA-Z0-9_-]*$|^\d+$/` (identifiers, kebab-case, or array indices) - this is what blocks SQL injection through a path.
|
|
567
|
+
- **Column type validation:** only `json` and `jsonb` columns are allowed.
|
|
568
|
+
- **Values:** serialized to JSONB literals with proper escaping.
|
|
680
569
|
|
|
681
570
|
> [!NOTE]
|
|
682
|
-
> This feature uses PostgreSQL's `jsonb_set` function
|
|
571
|
+
> This feature uses PostgreSQL's `jsonb_set` function and only applies to columns defined as `json` or `jsonb`.
|
|
683
572
|
|
|
684
573
|
## ExtraOptions Reference
|
|
685
574
|
|
|
686
|
-
|
|
575
|
+
Every repository operation accepts an `options` parameter (`IExtraOptions`, narrowed by postgres to `IDatabaseExtraOptions`):
|
|
687
576
|
|
|
688
577
|
| Option | Type | Default | Description |
|
|
689
578
|
|--------|------|---------|-------------|
|
|
@@ -692,12 +581,18 @@ All repository operations accept an `options` parameter (`IExtraOptions`/its pos
|
|
|
692
581
|
| `shouldSkipDefaultFilter` | `boolean` | `false` | Bypass the default filter from model settings |
|
|
693
582
|
| `lock` | `TLockOptions` | - | Row-level locking (requires transaction, Core API only) |
|
|
694
583
|
|
|
584
|
+
Read operations (`find`, `findOne`, `findById`) additionally support:
|
|
585
|
+
|
|
586
|
+
| Option | Type | Default | Description |
|
|
587
|
+
|--------|------|---------|-------------|
|
|
588
|
+
| `retry` | `IReadRetryOptions` | - | Re-read with backoff until a predicate passes - see [Read Retry](#read-retry-replica-lag). Skipped inside a transaction. Not accepted by write operations. |
|
|
589
|
+
|
|
695
590
|
Write operations additionally support:
|
|
696
591
|
|
|
697
592
|
| Option | Type | Default | Description |
|
|
698
593
|
|--------|------|---------|-------------|
|
|
699
594
|
| `shouldReturn` | `boolean` | `true` | Return the created/updated/deleted data |
|
|
700
|
-
| `force` | `boolean` | `false` | Allow empty `where` condition on bulk operations |
|
|
595
|
+
| `force` | `boolean` | `false` | Allow an empty `where` condition on bulk operations |
|
|
701
596
|
| `shouldQueryRange` | `boolean` | `false` | Return `{ data, range }` with total count (find only) |
|
|
702
597
|
|
|
703
598
|
## Quick Reference
|
|
@@ -711,6 +606,7 @@ Write operations additionally support:
|
|
|
711
606
|
| Bypass default filter | `options: { shouldSkipDefaultFilter: true }` |
|
|
712
607
|
| Lock rows for update | `options: { transaction: tx, lock: { strength: 'update' } }` |
|
|
713
608
|
| Lock + skip locked | `options: { transaction: tx, lock: { strength: 'update', config: { skipLocked: true } } }` |
|
|
609
|
+
| Retry a read until fresh | `options: { retry: { until: result => result?.status === 'PAID' } }` |
|
|
714
610
|
| Enable logging | `options: { log: { use: true, level: 'debug' } }` |
|
|
715
611
|
| Force delete all | `options: { force: true }` |
|
|
716
612
|
| Skip returning data | `options: { shouldReturn: false }` |
|
|
@@ -727,3 +623,5 @@ Write operations additionally support:
|
|
|
727
623
|
- [Default Filter](/references/base/filter-system/default-filter) - automatic filter configuration
|
|
728
624
|
- [DataSources - Full Reference](/references/base/datasources-reference) - transaction internals, isolation levels, driver seam
|
|
729
625
|
- [Transactions guide](/guides/core-concepts/persistent/transactions) - multi-operation database transactions
|
|
626
|
+
- [Retry Utility](/references/utilities/retry) - `executeWithRetry`/`executeWithRetryUntil`, backoff strategies, jitter modes
|
|
627
|
+
- [Search & Typesense - Repository Tiers](/guides/core-concepts/persistent/search-typesense#repository-tiers) - `retry` on the search chain
|