@venizia/ignis-docs 0.2.0 → 0.2.1-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) 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 +22 -11
  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 +26 -2
  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 +6 -2
  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 +182 -93
  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 +107 -322
  26. package/content/extensions/components/authentication/api.md +454 -603
  27. package/content/extensions/components/authentication/errors.md +121 -498
  28. package/content/extensions/components/authentication/index.md +88 -801
  29. package/content/extensions/components/authentication/usage.md +207 -956
  30. package/content/extensions/components/authorization/api.md +736 -656
  31. package/content/extensions/components/authorization/errors.md +168 -206
  32. package/content/extensions/components/authorization/index.md +82 -797
  33. package/content/extensions/components/authorization/usage.md +194 -527
  34. package/content/extensions/components/health-check.md +71 -243
  35. package/content/extensions/components/mail/api.md +504 -287
  36. package/content/extensions/components/mail/errors.md +73 -61
  37. package/content/extensions/components/mail/index.md +96 -467
  38. package/content/extensions/components/mail/usage.md +130 -172
  39. package/content/extensions/components/request-tracker.md +66 -173
  40. package/content/extensions/components/socket-io/api.md +195 -13
  41. package/content/extensions/components/socket-io/errors.md +3 -3
  42. package/content/extensions/components/socket-io/index.md +50 -337
  43. package/content/extensions/components/socket-io/usage.md +143 -26
  44. package/content/extensions/components/static-asset/api.md +410 -142
  45. package/content/extensions/components/static-asset/errors.md +110 -53
  46. package/content/extensions/components/static-asset/index.md +79 -608
  47. package/content/extensions/components/static-asset/usage.md +180 -300
  48. package/content/extensions/components/websocket/api.md +275 -399
  49. package/content/extensions/components/websocket/errors.md +47 -56
  50. package/content/extensions/components/websocket/index.md +74 -407
  51. package/content/extensions/components/websocket/usage.md +110 -341
  52. package/content/extensions/helpers/cron/index.md +51 -160
  53. package/content/extensions/helpers/crypto/index.md +62 -483
  54. package/content/extensions/helpers/crypto/reference.md +456 -0
  55. package/content/extensions/helpers/env/index.md +60 -178
  56. package/content/extensions/helpers/error/index.md +221 -207
  57. package/content/extensions/helpers/inversion/index.md +65 -556
  58. package/content/extensions/helpers/inversion/reference.md +522 -0
  59. package/content/extensions/helpers/kafka/admin.md +20 -1
  60. package/content/extensions/helpers/kafka/compile-binary.md +41 -35
  61. package/content/extensions/helpers/kafka/consumer.md +54 -20
  62. package/content/extensions/helpers/kafka/examples.md +21 -16
  63. package/content/extensions/helpers/kafka/index.md +80 -610
  64. package/content/extensions/helpers/kafka/producer.md +134 -5
  65. package/content/extensions/helpers/kafka/schema-registry.md +45 -70
  66. package/content/extensions/helpers/logger/hf-logger.md +193 -0
  67. package/content/extensions/helpers/logger/index.md +64 -563
  68. package/content/extensions/helpers/logger/pino.md +85 -0
  69. package/content/extensions/helpers/logger/reference.md +746 -0
  70. package/content/extensions/helpers/network/api.md +241 -195
  71. package/content/extensions/helpers/network/index.md +72 -530
  72. package/content/extensions/helpers/queue/index.md +72 -900
  73. package/content/extensions/helpers/queue/reference.md +467 -0
  74. package/content/extensions/helpers/redis/index.md +73 -645
  75. package/content/extensions/helpers/redis/reference.md +727 -0
  76. package/content/extensions/helpers/secrets/index.md +66 -0
  77. package/content/extensions/helpers/socket-io/api.md +305 -203
  78. package/content/extensions/helpers/socket-io/index.md +66 -432
  79. package/content/extensions/helpers/storage/api.md +564 -462
  80. package/content/extensions/helpers/storage/index.md +77 -573
  81. package/content/extensions/helpers/types/index.md +66 -499
  82. package/content/extensions/helpers/types/reference.md +650 -0
  83. package/content/extensions/helpers/uid/index.md +58 -227
  84. package/content/extensions/helpers/websocket/api.md +329 -216
  85. package/content/extensions/helpers/websocket/index.md +65 -503
  86. package/content/extensions/helpers/worker-thread/index.md +58 -396
  87. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  88. package/content/guides/core-concepts/persistent/models.md +1 -1
  89. package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
  90. package/content/guides/core-concepts/persistent/transactions.md +1 -1
  91. package/content/guides/core-concepts/secrets-vault.md +177 -0
  92. package/content/guides/core-concepts/services.md +1 -1
  93. package/content/guides/migrations/redis-helpers-migration.md +1 -1
  94. package/content/guides/migrations/unified-connectors-migration.md +2 -2
  95. package/content/guides/tutorials/ecommerce-api.md +3 -8
  96. package/content/references/base/application.md +1 -1
  97. package/content/references/base/connectors.md +79 -136
  98. package/content/references/base/datasources-reference.md +599 -0
  99. package/content/references/base/datasources.md +84 -444
  100. package/content/references/base/dependency-injection.md +17 -39
  101. package/content/references/base/filter-system/application-usage.md +69 -121
  102. package/content/references/base/filter-system/array-operators.md +12 -0
  103. package/content/references/base/filter-system/comparison-operators.md +12 -0
  104. package/content/references/base/filter-system/default-filter.md +136 -348
  105. package/content/references/base/filter-system/fields-order-pagination.md +38 -16
  106. package/content/references/base/filter-system/index.md +106 -257
  107. package/content/references/base/filter-system/json-filtering.md +12 -2
  108. package/content/references/base/filter-system/list-operators.md +16 -2
  109. package/content/references/base/filter-system/logical-operators.md +13 -0
  110. package/content/references/base/filter-system/null-operators.md +13 -0
  111. package/content/references/base/filter-system/pattern-matching.md +12 -0
  112. package/content/references/base/filter-system/quick-reference.md +11 -2
  113. package/content/references/base/filter-system/range-operators.md +12 -0
  114. package/content/references/base/filter-system/tips.md +70 -133
  115. package/content/references/base/filter-system/use-cases.md +156 -233
  116. package/content/references/base/middlewares.md +35 -21
  117. package/content/references/base/models-reference.md +886 -0
  118. package/content/references/base/models.md +80 -1452
  119. package/content/references/base/repositories/advanced.md +156 -192
  120. package/content/references/base/repositories/index.md +77 -650
  121. package/content/references/base/repositories/mixins.md +22 -18
  122. package/content/references/base/repositories/relations.md +123 -171
  123. package/content/references/base/repositories/soft-deletable.md +58 -56
  124. package/content/references/base/secrets.md +263 -0
  125. package/content/references/base/services.md +2 -2
  126. package/content/references/configuration/environment-variables.md +48 -4
  127. package/content/references/configuration/index.md +49 -31
  128. package/content/references/quick-reference.md +3 -16
  129. package/content/references/utilities/crypto.md +35 -76
  130. package/content/references/utilities/date.md +33 -73
  131. package/content/references/utilities/index.md +1 -1
  132. package/content/references/utilities/jsx-reference.md +298 -0
  133. package/content/references/utilities/jsx.md +82 -525
  134. package/content/references/utilities/module.md +29 -62
  135. package/content/references/utilities/parse.md +34 -64
  136. package/content/references/utilities/performance.md +33 -58
  137. package/content/references/utilities/promise.md +28 -62
  138. package/content/references/utilities/request.md +57 -218
  139. package/content/references/utilities/schema.md +43 -137
  140. package/content/references/utilities/statuses-reference.md +361 -0
  141. package/content/references/utilities/statuses.md +63 -667
  142. package/package.json +8 -8
