@venizia/ignis-docs 0.0.8 → 0.2.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 +46 -22
- 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 +40 -31
- 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/compile-binary.md +92 -0
- package/content/extensions/helpers/kafka/examples.md +1 -1
- package/content/extensions/helpers/kafka/index.md +3 -0
- 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 +42 -43
- package/content/guides/core-concepts/persistent/index.md +16 -7
- package/content/guides/core-concepts/persistent/models.md +24 -20
- package/content/guides/core-concepts/persistent/postgres-drivers.md +201 -0
- package/content/guides/core-concepts/persistent/repositories.md +40 -23
- package/content/guides/core-concepts/persistent/search-meilisearch.md +185 -0
- package/content/guides/core-concepts/persistent/search-typesense.md +431 -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 +37 -44
- package/content/guides/tutorials/complete-installation.md +17 -17
- package/content/guides/tutorials/ecommerce-api.md +163 -124
- package/content/guides/tutorials/realtime-chat.md +181 -135
- 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 +163 -92
- 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 +49 -31
- 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
|
@@ -8,7 +8,7 @@ difficulty: intermediate
|
|
|
8
8
|
|
|
9
9
|
A repository that overrides delete operations to set a `deletedAt` timestamp instead of physically removing records. Extends `DefaultCRUDRepository` with restore capabilities.
|
|
10
10
|
|
|
11
|
-
**File:** `packages/core/src/
|
|
11
|
+
**File:** `packages/core/src/connectors/postgres/repositories/core/soft-deletable.ts` (PostgreSQL connector - soft delete via `deletedAt` is a Drizzle/SQL-specific pattern, not part of the engine-neutral `AbstractRepository`)
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
## Setup
|
|
@@ -70,17 +70,17 @@ All delete methods set `deletedAt = new Date()` instead of removing the row. The
|
|
|
70
70
|
|
|
71
71
|
```typescript
|
|
72
72
|
// Soft delete - sets deletedAt timestamp
|
|
73
|
-
const result = await
|
|
73
|
+
const result = await repository.deleteById({ id: '123' });
|
|
74
74
|
// { count: 1, data: { id: '123', name: 'Electronics', deletedAt: '2026-03-06T...' } }
|
|
75
75
|
|
|
76
76
|
// Without returning data
|
|
77
|
-
const result = await
|
|
77
|
+
const result = await repository.deleteById({
|
|
78
78
|
id: '123',
|
|
79
79
|
options: { shouldReturn: false },
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
// Hard delete - physically removes the row
|
|
83
|
-
const result = await
|
|
83
|
+
const result = await repository.deleteById({
|
|
84
84
|
id: '123',
|
|
85
85
|
options: { shouldHardDelete: true },
|
|
86
86
|
});
|
|
@@ -90,13 +90,13 @@ const result = await repo.deleteById({
|
|
|
90
90
|
|
|
91
91
|
```typescript
|
|
92
92
|
// Soft delete all matching records
|
|
93
|
-
const result = await
|
|
93
|
+
const result = await repository.deleteAll({
|
|
94
94
|
where: { status: 'archived' },
|
|
95
95
|
options: { force: true },
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
// Hard delete all matching records
|
|
99
|
-
const result = await
|
|
99
|
+
const result = await repository.deleteAll({
|
|
100
100
|
where: { status: 'archived' },
|
|
101
101
|
options: { shouldHardDelete: true, force: true },
|
|
102
102
|
});
|
|
@@ -106,12 +106,12 @@ const result = await repo.deleteAll({
|
|
|
106
106
|
|
|
107
107
|
```typescript
|
|
108
108
|
// Soft delete by where condition (alias for deleteAll)
|
|
109
|
-
const result = await
|
|
109
|
+
const result = await repository.deleteBy({
|
|
110
110
|
where: { name: 'Obsolete' },
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
// Hard delete by where condition
|
|
114
|
-
const result = await
|
|
114
|
+
const result = await repository.deleteBy({
|
|
115
115
|
where: { name: 'Obsolete' },
|
|
116
116
|
options: { shouldHardDelete: true },
|
|
117
117
|
});
|
|
@@ -125,11 +125,11 @@ Restore methods set `deletedAt = null` and automatically use `shouldSkipDefaultF
|
|
|
125
125
|
### restoreById
|
|
126
126
|
|
|
127
127
|
```typescript
|
|
128
|
-
const result = await
|
|
128
|
+
const result = await repository.restoreById({ id: '123' });
|
|
129
129
|
// { count: 1, data: { id: '123', name: 'Electronics', deletedAt: null } }
|
|
130
130
|
|
|
131
131
|
// Without returning data
|
|
132
|
-
const result = await
|
|
132
|
+
const result = await repository.restoreById({
|
|
133
133
|
id: '123',
|
|
134
134
|
options: { shouldReturn: false },
|
|
135
135
|
});
|
|
@@ -139,13 +139,13 @@ const result = await repo.restoreById({
|
|
|
139
139
|
|
|
140
140
|
```typescript
|
|
141
141
|
// Restore all soft-deleted records (requires force for empty where)
|
|
142
|
-
const result = await
|
|
142
|
+
const result = await repository.restoreAll({
|
|
143
143
|
where: {},
|
|
144
144
|
options: { force: true },
|
|
145
145
|
});
|
|
146
146
|
|
|
147
147
|
// Restore matching records
|
|
148
|
-
const result = await
|
|
148
|
+
const result = await repository.restoreAll({
|
|
149
149
|
where: { name: 'Electronics' },
|
|
150
150
|
});
|
|
151
151
|
```
|
|
@@ -154,7 +154,7 @@ const result = await repo.restoreAll({
|
|
|
154
154
|
|
|
155
155
|
```typescript
|
|
156
156
|
// Alias for restoreAll
|
|
157
|
-
const result = await
|
|
157
|
+
const result = await repository.restoreBy({
|
|
158
158
|
where: { status: 'archived' },
|
|
159
159
|
});
|
|
160
160
|
```
|
|
@@ -168,10 +168,10 @@ const result = await repo.restoreBy({
|
|
|
168
168
|
|
|
169
169
|
```typescript
|
|
170
170
|
// Returns null if not found (default)
|
|
171
|
-
const category = await
|
|
171
|
+
const category = await repository.findById({ id: '123' });
|
|
172
172
|
|
|
173
173
|
// Throws 404 if not found
|
|
174
|
-
const category = await
|
|
174
|
+
const category = await repository.findById({
|
|
175
175
|
id: '123',
|
|
176
176
|
options: { isStrict: true },
|
|
177
177
|
});
|
|
@@ -226,8 +226,8 @@ All other read operations (`find`, `findOne`, `count`, `existsWith`) work as nor
|
|
|
226
226
|
```typescript
|
|
227
227
|
const tx = await this.dataSource.beginTransaction();
|
|
228
228
|
try {
|
|
229
|
-
await this.
|
|
230
|
-
await this.
|
|
229
|
+
await this.categoryRepository.deleteById({ id: '123', options: { transaction: tx } });
|
|
230
|
+
await this.auditRepository.create({
|
|
231
231
|
data: { action: 'soft_delete', entityId: '123' },
|
|
232
232
|
options: { transaction: tx },
|
|
233
233
|
});
|
|
@@ -259,11 +259,12 @@ If your schema does not have a `deletedAt` column, you will get a TypeScript com
|
|
|
259
259
|
## Class Hierarchy
|
|
260
260
|
|
|
261
261
|
```
|
|
262
|
-
AbstractRepository
|
|
263
|
-
->
|
|
264
|
-
->
|
|
265
|
-
->
|
|
266
|
-
->
|
|
262
|
+
AbstractRepository (engine-neutral, src/base)
|
|
263
|
+
-> PostgresBaseRepository (connectors/postgres)
|
|
264
|
+
-> ReadableRepository
|
|
265
|
+
-> PersistableRepository
|
|
266
|
+
-> DefaultCRUDRepository
|
|
267
|
+
-> SoftDeletableRepository <-- you are here
|
|
267
268
|
```
|
|
268
269
|
|
|
269
270
|
|
|
@@ -271,19 +272,19 @@ AbstractRepository
|
|
|
271
272
|
|
|
272
273
|
| Want to... | Code |
|
|
273
274
|
|------------|------|
|
|
274
|
-
| Soft delete by ID | `
|
|
275
|
-
| Hard delete by ID | `
|
|
276
|
-
| Soft delete by condition | `
|
|
277
|
-
| Restore by ID | `
|
|
278
|
-
| Restore by condition | `
|
|
279
|
-
| Find including deleted | `
|
|
280
|
-
| Strict findById (404) | `
|
|
275
|
+
| Soft delete by ID | `repository.deleteById({ id })` |
|
|
276
|
+
| Hard delete by ID | `repository.deleteById({ id, options: { shouldHardDelete: true } })` |
|
|
277
|
+
| Soft delete by condition | `repository.deleteAll({ where, options: { force: true } })` |
|
|
278
|
+
| Restore by ID | `repository.restoreById({ id })` |
|
|
279
|
+
| Restore by condition | `repository.restoreAll({ where })` |
|
|
280
|
+
| Find including deleted | `repository.find({ filter, options: { shouldSkipDefaultFilter: true } })` |
|
|
281
|
+
| Strict findById (404) | `repository.findById({ id, options: { isStrict: true } })` |
|
|
281
282
|
|
|
282
283
|
|
|
283
284
|
## Next Steps
|
|
284
285
|
|
|
285
286
|
- [Advanced Features](./advanced.md) - Transactions, hidden properties
|
|
286
|
-
- [Repository Mixins](./mixins.md) -
|
|
287
|
+
- [Repository Mixins (Removed)](./mixins.md) - Where default-filter and fields-visibility behavior lives now
|
|
287
288
|
- [Repository Overview](./index.md) - Repository basics
|
|
288
289
|
|
|
289
290
|
## See Also
|
|
@@ -1,31 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Services Reference
|
|
3
|
-
description: Technical reference for BaseService and business logic layer
|
|
3
|
+
description: Technical reference for BaseService and the business logic layer in IGNIS
|
|
4
4
|
difficulty: beginner
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Deep Dive: Services
|
|
8
8
|
|
|
9
|
-
Technical reference for `BaseService` - the foundation for business logic
|
|
9
|
+
Technical reference for `BaseService` - the foundation for the business logic layer in IGNIS.
|
|
10
10
|
|
|
11
11
|
**File:** `packages/core/src/base/services/base.ts`
|
|
12
12
|
|
|
13
13
|
## Quick Reference
|
|
14
14
|
|
|
15
|
-
| Feature |
|
|
16
|
-
|
|
17
|
-
| **
|
|
18
|
-
| **
|
|
19
|
-
| **
|
|
20
|
-
| **
|
|
15
|
+
| Feature | Detail |
|
|
16
|
+
|---------|--------|
|
|
17
|
+
| **Import** | `import { BaseService, inject } from '@venizia/ignis'` |
|
|
18
|
+
| **Extends** | `BaseHelper` from `@venizia/ignis-helpers` |
|
|
19
|
+
| **Logging** | `this.logger` (scoped to constructor `scope`) |
|
|
20
|
+
| **Registration** | `this.service(MyService)` in application lifecycle |
|
|
21
|
+
| **Binding key** | `services.{ClassName}` (e.g., `services.AuthenticationService`) |
|
|
22
|
+
| **DI decorator** | `@injectable` is NOT used on services - registration is imperative only |
|
|
23
|
+
| **CRUD service** | Removed - use `DefaultCRUDRepository` for data access |
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
---
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
## `BaseService` Class
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
Abstract class that all application services must extend.
|
|
27
30
|
|
|
28
31
|
```typescript
|
|
32
|
+
// packages/core/src/base/services/base.ts
|
|
29
33
|
import { BaseHelper } from '@venizia/ignis-helpers';
|
|
30
34
|
import { IService } from './types';
|
|
31
35
|
|
|
@@ -36,161 +40,360 @@ export abstract class BaseService extends BaseHelper implements IService {
|
|
|
36
40
|
}
|
|
37
41
|
```
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
`BaseHelper` wires a scoped logger at `this.logger`. Pass `scope: ClassName.name` so log lines are tagged with the service name.
|
|
44
|
+
|
|
45
|
+
### `IService` Interface
|
|
40
46
|
|
|
41
|
-
|
|
42
|
-
| :--- | :--- |
|
|
43
|
-
| **Standardization** | Common base for all services, fits framework architecture |
|
|
44
|
-
| **Logging** | Extends `BaseHelper` from `@venizia/ignis-helpers` — auto-configured logger at `this.logger` (scope = class name) |
|
|
45
|
-
| **Clarity** | Signals the class contains business logic |
|
|
47
|
+
Marker interface with no required methods - it exists purely for type-level contracts:
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
```typescript
|
|
50
|
+
export interface IService {}
|
|
51
|
+
```
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Registering a Service
|
|
56
|
+
|
|
57
|
+
Services are registered imperatively in an application lifecycle method. There is no `@injectable` or `@service` decorator used on service classes in application code - `this.service()` handles everything.
|
|
50
58
|
|
|
51
59
|
```typescript
|
|
52
|
-
class
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
// In your Application class (e.g., in preConfigure())
|
|
61
|
+
this.service(AuthenticationService); // binds as 'services.AuthenticationService'
|
|
62
|
+
this.service(GreeterService); // binds as 'services.GreeterService'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`this.service(Ctor)` is implemented directly on `BaseApplication`:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// packages/core/src/base/applications/base.ts
|
|
69
|
+
service<Base extends IService, Args extends AnyObject = any>(
|
|
70
|
+
ctor: TClass<Base>,
|
|
71
|
+
opts?: TMixinOpts<Args>,
|
|
72
|
+
): Binding<Base> {
|
|
73
|
+
return this.bind<Base>({
|
|
74
|
+
key: BindingKeys.build(
|
|
75
|
+
opts?.binding ?? {
|
|
76
|
+
namespace: BindingNamespaces.SERVICE, // 'services'
|
|
77
|
+
key: ctor.name, // class name
|
|
78
|
+
},
|
|
79
|
+
),
|
|
80
|
+
}).toClass(ctor);
|
|
56
81
|
}
|
|
57
82
|
```
|
|
58
83
|
|
|
59
|
-
|
|
84
|
+
The resulting binding key defaults to `services.{ClassName}` (overridable via `opts.binding`).
|
|
85
|
+
|
|
86
|
+
### Lifecycle Placement
|
|
60
87
|
|
|
61
|
-
|
|
88
|
+
Register services before anything that depends on them. The correct hook is `preConfigure()`, or a private helper called from `preConfigure()`:
|
|
62
89
|
|
|
63
90
|
```typescript
|
|
64
|
-
export
|
|
91
|
+
export class Application extends BaseApplication {
|
|
92
|
+
preConfigure(): void {
|
|
93
|
+
// DataSources and repositories that services depend on must come first
|
|
94
|
+
this.dataSource(PostgresDataSource);
|
|
95
|
+
this.repository(UserRepository);
|
|
96
|
+
|
|
97
|
+
// Then register services
|
|
98
|
+
this.service(AuthenticationService);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
65
101
|
```
|
|
66
102
|
|
|
67
|
-
|
|
103
|
+
---
|
|
68
104
|
|
|
69
|
-
|
|
105
|
+
## Dependency Injection into Services
|
|
70
106
|
|
|
71
|
-
|
|
107
|
+
Inject repositories, other services, or datasources via `@inject` in the constructor. Two equivalent key forms are available:
|
|
72
108
|
|
|
73
|
-
|
|
109
|
+
### String-literal keys
|
|
74
110
|
|
|
75
111
|
```typescript
|
|
76
|
-
|
|
77
|
-
|
|
112
|
+
import { BaseService, inject } from '@venizia/ignis';
|
|
113
|
+
import { UserRepository } from '../repositories';
|
|
114
|
+
import { JWKSIssuerTokenService } from '@venizia/ignis';
|
|
78
115
|
|
|
79
|
-
|
|
80
|
-
|
|
116
|
+
export class AuthenticationService extends BaseService {
|
|
117
|
+
constructor(
|
|
118
|
+
@inject({ key: 'repositories.UserRepository' })
|
|
119
|
+
private userRepository: UserRepository,
|
|
120
|
+
|
|
121
|
+
@inject({ key: 'services.JWKSIssuerTokenService' })
|
|
122
|
+
private jwksTokenService: JWKSIssuerTokenService,
|
|
123
|
+
) {
|
|
124
|
+
super({ scope: AuthenticationService.name });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
81
127
|
```
|
|
82
128
|
|
|
83
|
-
|
|
129
|
+
### Programmatic keys with `BindingKeys.build`
|
|
84
130
|
|
|
85
|
-
|
|
131
|
+
`BindingKeys.build({ namespace, key })` produces the same `namespace.key` string. Use this form when you want a compile-time reference to a class name rather than a plain string:
|
|
86
132
|
|
|
87
|
-
|
|
133
|
+
```typescript
|
|
134
|
+
import { BaseService, BindingKeys, BindingNamespaces, inject } from '@venizia/ignis';
|
|
135
|
+
import { UserRepository } from '../repositories';
|
|
88
136
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
137
|
+
export class AuthenticationService extends BaseService {
|
|
138
|
+
constructor(
|
|
139
|
+
@inject({
|
|
140
|
+
key: BindingKeys.build({
|
|
141
|
+
namespace: BindingNamespaces.REPOSITORY, // 'repositories'
|
|
142
|
+
key: UserRepository.name, // 'UserRepository'
|
|
143
|
+
}),
|
|
144
|
+
})
|
|
145
|
+
private userRepository: UserRepository,
|
|
146
|
+
) {
|
|
147
|
+
super({ scope: AuthenticationService.name });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Both forms produce identical binding keys at runtime. The `BindingKeys.build` form avoids typo risk when the class name changes.
|
|
153
|
+
|
|
154
|
+
### Available `BindingNamespaces`
|
|
155
|
+
|
|
156
|
+
| Constant | Value |
|
|
157
|
+
|----------|-------|
|
|
158
|
+
| `BindingNamespaces.SERVICE` | `'services'` |
|
|
159
|
+
| `BindingNamespaces.REPOSITORY` | `'repositories'` |
|
|
160
|
+
| `BindingNamespaces.DATASOURCE` | `'datasources'` |
|
|
161
|
+
| `BindingNamespaces.CONTROLLER` | `'controllers'` |
|
|
162
|
+
| `BindingNamespaces.COMPONENT` | `'components'` |
|
|
163
|
+
| `BindingNamespaces.PROVIDER` | `'providers'` |
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Logging
|
|
168
|
+
|
|
169
|
+
`BaseService` inherits `this.logger` from `BaseHelper`. Log with method scope for structured output:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
export class AuthenticationService extends BaseService {
|
|
173
|
+
async signIn(opts: { identifier: string }): Promise<string> {
|
|
174
|
+
// Method-scoped log - produces tag "[signIn]" in log output
|
|
175
|
+
this.logger.for('signIn').info('SignIn called | identifier: %s', opts.identifier);
|
|
98
176
|
|
|
99
|
-
|
|
177
|
+
// ... business logic ...
|
|
178
|
+
|
|
179
|
+
this.logger.for('signIn').info('SignIn successful');
|
|
180
|
+
return token;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Use `this.logger.for('methodName')` to scope log lines to the current method. This matches the project-wide convention seen in all examples.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Service-to-Service Composition
|
|
190
|
+
|
|
191
|
+
Services can inject other services to compose business logic. Inject them the same way as repositories, using the `services.*` namespace:
|
|
100
192
|
|
|
101
193
|
```typescript
|
|
102
|
-
import { BaseService,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
import { TUser } from '../models/entities';
|
|
106
|
-
|
|
107
|
-
// 1. Service is decorated with @injectable (or registered via app.service())
|
|
108
|
-
@injectable()
|
|
109
|
-
export class UserService extends BaseService {
|
|
110
|
-
// 2. Dependencies (like UserRepository) are injected
|
|
194
|
+
import { BaseService, BindingKeys, BindingNamespaces, inject } from '@venizia/ignis';
|
|
195
|
+
|
|
196
|
+
export class OrderService extends BaseService {
|
|
111
197
|
constructor(
|
|
112
|
-
@inject({
|
|
113
|
-
|
|
198
|
+
@inject({
|
|
199
|
+
key: BindingKeys.build({
|
|
200
|
+
namespace: BindingNamespaces.REPOSITORY,
|
|
201
|
+
key: OrderRepository.name,
|
|
202
|
+
}),
|
|
203
|
+
})
|
|
204
|
+
private orderRepository: OrderRepository,
|
|
205
|
+
|
|
206
|
+
@inject({
|
|
207
|
+
key: BindingKeys.build({
|
|
208
|
+
namespace: BindingNamespaces.REPOSITORY,
|
|
209
|
+
key: InventoryRepository.name,
|
|
210
|
+
}),
|
|
211
|
+
})
|
|
212
|
+
private inventoryRepository: InventoryRepository,
|
|
213
|
+
|
|
214
|
+
@inject({
|
|
215
|
+
key: BindingKeys.build({
|
|
216
|
+
namespace: BindingNamespaces.SERVICE,
|
|
217
|
+
key: NotificationService.name,
|
|
218
|
+
}),
|
|
219
|
+
})
|
|
220
|
+
private notificationService: NotificationService,
|
|
114
221
|
) {
|
|
115
|
-
super({ scope:
|
|
222
|
+
super({ scope: OrderService.name });
|
|
116
223
|
}
|
|
117
224
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
this.logger.info(`Fetching profile for user ${opts.userId}`);
|
|
225
|
+
async createOrder(opts: { userId: string; items: OrderItem[] }): Promise<Order> {
|
|
226
|
+
this.logger.for('createOrder').info('Creating order | userId: %s', opts.userId);
|
|
121
227
|
|
|
122
|
-
|
|
123
|
-
const user = await this._userRepository.findById({ id: opts.userId });
|
|
228
|
+
const { data: order } = await this.orderRepository.create({ data: opts });
|
|
124
229
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
230
|
+
// Compose with another service
|
|
231
|
+
await this.notificationService.sendOrderConfirmation({ orderId: order.id });
|
|
128
232
|
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
id: user.id,
|
|
132
|
-
name: user.name,
|
|
133
|
-
email: user.email,
|
|
134
|
-
};
|
|
233
|
+
return order;
|
|
135
234
|
}
|
|
136
235
|
}
|
|
137
236
|
```
|
|
138
237
|
|
|
139
|
-
|
|
238
|
+
Register both services in `preConfigure()`:
|
|
140
239
|
|
|
141
|
-
|
|
240
|
+
```typescript
|
|
241
|
+
this.service(NotificationService);
|
|
242
|
+
this.service(OrderService);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Abstract Base Services
|
|
246
|
+
|
|
247
|
+
For shared dependencies across multiple related services, define an abstract base. The container only ever `instantiate()`s the **concrete** class (`this.service(UserAuditTestService)` registers `UserAuditTestService`, never `BaseTestService`), and the hard DI rule - every constructor parameter of a container-instantiated class must carry `@inject` - applies to that concrete constructor. A `scope: string` computed from `ClassName.name` is not something the container can supply anyway, so it cannot sit as a bare constructor parameter next to an `@inject`-decorated one. The shared repository is injected as a **property** on the base instead, and the concrete subclass's constructor is left with zero parameters - nothing to decorate, nothing to violate:
|
|
142
248
|
|
|
143
249
|
```typescript
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
250
|
+
// Shared repository access for a group of test services - property injection,
|
|
251
|
+
// so the concrete subclass's constructor stays free of undecorated parameters
|
|
252
|
+
export abstract class BaseTestService extends BaseService {
|
|
253
|
+
@inject({
|
|
254
|
+
key: BindingKeys.build({
|
|
255
|
+
namespace: BindingNamespaces.REPOSITORY,
|
|
256
|
+
key: UserRepository.name,
|
|
257
|
+
}),
|
|
258
|
+
})
|
|
259
|
+
protected userRepository!: UserRepository;
|
|
149
260
|
|
|
150
|
-
|
|
151
|
-
|
|
261
|
+
constructor(opts: { scope: string }) {
|
|
262
|
+
super(opts);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
abstract run(): Promise<void>;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Concrete subclass takes no constructor parameters - only container-instantiated
|
|
269
|
+
// classes are subject to the "every parameter decorated" rule, and an empty
|
|
270
|
+
// parameter list trivially satisfies it
|
|
271
|
+
export class UserAuditTestService extends BaseTestService {
|
|
272
|
+
constructor() {
|
|
273
|
+
super({ scope: UserAuditTestService.name });
|
|
274
|
+
}
|
|
152
275
|
|
|
276
|
+
async run(): Promise<void> {
|
|
277
|
+
this.logger.for('run').info('Running user audit tests');
|
|
278
|
+
// ...
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Register the concrete class - never the abstract base:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
this.service(UserAuditTestService);
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
> [!IMPORTANT]
|
|
290
|
+
> `BaseTestService`'s own constructor (`opts: { scope: string }`) is never processed by the container - `BaseTestService` is abstract and is never passed to `instantiate()`. Only the concrete class the container actually instantiates is subject to the "every parameter decorated" rule. See [Dependency Injection Reference](./dependency-injection.md#instantiation-algorithm-two-phase) for the full rule.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Transaction Orchestration
|
|
295
|
+
|
|
296
|
+
Services are the correct place to manage transactions that span multiple repository calls. Begin a transaction on the DataSource and pass it through repository options:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { BaseService, BindingKeys, BindingNamespaces, inject } from '@venizia/ignis';
|
|
300
|
+
import { PostgresDataSource } from '../datasources';
|
|
301
|
+
|
|
302
|
+
export class CheckoutService extends BaseService {
|
|
303
|
+
constructor(
|
|
153
304
|
@inject({ key: 'datasources.PostgresDataSource' })
|
|
154
305
|
private dataSource: PostgresDataSource,
|
|
306
|
+
|
|
307
|
+
@inject({ key: 'repositories.OrderRepository' })
|
|
308
|
+
private orderRepository: OrderRepository,
|
|
309
|
+
|
|
310
|
+
@inject({ key: 'repositories.InventoryRepository' })
|
|
311
|
+
private inventoryRepository: InventoryRepository,
|
|
155
312
|
) {
|
|
156
|
-
super({ scope:
|
|
313
|
+
super({ scope: CheckoutService.name });
|
|
157
314
|
}
|
|
158
315
|
|
|
159
|
-
async placeOrder(opts: { userId: string; items: OrderItem[] }) {
|
|
316
|
+
async placeOrder(opts: { userId: string; items: OrderItem[] }): Promise<Order> {
|
|
317
|
+
const log = this.logger.for('placeOrder');
|
|
160
318
|
const transaction = await this.dataSource.beginTransaction();
|
|
319
|
+
|
|
161
320
|
try {
|
|
162
|
-
const order = await this.
|
|
163
|
-
data: { userId: opts.userId
|
|
321
|
+
const { data: order } = await this.orderRepository.create({
|
|
322
|
+
data: { userId: opts.userId },
|
|
164
323
|
options: { transaction },
|
|
165
324
|
});
|
|
166
325
|
|
|
167
326
|
for (const item of opts.items) {
|
|
168
|
-
await this.
|
|
327
|
+
await this.inventoryRepository.updateById({
|
|
169
328
|
id: item.productId,
|
|
170
|
-
data: {
|
|
329
|
+
data: { stock: item.quantity },
|
|
171
330
|
options: { transaction },
|
|
172
331
|
});
|
|
173
332
|
}
|
|
174
333
|
|
|
175
334
|
await transaction.commit();
|
|
335
|
+
log.info('Order placed | orderId: %s', order.id);
|
|
176
336
|
return order;
|
|
177
337
|
} catch (error) {
|
|
178
338
|
await transaction.rollback();
|
|
339
|
+
log.error('Order failed, rolled back | error: %s', error);
|
|
179
340
|
throw error;
|
|
180
341
|
}
|
|
181
342
|
}
|
|
182
343
|
}
|
|
183
344
|
```
|
|
184
345
|
|
|
185
|
-
|
|
346
|
+
Pass `{ transaction }` in the `options` field of any repository call. The repository API is identical with or without a transaction - only the underlying Drizzle connector switches.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Resolving a Service Imperatively
|
|
351
|
+
|
|
352
|
+
When you need to pull a service out of the container at runtime (rather than through constructor injection), use `this.get<T>`:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// In Application.postConfigure() or a lifecycle hook
|
|
356
|
+
const testService = this.get<RowLockingTestService>({
|
|
357
|
+
key: BindingKeys.build({
|
|
358
|
+
namespace: BindingNamespaces.SERVICE,
|
|
359
|
+
key: RowLockingTestService.name,
|
|
360
|
+
}),
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
await testService.run();
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
This is useful for post-start hooks, one-off tasks that run after the server starts, or when the service is not a constructor dependency.
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## No Built-in CRUD Service
|
|
371
|
+
|
|
372
|
+
IGNIS intentionally does not provide a `BaseCrudService`. CRUD operations belong in the Repository layer (`DefaultCRUDRepository`). Services exist for business logic that cannot be expressed as pure data access: cross-cutting validation, multi-repository coordination, transaction management, and workflow orchestration.
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Provider vs Service
|
|
377
|
+
|
|
378
|
+
| Aspect | Service | Provider |
|
|
379
|
+
|--------|---------|----------|
|
|
380
|
+
| **Purpose** | Business logic and orchestration | Factory - produces values or instances |
|
|
381
|
+
| **Base class** | `BaseService` | `BaseProvider<T>` |
|
|
382
|
+
| **Key method** | Business methods | `value(container): T` |
|
|
383
|
+
| **Pattern** | Singleton in DI scope | Factory pattern |
|
|
384
|
+
| **Registration** | `this.service(Ctor)` | `this.bind(...).toProvider(Ctor)` |
|
|
385
|
+
|
|
386
|
+
See [Providers Reference](./providers.md) for the factory pattern details.
|
|
387
|
+
|
|
388
|
+
---
|
|
186
389
|
|
|
187
390
|
## See Also
|
|
188
391
|
|
|
189
392
|
- **Related References:**
|
|
190
|
-
- [Controllers](./controllers.md) - HTTP
|
|
191
|
-
- [Repositories](./repositories/) - Data access layer
|
|
192
|
-
- [Providers](./providers.md) - Factory pattern
|
|
193
|
-
- [Dependency Injection](./dependency-injection.md) -
|
|
393
|
+
- [Controllers](./controllers.md) - HTTP handlers that call services
|
|
394
|
+
- [Repositories](./repositories/) - Data access layer injected into services
|
|
395
|
+
- [Providers](./providers.md) - Factory pattern, compare with services
|
|
396
|
+
- [Dependency Injection](./dependency-injection.md) - Container and injection system
|
|
194
397
|
|
|
195
398
|
- **Guides:**
|
|
196
399
|
- [Building Services](/guides/core-concepts/services.md)
|