@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
|
@@ -6,10 +6,31 @@ difficulty: intermediate
|
|
|
6
6
|
|
|
7
7
|
# Use Case Gallery
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Runnable `filter` objects paired with the SQL `FilterBuilder` produces for them - copy the shape that's closest to what you need. For the operators themselves, start at the [Filter System Overview](./).
|
|
10
10
|
|
|
11
|
+
## Soft delete
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
The smallest real use case - `is`/`isn` against a nullable timestamp:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// Active (non-deleted) records
|
|
17
|
+
const activeRecords = await repository.find({
|
|
18
|
+
filter: { where: { deletedAt: { is: null } } },
|
|
19
|
+
});
|
|
20
|
+
// SQL: SELECT * FROM "Record" WHERE "deleted_at" IS NULL
|
|
21
|
+
|
|
22
|
+
// ONLY soft-deleted records
|
|
23
|
+
const deletedRecords = await repository.find({
|
|
24
|
+
filter: { where: { deletedAt: { isn: null } } },
|
|
25
|
+
});
|
|
26
|
+
// SQL: SELECT * FROM "Record" WHERE "deleted_at" IS NOT NULL
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
If every query on a model should exclude deleted rows, encode this once as `settings.defaultFilter` instead of repeating it at every call site - see [Default Filter](./default-filter).
|
|
30
|
+
|
|
31
|
+
## E-commerce product search
|
|
32
|
+
|
|
33
|
+
Range, list, and pattern operators combined with field selection and sorting:
|
|
13
34
|
|
|
14
35
|
```typescript
|
|
15
36
|
const products = await productRepository.find({
|
|
@@ -23,7 +44,7 @@ const products = await productRepository.find({
|
|
|
23
44
|
order: ['rating DESC', 'reviewCount DESC'],
|
|
24
45
|
fields: ['id', 'name', 'price', 'rating', 'imageUrl'],
|
|
25
46
|
limit: 24,
|
|
26
|
-
}
|
|
47
|
+
},
|
|
27
48
|
});
|
|
28
49
|
|
|
29
50
|
// SQL:
|
|
@@ -37,8 +58,9 @@ const products = await productRepository.find({
|
|
|
37
58
|
// LIMIT 24
|
|
38
59
|
```
|
|
39
60
|
|
|
61
|
+
## Admin dashboard: recent users
|
|
40
62
|
|
|
41
|
-
|
|
63
|
+
`gte` for a rolling window, `nin` to exclude states, `isn` for presence:
|
|
42
64
|
|
|
43
65
|
```typescript
|
|
44
66
|
const thirtyDaysAgo = new Date();
|
|
@@ -54,7 +76,7 @@ const recentUsers = await userRepository.find({
|
|
|
54
76
|
order: ['createdAt DESC'],
|
|
55
77
|
fields: ['id', 'email', 'name', 'createdAt', 'status'],
|
|
56
78
|
limit: 50,
|
|
57
|
-
}
|
|
79
|
+
},
|
|
58
80
|
});
|
|
59
81
|
|
|
60
82
|
// SQL:
|
|
@@ -67,8 +89,37 @@ const recentUsers = await userRepository.find({
|
|
|
67
89
|
// LIMIT 50
|
|
68
90
|
```
|
|
69
91
|
|
|
92
|
+
## Multi-tenant isolation at the call site
|
|
70
93
|
|
|
71
|
-
|
|
94
|
+
`settings.defaultFilter` (see [Default Filter](./default-filter)) is the model-level way to enforce a tenant scope. A helper that injects `tenantId` at every call site is the call-site alternative - useful when tenant isolation is a caller concern rather than a per-model constant:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
const getTenantProducts = (tenantId: string, filter: TFilter<TProductSchema>) =>
|
|
98
|
+
productRepository.find({
|
|
99
|
+
filter: {
|
|
100
|
+
...filter,
|
|
101
|
+
where: { ...filter.where, tenantId, deletedAt: { is: null } },
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await getTenantProducts('tenant-abc', {
|
|
106
|
+
where: { category: 'electronics' },
|
|
107
|
+
order: ['createdAt DESC'],
|
|
108
|
+
limit: 20,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// SQL:
|
|
112
|
+
// SELECT * FROM "Product"
|
|
113
|
+
// WHERE "category" = 'electronics' AND "tenant_id" = 'tenant-abc' AND "deleted_at" IS NULL
|
|
114
|
+
// ORDER BY "created_at" DESC
|
|
115
|
+
// LIMIT 20
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Unlike `defaultFilter`'s narrowing merge, this is a plain object spread - `tenantId`/`deletedAt` simply overwrite same-named keys from `filter.where` because they're spread last.
|
|
119
|
+
|
|
120
|
+
## Task management: priority tags
|
|
121
|
+
|
|
122
|
+
`nin` plus an array `overlaps` operator, with a relation include:
|
|
72
123
|
|
|
73
124
|
```typescript
|
|
74
125
|
const priorityTasks = await taskRepository.find({
|
|
@@ -80,7 +131,7 @@ const priorityTasks = await taskRepository.find({
|
|
|
80
131
|
},
|
|
81
132
|
order: ['dueDate ASC', 'createdAt ASC'],
|
|
82
133
|
include: [{ relation: 'project' }],
|
|
83
|
-
}
|
|
134
|
+
},
|
|
84
135
|
});
|
|
85
136
|
|
|
86
137
|
// SQL:
|
|
@@ -95,35 +146,81 @@ const priorityTasks = await taskRepository.find({
|
|
|
95
146
|
// SELECT * FROM "Project" WHERE "id" IN (...)
|
|
96
147
|
```
|
|
97
148
|
|
|
149
|
+
## Date range queries
|
|
98
150
|
|
|
99
|
-
|
|
151
|
+
`between` for a closed window, `gte` for a rolling one:
|
|
100
152
|
|
|
101
153
|
```typescript
|
|
102
|
-
|
|
103
|
-
const
|
|
154
|
+
const startOfWeek = new Date('2024-12-29');
|
|
155
|
+
const endOfWeek = new Date('2025-01-04');
|
|
156
|
+
|
|
157
|
+
const weekEvents = await eventRepository.find({
|
|
104
158
|
filter: {
|
|
105
|
-
where: {
|
|
106
|
-
|
|
159
|
+
where: { eventDate: { between: [startOfWeek, endOfWeek] } },
|
|
160
|
+
order: ['eventDate ASC'],
|
|
161
|
+
},
|
|
107
162
|
});
|
|
163
|
+
// SQL: SELECT * FROM "Event" WHERE "event_date" BETWEEN '2024-12-29' AND '2025-01-04' ORDER BY "event_date" ASC
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const sevenDaysAgo = new Date();
|
|
168
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
108
169
|
|
|
170
|
+
const recentOrders = await orderRepository.find({
|
|
171
|
+
filter: {
|
|
172
|
+
where: {
|
|
173
|
+
createdAt: { gte: sevenDaysAgo },
|
|
174
|
+
status: { in: ['completed', 'shipped'] },
|
|
175
|
+
total: { gte: 100 },
|
|
176
|
+
},
|
|
177
|
+
order: ['total DESC'],
|
|
178
|
+
limit: 100,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
109
181
|
// SQL:
|
|
110
|
-
// SELECT * FROM "
|
|
182
|
+
// SELECT * FROM "Order"
|
|
183
|
+
// WHERE "created_at" >= '2024-12-24T00:00:00.000Z' AND "status" IN ('completed', 'shipped') AND "total" >= 100
|
|
184
|
+
// ORDER BY "total" DESC LIMIT 100
|
|
111
185
|
```
|
|
112
186
|
|
|
187
|
+
## Inventory low-stock alert
|
|
188
|
+
|
|
189
|
+
Nested `or`/`and` groups plus a JSON path presence check:
|
|
190
|
+
|
|
113
191
|
```typescript
|
|
114
|
-
|
|
115
|
-
const deletedRecords = await repository.find({
|
|
192
|
+
const lowStockProducts = await productRepository.find({
|
|
116
193
|
filter: {
|
|
117
|
-
where: {
|
|
118
|
-
|
|
194
|
+
where: {
|
|
195
|
+
status: 'active',
|
|
196
|
+
quantity: { lte: 10 },
|
|
197
|
+
'metadata.reorderPoint': { isn: null },
|
|
198
|
+
or: [
|
|
199
|
+
{ quantity: { lt: 5 } }, // Critical: below 5
|
|
200
|
+
{ and: [{ quantity: { lte: 10 } }, { 'metadata.fastMoving': true }] },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
order: ['quantity ASC'],
|
|
204
|
+
fields: ['id', 'name', 'quantity', 'metadata'],
|
|
205
|
+
},
|
|
119
206
|
});
|
|
120
207
|
|
|
121
208
|
// SQL:
|
|
122
|
-
// SELECT
|
|
209
|
+
// SELECT "id", "name", "quantity", "metadata"
|
|
210
|
+
// FROM "Product"
|
|
211
|
+
// WHERE "status" = 'active'
|
|
212
|
+
// AND "quantity" <= 10
|
|
213
|
+
// AND "metadata" #>> '{reorderPoint}' IS NOT NULL
|
|
214
|
+
// AND (
|
|
215
|
+
// "quantity" < 5
|
|
216
|
+
// OR ("quantity" <= 10 AND "metadata" #>> '{fastMoving}' = 'true')
|
|
217
|
+
// )
|
|
218
|
+
// ORDER BY "quantity" ASC
|
|
123
219
|
```
|
|
124
220
|
|
|
221
|
+
## Complex authorization filter
|
|
125
222
|
|
|
126
|
-
|
|
223
|
+
A `where` builder branching on role, composed with the caller's own scope - the `or` group only appears for non-admins:
|
|
127
224
|
|
|
128
225
|
```typescript
|
|
129
226
|
const getAuthorizedFilter = (user: User): TWhere<TDocumentSchema> => {
|
|
@@ -143,23 +240,11 @@ const getAuthorizedFilter = (user: User): TWhere<TDocumentSchema> => {
|
|
|
143
240
|
};
|
|
144
241
|
|
|
145
242
|
const documents = await documentRepository.find({
|
|
146
|
-
filter: {
|
|
147
|
-
where: getAuthorizedFilter(currentUser),
|
|
148
|
-
order: ['updatedAt DESC'],
|
|
149
|
-
limit: 100,
|
|
150
|
-
},
|
|
243
|
+
filter: { where: getAuthorizedFilter(currentUser), order: ['updatedAt DESC'], limit: 100 },
|
|
151
244
|
});
|
|
152
245
|
|
|
153
|
-
// SQL (
|
|
154
|
-
// SELECT *
|
|
155
|
-
// FROM "Document"
|
|
156
|
-
// WHERE "deleted_at" IS NULL
|
|
157
|
-
// ORDER BY "updated_at" DESC
|
|
158
|
-
// LIMIT 100
|
|
159
|
-
|
|
160
|
-
// SQL (for regular user):
|
|
161
|
-
// SELECT *
|
|
162
|
-
// FROM "Document"
|
|
246
|
+
// SQL (regular user):
|
|
247
|
+
// SELECT * FROM "Document"
|
|
163
248
|
// WHERE "deleted_at" IS NULL
|
|
164
249
|
// AND (
|
|
165
250
|
// "owner_id" = 'user-123'
|
|
@@ -167,23 +252,19 @@ const documents = await documentRepository.find({
|
|
|
167
252
|
// OR "shared_with_teams"::text[] && ARRAY['team-1', 'team-2']::text[]
|
|
168
253
|
// OR "shared_with_users"::text[] @> ARRAY['user-123']::text[]
|
|
169
254
|
// )
|
|
170
|
-
// ORDER BY "updated_at" DESC
|
|
171
|
-
// LIMIT 100
|
|
255
|
+
// ORDER BY "updated_at" DESC LIMIT 100
|
|
172
256
|
```
|
|
173
257
|
|
|
258
|
+
## Full-text search with metadata
|
|
174
259
|
|
|
175
|
-
|
|
260
|
+
Conditional `where` assembly - each filter argument adds a key only if the caller supplied it:
|
|
176
261
|
|
|
177
262
|
```typescript
|
|
178
|
-
const searchProducts = async (
|
|
179
|
-
|
|
180
|
-
maxPrice?: number;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const where: TWhere<TProductSchema> = {
|
|
184
|
-
status: 'active',
|
|
185
|
-
deletedAt: { is: null },
|
|
186
|
-
};
|
|
263
|
+
const searchProducts = async (
|
|
264
|
+
query: string,
|
|
265
|
+
filters: { minRating?: number; maxPrice?: number; categories?: string[] },
|
|
266
|
+
) => {
|
|
267
|
+
const where: TWhere<TProductSchema> = { status: 'active', deletedAt: { is: null } };
|
|
187
268
|
|
|
188
269
|
if (query) {
|
|
189
270
|
where.or = [
|
|
@@ -192,50 +273,31 @@ const searchProducts = async (query: string, filters: {
|
|
|
192
273
|
{ 'metadata.keywords': { ilike: `%${query}%` } },
|
|
193
274
|
];
|
|
194
275
|
}
|
|
195
|
-
|
|
196
|
-
if (filters.
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (filters.maxPrice) {
|
|
201
|
-
where.price = { lte: filters.maxPrice };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (filters.categories?.length) {
|
|
205
|
-
// Use array operator on a PostgreSQL array column
|
|
206
|
-
where.categories = { contains: filters.categories };
|
|
207
|
-
}
|
|
276
|
+
if (filters.minRating) where.rating = { gte: filters.minRating };
|
|
277
|
+
if (filters.maxPrice) where.price = { lte: filters.maxPrice };
|
|
278
|
+
if (filters.categories?.length) where.categories = { contains: filters.categories };
|
|
208
279
|
|
|
209
280
|
return productRepository.find({
|
|
210
|
-
filter: {
|
|
211
|
-
where,
|
|
212
|
-
order: ['rating DESC', 'createdAt DESC'],
|
|
213
|
-
limit: 50,
|
|
214
|
-
},
|
|
281
|
+
filter: { where, order: ['rating DESC', 'createdAt DESC'], limit: 50 },
|
|
215
282
|
});
|
|
216
283
|
};
|
|
217
284
|
|
|
218
|
-
//
|
|
285
|
+
// searchProducts('wireless', { minRating: 4, maxPrice: 200, categories: ['electronics'] })
|
|
219
286
|
//
|
|
220
287
|
// SQL:
|
|
221
|
-
// SELECT *
|
|
222
|
-
// FROM "Product"
|
|
288
|
+
// SELECT * FROM "Product"
|
|
223
289
|
// WHERE "status" = 'active'
|
|
224
290
|
// AND "deleted_at" IS NULL
|
|
225
|
-
// AND (
|
|
226
|
-
// "name" ILIKE '%wireless%'
|
|
227
|
-
// OR "description" ILIKE '%wireless%'
|
|
228
|
-
// OR "metadata" #>> '{keywords}' ILIKE '%wireless%'
|
|
229
|
-
// )
|
|
291
|
+
// AND ("name" ILIKE '%wireless%' OR "description" ILIKE '%wireless%' OR "metadata" #>> '{keywords}' ILIKE '%wireless%')
|
|
230
292
|
// AND "rating" >= 4
|
|
231
293
|
// AND "price" <= 200
|
|
232
294
|
// AND "categories"::text[] @> ARRAY['electronics']::text[]
|
|
233
|
-
// ORDER BY "rating" DESC, "created_at" DESC
|
|
234
|
-
// LIMIT 50
|
|
295
|
+
// ORDER BY "rating" DESC, "created_at" DESC LIMIT 50
|
|
235
296
|
```
|
|
236
297
|
|
|
298
|
+
## Everything at once
|
|
237
299
|
|
|
238
|
-
|
|
300
|
+
Every operator family, a JSON path, a three-way `or`, and a scoped relation include in one filter - the ceiling of what a single `TFilter` can express:
|
|
239
301
|
|
|
240
302
|
```typescript
|
|
241
303
|
const massiveFilter: TFilter<TProductSchema> = {
|
|
@@ -254,12 +316,9 @@ const massiveFilter: TFilter<TProductSchema> = {
|
|
|
254
316
|
{ isFeatured: true },
|
|
255
317
|
{ 'metadata.promotion.active': true },
|
|
256
318
|
{ 'metadata.promotion.discount': { gte: 20 } },
|
|
257
|
-
]
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
createdAt: { gte: new Date('2024-12-01') },
|
|
261
|
-
'metadata.isNewArrival': true,
|
|
319
|
+
],
|
|
262
320
|
},
|
|
321
|
+
{ createdAt: { gte: new Date('2024-12-01') }, 'metadata.isNewArrival': true },
|
|
263
322
|
],
|
|
264
323
|
category: { nin: ['discontinued', 'recalled'] },
|
|
265
324
|
suppliers: { overlaps: ['supplier-a', 'supplier-b'] },
|
|
@@ -270,14 +329,7 @@ const massiveFilter: TFilter<TProductSchema> = {
|
|
|
270
329
|
skip: 0,
|
|
271
330
|
include: [
|
|
272
331
|
{ relation: 'category' },
|
|
273
|
-
{
|
|
274
|
-
relation: 'reviews',
|
|
275
|
-
scope: {
|
|
276
|
-
where: { rating: { gte: 4 } },
|
|
277
|
-
order: ['createdAt DESC'],
|
|
278
|
-
limit: 5,
|
|
279
|
-
},
|
|
280
|
-
},
|
|
332
|
+
{ relation: 'reviews', scope: { where: { rating: { gte: 4 } }, order: ['createdAt DESC'], limit: 5 } },
|
|
281
333
|
],
|
|
282
334
|
};
|
|
283
335
|
|
|
@@ -291,163 +343,34 @@ const products = await productRepository.find({ filter: massiveFilter });
|
|
|
291
343
|
// AND "price" >= 50 AND "price" <= 500
|
|
292
344
|
// AND "quantity" > 0
|
|
293
345
|
// AND "tags"::text[] @> ARRAY['electronics', 'portable']::text[]
|
|
294
|
-
// AND CASE
|
|
295
|
-
//
|
|
296
|
-
// THEN ("metadata" #>> '{priority}')::numeric ELSE NULL
|
|
297
|
-
// END >= 3
|
|
346
|
+
// AND CASE WHEN ("metadata" #>> '{priority}') ~ '^-?[0-9]+(\.[0-9]+)?$'
|
|
347
|
+
// THEN ("metadata" #>> '{priority}')::numeric ELSE NULL END >= 3
|
|
298
348
|
// AND "metadata" #>> '{features,wireless}' = 'true'
|
|
299
349
|
// AND (
|
|
300
350
|
// "rating" >= 4.5
|
|
301
|
-
// OR (
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
// WHEN ("metadata" #>> '{promotion,discount}') ~ '^-?[0-9]+(\.[0-9]+)?$'
|
|
306
|
-
// THEN ("metadata" #>> '{promotion,discount}')::numeric ELSE NULL
|
|
307
|
-
// END >= 20
|
|
308
|
-
// )
|
|
309
|
-
// OR (
|
|
310
|
-
// "created_at" >= '2024-12-01T00:00:00.000Z'
|
|
311
|
-
// AND "metadata" #>> '{isNewArrival}' = 'true'
|
|
312
|
-
// )
|
|
351
|
+
// OR ("is_featured" = true AND "metadata" #>> '{promotion,active}' = 'true'
|
|
352
|
+
// AND CASE WHEN ("metadata" #>> '{promotion,discount}') ~ '^-?[0-9]+(\.[0-9]+)?$'
|
|
353
|
+
// THEN ("metadata" #>> '{promotion,discount}')::numeric ELSE NULL END >= 20)
|
|
354
|
+
// OR ("created_at" >= '2024-12-01T00:00:00.000Z' AND "metadata" #>> '{isNewArrival}' = 'true')
|
|
313
355
|
// )
|
|
314
356
|
// AND "category" NOT IN ('discontinued', 'recalled')
|
|
315
357
|
// AND "suppliers"::text[] && ARRAY['supplier-a', 'supplier-b']::text[]
|
|
316
358
|
// ORDER BY "metadata" #> '{priority}' DESC, "rating" DESC, "created_at" DESC
|
|
317
359
|
// LIMIT 20 OFFSET 0
|
|
318
360
|
//
|
|
319
|
-
// -- Separate
|
|
361
|
+
// -- Separate queries for relations:
|
|
320
362
|
// SELECT * FROM "Category" WHERE "id" IN (...)
|
|
321
|
-
//
|
|
322
|
-
// -- Separate query for reviews relation:
|
|
323
|
-
// SELECT * FROM "Review"
|
|
324
|
-
// WHERE "product_id" IN (...) AND "rating" >= 4
|
|
325
|
-
// ORDER BY "created_at" DESC
|
|
326
|
-
// LIMIT 5
|
|
363
|
+
// SELECT * FROM "Review" WHERE "product_id" IN (...) AND "rating" >= 4 ORDER BY "created_at" DESC LIMIT 5
|
|
327
364
|
```
|
|
328
365
|
|
|
366
|
+
## See also
|
|
329
367
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const startOfWeek = new Date('2024-12-29');
|
|
335
|
-
const endOfWeek = new Date('2025-01-04');
|
|
368
|
+
- [Filter System Overview](./) - the `filter` shape and every `where` operator family
|
|
369
|
+
- [Default Filter](./default-filter) - model-level scoping instead of the call-site pattern shown above
|
|
370
|
+
- [Application Usage](./application-usage) - how a filter reaches the repository from an HTTP request
|
|
371
|
+
- [Tips & Edge Cases](./tips) - `NULL` handling, empty-array semantics, and other gotchas that show up in filters like these
|
|
336
372
|
|
|
337
|
-
|
|
338
|
-
filter: {
|
|
339
|
-
where: {
|
|
340
|
-
eventDate: { between: [startOfWeek, endOfWeek] }
|
|
341
|
-
},
|
|
342
|
-
order: ['eventDate ASC']
|
|
343
|
-
}
|
|
344
|
-
});
|
|
373
|
+
**Files:**
|
|
345
374
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
// FROM "Event"
|
|
349
|
-
// WHERE "event_date" BETWEEN '2024-12-29' AND '2025-01-04'
|
|
350
|
-
// ORDER BY "event_date" ASC
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
```typescript
|
|
354
|
-
// Orders in the last 7 days
|
|
355
|
-
const sevenDaysAgo = new Date();
|
|
356
|
-
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
357
|
-
|
|
358
|
-
const recentOrders = await orderRepository.find({
|
|
359
|
-
filter: {
|
|
360
|
-
where: {
|
|
361
|
-
createdAt: { gte: sevenDaysAgo },
|
|
362
|
-
status: { in: ['completed', 'shipped'] },
|
|
363
|
-
total: { gte: 100 }
|
|
364
|
-
},
|
|
365
|
-
order: ['total DESC'],
|
|
366
|
-
limit: 100
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// SQL:
|
|
371
|
-
// SELECT *
|
|
372
|
-
// FROM "Order"
|
|
373
|
-
// WHERE "created_at" >= '2024-12-24T00:00:00.000Z'
|
|
374
|
-
// AND "status" IN ('completed', 'shipped')
|
|
375
|
-
// AND "total" >= 100
|
|
376
|
-
// ORDER BY "total" DESC
|
|
377
|
-
// LIMIT 100
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
## Multi-Tenant Data Isolation
|
|
382
|
-
|
|
383
|
-
```typescript
|
|
384
|
-
const getTenantProducts = async (tenantId: string, filter: TFilter<TProductSchema>) => {
|
|
385
|
-
return productRepository.find({
|
|
386
|
-
filter: {
|
|
387
|
-
...filter,
|
|
388
|
-
where: {
|
|
389
|
-
...filter.where,
|
|
390
|
-
tenantId, // Always enforce tenant isolation
|
|
391
|
-
deletedAt: { is: null },
|
|
392
|
-
},
|
|
393
|
-
},
|
|
394
|
-
});
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
// Usage
|
|
398
|
-
await getTenantProducts('tenant-abc', {
|
|
399
|
-
where: { category: 'electronics' },
|
|
400
|
-
order: ['createdAt DESC'],
|
|
401
|
-
limit: 20
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
// SQL:
|
|
405
|
-
// SELECT *
|
|
406
|
-
// FROM "Product"
|
|
407
|
-
// WHERE "category" = 'electronics'
|
|
408
|
-
// AND "tenant_id" = 'tenant-abc'
|
|
409
|
-
// AND "deleted_at" IS NULL
|
|
410
|
-
// ORDER BY "created_at" DESC
|
|
411
|
-
// LIMIT 20
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
## Inventory Low Stock Alert
|
|
416
|
-
|
|
417
|
-
```typescript
|
|
418
|
-
const lowStockProducts = await productRepository.find({
|
|
419
|
-
filter: {
|
|
420
|
-
where: {
|
|
421
|
-
status: 'active',
|
|
422
|
-
quantity: { lte: 10 },
|
|
423
|
-
'metadata.reorderPoint': { isn: null },
|
|
424
|
-
or: [
|
|
425
|
-
{ quantity: { lt: 5 } }, // Critical: below 5
|
|
426
|
-
{
|
|
427
|
-
and: [
|
|
428
|
-
{ quantity: { lte: 10 } },
|
|
429
|
-
{ 'metadata.fastMoving': true }
|
|
430
|
-
]
|
|
431
|
-
}
|
|
432
|
-
]
|
|
433
|
-
},
|
|
434
|
-
order: ['quantity ASC'],
|
|
435
|
-
fields: ['id', 'name', 'quantity', 'metadata']
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
// SQL:
|
|
440
|
-
// SELECT "id", "name", "quantity", "metadata"
|
|
441
|
-
// FROM "Product"
|
|
442
|
-
// WHERE "status" = 'active'
|
|
443
|
-
// AND "quantity" <= 10
|
|
444
|
-
// AND "metadata" #>> '{reorderPoint}' IS NOT NULL
|
|
445
|
-
// AND (
|
|
446
|
-
// "quantity" < 5
|
|
447
|
-
// OR (
|
|
448
|
-
// "quantity" <= 10
|
|
449
|
-
// AND "metadata" #>> '{fastMoving}' = 'true'
|
|
450
|
-
// )
|
|
451
|
-
// )
|
|
452
|
-
// ORDER BY "quantity" ASC
|
|
453
|
-
```
|
|
375
|
+
- [`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`, translates `TFilter` to Drizzle/SQL
|
|
376
|
+
- [`packages/core/src/connectors/postgres/repositories/dialect/query.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/repositories/dialect/query.ts) - `PostgresQueryOperators.FNS`, per-operator SQL builders
|