@venizia/ignis-docs 0.2.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/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +24 -13
- package/content/best-practices/architecture-decisions.md +29 -16
- package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
- package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
- package/content/best-practices/code-style-standards/control-flow.md +33 -1
- package/content/best-practices/code-style-standards/documentation.md +28 -8
- package/content/best-practices/code-style-standards/function-patterns.md +15 -4
- package/content/best-practices/code-style-standards/index.md +7 -2
- package/content/best-practices/code-style-standards/naming-conventions.md +27 -3
- package/content/best-practices/code-style-standards/route-definitions.md +9 -5
- package/content/best-practices/code-style-standards/tooling.md +14 -1
- package/content/best-practices/code-style-standards/type-safety.md +39 -6
- package/content/best-practices/common-pitfalls.md +59 -35
- package/content/best-practices/contribution-workflow.md +8 -4
- package/content/best-practices/data-modeling.md +78 -62
- package/content/best-practices/deployment-strategies.md +56 -19
- package/content/best-practices/error-handling.md +247 -153
- package/content/best-practices/index.md +6 -0
- package/content/best-practices/performance-optimization.md +26 -13
- package/content/best-practices/security-guidelines.md +35 -9
- package/content/best-practices/testing-strategies.md +7 -2
- package/content/best-practices/troubleshooting-tips.md +27 -17
- package/content/extensions/components/api-reference.md +109 -323
- package/content/extensions/components/authentication/api.md +489 -602
- package/content/extensions/components/authentication/errors.md +135 -498
- package/content/extensions/components/authentication/index.md +89 -801
- package/content/extensions/components/authentication/usage.md +231 -955
- package/content/extensions/components/authorization/api.md +991 -644
- package/content/extensions/components/authorization/errors.md +204 -208
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +88 -795
- package/content/extensions/components/authorization/usage.md +219 -528
- package/content/extensions/components/health-check.md +77 -243
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +553 -285
- package/content/extensions/components/mail/errors.md +76 -62
- package/content/extensions/components/mail/index.md +111 -463
- package/content/extensions/components/mail/usage.md +139 -173
- package/content/extensions/components/request-tracker.md +70 -173
- package/content/extensions/components/socket-io/api.md +462 -785
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +69 -372
- package/content/extensions/components/socket-io/usage.md +212 -105
- package/content/extensions/components/static-asset/api.md +461 -141
- package/content/extensions/components/static-asset/errors.md +121 -53
- package/content/extensions/components/static-asset/index.md +84 -606
- package/content/extensions/components/static-asset/usage.md +184 -299
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +311 -404
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +75 -407
- package/content/extensions/components/websocket/usage.md +120 -338
- package/content/extensions/helpers/cron/index.md +52 -160
- package/content/extensions/helpers/crypto/index.md +67 -480
- package/content/extensions/helpers/crypto/reference.md +528 -0
- package/content/extensions/helpers/env/index.md +64 -178
- package/content/extensions/helpers/error/index.md +286 -196
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +76 -550
- package/content/extensions/helpers/inversion/reference.md +530 -0
- package/content/extensions/helpers/kafka/admin.md +23 -3
- package/content/extensions/helpers/kafka/compile-binary.md +83 -52
- package/content/extensions/helpers/kafka/consumer.md +65 -28
- package/content/extensions/helpers/kafka/examples.md +46 -253
- package/content/extensions/helpers/kafka/index.md +55 -617
- package/content/extensions/helpers/kafka/producer.md +156 -22
- package/content/extensions/helpers/kafka/schema-registry.md +59 -79
- package/content/extensions/helpers/logger/hf-logger.md +220 -0
- package/content/extensions/helpers/logger/index.md +78 -552
- package/content/extensions/helpers/logger/pino.md +105 -0
- package/content/extensions/helpers/logger/reference.md +937 -0
- package/content/extensions/helpers/network/api.md +276 -195
- package/content/extensions/helpers/network/index.md +87 -524
- package/content/extensions/helpers/queue/index.md +80 -897
- package/content/extensions/helpers/queue/reference.md +494 -0
- package/content/extensions/helpers/redis/index.md +86 -640
- package/content/extensions/helpers/redis/reference.md +784 -0
- package/content/extensions/helpers/secrets/index.md +136 -0
- package/content/extensions/helpers/socket-io/api.md +325 -204
- package/content/extensions/helpers/socket-io/index.md +79 -429
- package/content/extensions/helpers/storage/api.md +584 -464
- package/content/extensions/helpers/storage/index.md +80 -573
- package/content/extensions/helpers/types/index.md +75 -495
- package/content/extensions/helpers/types/reference.md +689 -0
- package/content/extensions/helpers/uid/index.md +176 -189
- package/content/extensions/helpers/websocket/api.md +366 -214
- package/content/extensions/helpers/websocket/index.md +68 -503
- package/content/extensions/helpers/worker-thread/index.md +62 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- 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/models.md +1 -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 +28 -16
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- 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 +5 -4
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +8 -7
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +64 -22
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +600 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +25 -46
- package/content/references/base/filter-system/application-usage.md +95 -123
- package/content/references/base/filter-system/array-operators.md +33 -43
- package/content/references/base/filter-system/comparison-operators.md +55 -63
- package/content/references/base/filter-system/default-filter.md +144 -350
- package/content/references/base/filter-system/fields-order-pagination.md +116 -148
- package/content/references/base/filter-system/index.md +114 -253
- package/content/references/base/filter-system/json-filtering.md +52 -181
- package/content/references/base/filter-system/list-operators.md +28 -44
- package/content/references/base/filter-system/logical-operators.md +69 -114
- package/content/references/base/filter-system/null-operators.md +41 -98
- package/content/references/base/filter-system/pattern-matching.md +48 -51
- package/content/references/base/filter-system/quick-reference.md +93 -196
- package/content/references/base/filter-system/range-operators.md +22 -38
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +173 -232
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +43 -28
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +81 -1452
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +281 -419
- package/content/references/base/repositories/index.md +84 -644
- package/content/references/base/repositories/mixins.md +25 -21
- package/content/references/base/repositories/relations.md +194 -375
- package/content/references/base/repositories/soft-deletable.md +67 -55
- package/content/references/base/secrets.md +267 -0
- package/content/references/base/services.md +8 -6
- package/content/references/configuration/environment-variables.md +73 -21
- package/content/references/configuration/index.md +51 -31
- package/content/references/index.md +1 -1
- package/content/references/quick-reference.md +3 -16
- package/content/references/utilities/crypto.md +35 -76
- package/content/references/utilities/date.md +33 -73
- package/content/references/utilities/duration.md +85 -0
- package/content/references/utilities/index.md +6 -2
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +81 -61
- package/content/references/utilities/parse.md +34 -64
- package/content/references/utilities/performance.md +33 -58
- package/content/references/utilities/promise.md +28 -62
- package/content/references/utilities/request.md +58 -218
- package/content/references/utilities/retry.md +139 -0
- package/content/references/utilities/schema.md +43 -137
- package/content/references/utilities/statuses-reference.md +361 -0
- package/content/references/utilities/statuses.md +63 -667
- 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 +24 -23
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Advanced Repository Features
|
|
3
|
-
description: Transactions, hidden properties, and
|
|
3
|
+
description: Transactions, row-level locking, hidden properties, performance, and safety guards
|
|
4
4
|
difficulty: intermediate
|
|
5
5
|
---
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
-
|
|
18
|
-
-
|
|
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
|
|
19
22
|
|
|
20
23
|
## Transactions
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
### 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:
|
|
25
26
|
|
|
26
27
|
```typescript
|
|
27
28
|
const tx = await repository.beginTransaction();
|
|
@@ -30,66 +31,70 @@ try {
|
|
|
30
31
|
// All operations use the same transaction
|
|
31
32
|
const user = await userRepository.create({
|
|
32
33
|
data: { name: 'Alice', email: 'alice@example.com' },
|
|
33
|
-
options: { transaction: tx }
|
|
34
|
+
options: { transaction: tx },
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
const profile = await profileRepository.create({
|
|
37
38
|
data: { userId: user.data.id, bio: 'Hello!' },
|
|
38
|
-
options: { transaction: tx }
|
|
39
|
+
options: { transaction: tx },
|
|
39
40
|
});
|
|
40
41
|
|
|
41
|
-
// Commit if all succeeded
|
|
42
42
|
await tx.commit();
|
|
43
|
-
|
|
44
43
|
return { user: user.data, profile: profile.data };
|
|
45
44
|
} catch (error) {
|
|
46
|
-
// Rollback on any error
|
|
47
45
|
await tx.rollback();
|
|
48
46
|
throw error;
|
|
49
47
|
}
|
|
50
48
|
```
|
|
51
49
|
|
|
52
|
-
###
|
|
50
|
+
### The rules
|
|
53
51
|
|
|
54
|
-
|
|
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
|
+
|
|
56
|
+
> [!WARNING] `rollback()` can throw
|
|
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.
|
|
58
|
+
|
|
59
|
+
### Isolation levels
|
|
60
|
+
|
|
61
|
+
Pass `isolationLevel` to control how the transaction sees concurrent changes:
|
|
55
62
|
|
|
56
63
|
```typescript
|
|
64
|
+
import { IsolationLevels } from '@venizia/ignis/postgres';
|
|
65
|
+
|
|
57
66
|
const tx = await repository.beginTransaction({
|
|
58
|
-
isolationLevel:
|
|
67
|
+
isolationLevel: IsolationLevels.SERIALIZABLE,
|
|
59
68
|
});
|
|
60
69
|
```
|
|
61
70
|
|
|
62
|
-
| Level |
|
|
63
|
-
|
|
64
|
-
| `READ COMMITTED` | Default.
|
|
65
|
-
| `REPEATABLE READ` | Consistent reads within transaction |
|
|
66
|
-
| `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 |
|
|
76
|
+
|
|
77
|
+
A plain string literal (`'SERIALIZABLE'`) works too - `isolationLevel` is typed `TIsolationLevel`, a string union the const class's values satisfy.
|
|
67
78
|
|
|
68
|
-
###
|
|
79
|
+
### Raw SQL inside a transaction
|
|
80
|
+
|
|
81
|
+
A transaction also covers Drizzle's `sql` template for atomic column updates:
|
|
69
82
|
|
|
70
83
|
```typescript
|
|
71
84
|
async function transferFunds(fromId: string, toId: string, amount: number) {
|
|
72
85
|
const tx = await accountRepository.beginTransaction();
|
|
73
86
|
|
|
74
87
|
try {
|
|
75
|
-
// Debit source account
|
|
76
88
|
await accountRepository.updateById({
|
|
77
89
|
id: fromId,
|
|
78
90
|
data: { balance: sql`balance - ${amount}` },
|
|
79
|
-
options: { transaction: tx }
|
|
91
|
+
options: { transaction: tx },
|
|
80
92
|
});
|
|
81
93
|
|
|
82
|
-
// Credit destination account
|
|
83
94
|
await accountRepository.updateById({
|
|
84
95
|
id: toId,
|
|
85
96
|
data: { balance: sql`balance + ${amount}` },
|
|
86
|
-
options: { transaction: tx }
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// Record the transfer
|
|
90
|
-
await transferRepository.create({
|
|
91
|
-
data: { fromId, toId, amount, status: 'completed' },
|
|
92
|
-
options: { transaction: tx }
|
|
97
|
+
options: { transaction: tx },
|
|
93
98
|
});
|
|
94
99
|
|
|
95
100
|
await tx.commit();
|
|
@@ -100,14 +105,9 @@ async function transferFunds(fromId: string, toId: string, amount: number) {
|
|
|
100
105
|
}
|
|
101
106
|
```
|
|
102
107
|
|
|
103
|
-
|
|
104
108
|
## Row-Level Locking
|
|
105
109
|
|
|
106
|
-
Acquire pessimistic locks on selected rows
|
|
107
|
-
|
|
108
|
-
### Basic Usage
|
|
109
|
-
|
|
110
|
-
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`:
|
|
111
111
|
|
|
112
112
|
```typescript
|
|
113
113
|
const tx = await repository.beginTransaction();
|
|
@@ -116,10 +116,7 @@ try {
|
|
|
116
116
|
// Lock the row - other transactions will wait
|
|
117
117
|
const item = await repository.findOne({
|
|
118
118
|
filter: { where: { id: '123' } },
|
|
119
|
-
options: {
|
|
120
|
-
transaction: tx,
|
|
121
|
-
lock: { strength: 'update' },
|
|
122
|
-
},
|
|
119
|
+
options: { transaction: tx, lock: { strength: 'update' } },
|
|
123
120
|
});
|
|
124
121
|
|
|
125
122
|
// Safe to modify - no concurrent changes possible
|
|
@@ -136,86 +133,126 @@ try {
|
|
|
136
133
|
}
|
|
137
134
|
```
|
|
138
135
|
|
|
139
|
-
|
|
136
|
+
**Supported methods:** `find`, `findOne`, `findById` (delegates to `findOne`). Not `count`/`existsWith`.
|
|
137
|
+
|
|
138
|
+
### Lock strengths
|
|
140
139
|
|
|
141
|
-
Use the `LockStrengths`
|
|
140
|
+
Use the `LockStrengths` const class or an equivalent string literal:
|
|
142
141
|
|
|
143
142
|
```typescript
|
|
144
143
|
import { LockStrengths } from '@venizia/ignis';
|
|
145
144
|
|
|
146
|
-
// Using constant
|
|
147
145
|
lock: { strength: LockStrengths.UPDATE }
|
|
148
|
-
|
|
149
|
-
// Using string literal
|
|
146
|
+
// same as
|
|
150
147
|
lock: { strength: 'update' }
|
|
151
148
|
```
|
|
152
149
|
|
|
153
|
-
| Strength | SQL | Use
|
|
154
|
-
|
|
150
|
+
| Strength | SQL | Use case |
|
|
151
|
+
|---|---|---|
|
|
155
152
|
| `update` | `FOR UPDATE` | Exclusive lock for writes |
|
|
156
153
|
| `no key update` | `FOR NO KEY UPDATE` | Exclusive lock, allows concurrent `FOR KEY SHARE` |
|
|
157
154
|
| `share` | `FOR SHARE` | Shared read lock, prevents writes |
|
|
158
155
|
| `key share` | `FOR KEY SHARE` | Weakest lock, only prevents key changes |
|
|
159
156
|
|
|
160
|
-
### Wait
|
|
157
|
+
### Wait behavior
|
|
161
158
|
|
|
162
|
-
Control what happens when
|
|
159
|
+
Control what happens when a targeted row is already locked via `lock.config`:
|
|
163
160
|
|
|
164
161
|
```typescript
|
|
165
162
|
// Skip locked rows (queue-style worker pattern)
|
|
166
163
|
const items = await repository.find({
|
|
167
164
|
filter: { where: { status: 'pending' }, limit: 10 },
|
|
168
|
-
options: {
|
|
169
|
-
transaction: tx,
|
|
170
|
-
lock: { strength: 'update', config: { skipLocked: true } },
|
|
171
|
-
},
|
|
165
|
+
options: { transaction: tx, lock: { strength: 'update', config: { skipLocked: true } } },
|
|
172
166
|
});
|
|
173
167
|
|
|
174
168
|
// Fail immediately instead of waiting
|
|
175
169
|
const item = await repository.findOne({
|
|
176
170
|
filter: { where: { id: '123' } },
|
|
177
|
-
options: {
|
|
178
|
-
transaction: tx,
|
|
179
|
-
lock: { strength: 'update', config: { noWait: true } },
|
|
180
|
-
},
|
|
171
|
+
options: { transaction: tx, lock: { strength: 'update', config: { noWait: true } } },
|
|
181
172
|
});
|
|
182
173
|
```
|
|
183
174
|
|
|
184
175
|
| Config | SQL | Behavior |
|
|
185
|
-
|
|
186
|
-
| *(none)* | `FOR UPDATE` | Wait until lock is released |
|
|
187
|
-
| `{ 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 |
|
|
188
179
|
| `{ skipLocked: true }` | `FOR UPDATE SKIP LOCKED` | Silently skip locked rows |
|
|
189
180
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
> [!WARNING]
|
|
193
|
-
> Row-level locking
|
|
181
|
+
`noWait` and `skipLocked` are mutually exclusive at the type level (`TLockConfig`).
|
|
182
|
+
|
|
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:
|
|
194
203
|
|
|
195
204
|
```typescript
|
|
196
|
-
//
|
|
197
|
-
await
|
|
198
|
-
|
|
199
|
-
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 } },
|
|
200
209
|
});
|
|
201
210
|
|
|
202
|
-
//
|
|
203
|
-
await
|
|
204
|
-
|
|
205
|
-
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' } },
|
|
206
215
|
});
|
|
207
216
|
```
|
|
208
217
|
|
|
209
|
-
|
|
218
|
+
Works the same on PostgreSQL and search (Typesense, Meilisearch) repositories.
|
|
210
219
|
|
|
220
|
+
### The rules
|
|
211
221
|
|
|
212
|
-
|
|
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. |
|
|
213
238
|
|
|
214
|
-
|
|
239
|
+
`until` is typed per verb - the predicate sees exactly what the verb returns:
|
|
215
240
|
|
|
216
|
-
|
|
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 |
|
|
217
246
|
|
|
218
|
-
|
|
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.
|
|
249
|
+
|
|
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:
|
|
219
256
|
|
|
220
257
|
```typescript
|
|
221
258
|
@model({
|
|
@@ -229,496 +266,335 @@ export class User extends BaseEntity<typeof User.schema> {
|
|
|
229
266
|
}
|
|
230
267
|
```
|
|
231
268
|
|
|
232
|
-
### Automatic Exclusion
|
|
233
|
-
|
|
234
|
-
Hidden properties are excluded at the **SQL level** for maximum security:
|
|
235
|
-
|
|
236
269
|
```typescript
|
|
237
|
-
// Read operations exclude hidden properties
|
|
238
270
|
const user = await userRepository.findById({ id: '123' });
|
|
239
|
-
//
|
|
240
|
-
// Note: password, secret, apiKey are NOT included
|
|
241
|
-
|
|
242
|
-
// Write operations exclude hidden from RETURNING clause
|
|
243
|
-
const created = await userRepository.create({
|
|
244
|
-
data: { email: 'new@example.com', password: 'hashed_secret' }
|
|
245
|
-
});
|
|
246
|
-
// Result: { count: 1, data: { id: '456', email: 'new@example.com' } }
|
|
247
|
-
// Note: password stored in DB but not returned
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
### Filtering by Hidden Properties
|
|
251
|
-
|
|
252
|
-
You **can** filter by hidden properties - you just can't see them in results:
|
|
253
|
-
|
|
254
|
-
```typescript
|
|
255
|
-
// This works! Finds user but password not in result
|
|
256
|
-
const user = await userRepository.findOne({
|
|
257
|
-
filter: { where: { password: 'hashed_value' } }
|
|
258
|
-
});
|
|
271
|
+
// { id: '123', email: 'john@example.com', name: 'John' } - no password, secret, apiKey
|
|
259
272
|
```
|
|
260
273
|
|
|
261
|
-
###
|
|
262
|
-
|
|
263
|
-
Hidden properties are also excluded from included relations:
|
|
264
|
-
|
|
265
|
-
```typescript
|
|
266
|
-
const post = await postRepository.findOne({
|
|
267
|
-
filter: {
|
|
268
|
-
include: [{ relation: 'author' }]
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
// post.author will NOT include password, secret, etc.
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
### Accessing Hidden Data
|
|
275
|
-
|
|
276
|
-
When you need hidden fields (e.g., for authentication), bypass the repository:
|
|
277
|
-
|
|
278
|
-
```typescript
|
|
279
|
-
// Direct connector access - includes all fields
|
|
280
|
-
const connector = userRepository.getConnector();
|
|
281
|
-
const [fullUser] = await connector
|
|
282
|
-
.select()
|
|
283
|
-
.from(User.schema)
|
|
284
|
-
.where(eq(User.schema.email, 'john@example.com'));
|
|
285
|
-
// fullUser includes password, secret, apiKey
|
|
286
|
-
```
|
|
274
|
+
### The rules
|
|
287
275
|
|
|
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
|
-
### Core API for
|
|
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
|
-
| Filter
|
|
317
|
-
|
|
303
|
+
| Filter options | API used | Performance |
|
|
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: {
|
|
330
|
-
where: { status: 'active' },
|
|
331
|
-
limit: 100
|
|
332
|
-
}
|
|
333
|
-
});
|
|
314
|
+
// Bounded result set
|
|
315
|
+
await repository.find({ filter: { where: { status: 'active' }, limit: 100 } });
|
|
334
316
|
|
|
335
|
-
// Dangerous -
|
|
336
|
-
await repository.find({
|
|
337
|
-
filter: { where: { status: 'active' } }
|
|
338
|
-
});
|
|
317
|
+
// Dangerous - no limit in the filter
|
|
318
|
+
await repository.find({ filter: { where: { status: 'active' } } });
|
|
339
319
|
```
|
|
340
320
|
|
|
341
321
|
> [!NOTE]
|
|
342
|
-
> `find()` always applies a default limit of `10
|
|
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.
|
|
343
323
|
|
|
344
|
-
### Pagination with
|
|
324
|
+
### Pagination with data range
|
|
345
325
|
|
|
346
|
-
|
|
326
|
+
Pass `shouldQueryRange` to get data and total count from a single call:
|
|
347
327
|
|
|
348
328
|
```typescript
|
|
349
329
|
const result = await userRepository.find({
|
|
350
|
-
filter: {
|
|
351
|
-
|
|
352
|
-
limit: 20,
|
|
353
|
-
skip: 40,
|
|
354
|
-
order: ['createdAt DESC']
|
|
355
|
-
},
|
|
356
|
-
options: { shouldQueryRange: true }
|
|
330
|
+
filter: { where: { status: 'active' }, limit: 20, skip: 40, order: ['createdAt DESC'] },
|
|
331
|
+
options: { shouldQueryRange: true },
|
|
357
332
|
});
|
|
358
|
-
|
|
359
|
-
//
|
|
360
|
-
// range follows HTTP Content-Range standard (inclusive end index)
|
|
361
|
-
// 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)
|
|
362
335
|
```
|
|
363
336
|
|
|
364
|
-
|
|
337
|
+
- **Parallel by default.** `find` and `count` execute concurrently via `Promise.all`.
|
|
338
|
+
- **Sequential inside a transaction.** A transaction connector wraps a single client, so parallel queries on it are not safe - the two queries run one after another instead.
|
|
365
339
|
|
|
366
|
-
### WeakMap
|
|
367
|
-
|
|
368
|
-
The filter builder caches table column metadata, avoiding repeated reflection:
|
|
369
|
-
|
|
370
|
-
```typescript
|
|
371
|
-
// Internal optimization - automatic
|
|
372
|
-
// First query: getTableColumns(schema) -> cached in WeakMap
|
|
373
|
-
// Subsequent queries: retrieved from WeakMap cache
|
|
374
|
-
```
|
|
340
|
+
### WeakMap cache
|
|
375
341
|
|
|
342
|
+
- **Column metadata is cached per schema.** The filter builder caches table column metadata (`getCachedColumns`) to avoid repeated reflection.
|
|
343
|
+
- **Populated on first access.** The first access calls Drizzle's `getTableColumns()` and caches the result; later queries read the `WeakMap` instead.
|
|
344
|
+
- **No configuration needed.** The cache is automatic.
|
|
376
345
|
|
|
377
346
|
## TypeScript Return Types
|
|
378
347
|
|
|
379
|
-
### shouldReturn
|
|
348
|
+
### shouldReturn inference
|
|
380
349
|
|
|
381
|
-
|
|
350
|
+
`shouldReturn` decides the result shape at the type level, no manual casting needed:
|
|
382
351
|
|
|
383
352
|
```typescript
|
|
384
353
|
// shouldReturn: false - TypeScript knows data is null
|
|
385
354
|
const result1 = await repository.create({
|
|
386
355
|
data: { name: 'John' },
|
|
387
|
-
options: { shouldReturn: false }
|
|
356
|
+
options: { shouldReturn: false },
|
|
388
357
|
});
|
|
389
|
-
//
|
|
358
|
+
// Promise<{ count: number; data: undefined | null }>
|
|
390
359
|
|
|
391
360
|
// shouldReturn: true (default) - TypeScript knows data is the entity
|
|
392
|
-
const result2 = await repository.create({
|
|
393
|
-
|
|
394
|
-
options: { shouldReturn: true }
|
|
395
|
-
});
|
|
396
|
-
// Type: Promise<{ count: number; data: User }>
|
|
397
|
-
console.log(result2.data.name); // 'John' - fully typed!
|
|
398
|
-
|
|
399
|
-
// Array operations
|
|
400
|
-
const results = await repository.createAll({
|
|
401
|
-
data: [{ name: 'John' }, { name: 'Jane' }],
|
|
402
|
-
options: { shouldReturn: true }
|
|
403
|
-
});
|
|
404
|
-
// Type: Promise<{ count: number; data: User[] }>
|
|
361
|
+
const result2 = await repository.create({ data: { name: 'John' } });
|
|
362
|
+
console.log(result2.data.name); // 'John' - fully typed
|
|
405
363
|
```
|
|
406
364
|
|
|
407
|
-
|
|
365
|
+
The same inference applies to `createAll`, `updateById`, `updateAll`, `updateBy`, `deleteById`, `deleteAll`, `deleteBy`.
|
|
408
366
|
|
|
409
|
-
|
|
367
|
+
### Generic return types
|
|
368
|
+
|
|
369
|
+
Pass a type argument to widen the result for a query with relations:
|
|
410
370
|
|
|
411
371
|
```typescript
|
|
412
|
-
|
|
413
|
-
type UserWithPosts = User & {
|
|
414
|
-
posts: Post[];
|
|
415
|
-
};
|
|
372
|
+
type UserWithPosts = User & { posts: Post[] };
|
|
416
373
|
|
|
417
|
-
// Use generic override
|
|
418
374
|
const user = await userRepository.findOne<UserWithPosts>({
|
|
419
|
-
filter: {
|
|
420
|
-
where: { id: '123' },
|
|
421
|
-
include: [{ relation: 'posts' }]
|
|
422
|
-
}
|
|
375
|
+
filter: { where: { id: '123' }, include: [{ relation: 'posts' }] },
|
|
423
376
|
});
|
|
424
377
|
|
|
425
|
-
// TypeScript knows the structure!
|
|
426
378
|
if (user) {
|
|
427
379
|
console.log(user.posts[0].title); // Fully typed
|
|
428
380
|
}
|
|
429
381
|
```
|
|
430
382
|
|
|
431
|
-
**Supported
|
|
432
|
-
- `find<R>()`, `findOne<R>()`, `findById<R>()`
|
|
433
|
-
- `create<R>()`, `createAll<R>()`
|
|
434
|
-
- `updateById<R>()`, `updateAll<R>()`, `updateBy<R>()`
|
|
435
|
-
- `deleteById<R>()`, `deleteAll<R>()`, `deleteBy<R>()`
|
|
436
|
-
|
|
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>()`.
|
|
437
384
|
|
|
438
385
|
## Debugging
|
|
439
386
|
|
|
440
|
-
### Log
|
|
387
|
+
### Log option
|
|
441
388
|
|
|
442
|
-
|
|
389
|
+
Pass `log` to trace a single operation without turning on logging globally:
|
|
443
390
|
|
|
444
391
|
```typescript
|
|
445
|
-
// Enable debug logging
|
|
446
392
|
await repository.create({
|
|
447
393
|
data: { name: 'John', email: 'john@example.com' },
|
|
448
|
-
options: {
|
|
449
|
-
log: { use: true, level: 'debug' }
|
|
450
|
-
}
|
|
451
|
-
});
|
|
452
|
-
// Output: [_create] Executing with opts: { data: [...], options: {...} }
|
|
453
|
-
|
|
454
|
-
// Available levels: 'debug', 'info', 'warn', 'error'
|
|
455
|
-
await repository.updateById({
|
|
456
|
-
id: '123',
|
|
457
|
-
data: { name: 'Jane' },
|
|
458
|
-
options: { log: { use: true, level: 'info' } }
|
|
394
|
+
options: { log: { use: true, level: 'debug' } },
|
|
459
395
|
});
|
|
396
|
+
// [_create] Executing with opts: { data: [...], options: {...} }
|
|
460
397
|
```
|
|
461
398
|
|
|
462
|
-
|
|
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 |
|
|
463
405
|
|
|
464
|
-
|
|
406
|
+
**Supported on:** `create`, `createAll`, `updateById`, `updateAll`, `updateBy`, `deleteById`, `deleteAll`, `deleteBy` - every write operation that goes through the internal `_create`, `_update`, or `_delete` methods.
|
|
465
407
|
|
|
466
|
-
|
|
408
|
+
### Query interface validation
|
|
467
409
|
|
|
468
|
-
|
|
469
|
-
// If schema key doesn't match, you get a helpful error:
|
|
470
|
-
// Error: [UserRepository] Schema key mismatch
|
|
471
|
-
// | Entity name 'User' not found in connector.query
|
|
472
|
-
// | Available keys: [Configuration, Post]
|
|
473
|
-
// | Ensure the model's TABLE_NAME matches the schema registration key
|
|
474
|
-
```
|
|
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:
|
|
475
411
|
|
|
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
|
|
417
|
+
```
|
|
476
418
|
|
|
477
419
|
## Safety Features
|
|
478
420
|
|
|
479
|
-
### Empty
|
|
421
|
+
### Empty where protection
|
|
480
422
|
|
|
481
|
-
|
|
423
|
+
`updateAll`/`updateBy`/`deleteAll`/`deleteBy` refuse an empty `where` unless `force: true` is passed, to block accidental mass updates and deletes:
|
|
482
424
|
|
|
483
425
|
```typescript
|
|
484
|
-
// Throws
|
|
426
|
+
// Throws - empty where without force
|
|
485
427
|
await repository.deleteAll({ where: {} });
|
|
486
428
|
|
|
487
|
-
// Explicit force
|
|
488
|
-
await repository.deleteAll({
|
|
489
|
-
where: {},
|
|
490
|
-
options: { force: true }
|
|
491
|
-
});
|
|
429
|
+
// Explicit force - logs a warning, proceeds
|
|
430
|
+
await repository.deleteAll({ where: {}, options: { force: true } });
|
|
492
431
|
// Warning: [_delete] Entity: User | Performing delete with empty condition
|
|
493
432
|
```
|
|
494
433
|
|
|
495
434
|
| Scenario | `force: false` (default) | `force: true` |
|
|
496
|
-
|
|
497
|
-
| Empty `where` | Throws
|
|
435
|
+
|---|---|---|
|
|
436
|
+
| Empty `where` | Throws | Logs a warning, proceeds |
|
|
498
437
|
| Valid `where` | Executes normally | Executes normally |
|
|
499
438
|
|
|
500
439
|
> [!NOTE]
|
|
501
|
-
>
|
|
440
|
+
> `updateById` and `deleteById` always have a non-empty where (`{ id }`), so this guard never applies to them.
|
|
502
441
|
|
|
503
|
-
### Transaction
|
|
442
|
+
### Transaction safety
|
|
504
443
|
|
|
505
|
-
|
|
444
|
+
`resolveConnector` validates transaction state before every use. A transaction already committed or rolled back fails fast instead of running against a dead connection:
|
|
506
445
|
|
|
507
|
-
```typescript
|
|
508
|
-
// If a transaction has already been committed or rolled back:
|
|
509
|
-
// Error: [UserRepository][resolveConnector] Transaction is no longer active
|
|
510
446
|
```
|
|
511
|
-
|
|
447
|
+
Error: [UserRepository][resolveConnector] Transaction is no longer active
|
|
448
|
+
```
|
|
512
449
|
|
|
513
450
|
## Direct Connector Access
|
|
514
451
|
|
|
515
|
-
|
|
452
|
+
`repository.connector` is a getter (not a method) that resolves the datasource's Drizzle connector, for queries the repository API does not cover:
|
|
516
453
|
|
|
517
454
|
```typescript
|
|
518
|
-
|
|
519
|
-
const connector = repository.getConnector();
|
|
455
|
+
const connector = repository.connector;
|
|
520
456
|
|
|
521
|
-
// Raw Drizzle query
|
|
522
457
|
const results = await connector
|
|
523
|
-
.select({
|
|
524
|
-
userId: userTable.id,
|
|
525
|
-
postCount: sql<number>`count(${postTable.id})`,
|
|
526
|
-
})
|
|
458
|
+
.select({ userId: userTable.id, postCount: sql<number>`count(${postTable.id})` })
|
|
527
459
|
.from(userTable)
|
|
528
460
|
.leftJoin(postTable, eq(userTable.id, postTable.authorId))
|
|
529
461
|
.groupBy(userTable.id)
|
|
530
462
|
.having(sql`count(${postTable.id}) > 5`);
|
|
531
|
-
|
|
532
|
-
// Use with caution - bypasses repository features like hidden properties
|
|
533
463
|
```
|
|
534
464
|
|
|
465
|
+
> [!WARNING]
|
|
466
|
+
> Queries through `connector` bypass repository features - hidden-property exclusion included. Use it with intent, not as a default escape hatch.
|
|
535
467
|
|
|
536
468
|
## Repository Class Hierarchy
|
|
537
469
|
|
|
538
|
-
|
|
539
|
-
|-------|-------|-------------|
|
|
540
|
-
| `AbstractRepository` | N/A | Engine-neutral abstract base (`src/base`), defines all method signatures, lazy `dataSource`/`entity` resolution. No mixin composition - plain `BaseHelper` subclass. |
|
|
541
|
-
| `PostgresBaseRepository` | N/A | PostgreSQL connector base. Adds `FilterBuilder`, hidden-column exclusion (`getHiddenProperties`/`getVisibleProperties`), default-filter application (`getDefaultFilter`/`applyDefaultFilter`) - the behavior formerly provided by the now-removed `FieldsVisibilityMixin`/`DefaultFilterMixin` (see [Repository Mixins](./mixins.md)). |
|
|
542
|
-
| `ReadableRepository` | `READ_ONLY` | Read-only operations (`find`, `findOne`, `findById`, `count`, `existsWith`). Write operations throw errors. |
|
|
543
|
-
| `PersistableRepository` | `READ_WRITE` | Adds write operations (`create`, `update`, `delete`) with `UpdateBuilder` |
|
|
544
|
-
| `DefaultCRUDRepository` | `READ_WRITE` | Extends `PersistableRepository` with no additional logic - **recommended default** |
|
|
545
|
-
| `SoftDeletableRepository` | `READ_WRITE` | Extends `DefaultCRUDRepository` with soft delete + restore operations |
|
|
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.
|
|
546
471
|
|
|
547
|
-
|
|
472
|
+
| Canonical class | Alias | Scope | Description |
|
|
473
|
+
|---|---|---|---|
|
|
474
|
+
| `AbstractRepository` | - | N/A | Engine-neutral abstract base (`src/base`), defines every method signature, lazy `dataSource`/`entity` resolution. Plain `BaseHelper` subclass, no mixin composition. |
|
|
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)). |
|
|
476
|
+
| `ReadableRelationalRepository` | `ReadableRepository` | `READ_ONLY` | Read-only operations (`find`, `findOne`, `findById`, `count`, `existsWith`). Write operations throw. |
|
|
477
|
+
| `PersistableRelationalRepository` | `PersistableRepository` | `READ_WRITE` | Adds write operations (`create`, `update`, `delete`) with `UpdateBuilder`. |
|
|
478
|
+
| `DefaultRelationalRepository` | `DefaultCRUDRepository` | `READ_WRITE` | Extends `PersistableRelationalRepository` with no additional logic - **recommended default**. |
|
|
479
|
+
| `SoftDeletableRelationalRepository` | `SoftDeletableRepository` | `READ_WRITE` | Extends `DefaultRelationalRepository` with soft delete and restore - see [SoftDeletableRepository](./soft-deletable). |
|
|
480
|
+
|
|
481
|
+
Code samples throughout the docs use the alias names (`DefaultCRUDRepository`, `ReadableRepository`), since that is what `@venizia/ignis/postgres` code most commonly imports today.
|
|
548
482
|
|
|
549
483
|
```typescript
|
|
550
484
|
@repository({ model: AuditLog, dataSource: PostgresDataSource })
|
|
551
485
|
export class AuditLogRepository extends ReadableRepository<typeof AuditLog.schema> {
|
|
552
486
|
// Only has: find, findOne, findById, count, existsWith
|
|
553
|
-
// Write operations throw "NOT ALLOWED" error
|
|
487
|
+
// Write operations throw a "NOT ALLOWED" error
|
|
554
488
|
}
|
|
555
489
|
```
|
|
556
490
|
|
|
557
|
-
### Alias
|
|
558
|
-
|
|
559
|
-
`AbstractRepository` provides two alias methods for convenience:
|
|
491
|
+
### Alias methods
|
|
560
492
|
|
|
561
|
-
|
|
562
|
-
- `deleteBy(opts)` - Alias for `deleteAll(opts)`. Delegates directly.
|
|
563
|
-
|
|
564
|
-
Both accept the same parameters (`where`, `data`/`options`) and support `shouldReturn` and `force` options.
|
|
493
|
+
`AbstractRepository` provides two alias methods for convenience, both delegating directly and supporting the same `shouldReturn`/`force` options:
|
|
565
494
|
|
|
495
|
+
- `updateBy(opts)` - alias for `updateAll(opts)`.
|
|
496
|
+
- `deleteBy(opts)` - alias for `deleteAll(opts)`.
|
|
566
497
|
|
|
567
498
|
## Default Filter Bypass
|
|
568
499
|
|
|
569
|
-
|
|
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:
|
|
570
501
|
|
|
571
502
|
```typescript
|
|
572
503
|
// Normal query - default filter applies
|
|
573
|
-
await repository.find({
|
|
574
|
-
|
|
575
|
-
});
|
|
576
|
-
// 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)
|
|
577
506
|
|
|
578
|
-
// Admin query - bypass default filter
|
|
507
|
+
// Admin query - bypass the default filter
|
|
579
508
|
await repository.find({
|
|
580
509
|
filter: { where: { status: 'active' } },
|
|
581
|
-
options: { shouldSkipDefaultFilter: true }
|
|
510
|
+
options: { shouldSkipDefaultFilter: true },
|
|
582
511
|
});
|
|
583
512
|
// WHERE status = 'active' (includes deleted records)
|
|
584
513
|
```
|
|
585
514
|
|
|
586
|
-
**Supported on
|
|
515
|
+
**Supported on every operation:**
|
|
587
516
|
|
|
588
517
|
```typescript
|
|
589
|
-
// Read operations
|
|
590
518
|
await repository.find({ filter, options: { shouldSkipDefaultFilter: true } });
|
|
591
519
|
await repository.findOne({ filter, options: { shouldSkipDefaultFilter: true } });
|
|
592
520
|
await repository.count({ where, options: { shouldSkipDefaultFilter: true } });
|
|
593
|
-
|
|
594
|
-
// Write operations
|
|
595
521
|
await repository.updateAll({ where, data, options: { shouldSkipDefaultFilter: true } });
|
|
596
522
|
await repository.deleteAll({ where, options: { shouldSkipDefaultFilter: true, force: true } });
|
|
597
523
|
```
|
|
598
524
|
|
|
599
|
-
**Combined with transactions:**
|
|
600
|
-
|
|
601
|
-
```typescript
|
|
602
|
-
const tx = await repository.beginTransaction();
|
|
603
|
-
await repository.updateAll({
|
|
604
|
-
where: { status: 'archived' },
|
|
605
|
-
data: { isDeleted: true },
|
|
606
|
-
options: {
|
|
607
|
-
transaction: tx,
|
|
608
|
-
shouldSkipDefaultFilter: true
|
|
609
|
-
}
|
|
610
|
-
});
|
|
611
|
-
await tx.commit();
|
|
612
|
-
```
|
|
613
|
-
|
|
614
525
|
> [!TIP]
|
|
615
|
-
>
|
|
616
|
-
|
|
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.
|
|
617
537
|
|
|
618
538
|
## Nested JSON Updates
|
|
619
539
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
### Basic Usage
|
|
623
|
-
|
|
624
|
-
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:
|
|
625
541
|
|
|
626
542
|
```typescript
|
|
627
|
-
//
|
|
628
|
-
// Current value: { theme: 'light', notifications: { email: true } }
|
|
629
|
-
|
|
543
|
+
// 'metadata' is a jsonb column: { theme: 'light', notifications: { email: true } }
|
|
630
544
|
await repository.updateById({
|
|
631
545
|
id: '123',
|
|
632
546
|
data: {
|
|
633
|
-
|
|
634
|
-
'metadata.theme': 'dark'
|
|
635
|
-
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// New value: { theme: 'dark', notifications: { email: true } }
|
|
639
|
-
```
|
|
640
|
-
|
|
641
|
-
### Supported Features
|
|
642
|
-
|
|
643
|
-
- **Deep Nesting:** Update properties at any depth (e.g., `settings.display.font.size`).
|
|
644
|
-
- **Array Access:** Update array elements by index (e.g., `tags[0]`).
|
|
645
|
-
- **Auto-Creation:** Creates missing intermediate keys automatically (`jsonb_set` with `create_missing = true`).
|
|
646
|
-
- **Type Safety:** Validates that the target column is a JSON/JSONB type.
|
|
647
|
-
- **Multiple Updates:** Multiple updates to the same column are chained as nested `jsonb_set` calls.
|
|
648
|
-
- **Mixed Updates:** Combine regular column updates with JSON path updates in a single call.
|
|
649
|
-
|
|
650
|
-
### Examples
|
|
651
|
-
|
|
652
|
-
#### Deeply Nested Updates
|
|
653
|
-
|
|
654
|
-
```typescript
|
|
655
|
-
await repository.updateById({
|
|
656
|
-
id: '123',
|
|
657
|
-
data: {
|
|
658
|
-
'metadata.settings.display.fontSize': 16,
|
|
659
|
-
'metadata.settings.display.showSidebar': true
|
|
660
|
-
}
|
|
547
|
+
status: 'active', // regular column
|
|
548
|
+
'metadata.theme': 'dark', // JSON path, any depth
|
|
549
|
+
'metadata.addresses[0].primary': true, // array element by index
|
|
550
|
+
},
|
|
661
551
|
});
|
|
552
|
+
// metadata becomes: { theme: 'dark', notifications: { email: true }, addresses: [{ primary: true }, ...] }
|
|
662
553
|
```
|
|
663
554
|
|
|
664
|
-
|
|
555
|
+
### The rules
|
|
665
556
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
}
|
|
673
|
-
});
|
|
674
|
-
```
|
|
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.
|
|
675
563
|
|
|
676
|
-
|
|
564
|
+
### Security and validation
|
|
677
565
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
await repository.updateById({
|
|
682
|
-
id: '123',
|
|
683
|
-
data: {
|
|
684
|
-
status: 'active', // Regular column
|
|
685
|
-
'metadata.lastLogin': now, // JSON path
|
|
686
|
-
'preferences.lang': 'en' // Another JSON path
|
|
687
|
-
}
|
|
688
|
-
});
|
|
689
|
-
```
|
|
690
|
-
|
|
691
|
-
### Security & Validation
|
|
692
|
-
|
|
693
|
-
The framework validates JSON paths to prevent SQL injection:
|
|
694
|
-
- **Allowed Characters:** Path components must match `/^[a-zA-Z_][a-zA-Z0-9_-]*$|^\d+$/` (identifiers, kebab-case, or array indices).
|
|
695
|
-
- **Column Type Validation:** Only `json` and `jsonb` columns are allowed. Other column types throw an error.
|
|
696
|
-
- **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.
|
|
697
569
|
|
|
698
570
|
> [!NOTE]
|
|
699
|
-
> This feature uses PostgreSQL's `jsonb_set` function
|
|
700
|
-
|
|
571
|
+
> This feature uses PostgreSQL's `jsonb_set` function and only applies to columns defined as `json` or `jsonb`.
|
|
701
572
|
|
|
702
573
|
## ExtraOptions Reference
|
|
703
574
|
|
|
704
|
-
|
|
575
|
+
Every repository operation accepts an `options` parameter (`IExtraOptions`, narrowed by postgres to `IDatabaseExtraOptions`):
|
|
705
576
|
|
|
706
577
|
| Option | Type | Default | Description |
|
|
707
578
|
|--------|------|---------|-------------|
|
|
708
|
-
| `transaction` | `
|
|
579
|
+
| `transaction` | `IDatabaseTransaction` | - | Transaction context for the operation |
|
|
709
580
|
| `log` | `{ use: boolean; level?: TLogLevel }` | - | Enable operation logging |
|
|
710
581
|
| `shouldSkipDefaultFilter` | `boolean` | `false` | Bypass the default filter from model settings |
|
|
711
582
|
| `lock` | `TLockOptions` | - | Row-level locking (requires transaction, Core API only) |
|
|
712
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
|
+
|
|
713
590
|
Write operations additionally support:
|
|
714
591
|
|
|
715
592
|
| Option | Type | Default | Description |
|
|
716
593
|
|--------|------|---------|-------------|
|
|
717
594
|
| `shouldReturn` | `boolean` | `true` | Return the created/updated/deleted data |
|
|
718
|
-
| `force` | `boolean` | `false` | Allow empty `where` condition on bulk operations |
|
|
595
|
+
| `force` | `boolean` | `false` | Allow an empty `where` condition on bulk operations |
|
|
719
596
|
| `shouldQueryRange` | `boolean` | `false` | Return `{ data, range }` with total count (find only) |
|
|
720
597
|
|
|
721
|
-
|
|
722
598
|
## Quick Reference
|
|
723
599
|
|
|
724
600
|
| Feature | Code |
|
|
@@ -730,36 +606,22 @@ Write operations additionally support:
|
|
|
730
606
|
| Bypass default filter | `options: { shouldSkipDefaultFilter: true }` |
|
|
731
607
|
| Lock rows for update | `options: { transaction: tx, lock: { strength: 'update' } }` |
|
|
732
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' } }` |
|
|
733
610
|
| Enable logging | `options: { log: { use: true, level: 'debug' } }` |
|
|
734
611
|
| Force delete all | `options: { force: true }` |
|
|
735
612
|
| Skip returning data | `options: { shouldReturn: false }` |
|
|
736
613
|
| Get data + count | `options: { shouldQueryRange: true }` |
|
|
737
|
-
| Access connector | `repository.
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
- [
|
|
743
|
-
- [
|
|
744
|
-
- [
|
|
745
|
-
- [
|
|
746
|
-
- [
|
|
747
|
-
- [
|
|
748
|
-
- [
|
|
749
|
-
- [
|
|
750
|
-
|
|
751
|
-
## See Also
|
|
752
|
-
|
|
753
|
-
- **Related Concepts:**
|
|
754
|
-
- [Repositories Overview](./index) - Core repository operations
|
|
755
|
-
- [Transactions](/guides/core-concepts/persistent/transactions) - Transaction guide
|
|
756
|
-
- [DataSources](/guides/core-concepts/persistent/datasources) - Database connections
|
|
757
|
-
|
|
758
|
-
- **Related Topics:**
|
|
759
|
-
- [Repository Mixins (Removed)](./mixins) - Where mixin behavior lives now
|
|
760
|
-
- [Relations & Includes](./relations) - Loading related data
|
|
761
|
-
- [Filter System](/references/base/filter-system/) - Query operators
|
|
762
|
-
|
|
763
|
-
- **Best Practices:**
|
|
764
|
-
- [Performance Optimization](/best-practices/performance-optimization) - Query optimization
|
|
765
|
-
- [Data Modeling](/best-practices/data-modeling) - Repository patterns
|
|
614
|
+
| Access connector | `repository.connector` |
|
|
615
|
+
|
|
616
|
+
## See also
|
|
617
|
+
|
|
618
|
+
- [Repositories overview](/references/base/repositories/) - CRUD basics, common tasks
|
|
619
|
+
- [Relations & Includes](./relations) - eager loading, nested `scope` filters, many-to-many
|
|
620
|
+
- [SoftDeletableRepository](./soft-deletable) - soft delete, restore, hard delete
|
|
621
|
+
- [Repository Mixins (Removed)](./mixins) - where `FieldsVisibilityMixin`/`DefaultFilterMixin` behavior lives now
|
|
622
|
+
- [Filter System](/references/base/filter-system/) - every `where` operator, JSON paths, array operators
|
|
623
|
+
- [Default Filter](/references/base/filter-system/default-filter) - automatic filter configuration
|
|
624
|
+
- [DataSources - Full Reference](/references/base/datasources-reference) - transaction internals, isolation levels, driver seam
|
|
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
|