@venizia/ignis-docs 0.2.0 → 0.2.1-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +24 -13
- 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 +27 -3
- 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 +8 -4
- 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 +247 -153
- 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 +109 -323
- package/content/extensions/components/authentication/api.md +489 -602
- package/content/extensions/components/authentication/errors.md +135 -498
- package/content/extensions/components/authentication/index.md +89 -801
- package/content/extensions/components/authentication/usage.md +231 -955
- package/content/extensions/components/authorization/api.md +991 -644
- package/content/extensions/components/authorization/errors.md +204 -208
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +88 -795
- package/content/extensions/components/authorization/usage.md +219 -528
- package/content/extensions/components/health-check.md +77 -243
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +553 -285
- package/content/extensions/components/mail/errors.md +76 -62
- package/content/extensions/components/mail/index.md +111 -463
- package/content/extensions/components/mail/usage.md +139 -173
- package/content/extensions/components/request-tracker.md +70 -173
- package/content/extensions/components/socket-io/api.md +462 -785
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +69 -372
- package/content/extensions/components/socket-io/usage.md +212 -105
- package/content/extensions/components/static-asset/api.md +461 -141
- package/content/extensions/components/static-asset/errors.md +121 -53
- package/content/extensions/components/static-asset/index.md +84 -606
- package/content/extensions/components/static-asset/usage.md +184 -299
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +311 -404
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +75 -407
- package/content/extensions/components/websocket/usage.md +120 -338
- package/content/extensions/helpers/cron/index.md +52 -160
- package/content/extensions/helpers/crypto/index.md +67 -480
- package/content/extensions/helpers/crypto/reference.md +528 -0
- package/content/extensions/helpers/env/index.md +64 -178
- package/content/extensions/helpers/error/index.md +286 -196
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +76 -550
- package/content/extensions/helpers/inversion/reference.md +530 -0
- package/content/extensions/helpers/kafka/admin.md +23 -3
- package/content/extensions/helpers/kafka/compile-binary.md +83 -52
- package/content/extensions/helpers/kafka/consumer.md +65 -28
- package/content/extensions/helpers/kafka/examples.md +46 -253
- package/content/extensions/helpers/kafka/index.md +55 -617
- package/content/extensions/helpers/kafka/producer.md +156 -22
- package/content/extensions/helpers/kafka/schema-registry.md +59 -79
- package/content/extensions/helpers/logger/hf-logger.md +220 -0
- package/content/extensions/helpers/logger/index.md +78 -552
- package/content/extensions/helpers/logger/pino.md +105 -0
- package/content/extensions/helpers/logger/reference.md +937 -0
- package/content/extensions/helpers/network/api.md +276 -195
- package/content/extensions/helpers/network/index.md +87 -524
- package/content/extensions/helpers/queue/index.md +80 -897
- package/content/extensions/helpers/queue/reference.md +494 -0
- package/content/extensions/helpers/redis/index.md +86 -640
- package/content/extensions/helpers/redis/reference.md +784 -0
- package/content/extensions/helpers/secrets/index.md +136 -0
- package/content/extensions/helpers/socket-io/api.md +325 -204
- package/content/extensions/helpers/socket-io/index.md +79 -429
- package/content/extensions/helpers/storage/api.md +584 -464
- package/content/extensions/helpers/storage/index.md +80 -573
- package/content/extensions/helpers/types/index.md +75 -495
- package/content/extensions/helpers/types/reference.md +689 -0
- package/content/extensions/helpers/uid/index.md +176 -189
- package/content/extensions/helpers/websocket/api.md +366 -214
- package/content/extensions/helpers/websocket/index.md +68 -503
- package/content/extensions/helpers/worker-thread/index.md +62 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/extensions/index.md +38 -39
- package/content/extensions/src-details/mcp-server.md +96 -548
- package/content/guides/core-concepts/persistent/datasources.md +2 -2
- package/content/guides/core-concepts/persistent/index.md +5 -1
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/pglite.md +218 -0
- package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
- package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
- package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/get-started/5-minute-quickstart.md +59 -206
- package/content/guides/get-started/philosophy.md +135 -670
- package/content/guides/get-started/setup.md +53 -74
- package/content/guides/migrations/redis-helpers-migration.md +5 -4
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +8 -7
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +64 -22
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +600 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +25 -46
- package/content/references/base/filter-system/application-usage.md +95 -123
- package/content/references/base/filter-system/array-operators.md +33 -43
- package/content/references/base/filter-system/comparison-operators.md +55 -63
- package/content/references/base/filter-system/default-filter.md +144 -350
- package/content/references/base/filter-system/fields-order-pagination.md +116 -148
- package/content/references/base/filter-system/index.md +114 -253
- package/content/references/base/filter-system/json-filtering.md +52 -181
- package/content/references/base/filter-system/list-operators.md +28 -44
- package/content/references/base/filter-system/logical-operators.md +69 -114
- package/content/references/base/filter-system/null-operators.md +41 -98
- package/content/references/base/filter-system/pattern-matching.md +48 -51
- package/content/references/base/filter-system/quick-reference.md +93 -196
- package/content/references/base/filter-system/range-operators.md +22 -38
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +173 -232
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +43 -28
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +81 -1452
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +281 -419
- package/content/references/base/repositories/index.md +84 -644
- package/content/references/base/repositories/mixins.md +25 -21
- package/content/references/base/repositories/relations.md +194 -375
- package/content/references/base/repositories/soft-deletable.md +67 -55
- package/content/references/base/secrets.md +267 -0
- package/content/references/base/services.md +8 -6
- package/content/references/configuration/environment-variables.md +73 -21
- package/content/references/configuration/index.md +51 -31
- package/content/references/index.md +1 -1
- 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/duration.md +85 -0
- package/content/references/utilities/index.md +6 -2
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +81 -61
- 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 +58 -218
- package/content/references/utilities/retry.md +139 -0
- 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/dist/mcp-server/index.js +0 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
- package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
- package/package.json +24 -23
|
@@ -6,10 +6,33 @@ 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 closest to what you need. For the operators themselves, start at the [Filter System Overview](./).
|
|
10
10
|
|
|
11
|
+
## Soft delete
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
Goal: return only non-deleted rows, or only deleted ones.
|
|
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
|
+
Notice: `is`/`isn` against a nullable timestamp is the whole pattern - no separate `deleted: boolean` column needed.
|
|
30
|
+
|
|
31
|
+
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).
|
|
32
|
+
|
|
33
|
+
## E-commerce product search
|
|
34
|
+
|
|
35
|
+
Goal: a price range, a minimum quantity, and a status, sorted and paged for a listing page.
|
|
13
36
|
|
|
14
37
|
```typescript
|
|
15
38
|
const products = await productRepository.find({
|
|
@@ -23,7 +46,7 @@ const products = await productRepository.find({
|
|
|
23
46
|
order: ['rating DESC', 'reviewCount DESC'],
|
|
24
47
|
fields: ['id', 'name', 'price', 'rating', 'imageUrl'],
|
|
25
48
|
limit: 24,
|
|
26
|
-
}
|
|
49
|
+
},
|
|
27
50
|
});
|
|
28
51
|
|
|
29
52
|
// SQL:
|
|
@@ -37,8 +60,11 @@ const products = await productRepository.find({
|
|
|
37
60
|
// LIMIT 24
|
|
38
61
|
```
|
|
39
62
|
|
|
63
|
+
Notice: four `where` keys AND-compose automatically - no explicit `and` needed for a flat condition list.
|
|
64
|
+
|
|
65
|
+
## Admin dashboard: recent users
|
|
40
66
|
|
|
41
|
-
|
|
67
|
+
Goal: users created in the last 30 days, excluding banned or suspended accounts, with a verified email.
|
|
42
68
|
|
|
43
69
|
```typescript
|
|
44
70
|
const thirtyDaysAgo = new Date();
|
|
@@ -54,7 +80,7 @@ const recentUsers = await userRepository.find({
|
|
|
54
80
|
order: ['createdAt DESC'],
|
|
55
81
|
fields: ['id', 'email', 'name', 'createdAt', 'status'],
|
|
56
82
|
limit: 50,
|
|
57
|
-
}
|
|
83
|
+
},
|
|
58
84
|
});
|
|
59
85
|
|
|
60
86
|
// SQL:
|
|
@@ -67,8 +93,39 @@ const recentUsers = await userRepository.find({
|
|
|
67
93
|
// LIMIT 50
|
|
68
94
|
```
|
|
69
95
|
|
|
96
|
+
Notice: `nin` on `status` silently drops any row where `status` is `NULL`. See [Tips & Edge Cases](./tips) before relying on this for a nullable column.
|
|
97
|
+
|
|
98
|
+
## Multi-tenant isolation at the call site
|
|
70
99
|
|
|
71
|
-
|
|
100
|
+
`settings.defaultFilter` (see [Default Filter](./default-filter)) is the model-level way to enforce a tenant scope. The alternative below is a helper that injects `tenantId` at every call site instead. Use it when tenant isolation is a caller concern, not a per-model constant.
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const getTenantProducts = (tenantId: string, filter: TFilter<TProductSchema>) =>
|
|
104
|
+
productRepository.find({
|
|
105
|
+
filter: {
|
|
106
|
+
...filter,
|
|
107
|
+
where: { ...filter.where, tenantId, deletedAt: { is: null } },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await getTenantProducts('tenant-abc', {
|
|
112
|
+
where: { category: 'electronics' },
|
|
113
|
+
order: ['createdAt DESC'],
|
|
114
|
+
limit: 20,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// SQL:
|
|
118
|
+
// SELECT * FROM "Product"
|
|
119
|
+
// WHERE "category" = 'electronics' AND "tenant_id" = 'tenant-abc' AND "deleted_at" IS NULL
|
|
120
|
+
// ORDER BY "created_at" DESC
|
|
121
|
+
// LIMIT 20
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Notice: this is a plain object spread, not `mergeFilter`'s narrowing merge - `tenantId`/`deletedAt` overwrite same-named keys from `filter.where` because they're spread last.
|
|
125
|
+
|
|
126
|
+
## Task management: priority tags
|
|
127
|
+
|
|
128
|
+
Goal: open tasks assigned to the current user that carry an urgent or high-priority tag, with the parent project loaded.
|
|
72
129
|
|
|
73
130
|
```typescript
|
|
74
131
|
const priorityTasks = await taskRepository.find({
|
|
@@ -80,7 +137,7 @@ const priorityTasks = await taskRepository.find({
|
|
|
80
137
|
},
|
|
81
138
|
order: ['dueDate ASC', 'createdAt ASC'],
|
|
82
139
|
include: [{ relation: 'project' }],
|
|
83
|
-
}
|
|
140
|
+
},
|
|
84
141
|
});
|
|
85
142
|
|
|
86
143
|
// SQL:
|
|
@@ -95,35 +152,87 @@ const priorityTasks = await taskRepository.find({
|
|
|
95
152
|
// SELECT * FROM "Project" WHERE "id" IN (...)
|
|
96
153
|
```
|
|
97
154
|
|
|
155
|
+
Notice: `include` runs as a separate query, not a SQL `JOIN` - see [Relations & Includes](../repositories/relations).
|
|
98
156
|
|
|
99
|
-
##
|
|
157
|
+
## Date range queries
|
|
158
|
+
|
|
159
|
+
Goal: a closed window (a specific week) versus a rolling window (the last 7 days).
|
|
100
160
|
|
|
101
161
|
```typescript
|
|
102
|
-
|
|
103
|
-
const
|
|
162
|
+
const startOfWeek = new Date('2024-12-29');
|
|
163
|
+
const endOfWeek = new Date('2025-01-04');
|
|
164
|
+
|
|
165
|
+
const weekEvents = await eventRepository.find({
|
|
104
166
|
filter: {
|
|
105
|
-
where: {
|
|
106
|
-
|
|
167
|
+
where: { eventDate: { between: [startOfWeek, endOfWeek] } },
|
|
168
|
+
order: ['eventDate ASC'],
|
|
169
|
+
},
|
|
107
170
|
});
|
|
171
|
+
// SQL: SELECT * FROM "Event" WHERE "event_date" BETWEEN '2024-12-29' AND '2025-01-04' ORDER BY "event_date" ASC
|
|
172
|
+
```
|
|
108
173
|
|
|
174
|
+
```typescript
|
|
175
|
+
const sevenDaysAgo = new Date();
|
|
176
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
177
|
+
|
|
178
|
+
const recentOrders = await orderRepository.find({
|
|
179
|
+
filter: {
|
|
180
|
+
where: {
|
|
181
|
+
createdAt: { gte: sevenDaysAgo },
|
|
182
|
+
status: { in: ['completed', 'shipped'] },
|
|
183
|
+
total: { gte: 100 },
|
|
184
|
+
},
|
|
185
|
+
order: ['total DESC'],
|
|
186
|
+
limit: 100,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
109
189
|
// SQL:
|
|
110
|
-
// SELECT * FROM "
|
|
190
|
+
// SELECT * FROM "Order"
|
|
191
|
+
// WHERE "created_at" >= '2024-12-24T00:00:00.000Z' AND "status" IN ('completed', 'shipped') AND "total" >= 100
|
|
192
|
+
// ORDER BY "total" DESC LIMIT 100
|
|
111
193
|
```
|
|
112
194
|
|
|
195
|
+
Notice: `between` needs exactly two elements - `FilterBuilder` throws on any other array length.
|
|
196
|
+
|
|
197
|
+
## Inventory low-stock alert
|
|
198
|
+
|
|
199
|
+
Goal: active products at or under a reorder threshold, flagged critical below 5 units or fast-moving stock at 10 or fewer.
|
|
200
|
+
|
|
113
201
|
```typescript
|
|
114
|
-
|
|
115
|
-
const deletedRecords = await repository.find({
|
|
202
|
+
const lowStockProducts = await productRepository.find({
|
|
116
203
|
filter: {
|
|
117
|
-
where: {
|
|
118
|
-
|
|
204
|
+
where: {
|
|
205
|
+
status: 'active',
|
|
206
|
+
quantity: { lte: 10 },
|
|
207
|
+
'metadata.reorderPoint': { isn: null },
|
|
208
|
+
or: [
|
|
209
|
+
{ quantity: { lt: 5 } }, // Critical: below 5
|
|
210
|
+
{ and: [{ quantity: { lte: 10 } }, { 'metadata.fastMoving': true }] },
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
order: ['quantity ASC'],
|
|
214
|
+
fields: ['id', 'name', 'quantity', 'metadata'],
|
|
215
|
+
},
|
|
119
216
|
});
|
|
120
217
|
|
|
121
218
|
// SQL:
|
|
122
|
-
// SELECT
|
|
219
|
+
// SELECT "id", "name", "quantity", "metadata"
|
|
220
|
+
// FROM "Product"
|
|
221
|
+
// WHERE "status" = 'active'
|
|
222
|
+
// AND "quantity" <= 10
|
|
223
|
+
// AND "metadata" #>> '{reorderPoint}' IS NOT NULL
|
|
224
|
+
// AND (
|
|
225
|
+
// "quantity" < 5
|
|
226
|
+
// OR ("quantity" <= 10 AND "metadata" #>> '{fastMoving}' = 'true')
|
|
227
|
+
// )
|
|
228
|
+
// ORDER BY "quantity" ASC
|
|
123
229
|
```
|
|
124
230
|
|
|
231
|
+
Notice: `or` nests an `and` group one level deep - `FilterBuilder` recurses through logical groups, so nesting depth is not limited to one.
|
|
232
|
+
|
|
233
|
+
## Complex authorization filter
|
|
125
234
|
|
|
126
|
-
|
|
235
|
+
Goal: an admin sees everything; everyone else sees only what they own, what is public, or what is shared with them.
|
|
127
236
|
|
|
128
237
|
```typescript
|
|
129
238
|
const getAuthorizedFilter = (user: User): TWhere<TDocumentSchema> => {
|
|
@@ -143,23 +252,11 @@ const getAuthorizedFilter = (user: User): TWhere<TDocumentSchema> => {
|
|
|
143
252
|
};
|
|
144
253
|
|
|
145
254
|
const documents = await documentRepository.find({
|
|
146
|
-
filter: {
|
|
147
|
-
where: getAuthorizedFilter(currentUser),
|
|
148
|
-
order: ['updatedAt DESC'],
|
|
149
|
-
limit: 100,
|
|
150
|
-
},
|
|
255
|
+
filter: { where: getAuthorizedFilter(currentUser), order: ['updatedAt DESC'], limit: 100 },
|
|
151
256
|
});
|
|
152
257
|
|
|
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"
|
|
258
|
+
// SQL (regular user):
|
|
259
|
+
// SELECT * FROM "Document"
|
|
163
260
|
// WHERE "deleted_at" IS NULL
|
|
164
261
|
// AND (
|
|
165
262
|
// "owner_id" = 'user-123'
|
|
@@ -167,23 +264,21 @@ const documents = await documentRepository.find({
|
|
|
167
264
|
// OR "shared_with_teams"::text[] && ARRAY['team-1', 'team-2']::text[]
|
|
168
265
|
// OR "shared_with_users"::text[] @> ARRAY['user-123']::text[]
|
|
169
266
|
// )
|
|
170
|
-
// ORDER BY "updated_at" DESC
|
|
171
|
-
// LIMIT 100
|
|
267
|
+
// ORDER BY "updated_at" DESC LIMIT 100
|
|
172
268
|
```
|
|
173
269
|
|
|
270
|
+
Notice: a plain TypeScript function builds the `where`, branching on role. A filter is a normal object, not a DSL with its own control flow.
|
|
271
|
+
|
|
272
|
+
## Full-text search with metadata
|
|
174
273
|
|
|
175
|
-
|
|
274
|
+
Goal: assemble a `where` clause from optional caller input, adding a key only when the caller supplied it.
|
|
176
275
|
|
|
177
276
|
```typescript
|
|
178
|
-
const searchProducts = async (
|
|
179
|
-
|
|
180
|
-
maxPrice?: number;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const where: TWhere<TProductSchema> = {
|
|
184
|
-
status: 'active',
|
|
185
|
-
deletedAt: { is: null },
|
|
186
|
-
};
|
|
277
|
+
const searchProducts = async (
|
|
278
|
+
query: string,
|
|
279
|
+
filters: { minRating?: number; maxPrice?: number; categories?: string[] },
|
|
280
|
+
) => {
|
|
281
|
+
const where: TWhere<TProductSchema> = { status: 'active', deletedAt: { is: null } };
|
|
187
282
|
|
|
188
283
|
if (query) {
|
|
189
284
|
where.or = [
|
|
@@ -192,50 +287,33 @@ const searchProducts = async (query: string, filters: {
|
|
|
192
287
|
{ 'metadata.keywords': { ilike: `%${query}%` } },
|
|
193
288
|
];
|
|
194
289
|
}
|
|
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
|
-
}
|
|
290
|
+
if (filters.minRating) where.rating = { gte: filters.minRating };
|
|
291
|
+
if (filters.maxPrice) where.price = { lte: filters.maxPrice };
|
|
292
|
+
if (filters.categories?.length) where.categories = { contains: filters.categories };
|
|
208
293
|
|
|
209
294
|
return productRepository.find({
|
|
210
|
-
filter: {
|
|
211
|
-
where,
|
|
212
|
-
order: ['rating DESC', 'createdAt DESC'],
|
|
213
|
-
limit: 50,
|
|
214
|
-
},
|
|
295
|
+
filter: { where, order: ['rating DESC', 'createdAt DESC'], limit: 50 },
|
|
215
296
|
});
|
|
216
297
|
};
|
|
217
298
|
|
|
218
|
-
//
|
|
299
|
+
// searchProducts('wireless', { minRating: 4, maxPrice: 200, categories: ['electronics'] })
|
|
219
300
|
//
|
|
220
301
|
// SQL:
|
|
221
|
-
// SELECT *
|
|
222
|
-
// FROM "Product"
|
|
302
|
+
// SELECT * FROM "Product"
|
|
223
303
|
// WHERE "status" = 'active'
|
|
224
304
|
// AND "deleted_at" IS NULL
|
|
225
|
-
// AND (
|
|
226
|
-
// "name" ILIKE '%wireless%'
|
|
227
|
-
// OR "description" ILIKE '%wireless%'
|
|
228
|
-
// OR "metadata" #>> '{keywords}' ILIKE '%wireless%'
|
|
229
|
-
// )
|
|
305
|
+
// AND ("name" ILIKE '%wireless%' OR "description" ILIKE '%wireless%' OR "metadata" #>> '{keywords}' ILIKE '%wireless%')
|
|
230
306
|
// AND "rating" >= 4
|
|
231
307
|
// AND "price" <= 200
|
|
232
308
|
// AND "categories"::text[] @> ARRAY['electronics']::text[]
|
|
233
|
-
// ORDER BY "rating" DESC, "created_at" DESC
|
|
234
|
-
// LIMIT 50
|
|
309
|
+
// ORDER BY "rating" DESC, "created_at" DESC LIMIT 50
|
|
235
310
|
```
|
|
236
311
|
|
|
312
|
+
Notice: `ilike` reaches into a JSON path (`'metadata.keywords'`) the same way it reaches a top-level column.
|
|
313
|
+
|
|
314
|
+
## Everything at once
|
|
237
315
|
|
|
238
|
-
|
|
316
|
+
Every operator family, a JSON path, a three-way `or`, and a scoped relation include - all in one filter. This is the ceiling of what a single `TFilter` can express.
|
|
239
317
|
|
|
240
318
|
```typescript
|
|
241
319
|
const massiveFilter: TFilter<TProductSchema> = {
|
|
@@ -254,12 +332,9 @@ const massiveFilter: TFilter<TProductSchema> = {
|
|
|
254
332
|
{ isFeatured: true },
|
|
255
333
|
{ 'metadata.promotion.active': true },
|
|
256
334
|
{ 'metadata.promotion.discount': { gte: 20 } },
|
|
257
|
-
]
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
createdAt: { gte: new Date('2024-12-01') },
|
|
261
|
-
'metadata.isNewArrival': true,
|
|
335
|
+
],
|
|
262
336
|
},
|
|
337
|
+
{ createdAt: { gte: new Date('2024-12-01') }, 'metadata.isNewArrival': true },
|
|
263
338
|
],
|
|
264
339
|
category: { nin: ['discontinued', 'recalled'] },
|
|
265
340
|
suppliers: { overlaps: ['supplier-a', 'supplier-b'] },
|
|
@@ -270,14 +345,7 @@ const massiveFilter: TFilter<TProductSchema> = {
|
|
|
270
345
|
skip: 0,
|
|
271
346
|
include: [
|
|
272
347
|
{ relation: 'category' },
|
|
273
|
-
{
|
|
274
|
-
relation: 'reviews',
|
|
275
|
-
scope: {
|
|
276
|
-
where: { rating: { gte: 4 } },
|
|
277
|
-
order: ['createdAt DESC'],
|
|
278
|
-
limit: 5,
|
|
279
|
-
},
|
|
280
|
-
},
|
|
348
|
+
{ relation: 'reviews', scope: { where: { rating: { gte: 4 } }, order: ['createdAt DESC'], limit: 5 } },
|
|
281
349
|
],
|
|
282
350
|
};
|
|
283
351
|
|
|
@@ -291,163 +359,36 @@ const products = await productRepository.find({ filter: massiveFilter });
|
|
|
291
359
|
// AND "price" >= 50 AND "price" <= 500
|
|
292
360
|
// AND "quantity" > 0
|
|
293
361
|
// AND "tags"::text[] @> ARRAY['electronics', 'portable']::text[]
|
|
294
|
-
// AND CASE
|
|
295
|
-
//
|
|
296
|
-
// THEN ("metadata" #>> '{priority}')::numeric ELSE NULL
|
|
297
|
-
// END >= 3
|
|
362
|
+
// AND CASE WHEN ("metadata" #>> '{priority}') ~ '^-?[0-9]+(\.[0-9]+)?$'
|
|
363
|
+
// THEN ("metadata" #>> '{priority}')::numeric ELSE NULL END >= 3
|
|
298
364
|
// AND "metadata" #>> '{features,wireless}' = 'true'
|
|
299
365
|
// AND (
|
|
300
366
|
// "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
|
-
// )
|
|
367
|
+
// OR ("is_featured" = true AND "metadata" #>> '{promotion,active}' = 'true'
|
|
368
|
+
// AND CASE WHEN ("metadata" #>> '{promotion,discount}') ~ '^-?[0-9]+(\.[0-9]+)?$'
|
|
369
|
+
// THEN ("metadata" #>> '{promotion,discount}')::numeric ELSE NULL END >= 20)
|
|
370
|
+
// OR ("created_at" >= '2024-12-01T00:00:00.000Z' AND "metadata" #>> '{isNewArrival}' = 'true')
|
|
313
371
|
// )
|
|
314
372
|
// AND "category" NOT IN ('discontinued', 'recalled')
|
|
315
373
|
// AND "suppliers"::text[] && ARRAY['supplier-a', 'supplier-b']::text[]
|
|
316
374
|
// ORDER BY "metadata" #> '{priority}' DESC, "rating" DESC, "created_at" DESC
|
|
317
375
|
// LIMIT 20 OFFSET 0
|
|
318
376
|
//
|
|
319
|
-
// -- Separate
|
|
377
|
+
// -- Separate queries for relations:
|
|
320
378
|
// 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
|
|
379
|
+
// SELECT * FROM "Review" WHERE "product_id" IN (...) AND "rating" >= 4 ORDER BY "created_at" DESC LIMIT 5
|
|
327
380
|
```
|
|
328
381
|
|
|
382
|
+
Notice: `'metadata.priority': { gte: 3 }` gets the numeric `CASE` cast because the operand is a number. `'metadata.isNewArrival': true` does not, because it compares as text. See [Tips & Edge Cases](./tips) for the full casting rule.
|
|
329
383
|
|
|
330
|
-
##
|
|
331
|
-
|
|
332
|
-
```typescript
|
|
333
|
-
// Events this week
|
|
334
|
-
const startOfWeek = new Date('2024-12-29');
|
|
335
|
-
const endOfWeek = new Date('2025-01-04');
|
|
336
|
-
|
|
337
|
-
const weekEvents = await eventRepository.find({
|
|
338
|
-
filter: {
|
|
339
|
-
where: {
|
|
340
|
-
eventDate: { between: [startOfWeek, endOfWeek] }
|
|
341
|
-
},
|
|
342
|
-
order: ['eventDate ASC']
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
// SQL:
|
|
347
|
-
// SELECT *
|
|
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
|
-
```
|
|
384
|
+
## See also
|
|
379
385
|
|
|
386
|
+
- [Filter System Overview](./) - the `filter` shape and every `where` operator family
|
|
387
|
+
- [Default Filter](./default-filter) - model-level scoping instead of the call-site pattern shown above
|
|
388
|
+
- [Application Usage](./application-usage) - how a filter reaches the repository from an HTTP request
|
|
389
|
+
- [Tips & Edge Cases](./tips) - `NULL` handling, empty-array semantics, and other gotchas that show up in filters like these
|
|
380
390
|
|
|
381
|
-
|
|
391
|
+
**Files:**
|
|
382
392
|
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
```
|
|
393
|
+
- [`packages/core-server/src/connectors/relational/repositories/dialect/filter.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/relational/repositories/dialect/filter.ts) - `FilterBuilder`, translates `TFilter` to Drizzle/SQL
|
|
394
|
+
- [`packages/core-server/src/connectors/postgres/repositories/dialect/query.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/postgres/repositories/dialect/query.ts) - `PostgresQueryOperators.FNS`, per-operator SQL builders
|