@venizia/ignis-docs 0.0.8 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/content/best-practices/api-usage-examples.md +15 -12
- package/content/best-practices/architectural-patterns.md +70 -78
- package/content/best-practices/architecture-decisions.md +91 -60
- package/content/best-practices/code-style-standards/advanced-patterns.md +56 -44
- package/content/best-practices/code-style-standards/constants-configuration.md +11 -11
- package/content/best-practices/code-style-standards/control-flow.md +5 -2
- package/content/best-practices/code-style-standards/documentation.md +13 -13
- package/content/best-practices/code-style-standards/function-patterns.md +9 -10
- package/content/best-practices/code-style-standards/index.md +1 -1
- package/content/best-practices/code-style-standards/naming-conventions.md +10 -8
- package/content/best-practices/code-style-standards/route-definitions.md +30 -12
- package/content/best-practices/code-style-standards/tooling.md +8 -5
- package/content/best-practices/code-style-standards/type-safety.md +13 -12
- package/content/best-practices/common-pitfalls.md +56 -37
- package/content/best-practices/contribution-workflow.md +13 -14
- package/content/best-practices/data-modeling.md +44 -20
- package/content/best-practices/deployment-strategies.md +28 -27
- package/content/best-practices/error-handling.md +48 -24
- package/content/best-practices/index.md +5 -5
- package/content/best-practices/performance-optimization.md +36 -28
- package/content/best-practices/security-guidelines.md +52 -23
- package/content/best-practices/testing-strategies.md +65 -51
- package/content/best-practices/troubleshooting-tips.md +24 -24
- package/content/extensions/components/{swagger.md → api-reference.md} +40 -31
- package/content/extensions/components/authentication/api.md +19 -19
- package/content/extensions/components/authentication/errors.md +7 -7
- package/content/extensions/components/authentication/index.md +10 -8
- package/content/extensions/components/authentication/usage.md +101 -6
- package/content/extensions/components/authorization/api.md +45 -25
- package/content/extensions/components/authorization/errors.md +6 -6
- package/content/extensions/components/authorization/index.md +11 -10
- package/content/extensions/components/authorization/usage.md +21 -21
- package/content/extensions/components/health-check.md +1 -1
- package/content/extensions/components/index.md +5 -5
- package/content/extensions/components/mail/errors.md +15 -15
- package/content/extensions/components/mail/index.md +1 -2
- package/content/extensions/components/mail/usage.md +1 -1
- package/content/extensions/components/request-tracker.md +1 -1
- package/content/extensions/components/socket-io/api.md +9 -9
- package/content/extensions/components/socket-io/errors.md +5 -5
- package/content/extensions/components/socket-io/index.md +8 -8
- package/content/extensions/components/socket-io/usage.md +1 -1
- package/content/extensions/components/static-asset/api.md +17 -4
- package/content/extensions/components/static-asset/errors.md +4 -4
- package/content/extensions/components/static-asset/index.md +26 -28
- package/content/extensions/components/static-asset/usage.md +13 -12
- package/content/extensions/components/template/index.md +2 -2
- package/content/extensions/components/template/setup-page.md +1 -1
- package/content/extensions/components/websocket/api.md +3 -3
- package/content/extensions/components/websocket/errors.md +5 -5
- package/content/extensions/components/websocket/index.md +5 -5
- package/content/extensions/components/websocket/usage.md +3 -3
- package/content/extensions/helpers/cron/index.md +2 -2
- package/content/extensions/helpers/crypto/index.md +1 -1
- package/content/extensions/helpers/env/index.md +27 -12
- package/content/extensions/helpers/error/index.md +81 -25
- package/content/extensions/helpers/index.md +2 -3
- package/content/extensions/helpers/inversion/index.md +15 -7
- package/content/extensions/helpers/kafka/examples.md +1 -1
- package/content/extensions/helpers/logger/index.md +32 -2
- package/content/extensions/helpers/network/index.md +6 -0
- package/content/extensions/helpers/queue/index.md +14 -17
- package/content/extensions/helpers/redis/index.md +548 -323
- package/content/extensions/helpers/socket-io/index.md +14 -10
- package/content/extensions/helpers/storage/api.md +44 -8
- package/content/extensions/helpers/storage/index.md +43 -7
- package/content/extensions/helpers/template/index.md +6 -3
- package/content/extensions/helpers/types/index.md +11 -8
- package/content/extensions/helpers/websocket/api.md +9 -9
- package/content/extensions/helpers/websocket/index.md +7 -7
- package/content/extensions/helpers/worker-thread/index.md +2 -2
- package/content/extensions/index.md +3 -4
- package/content/extensions/src-details/mcp-server.md +18 -24
- package/content/guides/core-concepts/application/bootstrapping.md +11 -14
- package/content/guides/core-concepts/application/index.md +3 -3
- package/content/guides/core-concepts/components.md +19 -10
- package/content/guides/core-concepts/dependency-injection.md +6 -3
- package/content/guides/core-concepts/grpc-controllers.md +6 -5
- package/content/guides/core-concepts/persistent/datasources.md +33 -27
- package/content/guides/core-concepts/persistent/index.md +16 -5
- package/content/guides/core-concepts/persistent/models.md +24 -20
- package/content/guides/core-concepts/persistent/postgres-drivers.md +167 -0
- package/content/guides/core-concepts/persistent/repositories.md +40 -23
- package/content/guides/core-concepts/persistent/search-meilisearch.md +183 -0
- package/content/guides/core-concepts/persistent/search-typesense.md +429 -0
- package/content/guides/core-concepts/persistent/transactions.md +61 -25
- package/content/guides/core-concepts/rest-controllers.md +12 -9
- package/content/guides/core-concepts/services.md +330 -60
- package/content/guides/get-started/5-minute-quickstart.md +15 -15
- package/content/guides/get-started/philosophy.md +36 -36
- package/content/guides/get-started/setup.md +3 -3
- package/content/guides/index.md +3 -3
- package/content/guides/migrations/redis-helpers-migration.md +177 -0
- package/content/guides/migrations/scoped-rbac-migration.md +17 -17
- package/content/guides/migrations/unified-connectors-migration.md +113 -0
- package/content/guides/reference/glossary.md +19 -12
- package/content/guides/reference/mcp-docs-server.md +22 -18
- package/content/guides/tutorials/building-a-crud-api.md +30 -33
- package/content/guides/tutorials/complete-installation.md +17 -17
- package/content/guides/tutorials/ecommerce-api.md +158 -119
- package/content/guides/tutorials/realtime-chat.md +176 -130
- package/content/guides/tutorials/testing.md +65 -523
- package/content/index.md +2 -180
- package/content/public/apple-touch-icon.png +0 -0
- package/content/public/og-image.png +0 -0
- package/content/public/site.webmanifest +11 -0
- package/content/references/base/application.md +4 -5
- package/content/references/base/bootstrapping.md +18 -5
- package/content/references/base/components.md +149 -120
- package/content/references/base/connectors.md +178 -0
- package/content/references/base/controllers.md +41 -30
- package/content/references/base/datasources.md +154 -81
- package/content/references/base/dependency-injection.md +34 -22
- package/content/references/base/filter-system/application-usage.md +17 -14
- package/content/references/base/filter-system/array-operators.md +7 -2
- package/content/references/base/filter-system/comparison-operators.md +3 -0
- package/content/references/base/filter-system/default-filter.md +89 -71
- package/content/references/base/filter-system/fields-order-pagination.md +22 -22
- package/content/references/base/filter-system/index.md +6 -3
- package/content/references/base/filter-system/json-filtering.md +20 -1
- package/content/references/base/filter-system/list-operators.md +1 -1
- package/content/references/base/filter-system/logical-operators.md +33 -1
- package/content/references/base/filter-system/null-operators.md +30 -1
- package/content/references/base/filter-system/quick-reference.md +23 -4
- package/content/references/base/filter-system/tips.md +5 -5
- package/content/references/base/filter-system/use-cases.md +12 -12
- package/content/references/base/grpc-controllers.md +13 -13
- package/content/references/base/index.md +24 -12
- package/content/references/base/middlewares.md +265 -327
- package/content/references/base/models.md +63 -49
- package/content/references/base/providers.md +136 -130
- package/content/references/base/repositories/advanced.md +59 -58
- package/content/references/base/repositories/index.md +115 -91
- package/content/references/base/repositories/mixins.md +55 -291
- package/content/references/base/repositories/relations.md +54 -64
- package/content/references/base/repositories/soft-deletable.md +31 -30
- package/content/references/base/services.md +296 -93
- package/content/references/configuration/environment-variables.md +46 -30
- package/content/references/configuration/index.md +6 -6
- package/content/references/index.md +17 -12
- package/content/references/quick-reference.md +65 -106
- package/content/references/utilities/crypto.md +65 -23
- package/content/references/utilities/index.md +3 -3
- package/content/references/utilities/jsx.md +6 -4
- package/content/references/utilities/module.md +68 -20
- package/content/references/utilities/parse.md +4 -14
- package/content/references/utilities/promise.md +9 -7
- package/content/references/utilities/schema.md +5 -3
- package/dist/mcp-server/common/guards.d.ts +8 -0
- package/dist/mcp-server/common/guards.d.ts.map +1 -0
- package/dist/mcp-server/common/guards.js +14 -0
- package/dist/mcp-server/common/guards.js.map +1 -0
- package/dist/mcp-server/common/index.d.ts +1 -0
- package/dist/mcp-server/common/index.d.ts.map +1 -1
- package/dist/mcp-server/common/index.js +1 -0
- package/dist/mcp-server/common/index.js.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.js +4 -2
- package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
- package/dist/mcp-server/helpers/github.helper.js +1 -1
- package/dist/mcp-server/index.js +7 -2
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/mcp-server/tools/base.tool.d.ts +6 -2
- package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/base.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.js +4 -1
- package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +3 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
- package/package.json +9 -9
- package/content/extensions/helpers/testing/index.md +0 -510
- package/content/references/base/middleware.md +0 -347
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Architecture Decisions Guide
|
|
2
2
|
|
|
3
|
-
This guide helps you make informed architectural decisions when building applications with
|
|
3
|
+
This guide helps you make informed architectural decisions when building applications with IGNIS. Learn when to use different patterns and how to scale your application.
|
|
4
4
|
|
|
5
5
|
## Common Decision Points
|
|
6
6
|
|
|
@@ -24,14 +24,14 @@ This guide helps you make informed architectural decisions when building applica
|
|
|
24
24
|
export class ItemController extends BaseRestController {
|
|
25
25
|
constructor(
|
|
26
26
|
@inject({ key: 'repositories.ItemRepository' })
|
|
27
|
-
private
|
|
27
|
+
private itemRepository: ItemRepository,
|
|
28
28
|
) {
|
|
29
29
|
super({ scope: 'ItemController', path: '/items' });
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
@get({ configs:
|
|
32
|
+
@get({ configs: RouteConfigs.GET_ITEM_BY_ID })
|
|
33
33
|
async getItem(c: Context) {
|
|
34
|
-
const item = await this.
|
|
34
|
+
const item = await this.itemRepository.findById({ id: c.req.param('id') });
|
|
35
35
|
return c.json(item);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -56,7 +56,7 @@ export class OrderController extends BaseRestController {
|
|
|
56
56
|
super({ scope: 'OrderController', path: '/orders' });
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
@post({ configs:
|
|
59
|
+
@post({ configs: RouteConfigs.CREATE_ORDER })
|
|
60
60
|
async createOrder(c: Context) {
|
|
61
61
|
const data = await c.req.json();
|
|
62
62
|
// Service handles: validation, inventory check, payment, notifications
|
|
@@ -96,22 +96,28 @@ export class OrderController extends BaseRestController {
|
|
|
96
96
|
|
|
97
97
|
```typescript
|
|
98
98
|
// Component: Self-contained, configurable, reusable
|
|
99
|
-
@component({ scope: 'NotificationComponent' })
|
|
100
99
|
export class NotificationComponent extends BaseComponent {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
100
|
+
constructor(
|
|
101
|
+
@inject({ key: CoreBindings.APPLICATION_INSTANCE }) private application: BaseApplication,
|
|
102
|
+
) {
|
|
103
|
+
super({
|
|
104
|
+
scope: NotificationComponent.name,
|
|
105
|
+
initDefault: { enable: true, container: application },
|
|
106
|
+
bindings: {
|
|
107
|
+
// Default configuration - applications can rebind to override
|
|
108
|
+
'notification.options': Binding.bind({ key: 'notification.options' }).toValue({
|
|
109
|
+
email: { enabled: true },
|
|
110
|
+
sms: { enabled: false },
|
|
111
|
+
}),
|
|
112
|
+
},
|
|
113
|
+
});
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
// Called when the application configures components (registerComponents)
|
|
117
|
+
override binding(): void {
|
|
118
|
+
const options = this.application.get({ key: 'notification.options' });
|
|
119
|
+
// Register notification services based on the resolved options
|
|
120
|
+
this.application.service(EmailNotificationService);
|
|
115
121
|
}
|
|
116
122
|
}
|
|
117
123
|
```
|
|
@@ -126,7 +132,7 @@ export class NotificationComponent extends BaseComponent {
|
|
|
126
132
|
// Inline: Simple, one-off, no need for abstraction
|
|
127
133
|
@controller({ path: '/health' })
|
|
128
134
|
export class HealthController extends BaseRestController {
|
|
129
|
-
@get({ configs:
|
|
135
|
+
@get({ configs: RouteConfigs.HEALTH_CHECK })
|
|
130
136
|
healthCheck(c: Context) {
|
|
131
137
|
return c.json({ status: 'ok', timestamp: new Date() });
|
|
132
138
|
}
|
|
@@ -146,17 +152,19 @@ export class HealthController extends BaseRestController {
|
|
|
146
152
|
|
|
147
153
|
### Start with Standard CRUD
|
|
148
154
|
|
|
149
|
-
Every repository gets these methods from `
|
|
155
|
+
Every repository gets these methods from `DefaultCRUDRepository`:
|
|
150
156
|
|
|
151
157
|
```typescript
|
|
152
|
-
// Inherited methods - use these first
|
|
153
|
-
find(filter)
|
|
154
|
-
findById(id)
|
|
155
|
-
findOne(filter)
|
|
156
|
-
create(data)
|
|
157
|
-
updateById(id, data)
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
// Inherited methods (options-object API) - use these first
|
|
159
|
+
find({ filter }) // List with filters
|
|
160
|
+
findById({ id }) // Get by ID
|
|
161
|
+
findOne({ filter }) // Get first match
|
|
162
|
+
create({ data }) // Create new
|
|
163
|
+
updateById({ id, data }) // Update existing
|
|
164
|
+
updateAll({ data, where }) // Bulk update (updateBy is an alias)
|
|
165
|
+
deleteById({ id }) // Delete
|
|
166
|
+
deleteAll({ where }) // Bulk delete (deleteBy is an alias)
|
|
167
|
+
count({ where }) // Count matches
|
|
160
168
|
```
|
|
161
169
|
|
|
162
170
|
### Add Custom Methods When:
|
|
@@ -167,22 +175,24 @@ count(filter) // Count matches
|
|
|
167
175
|
|
|
168
176
|
```typescript
|
|
169
177
|
// Custom repository methods
|
|
170
|
-
export class OrderRepository extends
|
|
178
|
+
export class OrderRepository extends DefaultCRUDRepository<typeof Order.schema> {
|
|
171
179
|
// Complex query that's used in multiple places
|
|
172
180
|
async findPendingOrdersOlderThan(hours: number) {
|
|
173
181
|
const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000);
|
|
174
182
|
return this.find({
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
183
|
+
filter: {
|
|
184
|
+
where: {
|
|
185
|
+
status: 'pending',
|
|
186
|
+
createdAt: { lt: cutoff },
|
|
187
|
+
},
|
|
188
|
+
order: ['createdAt ASC'],
|
|
178
189
|
},
|
|
179
|
-
orderBy: { createdAt: 'asc' },
|
|
180
190
|
});
|
|
181
191
|
}
|
|
182
192
|
|
|
183
|
-
// Performance-optimized query
|
|
193
|
+
// Performance-optimized query (raw SQL through the Drizzle connector)
|
|
184
194
|
async getOrderStats(userId: string) {
|
|
185
|
-
return this.
|
|
195
|
+
return this.dataSource.connector.execute(sql`
|
|
186
196
|
SELECT
|
|
187
197
|
COUNT(*) as total,
|
|
188
198
|
SUM(total) as revenue,
|
|
@@ -194,9 +204,12 @@ export class OrderRepository extends BaseRepository<Order> {
|
|
|
194
204
|
|
|
195
205
|
// Business logic at data layer
|
|
196
206
|
async softDelete(id: string) {
|
|
197
|
-
return this.updateById(
|
|
198
|
-
|
|
199
|
-
|
|
207
|
+
return this.updateById({
|
|
208
|
+
id,
|
|
209
|
+
data: {
|
|
210
|
+
deletedAt: new Date(),
|
|
211
|
+
status: 'deleted',
|
|
212
|
+
},
|
|
200
213
|
});
|
|
201
214
|
}
|
|
202
215
|
}
|
|
@@ -210,15 +223,15 @@ export class OrderRepository extends BaseRepository<Order> {
|
|
|
210
223
|
```typescript
|
|
211
224
|
@controller({ path: '/users' })
|
|
212
225
|
export class UserController extends BaseRestController {
|
|
213
|
-
@post({ configs:
|
|
226
|
+
@post({ configs: RouteConfigs.CREATE_USER })
|
|
214
227
|
async createUser(c: Context) {
|
|
215
228
|
try {
|
|
216
229
|
const data = await c.req.json();
|
|
217
230
|
const user = await this.userService.create(data);
|
|
218
231
|
return c.json(user, 201);
|
|
219
232
|
} catch (error) {
|
|
220
|
-
// Format error for API response
|
|
221
|
-
if (error.
|
|
233
|
+
// Format error for API response -- messageCode is always lower-cased by ApplicationError
|
|
234
|
+
if (error instanceof ApplicationError && error.messageCode === 'app.user.duplicate_email') {
|
|
222
235
|
return c.json({ error: 'Email already exists' }, 400);
|
|
223
236
|
}
|
|
224
237
|
throw error; // Let global handler catch unknown errors
|
|
@@ -230,23 +243,29 @@ export class UserController extends BaseRestController {
|
|
|
230
243
|
### Service Level: Throw Domain Errors
|
|
231
244
|
|
|
232
245
|
```typescript
|
|
233
|
-
@injectable()
|
|
234
246
|
export class UserService extends BaseService {
|
|
247
|
+
constructor(
|
|
248
|
+
@inject({ key: 'repositories.UserRepository' })
|
|
249
|
+
private userRepository: UserRepository,
|
|
250
|
+
) {
|
|
251
|
+
super({ scope: UserService.name });
|
|
252
|
+
}
|
|
253
|
+
|
|
235
254
|
async create(data: CreateUserInput) {
|
|
236
255
|
// Validate and throw domain-specific errors
|
|
237
|
-
const existing = await this.
|
|
256
|
+
const existing = await this.userRepository.findByEmail(data.email);
|
|
238
257
|
if (existing) {
|
|
239
258
|
throw getError({
|
|
240
259
|
statusCode: 400,
|
|
241
|
-
|
|
260
|
+
messageCode: MessageCode.build({ parts: ['app', 'user', 'duplicate_email'] }),
|
|
242
261
|
message: 'User with this email already exists',
|
|
243
262
|
});
|
|
244
263
|
}
|
|
245
264
|
|
|
246
265
|
// Log operations
|
|
247
|
-
this.logger.info('Creating user
|
|
266
|
+
this.logger.info('Creating user | email: %s', data.email);
|
|
248
267
|
|
|
249
|
-
return this.
|
|
268
|
+
return this.userRepository.create({ data });
|
|
250
269
|
}
|
|
251
270
|
}
|
|
252
271
|
```
|
|
@@ -254,11 +273,11 @@ export class UserService extends BaseService {
|
|
|
254
273
|
### Repository Level: Let Errors Bubble
|
|
255
274
|
|
|
256
275
|
```typescript
|
|
257
|
-
export class UserRepository extends
|
|
276
|
+
export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
|
|
258
277
|
// Don't catch database errors here
|
|
259
278
|
// Let them bubble up to service/controller
|
|
260
279
|
async findByEmail(email: string) {
|
|
261
|
-
return this.findOne({ where: { email } });
|
|
280
|
+
return this.findOne({ filter: { where: { email } } });
|
|
262
281
|
}
|
|
263
282
|
}
|
|
264
283
|
```
|
|
@@ -356,25 +375,27 @@ OrderStatusRepository // Probably doesn't need its own repo
|
|
|
356
375
|
|
|
357
376
|
```typescript
|
|
358
377
|
// Use repository methods for most cases
|
|
359
|
-
const orders = await
|
|
378
|
+
const orders = await orderRepository.find({ filter: { where: { userId } } });
|
|
379
|
+
|
|
380
|
+
// Use raw queries (via the datasource's Drizzle connector) for:
|
|
381
|
+
const connector = this.dataSource.connector;
|
|
360
382
|
|
|
361
|
-
// Use raw queries for:
|
|
362
383
|
// 1. Complex aggregations
|
|
363
|
-
const stats = await
|
|
384
|
+
const stats = await connector.execute(sql`
|
|
364
385
|
SELECT category, COUNT(*), AVG(price)
|
|
365
386
|
FROM products
|
|
366
387
|
GROUP BY category
|
|
367
388
|
`);
|
|
368
389
|
|
|
369
390
|
// 2. Performance-critical paths
|
|
370
|
-
const results = await
|
|
391
|
+
const results = await connector.execute(sql`
|
|
371
392
|
SELECT * FROM products
|
|
372
393
|
WHERE tsv @@ plainto_tsquery(${search})
|
|
373
394
|
LIMIT 10
|
|
374
395
|
`);
|
|
375
396
|
|
|
376
397
|
// 3. Database-specific features
|
|
377
|
-
const nearby = await
|
|
398
|
+
const nearby = await connector.execute(sql`
|
|
378
399
|
SELECT * FROM stores
|
|
379
400
|
WHERE ST_DWithin(location, ${point}, 5000)
|
|
380
401
|
`);
|
|
@@ -386,14 +407,16 @@ const nearby = await db.execute(sql`
|
|
|
386
407
|
### Environment Variables
|
|
387
408
|
|
|
388
409
|
```typescript
|
|
410
|
+
import { applicationEnvironment } from '@venizia/ignis-helpers';
|
|
411
|
+
|
|
389
412
|
// Use for: secrets, environment-specific values
|
|
390
413
|
const config = {
|
|
391
414
|
database: {
|
|
392
|
-
host:
|
|
393
|
-
password:
|
|
415
|
+
host: applicationEnvironment.get<string>('APP_ENV_POSTGRES_HOST'),
|
|
416
|
+
password: applicationEnvironment.get<string>('APP_ENV_POSTGRES_PASSWORD'),
|
|
394
417
|
},
|
|
395
418
|
stripe: {
|
|
396
|
-
secretKey:
|
|
419
|
+
secretKey: applicationEnvironment.get<string>('APP_ENV_STRIPE_SECRET_KEY'),
|
|
397
420
|
},
|
|
398
421
|
};
|
|
399
422
|
```
|
|
@@ -417,11 +440,19 @@ const appConfig = {
|
|
|
417
440
|
|
|
418
441
|
```typescript
|
|
419
442
|
// Use for: component-specific settings
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
443
|
+
// Components read their options from bindings - rebind before registering
|
|
444
|
+
this.bind<IApiReferenceOptions>({ key: ApiReferenceBindingKeys.API_REFERENCE_OPTIONS }).toValue({
|
|
445
|
+
restOptions: {
|
|
446
|
+
base: { path: '/doc' },
|
|
447
|
+
doc: { path: '/openapi.json' },
|
|
448
|
+
ui: { path: '/explorer', type: 'swagger' },
|
|
449
|
+
},
|
|
450
|
+
explorer: {
|
|
451
|
+
openapi: '3.1.0',
|
|
452
|
+
info: { title: 'My API', version: '1.0.0', description: 'My API documentation' },
|
|
453
|
+
},
|
|
424
454
|
});
|
|
455
|
+
this.component(ApiReferenceComponent);
|
|
425
456
|
```
|
|
426
457
|
|
|
427
458
|
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# Advanced Patterns
|
|
2
2
|
|
|
3
|
-
Advanced TypeScript patterns used throughout the
|
|
3
|
+
Advanced TypeScript patterns used throughout the IGNIS framework.
|
|
4
4
|
|
|
5
5
|
## Mixin Pattern
|
|
6
6
|
|
|
7
7
|
Create reusable class extensions without deep inheritance:
|
|
8
8
|
|
|
9
9
|
```typescript
|
|
10
|
-
import { TMixinTarget } from '@venizia/ignis-helpers';
|
|
10
|
+
import { LoggerFactory, TMixinTarget } from '@venizia/ignis-helpers';
|
|
11
11
|
|
|
12
|
-
export const LoggableMixin = <BaseClass extends TMixinTarget<
|
|
12
|
+
export const LoggableMixin = <BaseClass extends TMixinTarget<object>>(
|
|
13
13
|
baseClass: BaseClass,
|
|
14
14
|
) => {
|
|
15
15
|
return class extends baseClass {
|
|
16
|
-
protected logger = LoggerFactory.getLogger(this.constructor.name);
|
|
16
|
+
protected logger = LoggerFactory.getLogger([this.constructor.name]);
|
|
17
17
|
|
|
18
18
|
log(message: string): void {
|
|
19
19
|
this.logger.info(message);
|
|
@@ -32,7 +32,7 @@ class MyService extends LoggableMixin(BaseService) {
|
|
|
32
32
|
### Multiple Mixins
|
|
33
33
|
|
|
34
34
|
```typescript
|
|
35
|
-
class
|
|
35
|
+
class MyService extends LoggableMixin(CacheableMixin(BaseService)) {
|
|
36
36
|
// Has both logging and caching capabilities
|
|
37
37
|
}
|
|
38
38
|
```
|
|
@@ -61,46 +61,51 @@ export const TimestampMixin = <
|
|
|
61
61
|
Generate classes dynamically with configuration:
|
|
62
62
|
|
|
63
63
|
```typescript
|
|
64
|
-
class ControllerFactory {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
class ControllerFactory extends BaseHelper {
|
|
65
|
+
/** `TDataObject`/`TPersistObject` cannot be inferred from `entity` -
|
|
66
|
+
* pass them explicitly for typed CRUD handlers. */
|
|
67
|
+
static defineCrudController<
|
|
68
|
+
TDataObject extends object = object,
|
|
69
|
+
TPersistObject extends object = TDataObject,
|
|
70
|
+
>(defOpts: ICrudControllerOptions) {
|
|
71
|
+
const { controller, entity } = defOpts;
|
|
72
|
+
|
|
73
|
+
// `entity` accepts a class directly or a resolver function
|
|
74
|
+
const entityClass = isClass(entity) ? entity : entity();
|
|
75
|
+
const entityInstance = new entityClass();
|
|
76
|
+
|
|
77
|
+
// Derive request/response schemas + route configs from the entity instance
|
|
78
|
+
const routeDefinitions = buildRouteDefinitions({ entity: entityInstance });
|
|
79
|
+
|
|
68
80
|
return class extends BaseRestController {
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
repository: AbstractRepository<TDataObject, TPersistObject>;
|
|
82
|
+
|
|
83
|
+
constructor(repository: AbstractRepository<TDataObject, TPersistObject>) {
|
|
84
|
+
super({ scope: controller.name, path: controller.basePath });
|
|
71
85
|
this.repository = repository;
|
|
72
|
-
this.setupRoutes();
|
|
73
86
|
}
|
|
74
87
|
|
|
75
|
-
|
|
76
|
-
|
|
88
|
+
/** Registers all CRUD route handlers. */
|
|
89
|
+
override binding(): ValueOrPromise<void> {
|
|
77
90
|
this.defineRoute({
|
|
78
|
-
configs:
|
|
79
|
-
handler:
|
|
91
|
+
configs: routeDefinitions.FIND,
|
|
92
|
+
handler: async context => this.find({ context }),
|
|
80
93
|
});
|
|
81
94
|
this.defineRoute({
|
|
82
|
-
configs:
|
|
83
|
-
handler:
|
|
95
|
+
configs: routeDefinitions.FIND_BY_ID,
|
|
96
|
+
handler: async context => this.findById({ context }),
|
|
84
97
|
});
|
|
85
|
-
// ... more routes
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async list(c: Context) {
|
|
89
|
-
const data = await this.repository.find({});
|
|
90
|
-
return c.json(data);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async getById(c: Context) {
|
|
94
|
-
const { id } = c.req.param();
|
|
95
|
-
const data = await this.repository.findById({ id });
|
|
96
|
-
return c.json(data);
|
|
98
|
+
// ... more routes (count/findOne/create/updateById/deleteById/...)
|
|
97
99
|
}
|
|
98
100
|
};
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
// Usage
|
|
103
|
-
|
|
104
|
+
// Usage - type parameters are explicit, entity can be a class or a resolver
|
|
105
|
+
type TUser = typeof User.schema.$inferSelect;
|
|
106
|
+
type TNewUser = typeof User.schema.$inferInsert;
|
|
107
|
+
|
|
108
|
+
const UserCrudController = ControllerFactory.defineCrudController<TUser, TNewUser>({
|
|
104
109
|
controller: { name: 'UserController', basePath: '/users' },
|
|
105
110
|
repository: { name: UserRepository.name },
|
|
106
111
|
entity: () => User,
|
|
@@ -117,26 +122,33 @@ export class UserController extends UserCrudController {
|
|
|
117
122
|
Support multiple input types that resolve to a single value:
|
|
118
123
|
|
|
119
124
|
```typescript
|
|
120
|
-
// Type definitions
|
|
121
|
-
export type TResolver<T> = () => T;
|
|
122
|
-
export type
|
|
123
|
-
export type TValueOrResolver<T> = T | TResolver<T> | TConstructor<T>;
|
|
125
|
+
// Type definitions (from @venizia/ignis-helpers)
|
|
126
|
+
export type TResolver<T> = (...args: any[]) => T;
|
|
127
|
+
export type TValueOrResolver<T> = T | TResolver<T>;
|
|
124
128
|
|
|
125
129
|
// Resolver function
|
|
126
130
|
export const resolveValue = <T>(valueOrResolver: TValueOrResolver<T>): T => {
|
|
127
131
|
if (typeof valueOrResolver !== 'function') {
|
|
128
132
|
return valueOrResolver; // Direct value
|
|
129
133
|
}
|
|
130
|
-
|
|
134
|
+
|
|
135
|
+
if (isClass(valueOrResolver as Function)) {
|
|
131
136
|
return valueOrResolver as T; // Class constructor (return as-is)
|
|
132
137
|
}
|
|
138
|
+
|
|
133
139
|
return (valueOrResolver as TResolver<T>)(); // Function resolver
|
|
134
140
|
};
|
|
135
141
|
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
// isClass (from @venizia/ignis-inversion, re-exported by helpers) - distinguishes class
|
|
143
|
+
// constructors from arrow/regular functions by testing the SOURCE against /^class[\s{]/,
|
|
144
|
+
// since every non-arrow function has a prototype
|
|
145
|
+
export const isClass = <T>(target: any): target is TClass<T> => {
|
|
146
|
+
if (typeof target !== 'function' || target.prototype === undefined) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return /^class[\s{]/.test(Function.prototype.toString.call(target));
|
|
151
|
+
};
|
|
140
152
|
```
|
|
141
153
|
|
|
142
154
|
### Usage
|
|
@@ -222,7 +234,7 @@ class StrategyRegistry<T> {
|
|
|
222
234
|
|
|
223
235
|
register(name: string, strategy: T): void {
|
|
224
236
|
if (this.strategies.has(name)) {
|
|
225
|
-
throw
|
|
237
|
+
throw getError({ message: `[register] Strategy '${name}' already registered` });
|
|
226
238
|
}
|
|
227
239
|
this.strategies.set(name, strategy);
|
|
228
240
|
}
|
|
@@ -230,7 +242,7 @@ class StrategyRegistry<T> {
|
|
|
230
242
|
get(name: string): T {
|
|
231
243
|
const strategy = this.strategies.get(name);
|
|
232
244
|
if (!strategy) {
|
|
233
|
-
throw
|
|
245
|
+
throw getError({ message: `[get] Strategy '${name}' not found` });
|
|
234
246
|
}
|
|
235
247
|
return strategy;
|
|
236
248
|
}
|
|
@@ -255,5 +267,5 @@ const strategy = authRegistry.get('jwt');
|
|
|
255
267
|
## See Also
|
|
256
268
|
|
|
257
269
|
- [Type Safety](./type-safety) - Generic type patterns
|
|
258
|
-
- [Repositories Reference](../../references/base/repositories/) -
|
|
270
|
+
- [Repositories Reference](../../references/base/repositories/) - Repository hierarchy
|
|
259
271
|
- [Architectural Patterns](../architectural-patterns) - High-level patterns
|
|
@@ -18,7 +18,6 @@ export class Authentication {
|
|
|
18
18
|
export class HealthCheckRestPaths {
|
|
19
19
|
static readonly ROOT = '/';
|
|
20
20
|
static readonly PING = '/ping';
|
|
21
|
-
static readonly METRICS = '/metrics';
|
|
22
21
|
}
|
|
23
22
|
```
|
|
24
23
|
|
|
@@ -162,12 +161,13 @@ const DEFAULT_SERVER_OPTIONS: Partial<IServerOptions> = {
|
|
|
162
161
|
|
|
163
162
|
```typescript
|
|
164
163
|
// In component constructor or binding
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
const extraServerOptions =
|
|
165
|
+
this.application.get<Partial<ServerOptions>>({
|
|
166
|
+
key: SocketIOBindingKeys.SERVER_OPTIONS,
|
|
167
|
+
isOptional: true,
|
|
168
|
+
}) ?? {};
|
|
169
169
|
|
|
170
|
-
this.
|
|
170
|
+
this.serverOptions = Object.assign({}, DEFAULT_SERVER_OPTIONS, extraServerOptions);
|
|
171
171
|
```
|
|
172
172
|
|
|
173
173
|
### Constructor Validation
|
|
@@ -175,20 +175,20 @@ this.options = Object.assign({}, DEFAULT_OPTIONS, extraOptions);
|
|
|
175
175
|
Validate required options in the constructor:
|
|
176
176
|
|
|
177
177
|
```typescript
|
|
178
|
-
constructor(options:
|
|
179
|
-
super({ scope:
|
|
178
|
+
constructor(options: IJWSTokenServiceOptions) {
|
|
179
|
+
super({ scope: JWSTokenService.name });
|
|
180
180
|
|
|
181
181
|
if (!options.jwtSecret) {
|
|
182
182
|
throw getError({
|
|
183
183
|
statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
|
|
184
|
-
message: '[
|
|
184
|
+
message: '[JWSTokenService] Invalid jwtSecret',
|
|
185
185
|
});
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
if (!options.
|
|
188
|
+
if (!options.getTokenExpiresFn) {
|
|
189
189
|
throw getError({
|
|
190
190
|
statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
|
|
191
|
-
message: '[
|
|
191
|
+
message: '[JWSTokenService] Invalid getTokenExpiresFn',
|
|
192
192
|
});
|
|
193
193
|
}
|
|
194
194
|
|
|
@@ -107,6 +107,9 @@ this.logger.info('[create] User created | ID: %s | Email: %s', user.id, user.ema
|
|
|
107
107
|
this.logger.debug('[config] Server options: %j', this.serverOptions);
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
> [!NOTE]
|
|
111
|
+
> `%j` on an `Error` instance drops `message` and `stack` -- they are non-enumerable, so `JSON.stringify` never sees them. Use `%s` for errors, `%j`/`%o` for plain data objects. An object passed to `%s` is inspected up to `APP_ENV_LOGGER_INSPECT_DEPTH` levels deep (default `5`) instead of Node's hard-coded `depth: 0`, so nested fields print instead of collapsing to `[Object]`. See [Logger Helper](/extensions/helpers/logger/) for details.
|
|
112
|
+
|
|
110
113
|
### Log Levels
|
|
111
114
|
|
|
112
115
|
| Level | Use For |
|
|
@@ -142,7 +145,7 @@ Include class/method context in error messages:
|
|
|
142
145
|
// Format: [ClassName][methodName] Descriptive message
|
|
143
146
|
throw getError({
|
|
144
147
|
statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
|
|
145
|
-
message: '[
|
|
148
|
+
message: '[JWSTokenService][generate] Failed to generate token',
|
|
146
149
|
});
|
|
147
150
|
|
|
148
151
|
throw getError({
|
|
@@ -224,7 +227,7 @@ import dayjs from 'dayjs';
|
|
|
224
227
|
|
|
225
228
|
// 3. Internal absolute imports (by domain/package)
|
|
226
229
|
import { getError } from '@venizia/ignis-helpers';
|
|
227
|
-
import {
|
|
230
|
+
import { AbstractEntity } from '@/base/models';
|
|
228
231
|
import { UserService } from '@/services';
|
|
229
232
|
|
|
230
233
|
// 4. Relative imports (same feature) - LAST
|
|
@@ -24,14 +24,14 @@ Use JSDoc comments for public APIs to improve IDE support and generate documenta
|
|
|
24
24
|
* @returns Promise resolving to the query result
|
|
25
25
|
*
|
|
26
26
|
* @example
|
|
27
|
-
* const users = await
|
|
27
|
+
* const users = await userRepository.find({
|
|
28
28
|
* filter: { where: { status: 'ACTIVE' }, limit: 10 },
|
|
29
29
|
* });
|
|
30
30
|
*
|
|
31
31
|
* @throws {ApplicationError} When validation fails
|
|
32
32
|
* @see {@link UserService} for business logic
|
|
33
33
|
*/
|
|
34
|
-
async find(opts:
|
|
34
|
+
async find(opts: { filter: TFilter<TUser>; options?: IExtraOptions }): Promise<TUser[]> {
|
|
35
35
|
// implementation
|
|
36
36
|
}
|
|
37
37
|
```
|
|
@@ -87,15 +87,15 @@ async createUser(data: TCreateUserRequest): Promise<TUser> {
|
|
|
87
87
|
* @param opts - Find options
|
|
88
88
|
* @param opts.filter - Query filter
|
|
89
89
|
* @param opts.filter.where - Conditions to match
|
|
90
|
-
* @param opts.filter.limit - Maximum records to return
|
|
90
|
+
* @param opts.filter.limit - Maximum records to return
|
|
91
91
|
* @param opts.filter.offset - Records to skip for pagination
|
|
92
92
|
* @param opts.filter.order - Sort order (e.g., ['createdAt DESC'])
|
|
93
93
|
* @param opts.filter.include - Relations to load
|
|
94
|
-
* @returns Promise
|
|
94
|
+
* @returns Promise resolving to the array of matching entities
|
|
95
95
|
*
|
|
96
96
|
* @example
|
|
97
97
|
* // Find active users, sorted by name
|
|
98
|
-
* const
|
|
98
|
+
* const users = await userRepository.find({
|
|
99
99
|
* filter: {
|
|
100
100
|
* where: { status: 'ACTIVE' },
|
|
101
101
|
* order: ['name ASC'],
|
|
@@ -103,7 +103,7 @@ async createUser(data: TCreateUserRequest): Promise<TUser> {
|
|
|
103
103
|
* },
|
|
104
104
|
* });
|
|
105
105
|
*/
|
|
106
|
-
async find(opts:
|
|
106
|
+
async find<R = TUser>(opts: { filter: TFilter<TUser>; options?: IExtraOptions }): Promise<R[]> {
|
|
107
107
|
// ...
|
|
108
108
|
}
|
|
109
109
|
```
|
|
@@ -116,13 +116,13 @@ async find(opts: TFindOpts<Schema>): Promise<TFindResult<TEntity>> {
|
|
|
116
116
|
*
|
|
117
117
|
* @example
|
|
118
118
|
* // Before (deprecated)
|
|
119
|
-
* const user = await
|
|
119
|
+
* const user = await userRepository.getById('123');
|
|
120
120
|
*
|
|
121
121
|
* // After (recommended)
|
|
122
|
-
* const
|
|
122
|
+
* const user = await userRepository.findById({ id: '123' });
|
|
123
123
|
*/
|
|
124
124
|
async getById(id: string): Promise<TUser | null> {
|
|
125
|
-
return this.findById({ id })
|
|
125
|
+
return this.findById({ id });
|
|
126
126
|
}
|
|
127
127
|
```
|
|
128
128
|
|
|
@@ -181,7 +181,7 @@ interface IJWTStrategyOptions {
|
|
|
181
181
|
* Extends this class to create entity-specific repositories with
|
|
182
182
|
* type-safe operations and automatic schema binding.
|
|
183
183
|
*
|
|
184
|
-
* @template
|
|
184
|
+
* @template EntitySchema - The Drizzle table schema type
|
|
185
185
|
*
|
|
186
186
|
* @example
|
|
187
187
|
* @repository({ model: User, dataSource: PostgresDataSource })
|
|
@@ -189,10 +189,10 @@ interface IJWTStrategyOptions {
|
|
|
189
189
|
* // Custom methods here
|
|
190
190
|
* }
|
|
191
191
|
*
|
|
192
|
-
* @see {@link
|
|
193
|
-
* @see {@link
|
|
192
|
+
* @see {@link BasePostgresEntity} for model definition
|
|
193
|
+
* @see {@link BasePostgresDataSource} for database connection
|
|
194
194
|
*/
|
|
195
|
-
|
|
195
|
+
class DefaultCRUDRepository<EntitySchema extends TTableSchemaWithId = TTableSchemaWithId> {
|
|
196
196
|
// ...
|
|
197
197
|
}
|
|
198
198
|
```
|