@@ -7,22 +7,9 @@ lastUpdated: 2026-03-15
7
7
 
8
8
  # Default Filter <Badge type="tip" text="v0.0.5+" />
9
9
 
10
- Automatically apply filter conditions to all repository queries at the model level.
10
+ A model's `settings.defaultFilter` merges into every `find`/`findOne`/`findById`/`count`/`updateAll`/`deleteAll` call for that model - the standard way to implement soft delete, multi-tenancy, active-record scoping, and query-limit protection without repeating a `where` clause at every call site.
11
11
 
12
- > [!NOTE] Added in v0.0.5
13
- > This feature was introduced in IGNIS v0.0.5 to support soft delete, multi-tenancy, and other automatic filtering patterns.
14
-
15
- > [!NOTE]
16
- > Default filters are ideal for:
17
- > - **Soft Delete**: Automatically exclude deleted records
18
- > - **Multi-Tenancy**: Isolate data by tenant
19
- > - **Active Records**: Filter to active/non-expired records
20
- > - **Query Limits**: Prevent unbounded queries
21
-
22
-
23
- ## Quick Start
24
-
25
- Configure a default filter in your model:
12
+ ## Quick start
26
13
 
27
14
  ```typescript
28
15
  import { model, BaseEntity } from '@venizia/ignis';
@@ -31,11 +18,7 @@ import { userTable } from '@/schemas';
31
18
  @model({
32
19
  type: 'entity',
33
20
  settings: {
34
- // Applied to all repository queries
35
- defaultFilter: {
36
- where: { isDeleted: false },
37
- limit: 100,
38
- },
21
+ defaultFilter: { where: { isDeleted: false }, limit: 100 },
39
22
  },
40
23
  })
41
24
  export class User extends BaseEntity<typeof User.schema> {
@@ -43,431 +26,236 @@ export class User extends BaseEntity<typeof User.schema> {
43
26
  }
44
27
  ```
