@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
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Controllers Reference
|
|
3
|
+
description: Technical reference for controller classes and API endpoints
|
|
4
|
+
difficulty: beginner
|
|
5
|
+
---
|
|
6
|
+
|
|
1
7
|
# Deep Dive: Controllers
|
|
2
8
|
|
|
3
9
|
Technical reference for controller classes - the foundation for creating API endpoints in Ignis.
|
|
@@ -86,8 +92,8 @@ For convenience, `Ignis` provides decorator shortcuts for each HTTP method: Thes
|
|
|
86
92
|
import { get, post, z, jsonContent, jsonResponse, Authentication, TRouteContext, HTTP } from '@venizia/ignis';
|
|
87
93
|
|
|
88
94
|
// Define route configs as const for full type inference
|
|
89
|
-
const
|
|
90
|
-
|
|
95
|
+
const UserRoutes = {
|
|
96
|
+
LIST_USERS: {
|
|
91
97
|
path: '/',
|
|
92
98
|
method: 'get',
|
|
93
99
|
responses: jsonResponse({
|
|
@@ -95,7 +101,7 @@ const USER_ROUTES = {
|
|
|
95
101
|
schema: z.array(z.object({ id: z.string(), name: z.string() })),
|
|
96
102
|
}),
|
|
97
103
|
},
|
|
98
|
-
|
|
104
|
+
GET_USER: {
|
|
99
105
|
path: '/:id',
|
|
100
106
|
method: 'get',
|
|
101
107
|
request: {
|
|
@@ -106,7 +112,7 @@ const USER_ROUTES = {
|
|
|
106
112
|
schema: z.object({ id: z.string(), name: z.string() }),
|
|
107
113
|
}),
|
|
108
114
|
},
|
|
109
|
-
|
|
115
|
+
CREATE_USER: {
|
|
110
116
|
path: '/',
|
|
111
117
|
method: 'post',
|
|
112
118
|
authStrategies: [Authentication.STRATEGY_JWT], // Secure this endpoint
|
|
@@ -123,26 +129,26 @@ const USER_ROUTES = {
|
|
|
123
129
|
|
|
124
130
|
// ... inside a controller class
|
|
125
131
|
|
|
126
|
-
@get({ configs:
|
|
127
|
-
getAllUsers(c: TRouteContext<typeof
|
|
132
|
+
@get({ configs: UserRoutes.LIST_USERS })
|
|
133
|
+
getAllUsers(c: TRouteContext<typeof UserRoutes.LIST_USERS>) { // Return type is automatically inferred
|
|
128
134
|
return c.json([{ id: '1', name: 'John Doe' }], HTTP.ResultCodes.RS_2.Ok);
|
|
129
135
|
}
|
|
130
136
|
|
|
131
|
-
@get({ configs:
|
|
132
|
-
getUserById(c: TRouteContext<typeof
|
|
137
|
+
@get({ configs: UserRoutes.GET_USER })
|
|
138
|
+
getUserById(c: TRouteContext<typeof UserRoutes.GET_USER>) { // Return type is automatically inferred
|
|
133
139
|
const { id } = c.req.valid('param'); // id is typed as string
|
|
134
140
|
return c.json({ id, name: 'John Doe' }, HTTP.ResultCodes.RS_2.Ok);
|
|
135
141
|
}
|
|
136
142
|
|
|
137
|
-
@post({ configs:
|
|
138
|
-
createUser(c: TRouteContext<typeof
|
|
143
|
+
@post({ configs: UserRoutes.CREATE_USER })
|
|
144
|
+
createUser(c: TRouteContext<typeof UserRoutes.CREATE_USER>) { // Return type is automatically inferred
|
|
139
145
|
const { name } = c.req.valid('json'); // name is typed as string
|
|
140
146
|
const newUser = { id: '2', name };
|
|
141
147
|
return c.json(newUser, HTTP.ResultCodes.RS_2.Created); // Return type is validated
|
|
142
148
|
}
|
|
143
149
|
```
|
|
144
150
|
|
|
145
|
-
**Example using shared `
|
|
151
|
+
**Example using shared `RouteConfigs`:**
|
|
146
152
|
|
|
147
153
|
For better organization, you can define all your route configurations in a constant and reference them in your decorators. This approach also allows you to get a typed context for your handler.
|
|
148
154
|
|
|
@@ -150,8 +156,8 @@ For better organization, you can define all your route configurations in a const
|
|
|
150
156
|
import { api, BaseController, controller, TRouteContext, jsonContent, jsonResponse, HTTP } from '@venizia/ignis';
|
|
151
157
|
import { z } from 'hono/zod-openapi';
|
|
152
158
|
|
|
153
|
-
const
|
|
154
|
-
|
|
159
|
+
const RouteConfigs = {
|
|
160
|
+
PING: {
|
|
155
161
|
method: HTTP.Methods.POST,
|
|
156
162
|
path: '/ping',
|
|
157
163
|
request: {
|
|
@@ -167,9 +173,9 @@ const HEALTH_CHECK_ROUTES = {
|
|
|
167
173
|
|
|
168
174
|
@controller({ path: '/health' })
|
|
169
175
|
export class HealthCheckController extends BaseController {
|
|
170
|
-
|
|
171
|
-
@api({ configs:
|
|
172
|
-
ping(c: TRouteContext<typeof
|
|
176
|
+
|
|
177
|
+
@api({ configs: RouteConfigs.PING })
|
|
178
|
+
ping(c: TRouteContext<typeof RouteConfigs.PING>) { // Return type is automatically inferred
|
|
173
179
|
const { message } = c.req.valid('json');
|
|
174
180
|
return c.json({ pong: message }, HTTP.ResultCodes.RS_2.Ok);
|
|
175
181
|
}
|
|
@@ -258,17 +264,17 @@ request: {
|
|
|
258
264
|
The `defineRouteConfigs` function is a simple helper for creating a typed object containing multiple route configurations. This is particularly useful for organizing all of a controller's route definitions in a single, type-checked constant.
|
|
259
265
|
|
|
260
266
|
```typescript
|
|
261
|
-
import { defineRouteConfigs, HTTP, jsonResponse, z } from '@venizia/ignis';
|
|
267
|
+
import { defineRouteConfigs, HTTP, jsonResponse, jsonContent, z } from '@venizia/ignis';
|
|
262
268
|
|
|
263
|
-
const
|
|
264
|
-
|
|
269
|
+
const RouteConfigs = defineRouteConfigs({
|
|
270
|
+
ROOT: {
|
|
265
271
|
method: HTTP.Methods.GET,
|
|
266
272
|
path: '/',
|
|
267
273
|
responses: jsonResponse({
|
|
268
274
|
schema: z.object({ status: z.string() }),
|
|
269
275
|
}),
|
|
270
276
|
},
|
|
271
|
-
|
|
277
|
+
PING: {
|
|
272
278
|
method: HTTP.Methods.POST,
|
|
273
279
|
path: '/ping',
|
|
274
280
|
request: {
|
|
@@ -452,3 +458,27 @@ export class ConfigurationController extends _ConfigurationController {
|
|
|
452
458
|
```
|
|
453
459
|
|
|
454
460
|
By leveraging these structured configuration options and the `ControllerFactory`, you ensure that your API is not only functional but also well-documented, easy to validate, and rapidly deployable for standard CRUD operations.
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## See Also
|
|
465
|
+
|
|
466
|
+
- **Related References:**
|
|
467
|
+
- [Services](./services.md) - Business logic layer called by controllers
|
|
468
|
+
- [Repositories](./repositories/) - Data access layer for CRUD operations
|
|
469
|
+
- [Middlewares](./middlewares.md) - Request/response middleware
|
|
470
|
+
- [Application](./application.md) - Application setup and controller mounting
|
|
471
|
+
- [Dependency Injection](./dependency-injection.md) - DI patterns and injection
|
|
472
|
+
|
|
473
|
+
- **Guides:**
|
|
474
|
+
- [Controllers Guide](/guides/core-concepts/controllers)
|
|
475
|
+
- [Building a CRUD API](/guides/tutorials/building-a-crud-api)
|
|
476
|
+
|
|
477
|
+
- **Best Practices:**
|
|
478
|
+
- [API Usage Examples](/best-practices/api-usage-examples)
|
|
479
|
+
- [Troubleshooting Tips](/best-practices/troubleshooting-tips)
|
|
480
|
+
- [Security Guidelines](/best-practices/security-guidelines)
|
|
481
|
+
|
|
482
|
+
- **External Resources:**
|
|
483
|
+
- [OpenAPI Specification](https://swagger.io/specification/)
|
|
484
|
+
- [HTTP Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: DataSources Reference
|
|
3
|
+
description: Technical reference for DataSource classes and database connections
|
|
4
|
+
difficulty: intermediate
|
|
5
|
+
---
|
|
6
|
+
|
|
1
7
|
# Deep Dive: DataSources
|
|
2
8
|
|
|
3
9
|
Technical reference for DataSource classes - managing database connections in Ignis.
|
|
@@ -338,3 +344,27 @@ try {
|
|
|
338
344
|
> **Note:** For most use cases, prefer using `repository.beginTransaction()` which provides a higher-level API. See [Repositories Reference](./repositories.md#transactions) for details.
|
|
339
345
|
|
|
340
346
|
This architecture ensures that datasources are configured consistently and that the fully-initialized Drizzle connector, aware of all schemas and relations, is available to repositories for querying.
|
|
347
|
+
|
|
348
|
+
## See Also
|
|
349
|
+
|
|
350
|
+
- **Related Concepts:**
|
|
351
|
+
- [DataSources Guide](/guides/core-concepts/persistent/datasources) - Creating DataSources tutorial
|
|
352
|
+
- [Repositories](/guides/core-concepts/persistent/repositories) - Using DataSources for database access
|
|
353
|
+
- [Models](/guides/core-concepts/persistent/models) - Entity schemas loaded by DataSource
|
|
354
|
+
- [Transactions](/guides/core-concepts/persistent/transactions) - Multi-operation database transactions
|
|
355
|
+
|
|
356
|
+
- **References:**
|
|
357
|
+
- [Repositories API](/references/base/repositories/) - Data access layer
|
|
358
|
+
- [Environment Variables](/references/configuration/environment-variables) - Configuration management
|
|
359
|
+
|
|
360
|
+
- **External Resources:**
|
|
361
|
+
- [Drizzle ORM Documentation](https://orm.drizzle.team/) - ORM configuration
|
|
362
|
+
- [node-postgres Documentation](https://node-postgres.com/) - Connection pooling guide
|
|
363
|
+
|
|
364
|
+
- **Best Practices:**
|
|
365
|
+
- [Performance Optimization](/best-practices/performance-optimization) - Connection pool tuning
|
|
366
|
+
- [Security Guidelines](/best-practices/security-guidelines) - Database credential management
|
|
367
|
+
|
|
368
|
+
- **Tutorials:**
|
|
369
|
+
- [Complete Installation](/guides/tutorials/complete-installation) - Database setup
|
|
370
|
+
- [Building a CRUD API](/guides/tutorials/building-a-crud-api) - DataSource configuration
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Dependency Injection Reference
|
|
3
|
+
description: Technical reference for the DI system in IGNIS
|
|
4
|
+
difficulty: advanced
|
|
5
|
+
---
|
|
6
|
+
|
|
1
7
|
# Deep Dive: Dependency Injection
|
|
2
8
|
|
|
3
9
|
Technical reference for the DI system in Ignis - managing resource lifecycles and dependency resolution.
|
|
@@ -18,6 +24,15 @@ Technical reference for the DI system in Ignis - managing resource lifecycles an
|
|
|
18
24
|
| **MetadataRegistry** | Stores decorator metadata | Singleton accessed via `getInstance()` |
|
|
19
25
|
| **Boot System** | Automatic artifact discovery and binding | Integrates with Container via tags and bindings |
|
|
20
26
|
|
|
27
|
+
## Prerequisites
|
|
28
|
+
|
|
29
|
+
Before reading this document, you should understand:
|
|
30
|
+
|
|
31
|
+
- [TypeScript Decorators](https://www.typescriptlang.org/docs/handbook/decorators.html) - How decorators work in TypeScript
|
|
32
|
+
- [IGNIS Application basics](./application.md) - Application lifecycle and initialization
|
|
33
|
+
- [Services](./services.md) and [Controllers](./controllers.md) - Basic understanding of IGNIS architecture
|
|
34
|
+
- Inversion of Control (IoC) pattern - [Martin Fowler's article](https://martinfowler.com/articles/injection.html)
|
|
35
|
+
|
|
21
36
|
## `Container` Class
|
|
22
37
|
|
|
23
38
|
Heart of the DI system - registry managing all application resources.
|
|
@@ -161,8 +176,8 @@ app.bind({ key: 'controllers.UserController' }).toClass(UserController);
|
|
|
161
176
|
@injectable()
|
|
162
177
|
class UserController {
|
|
163
178
|
constructor(
|
|
164
|
-
@inject({ key: 'services.UserService' })
|
|
165
|
-
private
|
|
179
|
+
@inject({ key: 'services.UserService' })
|
|
180
|
+
private _userService: UserService // Auto-injected!
|
|
166
181
|
) {}
|
|
167
182
|
}
|
|
168
183
|
```
|
|
@@ -174,4 +189,25 @@ class UserController {
|
|
|
174
189
|
- **Extensible**: Custom booters integrate seamlessly via tags
|
|
175
190
|
- **Type-safe**: Full TypeScript support throughout boot process
|
|
176
191
|
|
|
177
|
-
> **Learn More:** See [Bootstrapping Concepts](/
|
|
192
|
+
> **Learn More:** See [Bootstrapping Concepts](/guides/core-concepts/application/bootstrapping) and [Boot Package Reference](/references/src-details/boot.md)
|
|
193
|
+
|
|
194
|
+
## See Also
|
|
195
|
+
|
|
196
|
+
- **Related Concepts:**
|
|
197
|
+
- [Dependency Injection Guide](/guides/core-concepts/dependency-injection) - DI fundamentals tutorial
|
|
198
|
+
- [Application](/guides/core-concepts/application/) - Application extends Container
|
|
199
|
+
- [Controllers](/guides/core-concepts/controllers) - Use DI for injecting services
|
|
200
|
+
- [Services](/guides/core-concepts/services) - Use DI for injecting repositories
|
|
201
|
+
- [Providers](/references/base/providers) - Factory pattern for dynamic injection
|
|
202
|
+
|
|
203
|
+
- **References:**
|
|
204
|
+
- [Inversion Helper](/references/helpers/inversion) - DI container utilities
|
|
205
|
+
- [Bootstrapping API](/references/base/bootstrapping) - Auto-discovery and DI
|
|
206
|
+
- [Glossary](/guides/reference/glossary#dependency-injection-di) - DI concepts explained
|
|
207
|
+
|
|
208
|
+
- **Tutorials:**
|
|
209
|
+
- [Testing](/guides/tutorials/testing) - Unit testing with mocked dependencies
|
|
210
|
+
- [Building a CRUD API](/guides/tutorials/building-a-crud-api) - DI in practice
|
|
211
|
+
|
|
212
|
+
- **Best Practices:**
|
|
213
|
+
- [Architectural Patterns](/best-practices/architectural-patterns) - DI patterns and anti-patterns
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Using Filters in Your Application
|
|
3
|
+
description: How filters flow through application layers
|
|
4
|
+
difficulty: intermediate
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Using Filters in Your Application
|
|
8
|
+
|
|
9
|
+
How filters flow through the application layers.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## Architecture Overview
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
+-----------------------------------------------------------------+
|
|
16
|
+
| HTTP Request |
|
|
17
|
+
| GET /products?filter={"where":{"status":"active"},"limit":10} |
|
|
18
|
+
+--------------------------------+--------------------------------+
|
|
19
|
+
|
|
|
20
|
+
v
|
|
21
|
+
+-----------------------------------------------------------------+
|
|
22
|
+
| Controller Layer |
|
|
23
|
+
| - Validates filter via Zod schema |
|
|
24
|
+
| - Parses JSON string -> Filter object |
|
|
25
|
+
| - Passes to service/repository |
|
|
26
|
+
+-----------------------------------------------------------------+
|
|
27
|
+
|
|
|
28
|
+
v
|
|
29
|
+
+-----------------------------------------------------------------+
|
|
30
|
+
| Service Layer (Optional) |
|
|
31
|
+
| - Business logic, authorization |
|
|
32
|
+
| - May modify filter before passing |
|
|
33
|
+
+-----------------------------------------------------------------+
|
|
34
|
+
|
|
|
35
|
+
v
|
|
36
|
+
+-----------------------------------------------------------------+
|
|
37
|
+
| Repository Layer |
|
|
38
|
+
| - FilterBuilder transforms Filter -> SQL |
|
|
39
|
+
| - Executes query via Drizzle ORM |
|
|
40
|
+
| - Returns typed results |
|
|
41
|
+
+-----------------------------------------------------------------+
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## Controller Layer
|
|
46
|
+
|
|
47
|
+
### Using ControllerFactory (Recommended)
|
|
48
|
+
|
|
49
|
+
The `ControllerFactory` automatically handles filter parsing and validation:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// src/controllers/product.controller.ts
|
|
53
|
+
import { Product } from '@/models';
|
|
54
|
+
import { ProductRepository } from '@/repositories';
|
|
55
|
+
import {
|
|
56
|
+
controller,
|
|
57
|
+
ControllerFactory,
|
|
58
|
+
inject,
|
|
59
|
+
BindingKeys,
|
|
60
|
+
BindingNamespaces,
|
|
61
|
+
} from '@venizia/ignis';
|
|
62
|
+
|
|
63
|
+
const BASE_PATH = '/products';
|
|
64
|
+
|
|
65
|
+
const _Controller = ControllerFactory.defineCrudController({
|
|
66
|
+
repository: { name: ProductRepository.name },
|
|
67
|
+
controller: {
|
|
68
|
+
name: 'ProductController',
|
|
69
|
+
basePath: BASE_PATH,
|
|
70
|
+
isStrict: true,
|
|
71
|
+
defaultLimit: 20,
|
|
72
|
+
},
|
|
73
|
+
entity: () => Product,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
@controller({ path: BASE_PATH })
|
|
77
|
+
export class ProductController extends _Controller {
|
|
78
|
+
constructor(
|
|
79
|
+
@inject({
|
|
80
|
+
key: BindingKeys.build({
|
|
81
|
+
namespace: BindingNamespaces.REPOSITORY,
|
|
82
|
+
key: ProductRepository.name,
|
|
83
|
+
}),
|
|
84
|
+
})
|
|
85
|
+
repository: ProductRepository,
|
|
86
|
+
) {
|
|
87
|
+
super(repository);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Generated Endpoints:**
|
|
93
|
+
|
|
94
|
+
| Method | Endpoint | Filter Location |
|
|
95
|
+
|--------|----------|-----------------|
|
|
96
|
+
| GET | `/products` | Query param: `?filter={...}` |
|
|
97
|
+
| GET | `/products/:id` | Query param: `?filter={...}` (for includes) |
|
|
98
|
+
| GET | `/products/one` | Query param: `?filter={...}` |
|
|
99
|
+
| GET | `/products/count` | Query param: `?where={...}` |
|
|
100
|
+
|
|
101
|
+
### Custom Controller with Manual Filter Handling
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
@controller({ path: '/products' })
|
|
105
|
+
export class ProductController extends BaseController {
|
|
106
|
+
constructor(
|
|
107
|
+
@inject({ key: 'repositories.ProductRepository' })
|
|
108
|
+
private _productRepo: ProductRepository,
|
|
109
|
+
) {
|
|
110
|
+
super({ scope: 'ProductController', path: '/products' });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
override binding() {
|
|
114
|
+
this.defineRoute({
|
|
115
|
+
configs: {
|
|
116
|
+
path: '/search',
|
|
117
|
+
method: 'get',
|
|
118
|
+
query: {
|
|
119
|
+
filter: FilterSchema,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
handler: async (context) => {
|
|
123
|
+
const { filter = {} } = context.req.valid('query');
|
|
124
|
+
const results = await this._productRepo.find({ filter });
|
|
125
|
+
return context.json(results);
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
## Service Layer
|
|
134
|
+
|
|
135
|
+
Services can modify filters before passing to repositories:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
@service()
|
|
139
|
+
export class ProductService {
|
|
140
|
+
constructor(
|
|
141
|
+
@inject({ key: 'repositories.ProductRepository' })
|
|
142
|
+
private _productRepo: ProductRepository,
|
|
143
|
+
) {}
|
|
144
|
+
|
|
145
|
+
async findProducts(filter: TFilter<TProductSchema> = {}) {
|
|
146
|
+
// Merge user filter with soft-delete condition
|
|
147
|
+
const enhancedFilter: TFilter<TProductSchema> = {
|
|
148
|
+
...filter,
|
|
149
|
+
where: {
|
|
150
|
+
...filter.where,
|
|
151
|
+
deletedAt: { is: null },
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return this._productRepo.find({ filter: enhancedFilter });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async findProductsForTenant(
|
|
159
|
+
tenantId: string,
|
|
160
|
+
filter: TFilter<TProductSchema> = {},
|
|
161
|
+
) {
|
|
162
|
+
const isolatedFilter: TFilter<TProductSchema> = {
|
|
163
|
+
...filter,
|
|
164
|
+
where: {
|
|
165
|
+
...filter.where,
|
|
166
|
+
tenantId,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return this._productRepo.find({ filter: isolatedFilter });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
## HTTP Request Examples
|
|
177
|
+
|
|
178
|
+
**cURL:**
|
|
179
|
+
```bash
|
|
180
|
+
# Simple filter
|
|
181
|
+
curl "http://localhost:3000/products?filter=%7B%22where%22%3A%7B%22status%22%3A%22active%22%7D%2C%22limit%22%3A10%7D"
|
|
182
|
+
|
|
183
|
+
# Decoded filter: {"where":{"status":"active"},"limit":10}
|
|
184
|
+
|
|
185
|
+
# Complex filter with URL encoding
|
|
186
|
+
curl -G "http://localhost:3000/products" \
|
|
187
|
+
--data-urlencode 'filter={"where":{"price":{"gte":100,"lte":500},"tags":{"contains":["featured"]}},"order":["price ASC"],"limit":20}'
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**JavaScript/TypeScript:**
|
|
191
|
+
```typescript
|
|
192
|
+
// Using fetch
|
|
193
|
+
const filter = {
|
|
194
|
+
where: { status: 'active', price: { lte: 100 } },
|
|
195
|
+
order: ['createdAt DESC'],
|
|
196
|
+
limit: 10,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const response = await fetch(
|
|
200
|
+
`/api/products?filter=${encodeURIComponent(JSON.stringify(filter))}`
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Using axios
|
|
204
|
+
const response = await axios.get('/api/products', {
|
|
205
|
+
params: { filter: JSON.stringify(filter) },
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
## Debugging Filters
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// Enable logging to see generated SQL
|
|
214
|
+
const result = await repo.find({
|
|
215
|
+
filter: complexFilter,
|
|
216
|
+
options: {
|
|
217
|
+
log: { use: true, level: 'debug' },
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Or use buildQuery to inspect without executing
|
|
222
|
+
const queryOptions = repo.buildQuery({ filter: complexFilter });
|
|
223
|
+
console.log('Generated query options:', queryOptions);
|
|
224
|
+
```
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: PostgreSQL Array Operators
|
|
3
|
+
description: Operators for PostgreSQL array columns
|
|
4
|
+
difficulty: intermediate
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# PostgreSQL Array Operators
|
|
8
|
+
|
|
9
|
+
Operators for PostgreSQL array columns (`varchar[]`, `text[]`, `integer[]`, etc.).
|
|
10
|
+
|
|
11
|
+
| Operator | PostgreSQL | Description |
|
|
12
|
+
|----------|------------|-------------|
|
|
13
|
+
| `contains` | `@>` | Array contains **ALL** specified elements |
|
|
14
|
+
| `containedBy` | `<@` | Array is a **subset** of specified elements |
|
|
15
|
+
| `overlaps` | `&&` | Array shares **ANY** element with specified |
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## contains (@>)
|
|
19
|
+
|
|
20
|
+
Find rows where the array column contains **all** specified elements.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// Schema: tags varchar(100)[]
|
|
24
|
+
// Data: Product A has ['electronics', 'featured', 'sale']
|
|
25
|
+
|
|
26
|
+
// Find products with BOTH 'electronics' AND 'featured'
|
|
27
|
+
{ where: { tags: { contains: ['electronics', 'featured'] } } }
|
|
28
|
+
// SQL: "tags"::text[] @> ARRAY['electronics', 'featured']::text[]
|
|
29
|
+
|
|
30
|
+
// Single element
|
|
31
|
+
{ where: { tags: { contains: ['featured'] } } }
|
|
32
|
+
// Matches: ['featured'], ['featured', 'sale'], ['a', 'featured', 'b']
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## containedBy (<@)
|
|
37
|
+
|
|
38
|
+
Find rows where **all** array elements are within the specified set.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// Find products where ALL tags are in the allowed list
|
|
42
|
+
{ where: { tags: { containedBy: ['sale', 'featured', 'new', 'popular'] } } }
|
|
43
|
+
// SQL: "tags"::text[] <@ ARRAY['sale', 'featured', 'new', 'popular']::text[]
|
|
44
|
+
|
|
45
|
+
// Product A ['featured', 'sale'] -> matches (all in list)
|
|
46
|
+
// Product B ['featured', 'clearance'] -> no match ('clearance' not in list)
|
|
47
|
+
// Product C [] -> matches (empty is subset of everything)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## overlaps (&&)
|
|
52
|
+
|
|
53
|
+
Find rows where the arrays share at least one common element.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// Find products with 'premium' OR 'sale' tag
|
|
57
|
+
{ where: { tags: { overlaps: ['premium', 'sale'] } } }
|
|
58
|
+
// SQL: "tags"::text[] && ARRAY['premium', 'sale']::text[]
|
|
59
|
+
|
|
60
|
+
// Product A ['featured', 'sale'] -> matches (has 'sale')
|
|
61
|
+
// Product B ['premium', 'luxury'] -> matches (has 'premium')
|
|
62
|
+
// Product C ['new', 'featured'] -> no match (no overlap)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## Visual Comparison
|
|
67
|
+
|
|
68
|
+
| Product | tags | `contains ['featured']` | `containedBy ['a','b','featured']` | `overlaps ['sale','premium']` |
|
|
69
|
+
|---------|------|------------------------|-----------------------------------|------------------------------|
|
|
70
|
+
| A | `['featured', 'sale']` | Yes | No (has 'sale') | Yes (has 'sale') |
|
|
71
|
+
| B | `['featured']` | Yes | Yes | No |
|
|
72
|
+
| C | `['a', 'b']` | No | Yes | No |
|
|
73
|
+
| D | `['premium']` | No | No | Yes (has 'premium') |
|
|
74
|
+
| E | `[]` | No | Yes (empty subset) | No |
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## Decision Guide
|
|
78
|
+
|
|
79
|
+
| Question | Use |
|
|
80
|
+
|----------|-----|
|
|
81
|
+
| "Must have ALL these tags" | `contains` |
|
|
82
|
+
| "Tags must only be from this list" | `containedBy` |
|
|
83
|
+
| "Must have AT LEAST ONE of these tags" | `overlaps` |
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
## Empty Array Behavior
|
|
87
|
+
|
|
88
|
+
| Operator | Empty Value `[]` | Behavior |
|
|
89
|
+
|----------|------------------|----------|
|
|
90
|
+
| `contains: []` | Returns **ALL** rows | Everything contains empty set |
|
|
91
|
+
| `containedBy: []` | Returns only rows with **empty arrays** | Only `[]` is subset of `[]` |
|
|
92
|
+
| `overlaps: []` | Returns **NO** rows | Nothing overlaps with empty |
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
## Type Handling
|
|
96
|
+
|
|
97
|
+
**String Arrays** (`varchar[]`, `text[]`, `char[]`):
|
|
98
|
+
```typescript
|
|
99
|
+
{ where: { tags: { contains: ['a', 'b'] } } }
|
|
100
|
+
// SQL: "tags"::text[] @> ARRAY['a', 'b']::text[]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Numeric Arrays** (`integer[]`, `numeric[]`):
|
|
104
|
+
```typescript
|
|
105
|
+
{ where: { scores: { contains: [100, 200] } } }
|
|
106
|
+
// SQL: "scores" @> ARRAY[100, 200]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Boolean Arrays**:
|
|
110
|
+
```typescript
|
|
111
|
+
{ where: { flags: { contains: [true, false] } } }
|
|
112
|
+
// SQL: "flags" @> ARRAY[true, false]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
## Defining Array Columns
|
|
117
|
+
|
|
118
|
+
In your Drizzle schema:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { pgTable, text, varchar, integer } from 'drizzle-orm/pg-core';
|
|
122
|
+
|
|
123
|
+
export const productTable = pgTable('Product', {
|
|
124
|
+
id: text('id').primaryKey(),
|
|
125
|
+
name: text('name').notNull(),
|
|
126
|
+
|
|
127
|
+
// Array columns
|
|
128
|
+
tags: varchar('tags', { length: 100 }).array(), // varchar(100)[]
|
|
129
|
+
categories: text('categories').array(), // text[]
|
|
130
|
+
scores: integer('scores').array(), // integer[]
|
|
131
|
+
});
|
|
132
|
+
```
|