@venizia/ignis-docs 0.0.3 → 0.0.4-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 +1 -1
- package/package.json +4 -2
- package/wiki/best-practices/api-usage-examples.md +591 -0
- package/wiki/best-practices/architectural-patterns.md +415 -0
- package/wiki/best-practices/architecture-decisions.md +488 -0
- package/wiki/{get-started/best-practices → best-practices}/code-style-standards.md +406 -17
- package/wiki/{get-started/best-practices → best-practices}/common-pitfalls.md +109 -4
- package/wiki/{get-started/best-practices → best-practices}/contribution-workflow.md +34 -7
- package/wiki/best-practices/data-modeling.md +376 -0
- package/wiki/best-practices/deployment-strategies.md +698 -0
- package/wiki/best-practices/index.md +27 -0
- package/wiki/best-practices/performance-optimization.md +196 -0
- package/wiki/best-practices/security-guidelines.md +218 -0
- package/wiki/{get-started/best-practices → best-practices}/troubleshooting-tips.md +97 -1
- package/wiki/changelogs/2025-12-16-initial-architecture.md +1 -1
- package/wiki/changelogs/2025-12-16-model-repo-datasource-refactor.md +1 -1
- package/wiki/changelogs/2025-12-17-refactor.md +1 -1
- package/wiki/changelogs/2025-12-18-performance-optimizations.md +5 -5
- package/wiki/changelogs/2025-12-18-repository-validation-security.md +13 -7
- package/wiki/changelogs/2025-12-26-nested-relations-and-generics.md +2 -2
- package/wiki/changelogs/2025-12-29-dynamic-binding-registration.md +104 -0
- package/wiki/changelogs/2025-12-29-snowflake-uid-helper.md +100 -0
- package/wiki/changelogs/2025-12-30-repository-enhancements.md +214 -0
- package/wiki/changelogs/2025-12-31-json-path-filtering-array-operators.md +214 -0
- package/wiki/changelogs/2025-12-31-string-id-custom-generator.md +137 -0
- package/wiki/changelogs/2026-01-02-default-filter-and-repository-mixins.md +418 -0
- package/wiki/changelogs/index.md +6 -0
- package/wiki/changelogs/planned-schema-migrator.md +0 -8
- package/wiki/{get-started/core-concepts → guides/core-concepts/application}/bootstrapping.md +18 -5
- package/wiki/{get-started/core-concepts/application.md → guides/core-concepts/application/index.md} +47 -104
- package/wiki/guides/core-concepts/components-guide.md +509 -0
- package/wiki/{get-started → guides}/core-concepts/components.md +24 -17
- package/wiki/{get-started → guides}/core-concepts/controllers.md +30 -13
- package/wiki/{get-started → guides}/core-concepts/dependency-injection.md +97 -0
- package/wiki/guides/core-concepts/persistent/datasources.md +179 -0
- package/wiki/guides/core-concepts/persistent/index.md +119 -0
- package/wiki/guides/core-concepts/persistent/models.md +241 -0
- package/wiki/guides/core-concepts/persistent/repositories.md +219 -0
- package/wiki/guides/core-concepts/persistent/transactions.md +170 -0
- package/wiki/{get-started → guides}/core-concepts/services.md +26 -3
- package/wiki/{get-started → guides/get-started}/5-minute-quickstart.md +59 -14
- package/wiki/guides/get-started/philosophy.md +682 -0
- package/wiki/guides/get-started/setup.md +157 -0
- package/wiki/guides/index.md +89 -0
- package/wiki/guides/reference/glossary.md +243 -0
- package/wiki/{get-started → guides/reference}/mcp-docs-server.md +0 -10
- package/wiki/{get-started → guides/tutorials}/building-a-crud-api.md +134 -132
- package/wiki/{get-started/quickstart.md → guides/tutorials/complete-installation.md} +107 -71
- package/wiki/guides/tutorials/ecommerce-api.md +1399 -0
- package/wiki/guides/tutorials/realtime-chat.md +1261 -0
- package/wiki/guides/tutorials/testing.md +723 -0
- package/wiki/index.md +176 -37
- package/wiki/references/base/application.md +27 -0
- package/wiki/references/base/bootstrapping.md +31 -26
- package/wiki/references/base/components.md +24 -7
- package/wiki/references/base/controllers.md +50 -20
- package/wiki/references/base/datasources.md +30 -0
- package/wiki/references/base/dependency-injection.md +39 -3
- package/wiki/references/base/filter-system/application-usage.md +224 -0
- package/wiki/references/base/filter-system/array-operators.md +132 -0
- package/wiki/references/base/filter-system/comparison-operators.md +109 -0
- package/wiki/references/base/filter-system/default-filter.md +428 -0
- package/wiki/references/base/filter-system/fields-order-pagination.md +155 -0
- package/wiki/references/base/filter-system/index.md +127 -0
- package/wiki/references/base/filter-system/json-filtering.md +197 -0
- package/wiki/references/base/filter-system/list-operators.md +71 -0
- package/wiki/references/base/filter-system/logical-operators.md +156 -0
- package/wiki/references/base/filter-system/null-operators.md +58 -0
- package/wiki/references/base/filter-system/pattern-matching.md +108 -0
- package/wiki/references/base/filter-system/quick-reference.md +431 -0
- package/wiki/references/base/filter-system/range-operators.md +63 -0
- package/wiki/references/base/filter-system/tips.md +190 -0
- package/wiki/references/base/filter-system/use-cases.md +452 -0
- package/wiki/references/base/index.md +90 -0
- package/wiki/references/base/middlewares.md +604 -0
- package/wiki/references/base/models.md +215 -23
- package/wiki/references/base/providers.md +731 -0
- package/wiki/references/base/repositories/advanced.md +555 -0
- package/wiki/references/base/repositories/index.md +228 -0
- package/wiki/references/base/repositories/mixins.md +331 -0
- package/wiki/references/base/repositories/relations.md +486 -0
- package/wiki/references/base/repositories.md +40 -635
- package/wiki/references/base/services.md +28 -4
- package/wiki/references/components/authentication.md +22 -2
- package/wiki/references/components/health-check.md +12 -0
- package/wiki/references/components/index.md +23 -0
- package/wiki/references/components/mail.md +687 -0
- package/wiki/references/components/request-tracker.md +16 -0
- package/wiki/references/components/socket-io.md +18 -0
- package/wiki/references/components/static-asset.md +14 -26
- package/wiki/references/components/swagger.md +17 -0
- package/wiki/references/configuration/environment-variables.md +427 -0
- package/wiki/references/configuration/index.md +73 -0
- package/wiki/references/helpers/cron.md +14 -0
- package/wiki/references/helpers/crypto.md +15 -0
- package/wiki/references/helpers/env.md +16 -0
- package/wiki/references/helpers/error.md +17 -0
- package/wiki/references/helpers/index.md +14 -0
- package/wiki/references/helpers/inversion.md +24 -4
- package/wiki/references/helpers/logger.md +19 -0
- package/wiki/references/helpers/network.md +11 -0
- package/wiki/references/helpers/queue.md +19 -0
- package/wiki/references/helpers/redis.md +21 -0
- package/wiki/references/helpers/socket-io.md +24 -5
- package/wiki/references/helpers/storage.md +18 -10
- package/wiki/references/helpers/testing.md +18 -0
- package/wiki/references/helpers/types.md +16 -0
- package/wiki/references/helpers/uid.md +167 -0
- package/wiki/references/helpers/worker-thread.md +16 -0
- package/wiki/references/index.md +177 -0
- package/wiki/references/quick-reference.md +634 -0
- package/wiki/references/src-details/boot.md +3 -3
- package/wiki/references/src-details/dev-configs.md +0 -4
- package/wiki/references/src-details/docs.md +2 -2
- package/wiki/references/src-details/index.md +86 -0
- package/wiki/references/src-details/inversion.md +1 -6
- package/wiki/references/src-details/mcp-server.md +3 -15
- package/wiki/references/utilities/index.md +86 -10
- package/wiki/references/utilities/jsx.md +577 -0
- package/wiki/references/utilities/request.md +0 -2
- package/wiki/references/utilities/statuses.md +740 -0
- package/wiki/get-started/best-practices/api-usage-examples.md +0 -266
- package/wiki/get-started/best-practices/architectural-patterns.md +0 -170
- package/wiki/get-started/best-practices/data-modeling.md +0 -177
- package/wiki/get-started/best-practices/deployment-strategies.md +0 -121
- package/wiki/get-started/best-practices/performance-optimization.md +0 -97
- package/wiki/get-started/best-practices/security-guidelines.md +0 -99
- package/wiki/get-started/core-concepts/persistent.md +0 -539
- package/wiki/get-started/index.md +0 -65
- package/wiki/get-started/philosophy.md +0 -296
- package/wiki/get-started/prerequisites.md +0 -113
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Pro Tips & Edge Cases
|
|
3
|
+
description: Advanced tips and common edge cases for filters
|
|
4
|
+
difficulty: intermediate
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Pro Tips & Edge Cases
|
|
8
|
+
|
|
9
|
+
Advanced tips and common edge cases when working with filters.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## Tip 1: JSON Numeric vs String Comparison
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// JSON field contains: { "priority": "3" } (string)
|
|
16
|
+
// This WON'T match numeric comparison!
|
|
17
|
+
{ where: { 'metadata.priority': { gt: 2 } } } // NULL due to safe casting
|
|
18
|
+
|
|
19
|
+
// Use string comparison instead
|
|
20
|
+
{ where: { 'metadata.priority': { gt: '2' } } } // Lexicographic compare
|
|
21
|
+
|
|
22
|
+
// Or ensure your data stores numbers properly
|
|
23
|
+
{ "priority": 3 } // Store as number, not string
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## Tip 2: Empty Array Handling
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// Empty IN -> no results
|
|
31
|
+
{ where: { id: { in: [] } } } // SQL: WHERE false
|
|
32
|
+
|
|
33
|
+
// Empty NIN -> all results
|
|
34
|
+
{ where: { id: { nin: [] } } } // SQL: WHERE true
|
|
35
|
+
|
|
36
|
+
// Check array length before filtering
|
|
37
|
+
const ids = getUserSelectedIds();
|
|
38
|
+
if (ids.length === 0) {
|
|
39
|
+
return []; // Early return instead of empty IN
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## Tip 3: Null-Safe JSON Paths
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// If JSON field doesn't exist, #>> returns NULL
|
|
48
|
+
// This is safe - no errors, just no matches
|
|
49
|
+
{ where: { 'metadata.nonexistent.field': 'value' } }
|
|
50
|
+
// SQL: "metadata" #>> '{nonexistent,field}' = 'value'
|
|
51
|
+
// Result: No rows (NULL != 'value')
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## Tip 4: Performance with Large IN Arrays
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// For very large arrays (1000+ items), consider chunking
|
|
59
|
+
const allIds = getLargeIdList(); // 5000 IDs
|
|
60
|
+
|
|
61
|
+
const chunkSize = 500;
|
|
62
|
+
const results = [];
|
|
63
|
+
for (let i = 0; i < allIds.length; i += chunkSize) {
|
|
64
|
+
const chunk = allIds.slice(i, i + chunkSize);
|
|
65
|
+
const chunkResults = await repo.find({
|
|
66
|
+
filter: { where: { id: { in: chunk } } }
|
|
67
|
+
});
|
|
68
|
+
results.push(...chunkResults);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
## Tip 5: Order By JSON Fields
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// JSON ordering uses #> (preserves type) not #>> (text)
|
|
77
|
+
{ order: ['metadata.priority DESC'] }
|
|
78
|
+
// SQL: "metadata" #> '{priority}' DESC
|
|
79
|
+
|
|
80
|
+
// JSONB comparison order:
|
|
81
|
+
// null < boolean < number < string < array < object
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
## Tip 6: Debugging Filters
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Enable logging to see generated SQL
|
|
89
|
+
const result = await repo.find({
|
|
90
|
+
filter: complexFilter,
|
|
91
|
+
options: {
|
|
92
|
+
log: { use: true, level: 'debug' },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Or use buildQuery to inspect without executing
|
|
97
|
+
const queryOptions = repo.buildQuery({ filter: complexFilter });
|
|
98
|
+
console.log('Generated query options:', queryOptions);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Tip 7: NOT IN with NULL Columns
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// NOT IN excludes NULL values!
|
|
106
|
+
{ where: { status: { nin: ['deleted'] } } }
|
|
107
|
+
// Rows where status IS NULL will NOT be returned
|
|
108
|
+
|
|
109
|
+
// Include NULL values explicitly
|
|
110
|
+
{
|
|
111
|
+
where: {
|
|
112
|
+
or: [
|
|
113
|
+
{ status: { nin: ['deleted'] } },
|
|
114
|
+
{ status: { is: null } }
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
## Tip 8: Combining Multiple Array Conditions
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
await productRepo.find({
|
|
125
|
+
filter: {
|
|
126
|
+
where: {
|
|
127
|
+
// Must have ALL these categories
|
|
128
|
+
categories: { contains: ['electronics', 'portable'] },
|
|
129
|
+
// Tags must be subset of allowed tags
|
|
130
|
+
tags: { containedBy: ['new', 'sale', 'featured', 'popular'] },
|
|
131
|
+
// Must have at least one of these suppliers
|
|
132
|
+
suppliers: { overlaps: ['supplier-a', 'supplier-b'] }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
## Tip 9: Date Range Queries
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// This week's events
|
|
143
|
+
const startOfWeek = new Date();
|
|
144
|
+
startOfWeek.setDate(startOfWeek.getDate() - startOfWeek.getDay());
|
|
145
|
+
const endOfWeek = new Date(startOfWeek);
|
|
146
|
+
endOfWeek.setDate(endOfWeek.getDate() + 6);
|
|
147
|
+
|
|
148
|
+
{
|
|
149
|
+
where: {
|
|
150
|
+
eventDate: { between: [startOfWeek, endOfWeek] }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Last 30 days
|
|
155
|
+
const thirtyDaysAgo = new Date();
|
|
156
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
157
|
+
|
|
158
|
+
{
|
|
159
|
+
where: {
|
|
160
|
+
createdAt: { gte: thirtyDaysAgo }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
## Tip 10: Reusable Filter Builders
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Create reusable filter builders
|
|
170
|
+
const createActiveFilter = <T extends { status: string; deletedAt: Date | null }>(): TWhere<T> => ({
|
|
171
|
+
status: 'active',
|
|
172
|
+
deletedAt: { is: null },
|
|
173
|
+
} as TWhere<T>);
|
|
174
|
+
|
|
175
|
+
const createPaginationFilter = (page: number, size: number = 20) => ({
|
|
176
|
+
limit: size,
|
|
177
|
+
skip: (page - 1) * size,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Usage
|
|
181
|
+
const products = await productRepo.find({
|
|
182
|
+
filter: {
|
|
183
|
+
where: {
|
|
184
|
+
...createActiveFilter(),
|
|
185
|
+
category: 'electronics',
|
|
186
|
+
},
|
|
187
|
+
...createPaginationFilter(3),
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
```
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Case Gallery
|
|
3
|
+
description: Real-world filter examples with corresponding SQL
|
|
4
|
+
difficulty: intermediate
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Use Case Gallery
|
|
8
|
+
|
|
9
|
+
Real-world examples of filter usage with corresponding SQL.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## E-commerce Product Search
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
const products = await productRepo.find({
|
|
16
|
+
filter: {
|
|
17
|
+
where: {
|
|
18
|
+
category: 'electronics',
|
|
19
|
+
price: { between: [100, 500] },
|
|
20
|
+
quantity: { gt: 0 },
|
|
21
|
+
status: 'active',
|
|
22
|
+
},
|
|
23
|
+
order: ['rating DESC', 'reviewCount DESC'],
|
|
24
|
+
fields: ['id', 'name', 'price', 'rating', 'imageUrl'],
|
|
25
|
+
limit: 24,
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// SQL:
|
|
30
|
+
// SELECT "id", "name", "price", "rating", "image_url"
|
|
31
|
+
// FROM "Product"
|
|
32
|
+
// WHERE "category" = 'electronics'
|
|
33
|
+
// AND "price" BETWEEN 100 AND 500
|
|
34
|
+
// AND "quantity" > 0
|
|
35
|
+
// AND "status" = 'active'
|
|
36
|
+
// ORDER BY "rating" DESC, "review_count" DESC
|
|
37
|
+
// LIMIT 24
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## Admin Dashboard: Recent Users
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const thirtyDaysAgo = new Date();
|
|
45
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
46
|
+
|
|
47
|
+
const recentUsers = await userRepo.find({
|
|
48
|
+
filter: {
|
|
49
|
+
where: {
|
|
50
|
+
createdAt: { gte: thirtyDaysAgo },
|
|
51
|
+
status: { nin: ['banned', 'suspended'] },
|
|
52
|
+
emailVerifiedAt: { isn: null },
|
|
53
|
+
},
|
|
54
|
+
order: ['createdAt DESC'],
|
|
55
|
+
fields: ['id', 'email', 'name', 'createdAt', 'status'],
|
|
56
|
+
limit: 50,
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// SQL:
|
|
61
|
+
// SELECT "id", "email", "name", "created_at", "status"
|
|
62
|
+
// FROM "User"
|
|
63
|
+
// WHERE "created_at" >= '2024-12-01T00:00:00.000Z'
|
|
64
|
+
// AND "status" NOT IN ('banned', 'suspended')
|
|
65
|
+
// AND "email_verified_at" IS NOT NULL
|
|
66
|
+
// ORDER BY "created_at" DESC
|
|
67
|
+
// LIMIT 50
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
## Task Management: Priority Tags
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const priorityTasks = await taskRepo.find({
|
|
75
|
+
filter: {
|
|
76
|
+
where: {
|
|
77
|
+
status: { nin: ['completed', 'cancelled'] },
|
|
78
|
+
tags: { overlaps: ['urgent', 'high-priority'] },
|
|
79
|
+
assigneeId: currentUserId,
|
|
80
|
+
},
|
|
81
|
+
order: ['dueDate ASC', 'createdAt ASC'],
|
|
82
|
+
include: [{ relation: 'project' }],
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// SQL:
|
|
87
|
+
// SELECT "Task".*
|
|
88
|
+
// FROM "Task"
|
|
89
|
+
// WHERE "status" NOT IN ('completed', 'cancelled')
|
|
90
|
+
// AND "tags"::text[] && ARRAY['urgent', 'high-priority']::text[]
|
|
91
|
+
// AND "assignee_id" = 'user-123'
|
|
92
|
+
// ORDER BY "due_date" ASC, "created_at" ASC
|
|
93
|
+
//
|
|
94
|
+
// -- Separate query for relation:
|
|
95
|
+
// SELECT * FROM "Project" WHERE "id" IN (...)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## Soft Delete Handling
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// Find active records (soft delete pattern)
|
|
103
|
+
const activeRecords = await repo.find({
|
|
104
|
+
filter: {
|
|
105
|
+
where: { deletedAt: { is: null } },
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// SQL:
|
|
110
|
+
// SELECT * FROM "Record" WHERE "deleted_at" IS NULL
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Find ONLY soft-deleted records
|
|
115
|
+
const deletedRecords = await repo.find({
|
|
116
|
+
filter: {
|
|
117
|
+
where: { deletedAt: { isn: null } },
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// SQL:
|
|
122
|
+
// SELECT * FROM "Record" WHERE "deleted_at" IS NOT NULL
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
## Complex Authorization Filter
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const getAuthorizedFilter = (user: User): TWhere<TDocumentSchema> => {
|
|
130
|
+
if (user.role === 'admin') {
|
|
131
|
+
return { deletedAt: { is: null } };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
deletedAt: { is: null },
|
|
136
|
+
or: [
|
|
137
|
+
{ ownerId: user.id },
|
|
138
|
+
{ isPublic: true },
|
|
139
|
+
{ sharedWithTeams: { overlaps: user.teamIds } },
|
|
140
|
+
{ sharedWithUsers: { contains: [user.id] } },
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const documents = await documentRepo.find({
|
|
146
|
+
filter: {
|
|
147
|
+
where: getAuthorizedFilter(currentUser),
|
|
148
|
+
order: ['updatedAt DESC'],
|
|
149
|
+
limit: 100,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// SQL (for admin):
|
|
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"
|
|
163
|
+
// WHERE "deleted_at" IS NULL
|
|
164
|
+
// AND (
|
|
165
|
+
// "owner_id" = 'user-123'
|
|
166
|
+
// OR "is_public" = true
|
|
167
|
+
// OR "shared_with_teams"::text[] && ARRAY['team-1', 'team-2']::text[]
|
|
168
|
+
// OR "shared_with_users"::text[] @> ARRAY['user-123']::text[]
|
|
169
|
+
// )
|
|
170
|
+
// ORDER BY "updated_at" DESC
|
|
171
|
+
// LIMIT 100
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
## Full-Text Search with Metadata
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const searchProducts = async (query: string, filters: {
|
|
179
|
+
minRating?: number;
|
|
180
|
+
maxPrice?: number;
|
|
181
|
+
features?: string[];
|
|
182
|
+
}) => {
|
|
183
|
+
const where: TWhere<TProductSchema> = {
|
|
184
|
+
status: 'active',
|
|
185
|
+
deletedAt: { is: null },
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (query) {
|
|
189
|
+
where.or = [
|
|
190
|
+
{ name: { ilike: `%${query}%` } },
|
|
191
|
+
{ description: { ilike: `%${query}%` } },
|
|
192
|
+
{ 'metadata.keywords': { ilike: `%${query}%` } },
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (filters.minRating) {
|
|
197
|
+
where.rating = { gte: filters.minRating };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (filters.maxPrice) {
|
|
201
|
+
where.price = { lte: filters.maxPrice };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (filters.features?.length) {
|
|
205
|
+
where['metadata.features'] = { contains: filters.features };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return productRepo.find({
|
|
209
|
+
filter: {
|
|
210
|
+
where,
|
|
211
|
+
order: ['rating DESC', 'createdAt DESC'],
|
|
212
|
+
limit: 50,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Example: searchProducts('wireless', { minRating: 4, maxPrice: 200, features: ['bluetooth'] })
|
|
218
|
+
//
|
|
219
|
+
// SQL:
|
|
220
|
+
// SELECT *
|
|
221
|
+
// FROM "Product"
|
|
222
|
+
// WHERE "status" = 'active'
|
|
223
|
+
// AND "deleted_at" IS NULL
|
|
224
|
+
// AND (
|
|
225
|
+
// "name" ILIKE '%wireless%'
|
|
226
|
+
// OR "description" ILIKE '%wireless%'
|
|
227
|
+
// OR "metadata" #>> '{keywords}' ILIKE '%wireless%'
|
|
228
|
+
// )
|
|
229
|
+
// AND "rating" >= 4
|
|
230
|
+
// AND "price" <= 200
|
|
231
|
+
// AND "metadata" #>> '{features}' @> '["bluetooth"]'
|
|
232
|
+
// ORDER BY "rating" DESC, "created_at" DESC
|
|
233
|
+
// LIMIT 50
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
## Massive Filter Example
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const massiveFilter: TFilter<TProductSchema> = {
|
|
241
|
+
where: {
|
|
242
|
+
status: 'active',
|
|
243
|
+
deletedAt: { is: null },
|
|
244
|
+
price: { gte: 50, lte: 500 },
|
|
245
|
+
quantity: { gt: 0 },
|
|
246
|
+
tags: { contains: ['electronics', 'portable'] },
|
|
247
|
+
'metadata.priority': { gte: 3 },
|
|
248
|
+
'metadata.features.wireless': true,
|
|
249
|
+
or: [
|
|
250
|
+
{ rating: { gte: 4.5 } },
|
|
251
|
+
{
|
|
252
|
+
and: [
|
|
253
|
+
{ isFeatured: true },
|
|
254
|
+
{ 'metadata.promotion.active': true },
|
|
255
|
+
{ 'metadata.promotion.discount': { gte: 20 } },
|
|
256
|
+
]
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
createdAt: { gte: new Date('2024-12-01') },
|
|
260
|
+
'metadata.isNewArrival': true,
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
category: { nin: ['discontinued', 'recalled'] },
|
|
264
|
+
suppliers: { overlaps: ['supplier-a', 'supplier-b'] },
|
|
265
|
+
},
|
|
266
|
+
fields: ['id', 'name', 'price', 'rating', 'tags', 'metadata'],
|
|
267
|
+
order: ['metadata.priority DESC', 'rating DESC', 'createdAt DESC'],
|
|
268
|
+
limit: 20,
|
|
269
|
+
skip: 0,
|
|
270
|
+
include: [
|
|
271
|
+
{ relation: 'category' },
|
|
272
|
+
{
|
|
273
|
+
relation: 'reviews',
|
|
274
|
+
scope: {
|
|
275
|
+
where: { rating: { gte: 4 } },
|
|
276
|
+
order: ['createdAt DESC'],
|
|
277
|
+
limit: 5,
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const products = await productRepo.find({ filter: massiveFilter });
|
|
284
|
+
|
|
285
|
+
// SQL:
|
|
286
|
+
// SELECT "id", "name", "price", "rating", "tags", "metadata"
|
|
287
|
+
// FROM "Product"
|
|
288
|
+
// WHERE "status" = 'active'
|
|
289
|
+
// AND "deleted_at" IS NULL
|
|
290
|
+
// AND "price" >= 50 AND "price" <= 500
|
|
291
|
+
// AND "quantity" > 0
|
|
292
|
+
// AND "tags"::text[] @> ARRAY['electronics', 'portable']::text[]
|
|
293
|
+
// AND CASE
|
|
294
|
+
// WHEN ("metadata" #>> '{priority}') ~ '^-?[0-9]+(\.[0-9]+)?$'
|
|
295
|
+
// THEN ("metadata" #>> '{priority}')::numeric ELSE NULL
|
|
296
|
+
// END >= 3
|
|
297
|
+
// AND "metadata" #>> '{features,wireless}' = 'true'
|
|
298
|
+
// AND (
|
|
299
|
+
// "rating" >= 4.5
|
|
300
|
+
// OR (
|
|
301
|
+
// "is_featured" = true
|
|
302
|
+
// AND "metadata" #>> '{promotion,active}' = 'true'
|
|
303
|
+
// AND CASE
|
|
304
|
+
// WHEN ("metadata" #>> '{promotion,discount}') ~ '^-?[0-9]+(\.[0-9]+)?$'
|
|
305
|
+
// THEN ("metadata" #>> '{promotion,discount}')::numeric ELSE NULL
|
|
306
|
+
// END >= 20
|
|
307
|
+
// )
|
|
308
|
+
// OR (
|
|
309
|
+
// "created_at" >= '2024-12-01T00:00:00.000Z'
|
|
310
|
+
// AND "metadata" #>> '{isNewArrival}' = 'true'
|
|
311
|
+
// )
|
|
312
|
+
// )
|
|
313
|
+
// AND "category" NOT IN ('discontinued', 'recalled')
|
|
314
|
+
// AND "suppliers"::text[] && ARRAY['supplier-a', 'supplier-b']::text[]
|
|
315
|
+
// ORDER BY "metadata" #> '{priority}' DESC, "rating" DESC, "created_at" DESC
|
|
316
|
+
// LIMIT 20 OFFSET 0
|
|
317
|
+
//
|
|
318
|
+
// -- Separate query for category relation:
|
|
319
|
+
// SELECT * FROM "Category" WHERE "id" IN (...)
|
|
320
|
+
//
|
|
321
|
+
// -- Separate query for reviews relation:
|
|
322
|
+
// SELECT * FROM "Review"
|
|
323
|
+
// WHERE "product_id" IN (...) AND "rating" >= 4
|
|
324
|
+
// ORDER BY "created_at" DESC
|
|
325
|
+
// LIMIT 5
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
## Date Range Queries
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
// Events this week
|
|
333
|
+
const startOfWeek = new Date('2024-12-29');
|
|
334
|
+
const endOfWeek = new Date('2025-01-04');
|
|
335
|
+
|
|
336
|
+
const weekEvents = await eventRepo.find({
|
|
337
|
+
filter: {
|
|
338
|
+
where: {
|
|
339
|
+
eventDate: { between: [startOfWeek, endOfWeek] }
|
|
340
|
+
},
|
|
341
|
+
order: ['eventDate ASC']
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// SQL:
|
|
346
|
+
// SELECT *
|
|
347
|
+
// FROM "Event"
|
|
348
|
+
// WHERE "event_date" BETWEEN '2024-12-29' AND '2025-01-04'
|
|
349
|
+
// ORDER BY "event_date" ASC
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
// Orders in the last 7 days
|
|
354
|
+
const sevenDaysAgo = new Date();
|
|
355
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
356
|
+
|
|
357
|
+
const recentOrders = await orderRepo.find({
|
|
358
|
+
filter: {
|
|
359
|
+
where: {
|
|
360
|
+
createdAt: { gte: sevenDaysAgo },
|
|
361
|
+
status: { in: ['completed', 'shipped'] },
|
|
362
|
+
total: { gte: 100 }
|
|
363
|
+
},
|
|
364
|
+
order: ['total DESC'],
|
|
365
|
+
limit: 100
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// SQL:
|
|
370
|
+
// SELECT *
|
|
371
|
+
// FROM "Order"
|
|
372
|
+
// WHERE "created_at" >= '2024-12-24T00:00:00.000Z'
|
|
373
|
+
// AND "status" IN ('completed', 'shipped')
|
|
374
|
+
// AND "total" >= 100
|
|
375
|
+
// ORDER BY "total" DESC
|
|
376
|
+
// LIMIT 100
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
## Multi-Tenant Data Isolation
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
const getTenantProducts = async (tenantId: string, filter: TFilter<TProductSchema>) => {
|
|
384
|
+
return productRepo.find({
|
|
385
|
+
filter: {
|
|
386
|
+
...filter,
|
|
387
|
+
where: {
|
|
388
|
+
...filter.where,
|
|
389
|
+
tenantId, // Always enforce tenant isolation
|
|
390
|
+
deletedAt: { is: null },
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// Usage
|
|
397
|
+
await getTenantProducts('tenant-abc', {
|
|
398
|
+
where: { category: 'electronics' },
|
|
399
|
+
order: ['createdAt DESC'],
|
|
400
|
+
limit: 20
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// SQL:
|
|
404
|
+
// SELECT *
|
|
405
|
+
// FROM "Product"
|
|
406
|
+
// WHERE "category" = 'electronics'
|
|
407
|
+
// AND "tenant_id" = 'tenant-abc'
|
|
408
|
+
// AND "deleted_at" IS NULL
|
|
409
|
+
// ORDER BY "created_at" DESC
|
|
410
|
+
// LIMIT 20
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
## Inventory Low Stock Alert
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
const lowStockProducts = await productRepo.find({
|
|
418
|
+
filter: {
|
|
419
|
+
where: {
|
|
420
|
+
status: 'active',
|
|
421
|
+
quantity: { lte: 10 },
|
|
422
|
+
'metadata.reorderPoint': { isn: null },
|
|
423
|
+
or: [
|
|
424
|
+
{ quantity: { lt: 5 } }, // Critical: below 5
|
|
425
|
+
{
|
|
426
|
+
and: [
|
|
427
|
+
{ quantity: { lte: 10 } },
|
|
428
|
+
{ 'metadata.fastMoving': true }
|
|
429
|
+
]
|
|
430
|
+
}
|
|
431
|
+
]
|
|
432
|
+
},
|
|
433
|
+
order: ['quantity ASC'],
|
|
434
|
+
fields: ['id', 'name', 'quantity', 'metadata']
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// SQL:
|
|
439
|
+
// SELECT "id", "name", "quantity", "metadata"
|
|
440
|
+
// FROM "Product"
|
|
441
|
+
// WHERE "status" = 'active'
|
|
442
|
+
// AND "quantity" <= 10
|
|
443
|
+
// AND "metadata" #>> '{reorderPoint}' IS NOT NULL
|
|
444
|
+
// AND (
|
|
445
|
+
// "quantity" < 5
|
|
446
|
+
// OR (
|
|
447
|
+
// "quantity" <= 10
|
|
448
|
+
// AND "metadata" #>> '{fastMoving}' = 'true'
|
|
449
|
+
// )
|
|
450
|
+
// )
|
|
451
|
+
// ORDER BY "quantity" ASC
|
|
452
|
+
```
|