45
28
 
46
- Now all queries automatically include the default filter:
47
-
48
29
  ```typescript
49
- // Your code
50
- await userRepository.find({
51
- filter: { where: { status: 'active' } }
52
- });
30
+ import { userRepository } from '@/repositories';
53
31
 
54
- // Actual query executed
55
- // WHERE isDeleted = false AND status = 'active' LIMIT 100
32
+ await userRepository.find({ filter: { where: { status: 'active' } } });
33
+ // WHERE "isDeleted" = false AND "status" = 'active' LIMIT 100
56
34
  ```
57
35
 
36
+ ## Merge semantics
58
37
 
59
- ## Configuration
60
-
61
- ### Default Filter Properties
62
-
63
- All standard filter properties are supported:
38
+ `applyDefaultFilter()` merges the model's `defaultFilter` with the caller's filter via `FilterBuilder.mergeFilter()`.
64
39
 
65
- ```typescript
66
- @model({
67
- type: 'entity',
68
- settings: {
69
- defaultFilter: {
70
- // WHERE conditions
71
- where: { isDeleted: false, tenantId: 'tenant-123' },
72
-
73
- // Maximum results (prevents unbounded queries)
74
- limit: 100,
40
+ - **`where` narrows per-key.** See the narrowing law below.
41
+ - **Everything else is user-wins-if-provided.** A caller value replaces the default, but a caller value of `undefined` never does - a filter built by spreading an optional object can't silently blow away a tenant scope or a limit.
75
42
 
76
- // Default pagination offset
77
- offset: 0,
43
+ | Property | Merge strategy |
44
+ |---|---|
45
+ | `where` | Per-key narrowing (below) |
46
+ | `limit`, `offset`/`skip`, `order`, `fields`, `include` | Caller replaces default, if the caller's value is defined |
78
47
 
79
- // Default sort order
80
- order: ['createdAt DESC'],
48
+ ### The `where` narrowing law
81
49
 
82
- // Default field selection
83
- fields: ['id', 'name', 'email', 'createdAt'],
84
-
85
- // Default relations to include
86
- include: [{ relation: 'profile' }],
87
- },
88
- },
89
- })
90
- export class User extends BaseEntity<typeof User.schema> {}
91
- ```
50
+ Keys present on only one side pass through untouched. When the **same key** appears on both sides, the outcome depends on shape:
92
51
 
52
+ | Default | Caller | Result |
53
+ |---|---|---|
54
+ | scalar | scalar | **Caller wins** - the one true override (`isDeleted: false` -> `isDeleted: true` opts an admin out of soft-delete) |
55
+ | operator object | operator object | **AND-composed** into an `and: [...]` group - both conditions apply |
56
+ | scalar | operator object | **AND-composed** |
57
+ | operator object | scalar | **AND-composed** |
93
58
 
