@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.
Files changed (174) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +24 -13
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +27 -3
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +8 -4
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +247 -153
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +109 -323
  26. package/content/extensions/components/authentication/api.md +489 -602
  27. package/content/extensions/components/authentication/errors.md +135 -498
  28. package/content/extensions/components/authentication/index.md +89 -801
  29. package/content/extensions/components/authentication/usage.md +231 -955
  30. package/content/extensions/components/authorization/api.md +991 -644
  31. package/content/extensions/components/authorization/errors.md +204 -208
  32. package/content/extensions/components/authorization/getting-started.md +227 -0
  33. package/content/extensions/components/authorization/index.md +88 -795
  34. package/content/extensions/components/authorization/usage.md +219 -528
  35. package/content/extensions/components/health-check.md +77 -243
  36. package/content/extensions/components/index.md +24 -90
  37. package/content/extensions/components/mail/api.md +553 -285
  38. package/content/extensions/components/mail/errors.md +76 -62
  39. package/content/extensions/components/mail/index.md +111 -463
  40. package/content/extensions/components/mail/usage.md +139 -173
  41. package/content/extensions/components/request-tracker.md +70 -173
  42. package/content/extensions/components/socket-io/api.md +462 -785
  43. package/content/extensions/components/socket-io/errors.md +49 -51
  44. package/content/extensions/components/socket-io/index.md +69 -372
  45. package/content/extensions/components/socket-io/usage.md +212 -105
  46. package/content/extensions/components/static-asset/api.md +461 -141
  47. package/content/extensions/components/static-asset/errors.md +121 -53
  48. package/content/extensions/components/static-asset/index.md +84 -606
  49. package/content/extensions/components/static-asset/usage.md +184 -299
  50. package/content/extensions/components/template/index.md +3 -3
  51. package/content/extensions/components/websocket/api.md +311 -404
  52. package/content/extensions/components/websocket/errors.md +47 -56
  53. package/content/extensions/components/websocket/index.md +75 -407
  54. package/content/extensions/components/websocket/usage.md +120 -338
  55. package/content/extensions/helpers/cron/index.md +52 -160
  56. package/content/extensions/helpers/crypto/index.md +67 -480
  57. package/content/extensions/helpers/crypto/reference.md +528 -0
  58. package/content/extensions/helpers/env/index.md +64 -178
  59. package/content/extensions/helpers/error/index.md +286 -196
  60. package/content/extensions/helpers/index.md +61 -47
  61. package/content/extensions/helpers/inversion/index.md +76 -550
  62. package/content/extensions/helpers/inversion/reference.md +530 -0
  63. package/content/extensions/helpers/kafka/admin.md +23 -3
  64. package/content/extensions/helpers/kafka/compile-binary.md +83 -52
  65. package/content/extensions/helpers/kafka/consumer.md +65 -28
  66. package/content/extensions/helpers/kafka/examples.md +46 -253
  67. package/content/extensions/helpers/kafka/index.md +55 -617
  68. package/content/extensions/helpers/kafka/producer.md +156 -22
  69. package/content/extensions/helpers/kafka/schema-registry.md +59 -79
  70. package/content/extensions/helpers/logger/hf-logger.md +220 -0
  71. package/content/extensions/helpers/logger/index.md +78 -552
  72. package/content/extensions/helpers/logger/pino.md +105 -0
  73. package/content/extensions/helpers/logger/reference.md +937 -0
  74. package/content/extensions/helpers/network/api.md +276 -195
  75. package/content/extensions/helpers/network/index.md +87 -524
  76. package/content/extensions/helpers/queue/index.md +80 -897
  77. package/content/extensions/helpers/queue/reference.md +494 -0
  78. package/content/extensions/helpers/redis/index.md +86 -640
  79. package/content/extensions/helpers/redis/reference.md +784 -0
  80. package/content/extensions/helpers/secrets/index.md +136 -0
  81. package/content/extensions/helpers/socket-io/api.md +325 -204
  82. package/content/extensions/helpers/socket-io/index.md +79 -429
  83. package/content/extensions/helpers/storage/api.md +584 -464
  84. package/content/extensions/helpers/storage/index.md +80 -573
  85. package/content/extensions/helpers/types/index.md +75 -495
  86. package/content/extensions/helpers/types/reference.md +689 -0
  87. package/content/extensions/helpers/uid/index.md +176 -189
  88. package/content/extensions/helpers/websocket/api.md +366 -214
  89. package/content/extensions/helpers/websocket/index.md +68 -503
  90. package/content/extensions/helpers/worker-thread/index.md +62 -396
  91. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  92. package/content/extensions/index.md +38 -39
  93. package/content/extensions/src-details/mcp-server.md +96 -548
  94. package/content/guides/core-concepts/persistent/datasources.md +2 -2
  95. package/content/guides/core-concepts/persistent/index.md +5 -1
  96. package/content/guides/core-concepts/persistent/models.md +1 -1
  97. package/content/guides/core-concepts/persistent/pglite.md +218 -0
  98. package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
  99. package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
  100. package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
  101. package/content/guides/core-concepts/persistent/sqlite.md +296 -0
  102. package/content/guides/core-concepts/persistent/transactions.md +12 -11
  103. package/content/guides/core-concepts/secrets-vault.md +177 -0
  104. package/content/guides/core-concepts/services.md +1 -1
  105. package/content/guides/get-started/5-minute-quickstart.md +59 -206
  106. package/content/guides/get-started/philosophy.md +135 -670
  107. package/content/guides/get-started/setup.md +53 -74
  108. package/content/guides/migrations/redis-helpers-migration.md +5 -4
  109. package/content/guides/migrations/scoped-rbac-migration.md +5 -5
  110. package/content/guides/migrations/unified-connectors-migration.md +8 -7
  111. package/content/guides/tutorials/ecommerce-api.md +3 -8
  112. package/content/guides/tutorials/realtime-chat.md +1 -1
  113. package/content/references/base/application.md +64 -22
  114. package/content/references/base/components.md +3 -3
  115. package/content/references/base/connectors.md +79 -136
  116. package/content/references/base/controllers.md +12 -12
  117. package/content/references/base/datasources-reference.md +600 -0
  118. package/content/references/base/datasources.md +84 -444
  119. package/content/references/base/dependency-injection.md +25 -46
  120. package/content/references/base/filter-system/application-usage.md +95 -123
  121. package/content/references/base/filter-system/array-operators.md +33 -43
  122. package/content/references/base/filter-system/comparison-operators.md +55 -63
  123. package/content/references/base/filter-system/default-filter.md +144 -350
  124. package/content/references/base/filter-system/fields-order-pagination.md +116 -148
  125. package/content/references/base/filter-system/index.md +114 -253
  126. package/content/references/base/filter-system/json-filtering.md +52 -181
  127. package/content/references/base/filter-system/list-operators.md +28 -44
  128. package/content/references/base/filter-system/logical-operators.md +69 -114
  129. package/content/references/base/filter-system/null-operators.md +41 -98
  130. package/content/references/base/filter-system/pattern-matching.md +48 -51
  131. package/content/references/base/filter-system/quick-reference.md +93 -196
  132. package/content/references/base/filter-system/range-operators.md +22 -38
  133. package/content/references/base/filter-system/tips.md +70 -133
  134. package/content/references/base/filter-system/use-cases.md +173 -232
  135. package/content/references/base/grpc-controllers.md +53 -17
  136. package/content/references/base/index.md +5 -3
  137. package/content/references/base/middlewares.md +43 -28
  138. package/content/references/base/models-reference.md +886 -0
  139. package/content/references/base/models.md +81 -1452
  140. package/content/references/base/providers.md +8 -8
  141. package/content/references/base/repositories/advanced.md +281 -419
  142. package/content/references/base/repositories/index.md +84 -644
  143. package/content/references/base/repositories/mixins.md +25 -21
  144. package/content/references/base/repositories/relations.md +194 -375
  145. package/content/references/base/repositories/soft-deletable.md +67 -55
  146. package/content/references/base/secrets.md +267 -0
  147. package/content/references/base/services.md +8 -6
  148. package/content/references/configuration/environment-variables.md +73 -21
  149. package/content/references/configuration/index.md +51 -31
  150. package/content/references/index.md +1 -1
  151. package/content/references/quick-reference.md +3 -16
  152. package/content/references/utilities/crypto.md +35 -76
  153. package/content/references/utilities/date.md +33 -73
  154. package/content/references/utilities/duration.md +85 -0
  155. package/content/references/utilities/index.md +6 -2
  156. package/content/references/utilities/jsx-reference.md +298 -0
  157. package/content/references/utilities/jsx.md +82 -525
  158. package/content/references/utilities/module.md +81 -61
  159. package/content/references/utilities/parse.md +34 -64
  160. package/content/references/utilities/performance.md +33 -58
  161. package/content/references/utilities/promise.md +28 -62
  162. package/content/references/utilities/request.md +58 -218
  163. package/content/references/utilities/retry.md +139 -0
  164. package/content/references/utilities/schema.md +43 -137
  165. package/content/references/utilities/statuses-reference.md +361 -0
  166. package/content/references/utilities/statuses.md +63 -667
  167. package/dist/mcp-server/index.js +0 -0
  168. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  169. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
  171. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  172. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
  173. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
  174. package/package.json +24 -23
