@venizia/ignis-docs 0.0.8-3 → 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.
Files changed (213) hide show
  1. package/README.md +7 -7
  2. package/{wiki → content}/best-practices/api-usage-examples.md +15 -12
  3. package/{wiki → content}/best-practices/architectural-patterns.md +70 -78
  4. package/{wiki → content}/best-practices/architecture-decisions.md +91 -60
  5. package/{wiki → content}/best-practices/code-style-standards/advanced-patterns.md +56 -44
  6. package/{wiki → content}/best-practices/code-style-standards/constants-configuration.md +11 -11
  7. package/{wiki → content}/best-practices/code-style-standards/control-flow.md +5 -2
  8. package/{wiki → content}/best-practices/code-style-standards/documentation.md +13 -13
  9. package/{wiki → content}/best-practices/code-style-standards/function-patterns.md +9 -10
  10. package/{wiki → content}/best-practices/code-style-standards/index.md +1 -1
  11. package/{wiki → content}/best-practices/code-style-standards/naming-conventions.md +10 -8
  12. package/{wiki → content}/best-practices/code-style-standards/route-definitions.md +30 -12
  13. package/{wiki → content}/best-practices/code-style-standards/tooling.md +8 -5
  14. package/{wiki → content}/best-practices/code-style-standards/type-safety.md +13 -12
  15. package/{wiki → content}/best-practices/common-pitfalls.md +56 -37
  16. package/{wiki → content}/best-practices/contribution-workflow.md +13 -14
  17. package/{wiki → content}/best-practices/data-modeling.md +44 -20
  18. package/{wiki → content}/best-practices/deployment-strategies.md +28 -27
  19. package/{wiki → content}/best-practices/error-handling.md +48 -24
  20. package/{wiki → content}/best-practices/index.md +5 -5
  21. package/{wiki → content}/best-practices/performance-optimization.md +36 -28
  22. package/{wiki → content}/best-practices/security-guidelines.md +52 -23
  23. package/{wiki → content}/best-practices/testing-strategies.md +65 -51
  24. package/{wiki → content}/best-practices/troubleshooting-tips.md +24 -24
  25. package/{wiki/extensions/components/swagger.md → content/extensions/components/api-reference.md} +40 -31
  26. package/{wiki → content}/extensions/components/authentication/api.md +19 -19
  27. package/{wiki → content}/extensions/components/authentication/errors.md +7 -7
  28. package/{wiki → content}/extensions/components/authentication/index.md +10 -8
  29. package/{wiki → content}/extensions/components/authentication/usage.md +101 -6
  30. package/{wiki → content}/extensions/components/authorization/api.md +45 -25
  31. package/{wiki → content}/extensions/components/authorization/errors.md +6 -6
  32. package/{wiki → content}/extensions/components/authorization/index.md +11 -10
  33. package/{wiki → content}/extensions/components/authorization/usage.md +21 -21
  34. package/{wiki → content}/extensions/components/health-check.md +1 -1
  35. package/{wiki → content}/extensions/components/index.md +5 -5
  36. package/{wiki → content}/extensions/components/mail/errors.md +15 -15
  37. package/{wiki → content}/extensions/components/mail/index.md +1 -2
  38. package/{wiki → content}/extensions/components/mail/usage.md +1 -1
  39. package/{wiki → content}/extensions/components/request-tracker.md +1 -1
  40. package/{wiki → content}/extensions/components/socket-io/api.md +9 -9
  41. package/{wiki → content}/extensions/components/socket-io/errors.md +5 -5
  42. package/{wiki → content}/extensions/components/socket-io/index.md +8 -8
  43. package/{wiki → content}/extensions/components/socket-io/usage.md +1 -1
  44. package/{wiki → content}/extensions/components/static-asset/api.md +17 -4
  45. package/{wiki → content}/extensions/components/static-asset/errors.md +4 -4
  46. package/{wiki → content}/extensions/components/static-asset/index.md +26 -28
  47. package/{wiki → content}/extensions/components/static-asset/usage.md +13 -12
  48. package/{wiki → content}/extensions/components/template/index.md +2 -2
  49. package/{wiki → content}/extensions/components/template/setup-page.md +1 -1
  50. package/{wiki → content}/extensions/components/websocket/api.md +3 -3
  51. package/{wiki → content}/extensions/components/websocket/errors.md +5 -5
  52. package/{wiki → content}/extensions/components/websocket/index.md +5 -5
  53. package/{wiki → content}/extensions/components/websocket/usage.md +3 -3
  54. package/{wiki → content}/extensions/helpers/cron/index.md +2 -2
  55. package/{wiki → content}/extensions/helpers/crypto/index.md +1 -1
  56. package/{wiki → content}/extensions/helpers/env/index.md +27 -12
  57. package/content/extensions/helpers/error/index.md +283 -0
  58. package/{wiki → content}/extensions/helpers/index.md +2 -3
  59. package/{wiki → content}/extensions/helpers/inversion/index.md +15 -7
  60. package/{wiki → content}/extensions/helpers/kafka/examples.md +1 -1
  61. package/{wiki → content}/extensions/helpers/logger/index.md +32 -2
  62. package/{wiki → content}/extensions/helpers/network/index.md +6 -0
  63. package/{wiki → content}/extensions/helpers/queue/index.md +14 -17
  64. package/content/extensions/helpers/redis/index.md +713 -0
  65. package/{wiki → content}/extensions/helpers/socket-io/index.md +14 -10
  66. package/{wiki → content}/extensions/helpers/storage/api.md +44 -8
  67. package/{wiki → content}/extensions/helpers/storage/index.md +43 -7
  68. package/{wiki → content}/extensions/helpers/template/index.md +6 -3
  69. package/{wiki → content}/extensions/helpers/types/index.md +11 -8
  70. package/{wiki → content}/extensions/helpers/websocket/api.md +9 -9
  71. package/{wiki → content}/extensions/helpers/websocket/index.md +7 -7
  72. package/{wiki → content}/extensions/helpers/worker-thread/index.md +2 -2
  73. package/{wiki → content}/extensions/index.md +3 -4
  74. package/{wiki → content}/extensions/src-details/mcp-server.md +18 -24
  75. package/{wiki → content}/guides/core-concepts/application/bootstrapping.md +11 -14
  76. package/{wiki → content}/guides/core-concepts/application/index.md +3 -3
  77. package/{wiki → content}/guides/core-concepts/components.md +19 -10
  78. package/{wiki → content}/guides/core-concepts/dependency-injection.md +6 -3
  79. package/{wiki → content}/guides/core-concepts/grpc-controllers.md +6 -5
  80. package/{wiki → content}/guides/core-concepts/persistent/datasources.md +33 -27
  81. package/{wiki → content}/guides/core-concepts/persistent/index.md +16 -5
  82. package/{wiki → content}/guides/core-concepts/persistent/models.md +24 -20
  83. package/content/guides/core-concepts/persistent/postgres-drivers.md +167 -0
  84. package/{wiki → content}/guides/core-concepts/persistent/repositories.md +40 -23
  85. package/content/guides/core-concepts/persistent/search-meilisearch.md +183 -0
  86. package/content/guides/core-concepts/persistent/search-typesense.md +429 -0
  87. package/{wiki → content}/guides/core-concepts/persistent/transactions.md +61 -25
  88. package/{wiki → content}/guides/core-concepts/rest-controllers.md +12 -9
  89. package/content/guides/core-concepts/services.md +389 -0
  90. package/{wiki → content}/guides/get-started/5-minute-quickstart.md +19 -19
  91. package/{wiki → content}/guides/get-started/philosophy.md +36 -36
  92. package/{wiki → content}/guides/get-started/setup.md +3 -3
  93. package/{wiki → content}/guides/index.md +3 -3
  94. package/content/guides/migrations/redis-helpers-migration.md +177 -0
  95. package/{wiki → content}/guides/migrations/scoped-rbac-migration.md +17 -17
  96. package/content/guides/migrations/unified-connectors-migration.md +113 -0
  97. package/{wiki → content}/guides/reference/glossary.md +19 -12
  98. package/{wiki → content}/guides/reference/mcp-docs-server.md +22 -18
  99. package/{wiki → content}/guides/tutorials/building-a-crud-api.md +30 -33
  100. package/{wiki → content}/guides/tutorials/complete-installation.md +17 -17
  101. package/{wiki → content}/guides/tutorials/ecommerce-api.md +158 -119
  102. package/{wiki → content}/guides/tutorials/realtime-chat.md +176 -130
  103. package/content/guides/tutorials/testing.md +264 -0
  104. package/content/index.md +5 -0
  105. package/content/public/apple-touch-icon.png +0 -0
  106. package/content/public/og-image.png +0 -0
  107. package/content/public/site.webmanifest +11 -0
  108. package/{wiki → content}/references/base/application.md +4 -5
  109. package/{wiki → content}/references/base/bootstrapping.md +18 -5
  110. package/{wiki → content}/references/base/components.md +149 -120
  111. package/content/references/base/connectors.md +178 -0
  112. package/{wiki → content}/references/base/controllers.md +41 -30
  113. package/content/references/base/datasources.md +527 -0
  114. package/{wiki → content}/references/base/dependency-injection.md +34 -22
  115. package/{wiki → content}/references/base/filter-system/application-usage.md +17 -14
  116. package/{wiki → content}/references/base/filter-system/array-operators.md +7 -2
  117. package/{wiki → content}/references/base/filter-system/comparison-operators.md +3 -0
  118. package/{wiki → content}/references/base/filter-system/default-filter.md +89 -71
  119. package/{wiki → content}/references/base/filter-system/fields-order-pagination.md +22 -22
  120. package/{wiki → content}/references/base/filter-system/index.md +6 -3
  121. package/{wiki → content}/references/base/filter-system/json-filtering.md +20 -1
  122. package/{wiki → content}/references/base/filter-system/list-operators.md +1 -1
  123. package/{wiki → content}/references/base/filter-system/logical-operators.md +33 -1
  124. package/{wiki → content}/references/base/filter-system/null-operators.md +30 -1
  125. package/{wiki → content}/references/base/filter-system/quick-reference.md +23 -4
  126. package/{wiki → content}/references/base/filter-system/tips.md +5 -5
  127. package/{wiki → content}/references/base/filter-system/use-cases.md +12 -12
  128. package/{wiki → content}/references/base/grpc-controllers.md +13 -13
  129. package/{wiki → content}/references/base/index.md +24 -12
  130. package/{wiki/references/base/middleware.md → content/references/base/middlewares.md} +205 -24
  131. package/{wiki → content}/references/base/models.md +63 -49
  132. package/{wiki → content}/references/base/providers.md +136 -130
  133. package/{wiki → content}/references/base/repositories/advanced.md +59 -58
  134. package/{wiki → content}/references/base/repositories/index.md +115 -91
  135. package/content/references/base/repositories/mixins.md +99 -0
  136. package/{wiki → content}/references/base/repositories/relations.md +54 -64
  137. package/{wiki → content}/references/base/repositories/soft-deletable.md +31 -30
  138. package/content/references/base/services.md +404 -0
  139. package/{wiki → content}/references/configuration/environment-variables.md +46 -30
  140. package/{wiki → content}/references/configuration/index.md +6 -6
  141. package/{wiki → content}/references/index.md +17 -12
  142. package/{wiki → content}/references/quick-reference.md +65 -106
  143. package/content/references/utilities/crypto.md +98 -0
  144. package/{wiki → content}/references/utilities/index.md +3 -3
  145. package/{wiki → content}/references/utilities/jsx.md +6 -4
  146. package/content/references/utilities/module.md +90 -0
  147. package/{wiki → content}/references/utilities/parse.md +4 -14
  148. package/{wiki → content}/references/utilities/promise.md +9 -7
  149. package/{wiki → content}/references/utilities/schema.md +5 -3
  150. package/dist/mcp-server/common/guards.d.ts +8 -0
  151. package/dist/mcp-server/common/guards.d.ts.map +1 -0
  152. package/dist/mcp-server/common/guards.js +14 -0
  153. package/dist/mcp-server/common/guards.js.map +1 -0
  154. package/dist/mcp-server/common/index.d.ts +1 -0
  155. package/dist/mcp-server/common/index.d.ts.map +1 -1
  156. package/dist/mcp-server/common/index.js +1 -0
  157. package/dist/mcp-server/common/index.js.map +1 -1
  158. package/dist/mcp-server/common/paths.d.ts.map +1 -1
  159. package/dist/mcp-server/common/paths.js +2 -2
  160. package/dist/mcp-server/common/paths.js.map +1 -1
  161. package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
  162. package/dist/mcp-server/helpers/docs.helper.js +4 -2
  163. package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
  164. package/dist/mcp-server/helpers/github.helper.js +1 -1
  165. package/dist/mcp-server/index.js +7 -2
  166. package/dist/mcp-server/index.js.map +1 -1
  167. package/dist/mcp-server/tools/base.tool.d.ts +6 -2
  168. package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
  169. package/dist/mcp-server/tools/base.tool.js.map +1 -1
  170. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
  171. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  172. package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
  173. package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
  174. package/dist/mcp-server/tools/github/search-code.tool.js +4 -1
  175. package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
  176. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
  177. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +3 -1
  178. package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
  179. package/package.json +12 -12
  180. package/wiki/extensions/helpers/error/index.md +0 -227
  181. package/wiki/extensions/helpers/redis/index.md +0 -488
  182. package/wiki/extensions/helpers/testing/index.md +0 -510
  183. package/wiki/guides/core-concepts/services.md +0 -119
  184. package/wiki/guides/tutorials/testing.md +0 -722
  185. package/wiki/index.md +0 -183
  186. package/wiki/references/base/datasources.md +0 -454
  187. package/wiki/references/base/middlewares.md +0 -590
  188. package/wiki/references/base/repositories/mixins.md +0 -335
  189. package/wiki/references/base/services.md +0 -201
  190. package/wiki/references/utilities/crypto.md +0 -56
  191. package/wiki/references/utilities/module.md +0 -42
  192. /package/{wiki → content}/extensions/components/mail/api.md +0 -0
  193. /package/{wiki → content}/extensions/components/template/api-page.md +0 -0
  194. /package/{wiki → content}/extensions/components/template/errors-page.md +0 -0
  195. /package/{wiki → content}/extensions/components/template/single-page.md +0 -0
  196. /package/{wiki → content}/extensions/components/template/usage-page.md +0 -0
  197. /package/{wiki → content}/extensions/helpers/kafka/admin.md +0 -0
  198. /package/{wiki → content}/extensions/helpers/kafka/consumer.md +0 -0
  199. /package/{wiki → content}/extensions/helpers/kafka/index.md +0 -0
  200. /package/{wiki → content}/extensions/helpers/kafka/producer.md +0 -0
  201. /package/{wiki → content}/extensions/helpers/kafka/schema-registry.md +0 -0
  202. /package/{wiki → content}/extensions/helpers/network/api.md +0 -0
  203. /package/{wiki → content}/extensions/helpers/socket-io/api.md +0 -0
  204. /package/{wiki → content}/extensions/helpers/template/single-page.md +0 -0
  205. /package/{wiki → content}/extensions/helpers/uid/index.md +0 -0
  206. /package/{wiki → content}/guides/core-concepts/components-guide.md +0 -0
  207. /package/{wiki → content}/public/logo.svg +0 -0
  208. /package/{wiki → content}/references/base/filter-system/pattern-matching.md +0 -0
  209. /package/{wiki → content}/references/base/filter-system/range-operators.md +0 -0
  210. /package/{wiki → content}/references/utilities/date.md +0 -0
  211. /package/{wiki → content}/references/utilities/performance.md +0 -0
  212. /package/{wiki → content}/references/utilities/request.md +0 -0
  213. /package/{wiki → content}/references/utilities/statuses.md +0 -0