94
- ## Merge Behavior
95
-
96
- When a user provides a filter, it is merged with the default filter using `FilterBuilder.mergeFilter()`. Non-`where` properties are user-wins; the `where` clause follows a **narrowing** collision law so a default scope can never be widened or dropped.
97
-
98
- | Property | Merge Strategy |
99
- |----------|----------------|
100
- | `where` | **Per-key narrowing** -- non-colliding keys carry over; a colliding key is composed so the default condition always survives (see below) |
101
- | `limit` | User replaces default (if provided) |
102
- | `offset`/`skip` | User replaces default (if provided) |
103
- | `order` | User replaces default (if provided) |
104
- | `fields` | User replaces default (if provided) |
105
- | `include` | User replaces default (if provided) |
106
-
107
- A user value of `undefined` **never** overrides a defined default -- a caller cannot blow away a tenant or soft-delete scope by passing `undefined`.
108
-
109
- ### Where Clause Collision Law
110
-
111
- Within `where`, keys present on only one side pass through untouched. When the **same key** appears in both the default and the user filter, the outcome depends on the shapes:
112
-
113
- | Default | User | Result |
114
- |---------|------|--------|
115
- | scalar | scalar | **User wins** (the soft-delete opt-out -- e.g. `isDeleted: false` becomes `isDeleted: true`) |
116
- | operator | operator | **AND-composed** into an `and: [...]` group (both conditions enforced) |
117
- | scalar | operator | **AND-composed** |
118
- | operator | scalar | **AND-composed** |
119
-
120
- A colliding `and` key concatenates both conjunct lists (both survive). A colliding `or` key AND-composes the two disjunction groups as separate conjuncts -- the user's `or` cannot swallow the default's `or`. Every AND-composed pair is appended to any existing `and` group.
121
-
122
- > [!IMPORTANT]
123
- > Because operator collisions AND-compose rather than replace, a default scope (a `createdAt` floor, a tenant `inq`) can no longer be widened or dropped by a user filter. Only a bare scalar-over-scalar collision is a true override.
124
-
125
- ### Narrowing Example
59
+ - **`and` collisions concatenate.** Both conjunct lists merge into one.
60
+ - **`or` collisions cannot concatenate** - that would union, not narrow - so each side's `or` group becomes its own conjunct instead.
61
+ - **Non-scalar collisions always AND-compose.** A default scope - a `createdAt` floor, a tenant `inq` - can be narrowed by a caller filter but never widened or dropped.
62
+ - **Only scalar-over-scalar is a true override.** Every other collision shape composes rather than replaces.
126
63
 
127
64
  ```typescript
128
- // Default filter: a floor on createdAt plus a tenant scope
129
- const defaultFilter = {
130
- where: { createdAt: { gte: '2024-01-01' }, tenantId: { inq: ['t1', 't2'] } },
131
- };
132
-
133
- // User filter: an upper bound on createdAt
134
- const userFilter = {
135
- where: { createdAt: { lte: '2024-12-31' } },
136
- };
137
- ```
138
-
139
- **Before** (old wholesale-replace law) -- the user key replaced the default's `createdAt`, dropping the floor:
65
+ // Default: a floor on createdAt. Caller: an upper bound on the same key.
66
+ const defaultFilter = { where: { createdAt: { gte: '2024-01-01' } } };
67
+ const userFilter = { where: { createdAt: { lte: '2024-12-31' } } };
140
68
 
141
- ```typescript
142
- // { where: { createdAt: { lte: '2024-12-31' }, tenantId: { inq: ['t1', 't2'] } } }
69
+ // Both operator objects on the same key -> AND-composed, so the floor survives:
70
+ // { where: { and: [{ createdAt: { gte: '2024-01-01' } }, { createdAt: { lte: '2024-12-31' } }] } }
143
71
  ```
144
72
 
145
- **After** (narrowing law) -- operator over operator is AND-composed, so the floor survives:
73
+ Non-colliding keys still combine with an implicit AND, exactly like two `where` objects merged by hand:
146
74
 
147
75
  ```typescript
148
- {
149
- where: {
150
- tenantId: { inq: ['t1', 't2'] },
151
- and: [
152
- { createdAt: { gte: '2024-01-01' } },
153
- { createdAt: { lte: '2024-12-31' } },
154
- ],
155
- },
156
- }
76
+ // Default: { where: { isDeleted: false, tenantId: 'tenant-123' } }
77
+ // Caller: { where: { or: [{ status: 'active' }, { priority: 'high' }] } }
78
+ // Result: WHERE "isDeleted" = false AND "tenantId" = 'tenant-123' AND ("status" = 'active' OR "priority" = 'high')
157
79
  ```
158
80
 
159
- ### Scalar Override (soft-delete opt-out)
81
+ ## Bypassing the default filter
160
82
 
161
- A plain scalar on both sides is the one case where the user still wins outright -- this is what lets an admin flip a soft-delete flag:
83
+ Pass `shouldSkipDefaultFilter: true` in `options` to skip the merge entirely. It is honored by every repository verb - `find`, `findOne`, `findById`, `count`, `updateById`, `updateAll`, `deleteById`, `deleteAll`:
162
84
 
163
85
  ```typescript
164
- // Default: { where: { isDeleted: false } }
165
- // User: { where: { isDeleted: true } }
166
- // Result: { where: { isDeleted: true } }
167
- ```
168
-
169
- ### Complex Where Conditions
86
+ // Normal - default filter applies
87
+ await repository.find({ filter: { where: { role: 'admin' } } });
88
+ // WHERE "isDeleted" = false AND "role" = 'admin'
170
89
 
