@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.
Files changed (145) hide show
  1. package/content/best-practices/architectural-patterns.md +3 -3
  2. package/content/best-practices/code-style-standards/naming-conventions.md +1 -1
  3. package/content/best-practices/contribution-workflow.md +2 -2
  4. package/content/best-practices/error-handling.md +94 -89
  5. package/content/best-practices/security-guidelines.md +5 -5
  6. package/content/extensions/components/api-reference.md +22 -21
  7. package/content/extensions/components/authentication/api.md +64 -28
  8. package/content/extensions/components/authentication/errors.md +19 -5
  9. package/content/extensions/components/authentication/index.md +19 -18
  10. package/content/extensions/components/authentication/usage.md +55 -30
  11. package/content/extensions/components/authorization/api.md +351 -84
  12. package/content/extensions/components/authorization/errors.md +51 -17
  13. package/content/extensions/components/authorization/getting-started.md +227 -0
  14. package/content/extensions/components/authorization/index.md +24 -16
  15. package/content/extensions/components/authorization/usage.md +45 -21
  16. package/content/extensions/components/health-check.md +15 -9
  17. package/content/extensions/components/index.md +24 -90
  18. package/content/extensions/components/mail/api.md +105 -54
  19. package/content/extensions/components/mail/errors.md +12 -10
  20. package/content/extensions/components/mail/index.md +32 -13
  21. package/content/extensions/components/mail/usage.md +26 -18
  22. package/content/extensions/components/request-tracker.md +18 -14
  23. package/content/extensions/components/socket-io/api.md +377 -882
  24. package/content/extensions/components/socket-io/errors.md +49 -51
  25. package/content/extensions/components/socket-io/index.md +72 -88
  26. package/content/extensions/components/socket-io/usage.md +107 -117
  27. package/content/extensions/components/static-asset/api.md +83 -31
  28. package/content/extensions/components/static-asset/errors.md +18 -7
  29. package/content/extensions/components/static-asset/index.md +18 -11
  30. package/content/extensions/components/static-asset/usage.md +11 -6
  31. package/content/extensions/components/template/index.md +3 -3
  32. package/content/extensions/components/websocket/api.md +58 -27
  33. package/content/extensions/components/websocket/errors.md +3 -3
  34. package/content/extensions/components/websocket/index.md +8 -7
  35. package/content/extensions/components/websocket/usage.md +21 -8
  36. package/content/extensions/helpers/cron/index.md +8 -7
  37. package/content/extensions/helpers/crypto/index.md +16 -8
  38. package/content/extensions/helpers/crypto/reference.md +96 -24
  39. package/content/extensions/helpers/env/index.md +14 -10
  40. package/content/extensions/helpers/error/index.md +99 -23
  41. package/content/extensions/helpers/index.md +61 -47
  42. package/content/extensions/helpers/inversion/index.md +23 -6
  43. package/content/extensions/helpers/inversion/reference.md +30 -22
  44. package/content/extensions/helpers/kafka/admin.md +3 -2
  45. package/content/extensions/helpers/kafka/compile-binary.md +69 -44
  46. package/content/extensions/helpers/kafka/consumer.md +24 -21
  47. package/content/extensions/helpers/kafka/examples.md +22 -234
  48. package/content/extensions/helpers/kafka/index.md +30 -62
  49. package/content/extensions/helpers/kafka/producer.md +31 -26
  50. package/content/extensions/helpers/kafka/schema-registry.md +19 -14
  51. package/content/extensions/helpers/logger/hf-logger.md +49 -22
  52. package/content/extensions/helpers/logger/index.md +37 -12
  53. package/content/extensions/helpers/logger/pino.md +31 -11
  54. package/content/extensions/helpers/logger/reference.md +243 -52
  55. package/content/extensions/helpers/network/api.md +65 -30
  56. package/content/extensions/helpers/network/index.md +32 -11
  57. package/content/extensions/helpers/queue/index.md +17 -6
  58. package/content/extensions/helpers/queue/reference.md +52 -25
  59. package/content/extensions/helpers/redis/index.md +29 -11
  60. package/content/extensions/helpers/redis/reference.md +85 -28
  61. package/content/extensions/helpers/secrets/index.md +82 -12
  62. package/content/extensions/helpers/socket-io/api.md +40 -21
  63. package/content/extensions/helpers/socket-io/index.md +26 -10
  64. package/content/extensions/helpers/storage/api.md +42 -24
  65. package/content/extensions/helpers/storage/index.md +10 -7
  66. package/content/extensions/helpers/types/index.md +20 -7
  67. package/content/extensions/helpers/types/reference.md +53 -14
  68. package/content/extensions/helpers/uid/index.md +166 -10
  69. package/content/extensions/helpers/websocket/api.md +52 -13
  70. package/content/extensions/helpers/websocket/index.md +7 -4
  71. package/content/extensions/helpers/worker-thread/index.md +9 -5
  72. package/content/extensions/helpers/worker-thread/reference.md +20 -20
  73. package/content/extensions/index.md +38 -39
  74. package/content/extensions/src-details/mcp-server.md +96 -548
  75. package/content/guides/core-concepts/persistent/datasources.md +2 -2
  76. package/content/guides/core-concepts/persistent/index.md +5 -1
  77. package/content/guides/core-concepts/persistent/pglite.md +218 -0
  78. package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
  79. package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
  80. package/content/guides/core-concepts/persistent/search-typesense.md +26 -14
  81. package/content/guides/core-concepts/persistent/sqlite.md +296 -0
  82. package/content/guides/core-concepts/persistent/transactions.md +12 -11
  83. package/content/guides/get-started/5-minute-quickstart.md +59 -206
  84. package/content/guides/get-started/philosophy.md +135 -670
  85. package/content/guides/get-started/setup.md +53 -74
  86. package/content/guides/migrations/redis-helpers-migration.md +4 -3
  87. package/content/guides/migrations/scoped-rbac-migration.md +5 -5
  88. package/content/guides/migrations/unified-connectors-migration.md +7 -6
  89. package/content/guides/tutorials/realtime-chat.md +1 -1
  90. package/content/references/base/application.md +63 -21
  91. package/content/references/base/components.md +3 -3
  92. package/content/references/base/connectors.md +14 -14
  93. package/content/references/base/controllers.md +12 -12
  94. package/content/references/base/datasources-reference.md +32 -31
  95. package/content/references/base/datasources.md +6 -6
  96. package/content/references/base/dependency-injection.md +12 -11
  97. package/content/references/base/filter-system/application-usage.md +54 -30
  98. package/content/references/base/filter-system/array-operators.md +24 -46
  99. package/content/references/base/filter-system/comparison-operators.md +47 -67
  100. package/content/references/base/filter-system/default-filter.md +59 -53
  101. package/content/references/base/filter-system/fields-order-pagination.md +92 -146
  102. package/content/references/base/filter-system/index.md +25 -13
  103. package/content/references/base/filter-system/json-filtering.md +45 -184
  104. package/content/references/base/filter-system/list-operators.md +23 -53
  105. package/content/references/base/filter-system/logical-operators.md +63 -121
  106. package/content/references/base/filter-system/null-operators.md +34 -104
  107. package/content/references/base/filter-system/pattern-matching.md +40 -55
  108. package/content/references/base/filter-system/quick-reference.md +86 -198
  109. package/content/references/base/filter-system/range-operators.md +18 -46
  110. package/content/references/base/filter-system/tips.md +6 -6
  111. package/content/references/base/filter-system/use-cases.md +33 -15
  112. package/content/references/base/grpc-controllers.md +53 -17
  113. package/content/references/base/index.md +5 -3
  114. package/content/references/base/middlewares.md +11 -10
  115. package/content/references/base/models-reference.md +17 -17
  116. package/content/references/base/models.md +4 -3
  117. package/content/references/base/providers.md +8 -8
  118. package/content/references/base/repositories/advanced.md +224 -326
  119. package/content/references/base/repositories/index.md +19 -6
  120. package/content/references/base/repositories/mixins.md +5 -5
  121. package/content/references/base/repositories/relations.md +160 -293
  122. package/content/references/base/repositories/soft-deletable.md +16 -6
  123. package/content/references/base/secrets.md +17 -13
  124. package/content/references/base/services.md +6 -4
  125. package/content/references/configuration/environment-variables.md +31 -23
  126. package/content/references/configuration/index.md +6 -4
  127. package/content/references/index.md +1 -1
  128. package/content/references/utilities/duration.md +85 -0
  129. package/content/references/utilities/index.md +5 -1
  130. package/content/references/utilities/jsx-reference.md +11 -11
  131. package/content/references/utilities/jsx.md +2 -2
  132. package/content/references/utilities/module.md +78 -25
  133. package/content/references/utilities/request.md +2 -1
  134. package/content/references/utilities/retry.md +139 -0
  135. package/content/references/utilities/schema.md +2 -2
  136. package/content/references/utilities/statuses-reference.md +4 -4
  137. package/content/references/utilities/statuses.md +5 -5
  138. package/dist/mcp-server/index.js +0 -0
  139. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  140. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  141. package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
  142. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  143. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
  144. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
  145. package/package.json +17 -16
