@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.
Files changed (180) hide show
  1. package/README.md +7 -7
  2. package/content/best-practices/api-usage-examples.md +15 -12
  3. package/content/best-practices/architectural-patterns.md +70 -78
  4. package/content/best-practices/architecture-decisions.md +91 -60
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +56 -44
  6. package/content/best-practices/code-style-standards/constants-configuration.md +11 -11
  7. package/content/best-practices/code-style-standards/control-flow.md +5 -2
  8. package/content/best-practices/code-style-standards/documentation.md +13 -13
  9. package/content/best-practices/code-style-standards/function-patterns.md +9 -10
  10. package/content/best-practices/code-style-standards/index.md +1 -1
  11. package/content/best-practices/code-style-standards/naming-conventions.md +10 -8
  12. package/content/best-practices/code-style-standards/route-definitions.md +30 -12
  13. package/content/best-practices/code-style-standards/tooling.md +8 -5
  14. package/content/best-practices/code-style-standards/type-safety.md +13 -12
  15. package/content/best-practices/common-pitfalls.md +56 -37
  16. package/content/best-practices/contribution-workflow.md +13 -14
  17. package/content/best-practices/data-modeling.md +46 -22
  18. package/content/best-practices/deployment-strategies.md +28 -27
  19. package/content/best-practices/error-handling.md +48 -24
  20. package/content/best-practices/index.md +5 -5
  21. package/content/best-practices/performance-optimization.md +40 -31
  22. package/content/best-practices/security-guidelines.md +52 -23
  23. package/content/best-practices/testing-strategies.md +65 -51
  24. package/content/best-practices/troubleshooting-tips.md +24 -24
  25. package/content/extensions/components/{swagger.md → api-reference.md} +40 -31
  26. package/content/extensions/components/authentication/api.md +19 -19
  27. package/content/extensions/components/authentication/errors.md +7 -7
  28. package/content/extensions/components/authentication/index.md +10 -8
  29. package/content/extensions/components/authentication/usage.md +101 -6
  30. package/content/extensions/components/authorization/api.md +45 -25
  31. package/content/extensions/components/authorization/errors.md +6 -6
  32. package/content/extensions/components/authorization/index.md +11 -10
  33. package/content/extensions/components/authorization/usage.md +21 -21
  34. package/content/extensions/components/health-check.md +1 -1
  35. package/content/extensions/components/index.md +5 -5
  36. package/content/extensions/components/mail/errors.md +15 -15
  37. package/content/extensions/components/mail/index.md +1 -2
  38. package/content/extensions/components/mail/usage.md +1 -1
  39. package/content/extensions/components/request-tracker.md +1 -1
  40. package/content/extensions/components/socket-io/api.md +9 -9
  41. package/content/extensions/components/socket-io/errors.md +5 -5
  42. package/content/extensions/components/socket-io/index.md +8 -8
  43. package/content/extensions/components/socket-io/usage.md +1 -1
  44. package/content/extensions/components/static-asset/api.md +17 -4
  45. package/content/extensions/components/static-asset/errors.md +4 -4
  46. package/content/extensions/components/static-asset/index.md +26 -28
  47. package/content/extensions/components/static-asset/usage.md +13 -12
  48. package/content/extensions/components/template/index.md +2 -2
  49. package/content/extensions/components/template/setup-page.md +1 -1
  50. package/content/extensions/components/websocket/api.md +3 -3
  51. package/content/extensions/components/websocket/errors.md +5 -5
  52. package/content/extensions/components/websocket/index.md +5 -5
  53. package/content/extensions/components/websocket/usage.md +3 -3
  54. package/content/extensions/helpers/cron/index.md +2 -2
  55. package/content/extensions/helpers/crypto/index.md +1 -1
  56. package/content/extensions/helpers/env/index.md +27 -12
  57. package/content/extensions/helpers/error/index.md +81 -25
  58. package/content/extensions/helpers/index.md +2 -3
  59. package/content/extensions/helpers/inversion/index.md +15 -7
  60. package/content/extensions/helpers/kafka/compile-binary.md +92 -0
  61. package/content/extensions/helpers/kafka/examples.md +1 -1
  62. package/content/extensions/helpers/kafka/index.md +3 -0
  63. package/content/extensions/helpers/logger/index.md +32 -2
  64. package/content/extensions/helpers/network/index.md +6 -0
  65. package/content/extensions/helpers/queue/index.md +14 -17
  66. package/content/extensions/helpers/redis/index.md +548 -323
  67. package/content/extensions/helpers/socket-io/index.md +14 -10
  68. package/content/extensions/helpers/storage/api.md +44 -8
  69. package/content/extensions/helpers/storage/index.md +43 -7
  70. package/content/extensions/helpers/template/index.md +6 -3
  71. package/content/extensions/helpers/types/index.md +11 -8
  72. package/content/extensions/helpers/websocket/api.md +9 -9
  73. package/content/extensions/helpers/websocket/index.md +7 -7
  74. package/content/extensions/helpers/worker-thread/index.md +2 -2
  75. package/content/extensions/index.md +3 -4
  76. package/content/extensions/src-details/mcp-server.md +18 -24
  77. package/content/guides/core-concepts/application/bootstrapping.md +11 -14
  78. package/content/guides/core-concepts/application/index.md +3 -3
  79. package/content/guides/core-concepts/components.md +19 -10
  80. package/content/guides/core-concepts/dependency-injection.md +6 -3
  81. package/content/guides/core-concepts/grpc-controllers.md +6 -5
  82. package/content/guides/core-concepts/persistent/datasources.md +42 -43
  83. package/content/guides/core-concepts/persistent/index.md +16 -7
  84. package/content/guides/core-concepts/persistent/models.md +24 -20
  85. package/content/guides/core-concepts/persistent/postgres-drivers.md +201 -0
  86. package/content/guides/core-concepts/persistent/repositories.md +40 -23
  87. package/content/guides/core-concepts/persistent/search-meilisearch.md +185 -0
  88. package/content/guides/core-concepts/persistent/search-typesense.md +431 -0
  89. package/content/guides/core-concepts/persistent/transactions.md +61 -25
  90. package/content/guides/core-concepts/rest-controllers.md +12 -9
  91. package/content/guides/core-concepts/services.md +330 -60
  92. package/content/guides/get-started/5-minute-quickstart.md +15 -15
  93. package/content/guides/get-started/philosophy.md +36 -36
  94. package/content/guides/get-started/setup.md +3 -3
  95. package/content/guides/index.md +3 -3
  96. package/content/guides/migrations/redis-helpers-migration.md +177 -0
  97. package/content/guides/migrations/scoped-rbac-migration.md +17 -17
  98. package/content/guides/migrations/unified-connectors-migration.md +113 -0
  99. package/content/guides/reference/glossary.md +19 -12
  100. package/content/guides/reference/mcp-docs-server.md +22 -18
  101. package/content/guides/tutorials/building-a-crud-api.md +37 -44
  102. package/content/guides/tutorials/complete-installation.md +17 -17
  103. package/content/guides/tutorials/ecommerce-api.md +163 -124
  104. package/content/guides/tutorials/realtime-chat.md +181 -135
  105. package/content/guides/tutorials/testing.md +65 -523
  106. package/content/index.md +2 -180
  107. package/content/public/apple-touch-icon.png +0 -0
  108. package/content/public/og-image.png +0 -0
  109. package/content/public/site.webmanifest +11 -0
  110. package/content/references/base/application.md +4 -5
  111. package/content/references/base/bootstrapping.md +18 -5
  112. package/content/references/base/components.md +149 -120
  113. package/content/references/base/connectors.md +178 -0
  114. package/content/references/base/controllers.md +41 -30
  115. package/content/references/base/datasources.md +163 -92
  116. package/content/references/base/dependency-injection.md +34 -22
  117. package/content/references/base/filter-system/application-usage.md +17 -14
  118. package/content/references/base/filter-system/array-operators.md +7 -2
  119. package/content/references/base/filter-system/comparison-operators.md +3 -0
  120. package/content/references/base/filter-system/default-filter.md +89 -71
  121. package/content/references/base/filter-system/fields-order-pagination.md +22 -22
  122. package/content/references/base/filter-system/index.md +6 -3
  123. package/content/references/base/filter-system/json-filtering.md +20 -1
  124. package/content/references/base/filter-system/list-operators.md +1 -1
  125. package/content/references/base/filter-system/logical-operators.md +33 -1
  126. package/content/references/base/filter-system/null-operators.md +30 -1
  127. package/content/references/base/filter-system/quick-reference.md +23 -4
  128. package/content/references/base/filter-system/tips.md +5 -5
  129. package/content/references/base/filter-system/use-cases.md +12 -12
  130. package/content/references/base/grpc-controllers.md +13 -13
  131. package/content/references/base/index.md +24 -12
  132. package/content/references/base/middlewares.md +265 -327
  133. package/content/references/base/models.md +63 -49
  134. package/content/references/base/providers.md +136 -130
  135. package/content/references/base/repositories/advanced.md +59 -58
  136. package/content/references/base/repositories/index.md +115 -91
  137. package/content/references/base/repositories/mixins.md +55 -291
  138. package/content/references/base/repositories/relations.md +54 -64
  139. package/content/references/base/repositories/soft-deletable.md +31 -30
  140. package/content/references/base/services.md +296 -93
  141. package/content/references/configuration/environment-variables.md +49 -31
  142. package/content/references/configuration/index.md +6 -6
  143. package/content/references/index.md +17 -12
  144. package/content/references/quick-reference.md +65 -106
  145. package/content/references/utilities/crypto.md +65 -23
  146. package/content/references/utilities/index.md +3 -3
  147. package/content/references/utilities/jsx.md +6 -4
  148. package/content/references/utilities/module.md +68 -20
  149. package/content/references/utilities/parse.md +4 -14
  150. package/content/references/utilities/promise.md +9 -7
  151. package/content/references/utilities/schema.md +5 -3
  152. package/dist/mcp-server/common/guards.d.ts +8 -0
  153. package/dist/mcp-server/common/guards.d.ts.map +1 -0
  154. package/dist/mcp-server/common/guards.js +14 -0
  155. package/dist/mcp-server/common/guards.js.map +1 -0
  156. package/dist/mcp-server/common/index.d.ts +1 -0
  157. package/dist/mcp-server/common/index.d.ts.map +1 -1
  158. package/dist/mcp-server/common/index.js +1 -0
  159. package/dist/mcp-server/common/index.js.map +1 -1
  160. package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
  161. package/dist/mcp-server/helpers/docs.helper.js +4 -2
  162. package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
  163. package/dist/mcp-server/helpers/github.helper.js +1 -1
  164. package/dist/mcp-server/index.js +7 -2
  165. package/dist/mcp-server/index.js.map +1 -1
  166. package/dist/mcp-server/tools/base.tool.d.ts +6 -2
  167. package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
  168. package/dist/mcp-server/tools/base.tool.js.map +1 -1
  169. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  171. package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
  172. package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
  173. package/dist/mcp-server/tools/github/search-code.tool.js +4 -1
  174. package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
  175. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
  176. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +3 -1
  177. package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
  178. package/package.json +9 -9
  179. package/content/extensions/helpers/testing/index.md +0 -510
  180. 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/base/repositories/core/soft-deletable.ts`
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 repo.deleteById({ id: '123' });
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 repo.deleteById({
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 repo.deleteById({
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 repo.deleteAll({
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 repo.deleteAll({
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 repo.deleteBy({
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 repo.deleteBy({
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 repo.restoreById({ id: '123' });
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 repo.restoreById({
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 repo.restoreAll({
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 repo.restoreAll({
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 repo.restoreBy({
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 repo.findById({ id: '123' });
171
+ const category = await repository.findById({ id: '123' });
172
172
 
173
173
  // Throws 404 if not found
174
- const category = await repo.findById({
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.categoryRepo.deleteById({ id: '123', options: { transaction: tx } });
230
- await this.auditRepo.create({
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
- -> ReadableRepository
264
- -> PersistableRepository
265
- -> DefaultCRUDRepository
266
- -> SoftDeletableRepository <-- you are here
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 | `repo.deleteById({ id })` |
275
- | Hard delete by ID | `repo.deleteById({ id, options: { shouldHardDelete: true } })` |
276
- | Soft delete by condition | `repo.deleteAll({ where, options: { force: true } })` |
277
- | Restore by ID | `repo.restoreById({ id })` |
278
- | Restore by condition | `repo.restoreAll({ where })` |
279
- | Find including deleted | `repo.find({ filter, options: { shouldSkipDefaultFilter: true } })` |
280
- | Strict findById (404) | `repo.findById({ id, options: { isStrict: true } })` |
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) - Default filter and fields visibility
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 layers in Ignis.
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 | Benefit |
16
- |---------|---------|
17
- | **Extends `BaseHelper`** | Auto-configured scoped logger (`this.logger`) |
18
- | **DI Integration** | Fits into framework's dependency injection system |
19
- | **Business Logic Layer** | Bridge between Controllers and Repositories |
20
- | **No built-in CRUD** | Services are for business logic, not data access — that's what Repositories are for |
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
- ## `BaseService` Class
25
+ ---
23
26
 
24
- Abstract class that all application services should extend. It implements the `IService` interface (currently a marker interface with no required methods).
27
+ ## `BaseService` Class
25
28
 
26
- ### Class Definition
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
- ### Key Features
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
- | Feature | Description |
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
- ### Constructor
49
+ ```typescript
50
+ export interface IService {}
51
+ ```
48
52
 
49
- The constructor requires an options object with a `scope` string, which is typically set to the class name:
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 UserService extends BaseService {
53
- constructor() {
54
- super({ scope: UserService.name });
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
- ## `IService` Interface
84
+ The resulting binding key defaults to `services.{ClassName}` (overridable via `opts.binding`).
85
+
86
+ ### Lifecycle Placement
60
87
 
61
- The `IService` interface is a marker interface with no required methods. It exists to provide a type-level contract for services.
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 interface IService {}
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
- ## No Built-in CRUD Service
103
+ ---
68
104
 
69
- Ignis intentionally does not provide a `BaseCrudService`. CRUD operations belong in the Repository layer (`DefaultCRUDRepository`). Services are for business logic that orchestrates one or more repositories, performs validation, handles transactions, or coordinates cross-cutting concerns.
105
+ ## Dependency Injection into Services
70
106
 
71
- ## Registration
107
+ Inject repositories, other services, or datasources via `@inject` in the constructor. Two equivalent key forms are available:
72
108
 
73
- Services are registered with the DI container using the `app.service()` method or via the boot system's auto-discovery:
109
+ ### String-literal keys
74
110
 
75
111
  ```typescript
