@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.
- package/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +22 -11
- package/content/best-practices/architecture-decisions.md +29 -16
- package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
- package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
- package/content/best-practices/code-style-standards/control-flow.md +33 -1
- package/content/best-practices/code-style-standards/documentation.md +28 -8
- package/content/best-practices/code-style-standards/function-patterns.md +15 -4
- package/content/best-practices/code-style-standards/index.md +7 -2
- package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
- package/content/best-practices/code-style-standards/route-definitions.md +9 -5
- package/content/best-practices/code-style-standards/tooling.md +14 -1
- package/content/best-practices/code-style-standards/type-safety.md +39 -6
- package/content/best-practices/common-pitfalls.md +59 -35
- package/content/best-practices/contribution-workflow.md +6 -2
- package/content/best-practices/data-modeling.md +78 -62
- package/content/best-practices/deployment-strategies.md +56 -19
- package/content/best-practices/error-handling.md +182 -93
- package/content/best-practices/index.md +6 -0
- package/content/best-practices/performance-optimization.md +26 -13
- package/content/best-practices/security-guidelines.md +35 -9
- package/content/best-practices/testing-strategies.md +7 -2
- package/content/best-practices/troubleshooting-tips.md +27 -17
- package/content/extensions/components/api-reference.md +107 -322
- package/content/extensions/components/authentication/api.md +454 -603
- package/content/extensions/components/authentication/errors.md +121 -498
- package/content/extensions/components/authentication/index.md +88 -801
- package/content/extensions/components/authentication/usage.md +207 -956
- package/content/extensions/components/authorization/api.md +736 -656
- package/content/extensions/components/authorization/errors.md +168 -206
- package/content/extensions/components/authorization/index.md +82 -797
- package/content/extensions/components/authorization/usage.md +194 -527
- package/content/extensions/components/health-check.md +71 -243
- package/content/extensions/components/mail/api.md +504 -287
- package/content/extensions/components/mail/errors.md +73 -61
- package/content/extensions/components/mail/index.md +96 -467
- package/content/extensions/components/mail/usage.md +130 -172
- package/content/extensions/components/request-tracker.md +66 -173
- package/content/extensions/components/socket-io/api.md +195 -13
- package/content/extensions/components/socket-io/errors.md +3 -3
- package/content/extensions/components/socket-io/index.md +50 -337
- package/content/extensions/components/socket-io/usage.md +143 -26
- package/content/extensions/components/static-asset/api.md +410 -142
- package/content/extensions/components/static-asset/errors.md +110 -53
- package/content/extensions/components/static-asset/index.md +79 -608
- package/content/extensions/components/static-asset/usage.md +180 -300
- package/content/extensions/components/websocket/api.md +275 -399
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +74 -407
- package/content/extensions/components/websocket/usage.md +110 -341
- package/content/extensions/helpers/cron/index.md +51 -160
- package/content/extensions/helpers/crypto/index.md +62 -483
- package/content/extensions/helpers/crypto/reference.md +456 -0
- package/content/extensions/helpers/env/index.md +60 -178
- package/content/extensions/helpers/error/index.md +221 -207
- package/content/extensions/helpers/inversion/index.md +65 -556
- package/content/extensions/helpers/inversion/reference.md +522 -0
- package/content/extensions/helpers/kafka/admin.md +20 -1
- package/content/extensions/helpers/kafka/compile-binary.md +41 -35
- package/content/extensions/helpers/kafka/consumer.md +54 -20
- package/content/extensions/helpers/kafka/examples.md +21 -16
- package/content/extensions/helpers/kafka/index.md +80 -610
- package/content/extensions/helpers/kafka/producer.md +134 -5
- package/content/extensions/helpers/kafka/schema-registry.md +45 -70
- package/content/extensions/helpers/logger/hf-logger.md +193 -0
- package/content/extensions/helpers/logger/index.md +64 -563
- package/content/extensions/helpers/logger/pino.md +85 -0
- package/content/extensions/helpers/logger/reference.md +746 -0
- package/content/extensions/helpers/network/api.md +241 -195
- package/content/extensions/helpers/network/index.md +72 -530
- package/content/extensions/helpers/queue/index.md +72 -900
- package/content/extensions/helpers/queue/reference.md +467 -0
- package/content/extensions/helpers/redis/index.md +73 -645
- package/content/extensions/helpers/redis/reference.md +727 -0
- package/content/extensions/helpers/secrets/index.md +66 -0
- package/content/extensions/helpers/socket-io/api.md +305 -203
- package/content/extensions/helpers/socket-io/index.md +66 -432
- package/content/extensions/helpers/storage/api.md +564 -462
- package/content/extensions/helpers/storage/index.md +77 -573
- package/content/extensions/helpers/types/index.md +66 -499
- package/content/extensions/helpers/types/reference.md +650 -0
- package/content/extensions/helpers/uid/index.md +58 -227
- package/content/extensions/helpers/websocket/api.md +329 -216
- package/content/extensions/helpers/websocket/index.md +65 -503
- package/content/extensions/helpers/worker-thread/index.md +58 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
- package/content/guides/core-concepts/persistent/transactions.md +1 -1
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/migrations/redis-helpers-migration.md +1 -1
- package/content/guides/migrations/unified-connectors-migration.md +2 -2
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/references/base/application.md +1 -1
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/datasources-reference.md +599 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +17 -39
- package/content/references/base/filter-system/application-usage.md +69 -121
- package/content/references/base/filter-system/array-operators.md +12 -0
- package/content/references/base/filter-system/comparison-operators.md +12 -0
- package/content/references/base/filter-system/default-filter.md +136 -348
- package/content/references/base/filter-system/fields-order-pagination.md +38 -16
- package/content/references/base/filter-system/index.md +106 -257
- package/content/references/base/filter-system/json-filtering.md +12 -2
- package/content/references/base/filter-system/list-operators.md +16 -2
- package/content/references/base/filter-system/logical-operators.md +13 -0
- package/content/references/base/filter-system/null-operators.md +13 -0
- package/content/references/base/filter-system/pattern-matching.md +12 -0
- package/content/references/base/filter-system/quick-reference.md +11 -2
- package/content/references/base/filter-system/range-operators.md +12 -0
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +156 -233
- package/content/references/base/middlewares.md +35 -21
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +80 -1452
- package/content/references/base/repositories/advanced.md +156 -192
- package/content/references/base/repositories/index.md +77 -650
- package/content/references/base/repositories/mixins.md +22 -18
- package/content/references/base/repositories/relations.md +123 -171
- package/content/references/base/repositories/soft-deletable.md +58 -56
- package/content/references/base/secrets.md +263 -0
- package/content/references/base/services.md +2 -2
- package/content/references/configuration/environment-variables.md +48 -4
- package/content/references/configuration/index.md +49 -31
- package/content/references/quick-reference.md +3 -16
- package/content/references/utilities/crypto.md +35 -76
- package/content/references/utilities/date.md +33 -73
- package/content/references/utilities/index.md +1 -1
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +29 -62
- package/content/references/utilities/parse.md +34 -64
- package/content/references/utilities/performance.md +33 -58
- package/content/references/utilities/promise.md +28 -62
- package/content/references/utilities/request.md +57 -218
- package/content/references/utilities/schema.md +43 -137
- package/content/references/utilities/statuses-reference.md +361 -0
- package/content/references/utilities/statuses.md +63 -667
- package/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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
await userRepository.find({
|
|
51
|
-
filter: { where: { status: 'active' } }
|
|
52
|
-
});
|
|
30
|
+
import { userRepository } from '@/repositories';
|
|
53
31
|
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
order: ['createdAt DESC'],
|
|
48
|
+
### The `where` narrowing law
|
|
81
49
|
|
|
82
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
129
|
-
const defaultFilter = {
|
|
130
|
-
|
|
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
|
-
|
|
142
|
-
// { where: { createdAt: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
+
## Bypassing the default filter
|
|
160
82
|
|
|
161
|
-
|
|
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
|
-
//
|
|
165
|
-
|
|
166
|
-
//
|
|
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
|
-
|
|
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
|
|
95
|
+
// WHERE "role" = 'admin' (includes soft-deleted rows)
|
|
212
96
|
```
|
|
213
97
|
|
|
214
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
await repository.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
await
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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 |
|
|
237
|
-
|
|
238
|
-
| Admin dashboard | View
|
|
239
|
-
| Data recovery | Restore soft-deleted
|
|
240
|
-
|
|
|
241
|
-
| Data migration | Update
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
162
|
+
## Relation include default filters
|
|
338
163
|
|
|
339
|
-
|
|
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
|
-
|
|
346
|
-
{ relation: '
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
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 (
|
|
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
|
|
207
|
+
The merge itself is `FilterBuilder.mergeFilter()`:
|
|
443
208
|
|
|
444
209
|
```typescript
|
|
445
210
|
const filterBuilder = new FilterBuilder();
|
|
446
211
|
|
|
447
|
-
|
|
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
|
-
|
|
221
|
+
`shouldSkipDefaultFilter` lives on the same options interface every repository verb accepts:
|
|
458
222
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
##
|
|
237
|
+
## Quick reference
|
|
470
238
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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`
|