@@ -6,24 +6,23 @@ difficulty: intermediate
6
6
 
7
7
  # Advanced Repository Features
8
8
 
9
- Exhaustive reference for 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/).
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/core/src/connectors/postgres/repositories/core/base.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/repositories/core/base.ts) - `RelationalBaseRepository` - hidden-column exclusion, `buildQuery`, `resolveConnector`, lock validation
16
- - [`packages/core/src/connectors/postgres/repositories/core/readable.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/repositories/core/readable.ts) - `ReadableRelationalRepository` - Core API vs. Query API selection, `shouldQueryRange`
17
- - [`packages/core/src/connectors/postgres/repositories/core/persistable.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/repositories/core/persistable.ts) - `PersistableRelationalRepository` - create/update/delete, empty-where guard
18
- - [`packages/core/src/connectors/postgres/repositories/core/soft-deletable.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/repositories/core/soft-deletable.ts) - `SoftDeletableRelationalRepository`
19
- - [`packages/core/src/connectors/postgres/repositories/dialect/update.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/repositories/dialect/update.ts) - `UpdateBuilder` - nested JSON path updates
20
- - [`packages/core/src/connectors/postgres/datasources/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/datasources/abstract.ts) - `beginTransaction()`, isolation levels
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
- Orchestrate atomic operations across multiple repositories. `repository.beginTransaction()` delegates to `dataSource.beginTransaction()` and returns an `IDatabaseTransaction` (`isActive`, `commit()`, `rollback()`, `connector`, `isolationLevel`).
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` **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, so the `catch { await tx.rollback(); throw error; }` pattern shown above is always safe. See [DataSources - Full Reference](/references/base/datasources-reference#transaction-support) for the full commit/rollback lifecycle.
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
- Control how transactions interact with concurrent operations using the `IsolationLevels` const class:
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 | Description | Use case |
67
- |-------|-------------|----------|
68
- | `IsolationLevels.READ_COMMITTED` | Default. See committed data only | Most applications |
69
- | `IsolationLevels.REPEATABLE_READ` | Consistent reads within transaction | Reports, analytics |
70
- | `IsolationLevels.SERIALIZABLE` | Full isolation, prevents anomalies | Financial, inventory |
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
- ### Transaction with multiple repositories
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 within a transaction using PostgreSQL's `SELECT ... FOR UPDATE/SHARE` syntax.
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 literals:
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 rows are already locked via `lock.config`:
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 error immediately if locked |
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
- ### Constraints
195
-
196
- > [!WARNING]
197
- > Row-level locking requires a **transaction** and is **incompatible with `include`/`fields`** in the filter (these force the Drizzle Query API, which does not support `.for()`).
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
- // Error - no transaction
201
- await repository.findOne({
202
- filter: { where: { id: '123' } },
203
- options: { lock: { strength: 'update' } },
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
- // Error - include forces the Query API
207
- await repository.findOne({
208
- filter: { where: { id: '123' }, include: [{ relation: 'posts' }] },
209
- options: { transaction: tx, lock: { strength: 'update' } },
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
- **Supported methods:** `find`, `findOne`, `findById` (delegates to `findOne`). Not `count`/`existsWith`.
218
+ Works the same on PostgreSQL and search (Typesense, Meilisearch) repositories.
214
219
 
215
- ## Hidden Properties
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
- Automatically exclude sensitive fields from query results.
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
- ### Configuration
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
- Define hidden properties in your model:
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
- // Result: { id: '123', email: 'john@example.com', name: 'John' }
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
- ### Filtering by hidden properties
274
+ ### The rules
254
275
 
255
- You **can** filter by hidden properties - you just can't see them in results:
256
-
257
- ```typescript
258
- // This works! Finds user but password not in result
259
- const user = await userRepository.findOne({
260
- filter: { where: { password: 'hashed_value' } },
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
- The repository automatically uses Drizzle's Core API (faster) for simple queries; a query with `include` or `fields` forces the Query API instead:
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
- // Uses: db.select().from(table).where(...).orderBy(...).limit(10)
294
+ // db.select().from(table).where(...).orderBy(...).limit(10)
305
295
 
306
- // Uses Query API (has relations)
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
- // Uses: db.query.tableName.findMany({ with: { posts: true }, ... })
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 use limit
309
+ ### Always set a limit
323
310
 
324
- Prevent memory exhaustion on large tables:
311
+ An unbounded `find` can return millions of rows:
325
312
 
326
313
  ```typescript
327
- // Good - bounded result set
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 - could return millions of rows
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 no `limit` is set in the filter - the model's `@model({ settings: { defaultLimit } })` if declared, otherwise the global default of `10`. Pass an explicit `limit` in the filter to override either default. `findOne`/`findById` are unaffected - they force `limit: 1` on the Core API path regardless.
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
- Use `shouldQueryRange` to get both data and total count in a single call:
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
- // Result type: { data: User[], range: { start: number, end: number, total: number } }
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; subsequent queries retrieve it from the `WeakMap`.
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
- Repository methods infer return types based on `shouldReturn`:
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
- // Type: Promise<{ count: number; data: undefined | null }>
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
- data: { name: 'John' },
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
- Override return types for queries with relations:
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 methods:**
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
- Enable logging for specific operations via `TRepositoryLogOptions` (`{ use: boolean; level?: TLogLevel }`):
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
- // Output: [_create] Executing with opts: { data: [...], options: {...} }
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
- **Available on:** `create`, `createAll`, `updateById`, `updateAll`, `updateBy`, `deleteById`, `deleteAll`, `deleteBy` - every write operation that goes through the internal `_create`, `_update`, or `_delete` methods.
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 on first Query API access:
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
- ```typescript
455
- // If schema key doesn't match, you get a helpful error:
456
- // Error: [UserRepository] Schema key mismatch
457
- // | Entity name 'User' not found in connector.query
458
- // | Available keys: [Configuration, Post]
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 - prevents accidental mass updates/deletes:
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 error - empty where without force
426
+ // Throws - empty where without force
470
427
  await repository.deleteAll({ where: {} });
471
428
 
472
- // Explicit force flag - logs warning, proceeds
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 error | Logs warning, proceeds |
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 they are not affected by this guard.
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
- ```typescript
493
- // If a transaction has already been committed or rolled back:
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
- For advanced queries not supported by the repository API, `repository.connector` is a getter (not a method) that resolves the datasource's Drizzle connector:
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; the historical `*Repository` names remain available as compatibility aliases re-exporting the exact same classes.
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 all method signatures, lazy `dataSource`/`entity` resolution. Plain `BaseHelper` subclass, no mixin composition. |
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 errors. |
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 + restore operations - see [SoftDeletableRepository](./soft-deletable). |
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
- ### Creating a read-only repository
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)` - Alias for `updateAll(opts)`. Delegates directly.
548
- - `deleteBy(opts)` - Alias for `deleteAll(opts)`. Delegates directly.
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
- When models have a `defaultFilter` configured, you can bypass it for admin/maintenance operations:
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
- filter: { where: { status: 'active' } },
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 all operations:**
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
- > See [Default Filter](../filter-system/default-filter) for full documentation on configuring model default filters.
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
- Repositories support updating specific fields within `json`/`jsonb` columns without overwriting the entire object, via **JSON path notation** in the update data - handled by `UpdateBuilder`.
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
- // Assume 'metadata' is a JSONB column
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
- // Update only the theme, preserving other fields
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
- ### Supported features
555
+ ### The rules
626
556
 
627
- - **Deep nesting:** update properties at any depth (e.g., `settings.display.font.size`).
628
- - **Array access:** update array elements by index (e.g., `tags[0]`).
629
- - **Auto-creation:** creates missing intermediate keys automatically (`jsonb_set` with `create_missing = true`).
630
- - **Type safety:** validates that the target column is a JSON/JSONB type.
631
- - **Multiple updates:** multiple updates to the same column are chained as nested `jsonb_set` calls.
632
- - **Mixed updates:** combine regular column updates with JSON path updates in a single call.
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
- ### Examples
564
+ ### Security and validation
635
565
 
636
- #### Deeply nested updates
637
-
638
- ```typescript
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. It is only available for columns defined as `json` or `jsonb`.
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
- All repository operations accept an `options` parameter (`IExtraOptions`/its postgres narrowing `IDatabaseExtraOptions`) with these fields:
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