@@ -1,27 +1,28 @@
1
1
  ---
2
2
  title: Advanced Repository Features
3
- description: Transactions, hidden properties, and performance optimization
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
- Transactions, hidden properties, performance optimization, type inference, and debugging.
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
- ## Prerequisites
11
+ **Files:**
12
12
 
13
- Before reading this document, you should understand:
14
-
15
- - [Basic Repository Operations](./index.md) - CRUD operations and basic filtering
16
- - [Filter System](../filter-system/) - Advanced query building
17
- - Database transactions - ACID properties and isolation levels
18
- - TypeScript advanced types - Utility types and type inference
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
- Orchestrate atomic operations across multiple repositories.
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
- ### Isolation Levels
50
+ ### The rules
53
51
 
54
- Control how transactions interact with concurrent operations:
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: 'SERIALIZABLE'
67
+ isolationLevel: IsolationLevels.SERIALIZABLE,
59
68
  });
60
69
  ```
61
70
 
62
- | Level | Description | Use Case |
63
- |-------|-------------|----------|
64
- | `READ COMMITTED` | Default. See committed data only | Most applications |
65
- | `REPEATABLE READ` | Consistent reads within transaction | Reports, analytics |
66
- | `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 |
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
- ### Transaction with Multiple Repositories
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 within a transaction using PostgreSQL's `SELECT ... FOR UPDATE/SHARE` syntax.
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
- ### Lock Strengths
136
+ **Supported methods:** `find`, `findOne`, `findById` (delegates to `findOne`). Not `count`/`existsWith`.
137
+
138
+ ### Lock strengths
140
139
 