171
- Keys that do not collide combine with an implicit AND:
172
-
173
- ```typescript
174
- // Default: soft delete and tenant isolation
175
- const defaultFilter = {
176
- where: {
177
- isDeleted: false,
178
- tenantId: 'tenant-123',
179
- }
180
- };
181
-
182
- // User: OR conditions (a distinct key, so it carries through)
183
- const userFilter = {
184
- where: {
185
- or: [{ status: 'active' }, { priority: 'high' }]
186
- }
187
- };
188
-
189
- // Result: AND of default + OR from user
190
- // WHERE isDeleted = false AND tenantId = 'tenant-123'
191
- // AND (status = 'active' OR priority = 'high')
192
- ```
193
-
194
-
195
- ## Bypassing Default Filter
196
-
197
- Use `shouldSkipDefaultFilter: true` to bypass the default filter:
198
-
199
- ```typescript
200
- // Normal query - default filter applies
201
- await repository.find({
202
- filter: { where: { role: 'admin' } }
203
- });
204
- // WHERE isDeleted = false AND role = 'admin'
205
-
206
- // Admin query - bypass default filter
90
+ // Admin/maintenance path - bypassed
207
91
  await repository.find({
208
92
  filter: { where: { role: 'admin' } },
209
- options: { shouldSkipDefaultFilter: true }
93
+ options: { shouldSkipDefaultFilter: true },
210
94
  });
211
- // WHERE role = 'admin' (includes deleted records)
95
+ // WHERE "role" = 'admin' (includes soft-deleted rows)
212
96
  ```
213
97
 
214
- ### Supported Operations
215
-
216
- `shouldSkipDefaultFilter` works with all repository methods:
98
+ It composes with a transaction the same way any other option does:
217
99
 
218
100
  ```typescript
219
- // Read operations
220
- await repository.find({ filter, options: { shouldSkipDefaultFilter: true } });
221
- await repository.findOne({ filter, options: { shouldSkipDefaultFilter: true } });
222
- await repository.findById({ id, options: { shouldSkipDefaultFilter: true } });
223
- await repository.count({ where, options: { shouldSkipDefaultFilter: true } });
224
-
225
- // Update operations
226
- await repository.updateById({ id, data, options: { shouldSkipDefaultFilter: true } });
227
- await repository.updateAll({ where, data, options: { shouldSkipDefaultFilter: true } });
228
-
229
- // Delete operations
230
- await repository.deleteById({ id, options: { shouldSkipDefaultFilter: true } });
231
- await repository.deleteAll({ where, options: { shouldSkipDefaultFilter: true, force: true } });
101
+ const tx = await repository.beginTransaction();
102
+ try {
103
+ await repository.updateAll({
104
+ where: { status: 'archived' },
105
+ data: { isDeleted: true },
106
+ options: { transaction: tx, shouldSkipDefaultFilter: true },
107
+ });
108
+ await tx.commit();
109
+ } catch (e) {
110
+ await tx.rollback();
111
+ throw e;
112
+ }
232
113
  ```
233
114
 