@@ -1,6 +1,9 @@
1
1
  # Transactions
2
2
 
3
- Ignis supports explicit transaction objects that can be passed across multiple services and repositories, allowing for complex, multi-step business logic to be atomic.
3
+ IGNIS supports explicit transaction objects that can be passed across multiple services and repositories, allowing for complex, multi-step business logic to be atomic.
4
+
5
+ > [!NOTE] PostgreSQL-only capability
6
+ > Real transactions are a **PostgreSQL connector** capability - `BasePostgresDataSource.getCapabilities()` returns `{ transactions: true }` and its `beginTransaction()` opens a real database transaction, as documented below. The typesense connector inherits the engine-neutral `AbstractDataSource` default: calling `beginTransaction()` on it throws a `501 Not Implemented` (`messageCode: 'core.not_supported'`) via the shared `throwNotSupported` utility. See [Connectors](/references/base/connectors) for the capabilities model.
4
7
 
5
8
  ## Using Transactions
6
9
 
@@ -14,8 +17,8 @@ const tx = await userRepo.beginTransaction({
14
17
 
15
18
  try {
16
19
  // 2. Pass transaction to operations
17
- // Create user
18
- const user = await userRepo.create({
20
+ // Create user (write methods return a { count, data } envelope)
21
+ const { data: user } = await userRepo.create({
19
22
  data: userData,
20
23
  options: { transaction: tx }
21
24
  });
@@ -32,29 +35,54 @@ try {
32
35
  // 3. Commit the transaction
33
36
  await tx.commit();
34
37
  } catch (err) {
35
- // 4. Rollback on error
36
- await tx.rollback();
37
- throw err;
38
+ // 4. Rollback on error. Nest it: rollback() throws if ROLLBACK itself fails, and a bare
39
+ // `await tx.rollback()` here would replace `err` with the rollback error.
40
+ try {
41
+ await tx.rollback();
42
+ } catch (rollbackError) {
43
+ logger.error('Rollback failed | %s', rollbackError);
44
+ }
45
+
46
+ throw err; // the original cause survives
38
47
  }
39
48
  ```
40
49
 
50
+ > [!WARNING] `commit()` and `rollback()` throw on failure
51
+ > `COMMIT` can genuinely fail - a deadlock, a serialization failure under `SERIALIZABLE`, a dropped
52
+ > connection, a deferred constraint firing at commit time. When it does, `commit()` throws rather
53
+ > than resolving, so you never report success on a write that was never persisted. The connection is
54
+ > then destroyed instead of being returned to the pool, because it may still hold an open
55
+ > transaction that the next borrower would inherit.
56
+ >
57
+ > `rollback()` behaves the same way when it is the FIRST verb to fail. One deliberate exception
58
+ > keeps the everyday `catch { await tx.rollback(); throw error; }` pattern safe: calling
59
+ > `rollback()` on a transaction that already ended BY FAILURE (a failed `COMMIT` or a failed prior
60
+ > `ROLLBACK`) is a silent no-op - nothing was committed and the connection is already destroyed, so
61
+ > the rollback's goal is achieved and your original error survives. The nested-try form above is
62
+ > still the safest general pattern, because a FIRST rollback that itself fails does throw.
63
+ >
64
+ > The destroy half is **driver-specific**: node-postgres (`pg`) discards the poisoned connection,
65
+ > but postgres-js has no destroy semantics (`ReservedSql.release()` takes no argument), so under the
66
+ > postgres-js driver the connection returns to the pool anyway. See
67
+ > [Postgres Drivers & Supabase](./postgres-drivers) for the full asymmetry.
68
+
41
69
  ## Transaction Object
42
70
 
43
- The transaction object returned by `beginTransaction()` has the following properties:
71
+ `beginTransaction()` returns an `IDatabaseTransaction` with the following properties:
44
72
 
45
73
  | Property/Method | Type | Description |
46
74
  | :--- | :--- | :--- |
47
- | `connector` | `TNodePostgresConnector` | A Drizzle connector bound to the transaction's database client |
75
+ | `connector` | `TRelationalConnector<Schema>` | A Drizzle connector bound to the transaction's dedicated connection |
48
76
  | `isolationLevel` | `TIsolationLevel` | The isolation level of this transaction |
49
77
  | `isActive` | `boolean` | Whether the transaction is still active (not yet committed/rolled back) |
50
- | `commit()` | `Promise<void>` | Commit the transaction and release the connection |
51
- | `rollback()` | `Promise<void>` | Rollback the transaction and release the connection |
78
+ | `commit()` | `Promise<void>` | Commit and release the connection. **Throws** if `COMMIT` fails, and destroys the connection rather than pooling it |
79
+ | `rollback()` | `Promise<void>` | Rollback and release the connection. **Throws** if `ROLLBACK` fails, and destroys the connection rather than pooling it |
52
80
 
53
- Calling `commit()` or `rollback()` on an already-ended transaction throws an error.
81
+ Calling `commit()` or `rollback()` on an already-ended transaction throws an error, with one exception: `rollback()` after the transaction ended BY FAILURE is a silent no-op (see the warning above).
54
82
 
55
83
  ## Isolation Levels
56
84
 
57
- Ignis supports standard PostgreSQL isolation levels:
85
+ IGNIS supports standard PostgreSQL isolation levels:
58
86
 
59
87
  | Level | Description | Use Case |
60
88
  |-------|-------------|----------|
@@ -63,17 +91,17 @@ Ignis supports standard PostgreSQL isolation levels:
63
91
  | `SERIALIZABLE` | Strictest level. Emulates serial execution. | Financial transactions, critical data integrity. |
64
92
 
65
93
  > [!NOTE]
66
- > Ignis only supports these three levels. `READ UNCOMMITTED` is **not** accepted PostgreSQL treats it as `READ COMMITTED` anyway, so Ignis omits it to avoid confusion.
94
+ > IGNIS only supports these three levels. The fourth SQL-standard level (uncommitted reads) is **not** accepted - PostgreSQL treats it as `READ COMMITTED` anyway, so IGNIS omits it to avoid confusion.
67
95
 
68
96
  ## Best Practices
69
97
 
70
- 1. **Always use `try...catch`**: Ensure `rollback()` is called on error to release the connection back to the pool.
98
+ 1. **Always use `try...catch`, and nest the rollback**: `rollback()` throws when `ROLLBACK` fails, so a bare `await tx.rollback()` inside a `catch` would discard the error that sent you there. Wrap it in its own `try...catch`, log the rollback failure, and rethrow the original cause.
71
99
  2. **Keep it short**: Long-running transactions hold database connections from the pool and can cause connection exhaustion.
72
100
  3. **Pass explicit options**: When calling other services inside a transaction, ensure they accept and use the `transaction` option.
73
101
 
74
102
  ```typescript
75
103
  // Service method supporting transactions
76
- async createInitialOrder(opts: { userId: string; transaction?: ITransaction }) {
104
+ async createInitialOrder(opts: { userId: string; transaction?: IDatabaseTransaction }) {
77
105
  return this.orderRepository.create({
78
106
  data: { userId: opts.userId, status: 'PENDING' },
79
107
  options: { transaction: opts.transaction } // Forward the transaction
@@ -99,12 +127,12 @@ export class OrderService extends BaseService {
99
127
  async createOrderWithItems(opts: {
100
128
  orderData: TOrderCreate;
101
129
  items: TOrderItemCreate[];
102
- transaction?: ITransaction;
130
+ transaction?: IDatabaseTransaction;
103
131
  }) {
104
132
  const { orderData, items, transaction } = opts;
105
133
 
106
134
  // Create order
107
- const order = await this._orderRepository.create({
135
+ const { data: order } = await this._orderRepository.create({
108
136
  data: orderData,
109
137
  options: { transaction },
110
138
  });
@@ -138,7 +166,7 @@ export class OrderController extends BaseRestController {
138
166
 
139
167
  @post({ configs: OrderRoutes.CREATE })
140
168
  async createOrder(c: TRouteContext) {
141
- const body = c.req.valid<{ order: any; items: any[] }>('json');
169
+ const body = await c.req.json<{ order: TOrderCreate; items: TOrderItemCreate[] }>();
142
170
 
143
171
  const tx = await this._orderRepository.beginTransaction({
144
172
  isolationLevel: 'SERIALIZABLE',
@@ -154,7 +182,12 @@ export class OrderController extends BaseRestController {
154
182
  await tx.commit();
155
183
  return c.json(order, HTTP.ResultCodes.RS_2.Created);
156
184
  } catch (err) {
157
- await tx.rollback();
185
+ try {
186
+ await tx.rollback();
187
+ } catch (rollbackError) {
188
+ this.logger.error('Rollback failed | %s', rollbackError);
189
+ }
190
+
158
191
  throw err;
159
192
  }
160
193
  }
@@ -163,15 +196,18 @@ export class OrderController extends BaseRestController {
163
196
 
164
197
  ## How Transactions Work Internally
165
198
 
166
- When you pass a `transaction` option to a repository method, the repository uses the transaction's `connector` (a Drizzle instance bound to the transaction's `PoolClient`) instead of the default datasource connector. This ensures all operations within the transaction use the same database connection and see a consistent view of the data.
199
+ When you pass a `transaction` option to a repository method, the repository uses the transaction's `connector` (a Drizzle instance bound to the transaction's dedicated connection) instead of the default datasource connector. This ensures all operations within the transaction use the same database connection and see a consistent view of the data.
167
200
 
168
201
  ```typescript
169
- // Inside AbstractRepository (simplified)
170
- protected resolveConnector(opts?: { transaction?: ITransaction }) {
171
- if (opts?.transaction) {
172
- return opts.transaction.connector;
202
+ // Inside PostgresBaseRepository (simplified)
203
+ protected resolveConnector(opts?: { transaction?: IDatabaseTransaction }) {
204
+ if (!opts?.transaction) {
205
+ return this.dataSource.connector;
173
206
  }
174
- return this.dataSource.connector;
207
+
208
+ // Throws if the transaction has already been committed/rolled back,
209
+ // or if it is not a postgres transaction.
210
+ return opts.transaction.connector;
175
211
  }
176
212
  ```
177
213
 
@@ -1,6 +1,6 @@
1
1
  # REST Controllers
2
2
 
3
- REST controllers handle incoming HTTP requests and return JSON responses -- they are your API endpoints. This is the default transport in Ignis and covers the majority of use cases.
3
+ REST controllers handle incoming HTTP requests and return JSON responses -- they are your API endpoints. This is the default transport in IGNIS and covers the majority of use cases.
4
4
 
5
5
  > **Deep Dive:** See [REST Controllers Reference](../../references/base/controllers.md) for the complete API.
6
6
 
@@ -9,8 +9,9 @@ REST controllers handle incoming HTTP requests and return JSON responses -- they
9
9
  Extend `BaseRestController` and use decorators to define routes:
10
10
 
11
11
  ```typescript
12
- import { BaseRestController, controller, get, jsonResponse, z, TRouteContext } from '@venizia/ignis';
12
+ import { BaseRestController, controller, get, jsonResponse, TRouteContext } from '@venizia/ignis';
13
13
  import { HTTP } from '@venizia/ignis-helpers';
14
+ import { z } from '@hono/zod-openapi';
14
15
 
15
16
  @controller({ path: '/users' })
16
17
  export class UserController extends BaseRestController {
@@ -55,7 +56,7 @@ For decorator-based routes, you do not need to explicitly annotate the return ty
55
56
 
56
57
  ### HTTP Method Decorators
57
58
 
58
- `Ignis` provides a decorator for each common HTTP method:
59
+ `IGNIS` provides a decorator for each common HTTP method:
59
60
 
60
61
  - `@get(opts)`
61
62
  - `@post(opts)`
@@ -132,7 +133,7 @@ export class MyItemsController extends BaseRestController {
132
133
 
133
134
  ## Manual Route Definition: An Alternative Approach
134
135
 
135
- While decorators are the recommended approach for most use cases, `Ignis` also provides a manual way to define routes within the controller's `binding()` method.
136
+ While decorators are the recommended approach for most use cases, `IGNIS` also provides a manual way to define routes within the controller's `binding()` method.
136
137
 
137
138
  ### Decorator vs Manual: Quick Comparison
138
139
 
@@ -161,8 +162,9 @@ When using this method, you will override the `binding()` method in your control
161
162
  Use this method for defining a single API endpoint with all its configurations and handler. It also benefits from type inference when used with `TRouteContext`.
162
163
 
163
164
  ```typescript
164
- import { Authentication, jsonResponse, z, TRouteContext } from '@venizia/ignis';
165
+ import { Authentication, jsonResponse, TRouteContext } from '@venizia/ignis';
165
166
  import { HTTP } from '@venizia/ignis-helpers';
167
+ import { z } from '@hono/zod-openapi';
166
168
 
167
169
  // ... inside the binding() method
168
170
 
@@ -189,8 +191,9 @@ this.defineRoute({
189
191
  This method offers a fluent API for defining routes, similar to `defineRoute`, but structured for chaining. It also benefits from `TRouteContext` for type safety.
190
192
 
191
193
  ```typescript
192
- import { jsonResponse, z, TRouteContext } from '@venizia/ignis';
194
+ import { jsonResponse, TRouteContext } from '@venizia/ignis';
193
195
  import { HTTP } from '@venizia/ignis-helpers';
196
+ import { z } from '@hono/zod-openapi';
194
197
 
195
198
  // ... inside the binding() method
196
199
 
@@ -215,7 +218,7 @@ this.bindRoute({
215
218
 
216
219
  ## `ControllerFactory` for CRUD Operations
217
220
 
218
- For standard CRUD (Create, Read, Update, Delete) operations, `Ignis` provides a `ControllerFactory` that can generate a full-featured controller for any given entity. This significantly reduces boilerplate code.
221
+ For standard CRUD (Create, Read, Update, Delete) operations, `IGNIS` provides a `ControllerFactory` that can generate a full-featured controller for any given entity. This significantly reduces boilerplate code.
219
222
 
220
223
  ```typescript
221
224
  // src/controllers/configuration/configuration.controller.ts (Example from @examples/vert)
@@ -363,7 +366,7 @@ export const MainLayout: FC<PropsWithChildren<MainLayoutProps>> = ({ title, chil
363
366
  };
364
367
  ```
365
368
 
366
- > **Note:** JSX support in `Ignis` uses Hono's built-in JSX runtime. Make sure your `tsconfig.json` includes the JSX configuration (this is already set up in the framework's base configuration).
369
+ > **Note:** JSX support in `IGNIS` uses Hono's built-in JSX runtime. Make sure your `tsconfig.json` includes the JSX configuration (this is already set up in the framework's base configuration).
367
370
 
368
371
  ## Accessing Validated Request Data
369
372
 
@@ -454,7 +457,7 @@ Using `TRouteContext` provides a typed context object. By using `c.req.valid<T>(
454
457
  - **References:**
455
458
  - [BaseRestController API](/references/base/controllers) - Complete REST controller API reference
456
459
  - [Middlewares](/references/base/middlewares) - Request interceptors
457
- - [Swagger Component](/extensions/components/swagger) - Auto-generate API docs
460
+ - [Swagger Component](/extensions/components/api-reference) - Auto-generate API docs
458
461
  - [Schema Utilities](/references/utilities/schema) - Request/response helpers
459
462
 
460
463
  - **Tutorials:**
@@ -0,0 +1,389 @@
1
+ # Services
2
+
3
+ Services contain your application's business logic, orchestrating data flow and executing use cases.
4
+
5
+ > **Deep Dive:** See [Services Reference](../../references/base/services.md) for the full API reference.
6
+
7
+ ## The Business Logic Layer
8
+
9
+ Services sit between controllers and repositories. A controller receives an HTTP request and immediately delegates to a service; the service applies business rules, calls one or more repositories, and returns the result.
10
+
11
+ Primary responsibilities:
12
+
13
+ - **Encapsulate business rules** - validation, state-machine transitions, authorization guards, calculations
14
+ - **Coordinate repositories** - fetch related data, combine writes, enforce consistency
15
+ - **Manage transactions** - begin, commit, or roll back across multiple repository calls
16
+ - **Compose services** - inject and call other services to avoid duplicating logic
17
+
18
+ Controllers stay thin. Repositories stay dumb. Everything in between is a service.
19
+
20
+ ---
21
+
22
+ ## Creating a Service
23
+
24
+ Extend `BaseService` and call `super({ scope: ClassName.name })` in the constructor:
25
+
26
+ ```typescript
27
+ import { BaseService, inject } from '@venizia/ignis';
28
+ import { getError, HTTP } from '@venizia/ignis-helpers';
29
+ import { UserRepository } from '../repositories';
30
+
31
+ export class UserService extends BaseService {
32
+ constructor(
33
+ @inject({ key: 'repositories.UserRepository' })
34
+ private userRepository: UserRepository,
35
+ ) {
36
+ super({ scope: UserService.name });
37
+ }
38
+
39
+ async getUser(opts: { id: string }) {
40
+ const user = await this.userRepository.findById({ id: opts.id });
41
+
42
+ if (!user) {
43
+ throw getError({
44
+ statusCode: HTTP.ResultCodes.RS_4.NotFound,
45
+ message: 'User not found',
46
+ });
47
+ }
48
+
49
+ return user;
50
+ }
51
+ }
52
+ ```
53
+
54
+ **`scope`** is the tag written into every log line produced by this service. Use `ClassName.name` - it's the project convention and avoids string drift when renaming the class.
55
+
56
+ ---
57
+
58
+ ## Registering a Service
59
+
60
+ Registration is always imperative - call `this.service(ClassName)` inside an application lifecycle method. Do not add `@injectable` to services; `this.service()` handles the binding.
61
+
62
+ ```typescript
63
+ import { BaseApplication } from '@venizia/ignis';
64
+ import { PostgresDataSource } from './datasources';
65
+ import { UserRepository } from './repositories';
66
+ import { UserService } from './services';
67
+
68
+ export class Application extends BaseApplication {
69
+ preConfigure(): void {
70
+ // Register dependencies first, then the service
71
+ this.dataSource(PostgresDataSource);
72
+ this.repository(UserRepository);
73
+ this.service(UserService); // binds as 'services.UserService'
74
+ }
75
+ }
76
+ ```
77
+
78
+ `this.service(UserService)` binds the class at the key `services.UserService`. Any other binding that injects `@inject({ key: 'services.UserService' })` resolves an instance from this binding. Service bindings are **transient** by default - each resolution creates a new instance. If your service must be shared (e.g., it holds state), set the scope explicitly: `this.service(UserService).setScope(BindingScopes.SINGLETON)`.
79
+
80
+ ---
81
+
82
+ ## Injecting Dependencies
83
+
84
+ Use `@inject({ key })` on each constructor parameter. Two equivalent forms:
85
+
86
+ ### Plain string keys (concise)
87
+
88
+ ```typescript
89
+ constructor(
90
+ @inject({ key: 'repositories.UserRepository' })
91
+ private userRepository: UserRepository,
92
+
93
+ @inject({ key: 'services.NotificationService' })
94
+ private notificationService: NotificationService,
95
+ ) {
96
+ super({ scope: UserService.name });
97
+ }
98
+ ```
99
+
100
+ ### `BindingKeys.build` (refactor-safe)
101
+
102
+ ```typescript
103
+ import { BindingKeys, BindingNamespaces } from '@venizia/ignis';
104
+
105
+ constructor(
106
+ @inject({
107
+ key: BindingKeys.build({
108
+ namespace: BindingNamespaces.REPOSITORY, // 'repositories'
109
+ key: UserRepository.name, // 'UserRepository'
110
+ }),
111
+ })
112
+ private userRepository: UserRepository,
113
+ ) {
114
+ super({ scope: UserService.name });
115
+ }
116
+ ```
117
+
118
+ Both produce the same runtime key (`repositories.UserRepository`). `BindingKeys.build` is preferred in larger codebases because a class rename caught by the TypeScript compiler automatically updates the key.
119
+
120
+ ---
121
+
122
+ ## Logging
123
+
124
+ `BaseService` inherits `this.logger` from `BaseHelper`. Scope log lines to the current method:
125
+
126
+ ```typescript
127
+ async signIn(opts: { username: string }): Promise<string> {
128
+ this.logger.for('signIn').info('SignIn called | username: %s', opts.username);
129
+
130
+ const user = await this.userRepository.findByUsername(opts.username);
131
+
132
+ if (!user) {
133
+ this.logger.for('signIn').warn('User not found | username: %s', opts.username);
134
+ throw getError({ statusCode: HTTP.ResultCodes.RS_4.Unauthorized, message: 'Invalid credentials' });
135
+ }
136
+
137
+ this.logger.for('signIn').info('SignIn successful | userId: %s', user.id);
138
+ return await this.generateToken({ userId: user.id });
139
+ }
140
+ ```
141
+
142
+ `this.logger.for('signIn')` adds a `[signIn]` tag to each line without creating a new logger object.
143
+
144
+ ---
145
+
146
+ ## Service-to-Service Composition
147
+
148
+ Services can inject other services the same way they inject repositories. This is the primary mechanism for logic reuse.
149
+
150
+ ```typescript
151
+ export class RepositoryTestService extends BaseService {
152
+ constructor(
153
+ @inject({
154
+ key: BindingKeys.build({
155
+ namespace: BindingNamespaces.SERVICE,
156
+ key: CrudTestService.name,
157
+ }),
158
+ })
159
+ private readonly crudTestService: CrudTestService,
160
+
161
+ @inject({
162
+ key: BindingKeys.build({
163
+ namespace: BindingNamespaces.SERVICE,
164
+ key: TransactionTestService.name,
165
+ }),
166
+ })
167
+ private readonly transactionTestService: TransactionTestService,
168
+ ) {
169
+ super({ scope: RepositoryTestService.name });
170
+ }
171
+
172
+ async runAll(): Promise<void> {
173
+ await this.crudTestService.run();
174
+ await this.transactionTestService.run();
175
+ }
176
+ }
177
+ ```
178
+
179
+ Register all participating services in `preConfigure()`:
180
+
181
+ ```typescript
182
+ this.service(CrudTestService);
183
+ this.service(TransactionTestService);
184
+ this.service(RepositoryTestService);
185
+ ```
186
+
187
+ The DI container resolves the dependency graph automatically - registration order within the same lifecycle phase does not matter.
188
+
189
+ ---
190
+
191
+ ## Worked Example: Authentication Service
192
+
193
+ The following is representative of `examples/vert/src/services/authentication.service.ts`. It shows the complete pattern: multiple injected dependencies, method-scoped logging, error handling, and calling other services.
194
+
195
+ ```typescript
196
+ import { BaseService, IAuthService, inject, JWKSIssuerTokenService, TContext } from '@venizia/ignis';
197
+ import { getError, HTTP } from '@venizia/ignis-helpers';
198
+ import { compare, genSalt, hash } from 'bcrypt';
199
+ import { Env } from 'hono';
200
+
201
+ export class AuthenticationService
202
+ extends BaseService
203
+ implements IAuthService<Env, TSignInRequest, TSignInResponse, ...>
204
+ {
205
+ constructor(
206
+ // Inject a repository by string key
207
+ @inject({ key: 'repositories.UserRepository' })
208
+ private userRepository: UserRepository,
209
+
210
+ // Inject a built-in framework service by string key
211
+ @inject({ key: 'services.JWKSIssuerTokenService' })
212
+ private jwksTokenService: JWKSIssuerTokenService,
213
+ ) {
214
+ super({ scope: AuthenticationService.name });
215
+ }
216
+
217
+ async signIn(_context: TContext<Env>, opts: TSignInRequest): Promise<TSignInResponse> {
218
+ this.logger.for('signIn').info('SignIn called | identifier: %j', opts.identifier);
219
+
220
+ const user = await this.userRepository.findByUsername(opts.identifier.value);
221
+
222
+ if (!user) {
223
+ throw getError({
224
+ statusCode: HTTP.ResultCodes.RS_4.Unauthorized,
225
+ message: 'Invalid credentials',
226
+ });
227
+ }
228
+
229
+ const isValid = await compare(opts.credential.value, user.password);
230
+ if (!isValid) {
231
+ throw getError({
232
+ statusCode: HTTP.ResultCodes.RS_4.Unauthorized,
233
+ message: 'Invalid credentials',
234
+ });
235
+ }
236
+
237
+ const token = await this.jwksTokenService.generate({
238
+ payload: { userId: user.id, email: user.email },
239
+ });
240
+
241
+ this.logger.for('signIn').info('SignIn successful | userId: %s', user.id);
242
+ return { token: { value: token, type: 'Bearer' } };
243
+ }
244
+
245
+ async signUp(_context: TContext<Env>, opts: TSignUpRequest): Promise<TSignUpResponse> {
246
+ this.logger.for('signUp').info('SignUp called | username: %s', opts.username);
247
+
248
+ const existing = await this.userRepository.findByUsername(opts.username);
249
+ if (existing) {
250
+ throw getError({
251
+ statusCode: HTTP.ResultCodes.RS_4.Conflict,
252
+ message: 'Username already exists',
253
+ });
254
+ }
255
+
256
+ const salt = await genSalt();
257
+ const hashedPassword = await hash(opts.credential, salt);
258
+
259
+ await this.userRepository.create({ data: { username: opts.username, password: hashedPassword } });
260
+
261
+ return { message: 'User registered successfully' };
262
+ }
263
+ }
264
+ ```
265
+
266
+ Registration:
267
+
268
+ ```typescript
269
+ // application.ts
270
+ preConfigure(): void {
271
+ this.dataSource(PostgresDataSource);
272
+ this.repository(UserRepository);
273
+ this.service(AuthenticationService); // 'services.AuthenticationService'
274
+ }
275
+ ```
276
+
277
+ ---
278
+
279
+ ## Transaction Orchestration
280
+
281
+ Use a datasource reference to begin a transaction, then pass the transaction handle through repository `options`:
282
+
283
+ ```typescript
284
+ export class CheckoutService extends BaseService {
285
+ constructor(
286
+ @inject({ key: 'datasources.PostgresDataSource' })
287
+ private dataSource: PostgresDataSource,
288
+
289
+ @inject({ key: 'repositories.OrderRepository' })
290
+ private orderRepository: OrderRepository,
291
+
292
+ @inject({ key: 'repositories.InventoryRepository' })
293
+ private inventoryRepository: InventoryRepository,
294
+ ) {
295
+ super({ scope: CheckoutService.name });
296
+ }
297
+
298
+ async placeOrder(opts: { userId: string; items: OrderItem[] }): Promise<Order> {
299
+ const log = this.logger.for('placeOrder');
300
+ const transaction = await this.dataSource.beginTransaction();
301
+
302
+ try {
303
+ const { data: order } = await this.orderRepository.create({
304
+ data: { userId: opts.userId },
305
+ options: { transaction },
306
+ });
307
+
308
+ for (const item of opts.items) {
309
+ await this.inventoryRepository.updateById({
310
+ id: item.productId,
311
+ data: { stock: item.quantity },
312
+ options: { transaction },
313
+ });
314
+ }
315
+
316
+ await transaction.commit();
317
+ log.info('Order placed | orderId: %s', order.id);
318
+ return order;
319
+ } catch (error) {
320
+ await transaction.rollback();
321
+ log.error('Order failed, rolled back | error: %s', error);
322
+ throw error;
323
+ }
324
+ }
325
+ }
326
+ ```
327
+
328
+ Always call `rollback()` in `catch` - an uncommitted transaction holds a database connection until it is released.
329
+
330
+ ---
331
+
332
+ ## Architecture Diagram
333
+
334
+ ```mermaid
335
+ graph LR
336
+ A[HTTP Request] --> B(Controller)
337
+ B --> C{Service A}
338
+ C --> D{Service B}
339
+ C --> E[Repository A]
340
+ D --> F[Repository B]
341
+ E --> G((Database))
342
+ F --> G
343
+
344
+ subgraph "Presentation"
345
+ B
346
+ end
347
+ subgraph "Business Logic"
348
+ C
349
+ D
350
+ end
351
+ subgraph "Data Access"
352
+ E
353
+ F
354
+ end
355
+ ```
356
+
357
+ Controllers call services. Services call other services and repositories. Repositories call the database. No layer reaches past its immediate neighbor.
358
+
359
+ ---
360
+
361
+ ## Provider vs Service
362
+
363
+ **Services** contain business logic and are resolved from the DI container (transient scope by default).
364
+
365
+ **Providers** implement the Factory pattern - their `value(container)` method produces a configured value or instance on demand (mail transport, cache driver, middleware). Use a Provider when you need to select between multiple implementations at runtime.
366
+
367
+ See [Providers Reference](/references/base/providers) for the full comparison and examples.
368
+
369
+ ---
370
+
371
+ ## See Also
372
+
373
+ - **Related Concepts:**
374
+ - [Controllers](/guides/core-concepts/rest-controllers) - Call services to handle requests
375
+ - [Repositories](/guides/core-concepts/persistent/repositories) - Data access layer used by services
376
+ - [Dependency Injection](/guides/core-concepts/dependency-injection) - Injecting dependencies into services
377
+
378
+ - **References:**
379
+ - [BaseService API](/references/base/services) - Complete API reference
380
+ - [Providers](/references/base/providers) - Factory pattern
381
+ - [Logger Helper](/extensions/helpers/logger/) - Logging in services
382
+
383
+ - **Best Practices:**
384
+ - [Architectural Patterns](/best-practices/architectural-patterns)
385
+ - [Testing](/guides/tutorials/testing)
386
+
387
+ - **Tutorials:**
388
+ - [Building a CRUD API](/guides/tutorials/building-a-crud-api)
389
+ - [E-commerce API](/guides/tutorials/ecommerce-api)