141
- Use the `LockStrengths` constant class or string literals:
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 Case |
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 Behavior
157
+ ### Wait behavior
161
158
 
162
- Control what happens when rows are already locked:
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 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 |
188
179
  | `{ skipLocked: true }` | `FOR UPDATE SKIP LOCKED` | Silently skip locked rows |
189
180
 
190
- ### Constraints
191
-
192
- > [!WARNING]
193
- > Row-level locking requires a **transaction** and is **incompatible with `include`/`fields`** in the filter (these use the Drizzle Query API which does not support `.for()`).
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
- // Error - no transaction
197
- await repository.findOne({
198
- filter: { where: { id: '123' } },
199
- 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 } },
200
209
  });
201
210
 
202
- // Error - include uses Query API
203
- await repository.findOne({
204
- filter: { where: { id: '123' }, include: [{ relation: 'posts' }] },
205
- 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' } },
206
215
  });
207
216
  ```
208
217
 
209
- **Supported methods:** `find`, `findOne`, `findById`
218
+ Works the same on PostgreSQL and search (Typesense, Meilisearch) repositories.
210
219
 
220
+ ### The rules
211
221
 
212
- ## Hidden Properties
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
- Automatically exclude sensitive fields from query results.
239
+ `until` is typed per verb - the predicate sees exactly what the verb returns:
215
240
 
216
- ### Configuration
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
- Define hidden properties in your model:
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
- // Result: { id: '123', email: 'john@example.com', name: 'John' }
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
- ### Relations with Hidden Properties
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 Flat Queries
285
+ ### Core API for flat queries
292
286
 
293
- The repository automatically uses Drizzle's Core API (faster) for simple queries:
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
- | Filter Options | API Used | Performance |
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 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: {
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 - could return millions of rows
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` when no `limit` is set in the filter. Pass an explicit `limit` in the filter to override this default.
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 Data Range
324
+ ### Pagination with data range
345
325
 
346
- 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:
347
327
 