76
- // Manual registration (in preConfigure or registerComponents)
77
- app.service(UserService); // Binds as 'services.UserService'
112
+ import { BaseService, inject } from '@venizia/ignis';
113
+ import { UserRepository } from '../repositories';
114
+ import { JWKSIssuerTokenService } from '@venizia/ignis';
78
115
 
79
- // Or via boot system auto-discovery:
80
- // Place file at src/services/user.service.ts → auto-discovered and bound
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
- ## How Services Fit into the Architecture
129
+ ### Programmatic keys with `BindingKeys.build`
84
130
 
85
- Services are the core of your application's logic. They act as a bridge between the presentation layer (Controllers) and the data access layer (Repositories).
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
- ### Typical Service Flow
133
+ ```typescript
134
+ import { BaseService, BindingKeys, BindingNamespaces, inject } from '@venizia/ignis';
135
+ import { UserRepository } from '../repositories';
88
136
 
89
- 1. **Instantiated by DI Container**: When the application starts, the DI container creates instances of your services.
90
- 2. **Dependencies Injected**: The service's constructor receives instances of any repositories or other services it depends on.
91
- 3. **Called by a Controller**: An HTTP request comes into a controller, which then calls a method on a service to handle the business logic for that request.
92
- 4. **Orchestrates Logic**: The service method executes the business logic. This may involve:
93
- - Validating input data.
94
- - Calling one or more repository methods to fetch or save data.
95
- - Calling other services to perform related tasks.
96
- - Performing calculations or data transformations.
97
- 5. **Returns Data**: The service returns the result of the operation back to the controller, which then formats it into an HTTP response.
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
- ### Example
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, inject, injectable } from '@venizia/ignis';
103
- import { getError } from '@venizia/ignis-helpers';
104
- import { UserRepository } from '../repositories/user.repository';
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({ key: 'repositories.UserRepository' })
113
- private _userRepository: UserRepository,
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: UserService.name });
222
+ super({ scope: OrderService.name });
116
223
  }
117
224
 
118
- // 3. Method is called by a controller
119
- async getUserProfile(opts: { userId: string }): Promise<Partial<TUser>> {
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
- // 4. Orchestrates logic: calls the repository
123
- const user = await this._userRepository.findById({ id: opts.userId });
228
+ const { data: order } = await this.orderRepository.create({ data: opts });
124
229
 
125
- if (!user) {
126
- throw getError({ message: 'User not found' });
127
- }
230
+ // Compose with another service
231
+ await this.notificationService.sendOrderConfirmation({ orderId: order.id });
128
232
 
129
- // 5. Returns transformed data
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
- ### Transaction Orchestration
238
+ Register both services in `preConfigure()`:
140
239
 
141
- A common service pattern is orchestrating transactions across multiple repositories:
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
- @injectable()
145
- export class OrderService extends BaseService {
146
- constructor(
147
- @inject({ key: 'repositories.OrderRepository' })
148
- private orderRepo: OrderRepository,
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
- @inject({ key: 'repositories.InventoryRepository' })
151
- private inventoryRepo: InventoryRepository,
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: OrderService.name });
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.orderRepo.create({
163
- data: { userId: opts.userId, items: opts.items },
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.inventoryRepo.updateById({
327
+ await this.inventoryRepository.updateById({
169
328
  id: item.productId,
170
- data: { quantity: item.quantity },
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
- By adhering to this pattern, you keep your code organized, testable, and maintainable. You can easily test `UserService` by providing a mock `UserRepository` without needing a real database connection.
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 request handlers that call services
191
- - [Repositories](./repositories/) - Data access layer used by services
192
- - [Providers](./providers.md) - Factory pattern for runtime instantiation
193
- - [Dependency Injection](./dependency-injection.md) - DI container and injection patterns
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)