234
- ### Use Cases for Bypassing
115
+ `updateAll`/`deleteAll` additionally require `force: true` when the resulting `where` is empty - see [Advanced Repository Features -> Empty where protection](../repositories/advanced#empty-where-protection).
235
116
 
236
- | Scenario | Example |
237
- |----------|---------|
238
- | Admin dashboard | View all records including deleted |
239
- | Data recovery | Restore soft-deleted records |
240
- | Analytics | Count across all tenants |
241
- | Data migration | Update records regardless of status |
242
- | Audit logs | Access historical data |
117
+ | Scenario | Why bypass |
118
+ |---|---|
119
+ | Admin dashboard | View records a default scope would otherwise hide |
120
+ | Data recovery | Restore soft-deleted rows |
121
+ | Cross-tenant analytics | Count/aggregate across every tenant |
122
+ | Data migration | Update rows regardless of status |
243
123
 
124
+ ## Configuring a default filter
244
125
 
245
- ## Common Patterns
126
+ Any `TFilter` property is valid inside `defaultFilter` - `where`, `limit`, `offset`, `order`, `fields`, `include` (see [Filter System Overview](./)). The two recurring shapes:
246
127
 
247
- ### Soft Delete
128
+ **Soft delete or multi-tenant scoping** - a `where` clause that every query must carry:
248
129
 
249
130
  ```typescript
250
131
  @model({
251
132
  type: 'entity',
252
- settings: {
253
- defaultFilter: {
254
- where: { deletedAt: null }, // or { isDeleted: false }
255
- },
256
- },
133
+ settings: { defaultFilter: { where: { deletedAt: null } } },
257
134
  })
258
135
  export class Post extends BaseEntity<typeof Post.schema> {}
259
136
 
260
- // All queries exclude deleted posts
261
137
  await postRepository.find({ filter: {} });
262
- // WHERE deletedAt IS NULL
138
+ // WHERE "deletedAt" IS NULL
263
139
 
264
- // Restore a deleted post
265
140
  await postRepository.updateById({
266
141
  id: postId,
267
142
  data: { deletedAt: null },
268
- options: { shouldSkipDefaultFilter: true }
269
- });
270
- ```
271
-
272
- ### Multi-Tenant Isolation
273
-
274
- ```typescript
275
- @model({
276
- type: 'entity',
277
- settings: {
278
- defaultFilter: {
279
- where: { tenantId: 'current-tenant' },
280
- },
281
- },
282
- })
283
- export class Document extends BaseEntity<typeof Document.schema> {}
284
-
285
- // Queries scoped to tenant
286
- await documentRepository.find({ filter: { where: { type: 'invoice' } } });
287
- // WHERE tenantId = 'current-tenant' AND type = 'invoice'
288
-
289
- // Cross-tenant admin query
290
- await documentRepository.find({
291
- filter: { where: { type: 'invoice' } },
292
- options: { shouldSkipDefaultFilter: true }
143
+ options: { shouldSkipDefaultFilter: true }, // restore
293
144
  });
294
- // WHERE type = 'invoice'
295
- ```
296
-
297
- ### Active Records
298
-
299
- ```typescript
300
- @model({
301
- type: 'entity',
302
- settings: {
303
- defaultFilter: {
304
- where: {
305
- isActive: true,
306
- expiresAt: { gt: new Date().toISOString() },
307
- },
308
- limit: 50,
309
- },
310
- },
311
- })
312
- export class Subscription extends BaseEntity<typeof Subscription.schema> {}
313
145
  ```
314
146
 
315
- ### Query Limit Protection
316
-
317
- Use the dedicated `settings.defaultLimit` to raise (or lower) the per-model default page size. Prefer it over putting `limit` inside `defaultFilter`:
147
+ **Query-limit protection** - prefer the dedicated `settings.defaultLimit` over a `limit` inside `defaultFilter`. It resolves independently (`query.limit ?? defaultLimit ?? 10`, see [Fields, Order & Pagination -> Default Limit](./fields-order-pagination#default-limit)) and, unlike `defaultFilter`, is **not** dropped by `shouldSkipDefaultFilter`:
318
148
 
319
149
  ```typescript
320
150
  @model({
321
151
  type: 'entity',
322
- settings: {
323
- defaultLimit: 1000, // Per-model default when a query omits `limit`
324
- },
152
+ settings: { defaultLimit: 1000 },
325
153
  })
326
154
  export class LogEntry extends BaseEntity<typeof LogEntry.schema> {}
327
155
 
328
- // User can override limit, but there's always a sensible default
329
156
  await logEntryRepository.find({ filter: {} }); // LIMIT 1000
330
157
  await logEntryRepository.find({ filter: { limit: 50 } }); // LIMIT 50
331
158
  ```
332
159
 
333
- > [!TIP]
334
- > `defaultLimit` is independent of `defaultFilter`: bypassing the default filter via `shouldSkipDefaultFilter` does **not** drop the limit. See [Pagination → Default Limit](/references/base/filter-system/fields-order-pagination#default-limit).
335
-
160
+ `@model` validates `defaultLimit` at decoration time - it must be a positive integer or the class throws on load.
336
161
 
337
- ## Relation Include Default Filters
162
+ ## Relation include default filters
338
163
 
339
- When using `include` to load relations, the default filter of the related model is also applied. You can bypass it per-relation:
164
+ `include` also applies the related model's `defaultFilter`, and it can be bypassed or scoped per relation:
340
165
 
341
166
  ```typescript
342
167
  await repository.find({
343
168
  filter: {
344
169
  include: [
345
- // Default filter of related model applies
346
- { relation: 'posts' },
347
-
348
- // Skip default filter for this specific relation
349
- { relation: 'comments', shouldSkipDefaultFilter: true },
350
-
351
- // Apply a custom scope (merged with relation's default filter)
352
- { relation: 'tags', scope: { limit: 10, order: ['name ASC'] } },
353
- ]
354
- }
170
+ { relation: 'posts' }, // related model's default filter applies
171
+ { relation: 'comments', shouldSkipDefaultFilter: true }, // skipped for this relation only
172
+ { relation: 'tags', scope: { limit: 10, order: ['name ASC'] } }, // scope merges with the default filter
173
+ ],
174
+ },
355
175
  });
356
176
  ```
357
177
 
178
+ ## How it works
358
179
 
359
- ## IExtraOptions Interface
360
-
361
- The `shouldSkipDefaultFilter` option is part of the `IExtraOptions` interface:
362
-
363
- ```typescript
364
- interface IExtraOptions extends IWithTransaction {
365
- /**
366
- * If true, bypass the default filter configured in model settings.
367
- */
368
- shouldSkipDefaultFilter?: boolean;
369
- }
370
-
371
- interface IWithTransaction {
372
- transaction?: ITransaction;
373
- }
374
180
  ```
375
-
376
- This allows combining with transactions:
377
-
378
- ```typescript
379
- const tx = await repository.beginTransaction();
380
-
381
- try {
382
- // Both transaction and shouldSkipDefaultFilter
383
- await repository.updateAll({
384
- where: { status: 'archived' },
385
- data: { isDeleted: true },
386
- options: {
387
- transaction: tx,
388
- shouldSkipDefaultFilter: true,
389
- }
390
- });
391
-
392
- await tx.commit();
393
- } catch (e) {
394
- await tx.rollback();
395
- throw e;
396
- }
397
- ```
398
-
399
-
400
- ## How It Works
401
-
402
- ### Architecture
403
-
404
- ```
405
- +------------------+ +----------------------+ +------------------+
406
- | Model Settings | --> | PostgresBaseRepository | --> | Repository Method |
407
- | defaultFilter | | applyDefaultFilter() | | find/count/etc |
408
- +------------------+ +----------------------+ +------------------+
409
- |
410
- v
411
- +------------------+
412
- | FilterBuilder |
413
- | mergeFilter() |
414
- +------------------+
181
+ +------------------+ +--------------------------+ +------------------+
182
+ | Model Settings | --> | RelationalBaseRepository | --> | Repository Method |
183
+ | defaultFilter | | applyDefaultFilter() | | find/count/etc |
184
+ +------------------+ +--------------------------+ +------------------+
185
+ |
186
+ v
187
+ +------------------+
188
+ | FilterBuilder |
189
+ | mergeFilter() |
190
+ +------------------+
415
191
  ```
416
192
 
417
- ### PostgresBaseRepository
418
-
419
- `PostgresBaseRepository` (`packages/core/src/connectors/postgres/repositories/core/base.ts`) implements the default-filter behavior directly as protected methods - no mixin is composed onto it:
193
+ `RelationalBaseRepository` (compatibility alias `PostgresBaseRepository`, `packages/core/src/connectors/postgres/repositories/core/base.ts`) implements the default-filter behavior directly - no mixin is composed onto it:
420
194
 
421
195
  ```typescript
422
- // Check if default filter is configured
423
196
  hasDefaultFilter(): boolean
424
-
425
- // Get the raw default filter from model metadata
426
197
  getDefaultFilter(): TFilter | undefined
427
-
428
- // Merge default filter with user filter
429
- applyDefaultFilter(opts: {
430
- userFilter?: TFilter;
431
- shouldSkipDefaultFilter?: boolean;
432
- }): TFilter
198
+ getDefaultLimit(): number | undefined
199
+ applyDefaultFilter(opts: { userFilter?: TFilter; shouldSkipDefaultFilter?: boolean }): TFilter
433
200
  ```
434
201
 
435
- `getDefaultFilter()` reads `this.modelSettings?.defaultFilter`, where `modelSettings` is a protected getter on `AbstractRepository` (`src/base/repositories/core/abstract.ts`) resolved from `MetadataRegistry` keyed by the entity's constructor (not by name string) on first access, and cached for subsequent calls.
202
+ `getDefaultFilter()` reads `this.modelSettings?.defaultFilter`, where `modelSettings` is a protected getter on `AbstractRepository` (`src/base/repositories/core/abstract.ts`) resolved from `MetadataRegistry` by the entity's constructor (not by name string) on first access, then memoized.
436
203
 
437
204
  > [!NOTE]
438
- > An older `DefaultFilterMixin` implemented this same behavior via mixin composition. It is no longer composed onto any repository class - see [Repository Mixins (Legacy)](../repositories/mixins.md) for history.
439
-
440
- ### FilterBuilder.mergeFilter()
205
+ > An older `DefaultFilterMixin` implemented this same behavior via mixin composition. It is no longer composed onto any repository class - see [Repository Mixins (Removed)](../repositories/mixins.md) for history.
441
206
 
442
- The merge logic is implemented in `FilterBuilder`:
207
+ The merge itself is `FilterBuilder.mergeFilter()`:
443
208
 
444
209
  ```typescript
445
210
  const filterBuilder = new FilterBuilder();
446
211
 
447
- const merged = filterBuilder.mergeFilter({
212
+ filterBuilder.mergeFilter({
448
213
  defaultFilter: { where: { isDeleted: false }, limit: 100 },
449
- userFilter: { where: { status: 'active' }, limit: 10 }
214
+ userFilter: { where: { status: 'active' }, limit: 10 },
450
215
  });
451
-
452
- // Result:
453
216
  // { where: { isDeleted: false, status: 'active' }, limit: 10 }
454
217
  ```
455
218
 
219
+ ### `IExtraOptions`
456
220
 
457
- ## Quick Reference
221
+ `shouldSkipDefaultFilter` lives on the same options interface every repository verb accepts:
458
222
 
459
- | Want to... | Code |
460
- |------------|------|
461
- | Configure default filter | `@model({ settings: { defaultFilter: { ... } } })` |
462
- | Bypass default filter | `options: { shouldSkipDefaultFilter: true }` |
463
- | Bypass for relation | `include: [{ relation: 'x', shouldSkipDefaultFilter: true }]` |
464
- | Combine with transaction | `options: { transaction: tx, shouldSkipDefaultFilter: true }` |
465
- | Check if model has default | `repository.hasDefaultFilter()` |
466
- | Get raw default filter | `repository.getDefaultFilter()` |
223
+ ```typescript
224
+ interface IExtraOptions extends IWithTransaction {
225
+ shouldSkipDefaultFilter?: boolean;
226
+ log?: TRepositoryLogOptions;
227
+ lock?: TLockOptions;
228
+ }
229
+
230
+ interface IWithTransaction {
231
+ transaction?: ITransaction;
232
+ }
233
+ ```
467
234
 
235
+ `log` and `lock` are documented in [Advanced Repository Features](../repositories/advanced.md) - `log` only takes effect on write verbs (`create`/`updateById`/`updateAll`/`deleteById`/`deleteAll`), not on reads.
468
236
 
469
- ## Next Steps
237
+ ## Quick reference
470
238
 
471
- - [Filter System Overview](./index.md) - Filter structure and operators
472
- - [Repository Mixins (Legacy)](../repositories/mixins.md) - Historical mixin architecture
473
- - [Advanced Features](../repositories/advanced.md) - Transactions, hidden properties
239
+ | Want to... | Code |
240
+ |---|---|
241
+ | Configure a default filter | `@model({ settings: { defaultFilter: { ... } } })` |
242
+ | Bypass the default filter | `options: { shouldSkipDefaultFilter: true }` |
243
+ | Bypass for one relation | `include: [{ relation: 'x', shouldSkipDefaultFilter: true }]` |
244
+ | Combine with a transaction | `options: { transaction: tx, shouldSkipDefaultFilter: true }` |
245
+ | Check if a model has a default | `repository.hasDefaultFilter()` |
246
+ | Read the raw default filter | `repository.getDefaultFilter()` |
247
+ | Read the raw default limit | `repository.getDefaultLimit()` |
248
+
249
+ ## See also
250
+
251
+ - [Filter System Overview](./) - the `filter` shape and every operator family
252
+ - [Fields, Order & Pagination](./fields-order-pagination) - `defaultLimit` resolution in full
253
+ - [Advanced Repository Features](../repositories/advanced.md) - transactions, `log`/`lock` options, empty-where protection
254
+ - [Repository Mixins (Removed)](../repositories/mixins.md) - history of the removed `DefaultFilterMixin`
255
+
256
+ **Files:**
257
+
258
+ - [`packages/core/src/connectors/postgres/repositories/dialect/filter.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/repositories/dialect/filter.ts) - `FilterBuilder.mergeFilter()`/`mergeWhere()`, the narrowing merge
259
+ - [`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`, `applyDefaultFilter`/`getDefaultFilter`/`getDefaultLimit`
260
+ - [`packages/core/src/base/metadata/persistents.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/metadata/persistents.ts) - `@model` decorator, `defaultLimit` validation
261
+ - [`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`, `IWithTransaction`