348
328
  ```typescript
349
329
  const result = await userRepository.find({
350
- filter: {
351
- where: { status: 'active' },
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
- // Result type: { data: User[], range: { start: number, end: number, total: number } }
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
- This runs `find` and `count` in parallel via `Promise.all` for optimal performance. Inside a transaction the two queries run sequentially instead - a transaction connector wraps a single client, so parallel queries on it are not safe.
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 Cache
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 Inference
348
+ ### shouldReturn inference
380
349
 
381
- Repository methods infer return types based on `shouldReturn`:
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
- // Type: Promise<{ count: number; data: undefined | null }>
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
- data: { name: 'John' },
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
- ### Generic Return Types
365
+ The same inference applies to `createAll`, `updateById`, `updateAll`, `updateBy`, `deleteById`, `deleteAll`, `deleteBy`.
408
366
 
409
- Override return types for queries with relations:
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
- // Define expected return type
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 Methods:**
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 Option
387
+ ### Log option
441
388
 
442
- Enable logging for specific operations:
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
- **Available on:** `create`, `createAll`, `updateById`, `updateAll`, `updateBy`, `deleteById`, `deleteAll`, `deleteBy` (all write operations that go through `_create`, `_update`, or `_delete` internal 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 |
463
405
 
464
- ### Query Interface Validation
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
- The repository validates schema registration on startup:
408
+ ### Query interface validation
467
409
 
468
- ```typescript
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 Where Protection
421
+ ### Empty where protection
480
422
 
481
- 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:
482
424
 
483
425
  ```typescript
484
- // Throws error - empty where without force
426
+ // Throws - empty where without force
485
427
  await repository.deleteAll({ where: {} });
486
428
 
487
- // Explicit force flag - logs warning, proceeds
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 error | Logs warning, proceeds |
435
+ |---|---|---|
436
+ | Empty `where` | Throws | Logs a warning, proceeds |
498
437
  | Valid `where` | Executes normally | Executes normally |
499
438
 
500
439
  > [!NOTE]
501
- > This protection applies to `updateAll`, `updateBy`, `deleteAll`, and `deleteBy`. The `updateById` and `deleteById` methods always have a non-empty where (`{ id }`) so they are not affected.
440
+ > `updateById` and `deleteById` always have a non-empty where (`{ id }`), so this guard never applies to them.
502
441
 
503
- ### Transaction Safety
442
+ ### Transaction safety
504
443
 
505
- The `resolveConnector` method 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:
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
- For advanced queries not supported by the repository API:
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
- // Get the Drizzle connector
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
- | Class | Scope | Description |
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
- ### Creating a Read-Only Repository
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 Methods
558
-
559
- `AbstractRepository` provides two alias methods for convenience:
491
+ ### Alias methods
560
492
 
561
- - `updateBy(opts)` - Alias for `updateAll(opts)`. Delegates directly.
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
- 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:
570
501
 
571
502
  ```typescript
572
503
  // Normal query - default filter applies
573
- await repository.find({
574
- filter: { where: { status: 'active' } }
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 all operations:**
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
- > See [Default Filter](../filter-system/default-filter.md) for full documentation on configuring model default filters.
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
- Repositories support updating specific fields within `json` or `jsonb` columns without overwriting the entire object. This is achieved using **JSON Path Notation** in the update data via the `UpdateBuilder`.
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
- // Assume 'metadata' is a JSONB column
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
- // Update only the theme, preserving other fields
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
- #### Array Element Updates
555
+ ### The rules
665
556
 
666
- ```typescript
667
- await repository.updateById({
668
- id: '123',
669
- data: {
670
- // Set the first address as primary
671
- 'metadata.addresses[0].primary': true
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
- #### Mixed Updates (Regular + JSON)
564
+ ### Security and validation
677
565
 
678
- You can mix regular column updates with JSON path updates:
679
-
680
- ```typescript
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. It is only available for columns defined as `json` or `jsonb`.
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
- All repository operations accept an `options` parameter with these fields:
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` | `ITransaction` | - | Transaction context for the operation |
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.getConnector()` |
738
-
739
-
740
- ## Next Steps
741
-
742
- - [Overview](./index.md) - Repository basics
743
- - [Filter System](../filter-system/) - Query operators
744
- - [Default Filter](../filter-system/default-filter.md) - Automatic filter configuration
745
- - [Repository Mixins (Removed)](./mixins.md) - Where mixin behavior lives now
746
- - [Relations & Includes](./relations.md) - Eager loading
747
- - [Soft-Deletable Repository](./soft-deletable.md) - Soft delete operations
748
- - [JSON Path Filtering](../filter-system/json-filtering) - JSONB queries
749
- - [Array Operators](../filter-system/array-operators) - PostgreSQL arrays
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