@venizia/ignis-docs 0.0.2 → 0.0.4-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 (134) hide show
  1. package/README.md +1 -1
  2. package/package.json +4 -2
  3. package/wiki/best-practices/api-usage-examples.md +591 -0
  4. package/wiki/best-practices/architectural-patterns.md +415 -0
  5. package/wiki/best-practices/architecture-decisions.md +488 -0
  6. package/wiki/{get-started/best-practices → best-practices}/code-style-standards.md +647 -182
  7. package/wiki/{get-started/best-practices → best-practices}/common-pitfalls.md +109 -4
  8. package/wiki/{get-started/best-practices → best-practices}/contribution-workflow.md +34 -7
  9. package/wiki/best-practices/data-modeling.md +376 -0
  10. package/wiki/best-practices/deployment-strategies.md +698 -0
  11. package/wiki/best-practices/index.md +27 -0
  12. package/wiki/best-practices/performance-optimization.md +196 -0
  13. package/wiki/best-practices/security-guidelines.md +218 -0
  14. package/wiki/{get-started/best-practices → best-practices}/troubleshooting-tips.md +97 -1
  15. package/wiki/changelogs/2025-12-16-initial-architecture.md +1 -1
  16. package/wiki/changelogs/2025-12-16-model-repo-datasource-refactor.md +1 -1
  17. package/wiki/changelogs/2025-12-17-refactor.md +1 -1
  18. package/wiki/changelogs/2025-12-18-performance-optimizations.md +5 -5
  19. package/wiki/changelogs/2025-12-18-repository-validation-security.md +13 -7
  20. package/wiki/changelogs/2025-12-26-nested-relations-and-generics.md +86 -0
  21. package/wiki/changelogs/2025-12-26-transaction-support.md +57 -0
  22. package/wiki/changelogs/2025-12-29-dynamic-binding-registration.md +104 -0
  23. package/wiki/changelogs/2025-12-29-snowflake-uid-helper.md +100 -0
  24. package/wiki/changelogs/2025-12-30-repository-enhancements.md +214 -0
  25. package/wiki/changelogs/2025-12-31-json-path-filtering-array-operators.md +214 -0
  26. package/wiki/changelogs/2025-12-31-string-id-custom-generator.md +137 -0
  27. package/wiki/changelogs/2026-01-02-default-filter-and-repository-mixins.md +418 -0
  28. package/wiki/changelogs/index.md +8 -1
  29. package/wiki/changelogs/planned-schema-migrator.md +2 -10
  30. package/wiki/{get-started/core-concepts → guides/core-concepts/application}/bootstrapping.md +18 -5
  31. package/wiki/{get-started/core-concepts/application.md → guides/core-concepts/application/index.md} +47 -104
  32. package/wiki/guides/core-concepts/components-guide.md +509 -0
  33. package/wiki/guides/core-concepts/components.md +122 -0
  34. package/wiki/{get-started → guides}/core-concepts/controllers.md +30 -13
  35. package/wiki/{get-started → guides}/core-concepts/dependency-injection.md +97 -0
  36. package/wiki/guides/core-concepts/persistent/datasources.md +179 -0
  37. package/wiki/guides/core-concepts/persistent/index.md +119 -0
  38. package/wiki/guides/core-concepts/persistent/models.md +241 -0
  39. package/wiki/guides/core-concepts/persistent/repositories.md +219 -0
  40. package/wiki/guides/core-concepts/persistent/transactions.md +170 -0
  41. package/wiki/{get-started → guides}/core-concepts/services.md +26 -3
  42. package/wiki/{get-started → guides/get-started}/5-minute-quickstart.md +59 -14
  43. package/wiki/guides/get-started/philosophy.md +682 -0
  44. package/wiki/guides/get-started/setup.md +157 -0
  45. package/wiki/guides/index.md +89 -0
  46. package/wiki/guides/reference/glossary.md +243 -0
  47. package/wiki/{get-started → guides/reference}/mcp-docs-server.md +0 -10
  48. package/wiki/{get-started → guides/tutorials}/building-a-crud-api.md +134 -132
  49. package/wiki/{get-started/quickstart.md → guides/tutorials/complete-installation.md} +107 -71
  50. package/wiki/guides/tutorials/ecommerce-api.md +1399 -0
  51. package/wiki/guides/tutorials/realtime-chat.md +1261 -0
  52. package/wiki/guides/tutorials/testing.md +723 -0
  53. package/wiki/index.md +176 -37
  54. package/wiki/references/base/application.md +27 -0
  55. package/wiki/references/base/bootstrapping.md +30 -26
  56. package/wiki/references/base/components.md +532 -31
  57. package/wiki/references/base/controllers.md +136 -38
  58. package/wiki/references/base/datasources.md +108 -5
  59. package/wiki/references/base/dependency-injection.md +39 -3
  60. package/wiki/references/base/filter-system/application-usage.md +224 -0
  61. package/wiki/references/base/filter-system/array-operators.md +132 -0
  62. package/wiki/references/base/filter-system/comparison-operators.md +109 -0
  63. package/wiki/references/base/filter-system/default-filter.md +428 -0
  64. package/wiki/references/base/filter-system/fields-order-pagination.md +155 -0
  65. package/wiki/references/base/filter-system/index.md +127 -0
  66. package/wiki/references/base/filter-system/json-filtering.md +197 -0
  67. package/wiki/references/base/filter-system/list-operators.md +71 -0
  68. package/wiki/references/base/filter-system/logical-operators.md +156 -0
  69. package/wiki/references/base/filter-system/null-operators.md +58 -0
  70. package/wiki/references/base/filter-system/pattern-matching.md +108 -0
  71. package/wiki/references/base/filter-system/quick-reference.md +431 -0
  72. package/wiki/references/base/filter-system/range-operators.md +63 -0
  73. package/wiki/references/base/filter-system/tips.md +190 -0
  74. package/wiki/references/base/filter-system/use-cases.md +452 -0
  75. package/wiki/references/base/index.md +90 -0
  76. package/wiki/references/base/middlewares.md +602 -0
  77. package/wiki/references/base/models.md +215 -23
  78. package/wiki/references/base/providers.md +732 -0
  79. package/wiki/references/base/repositories/advanced.md +555 -0
  80. package/wiki/references/base/repositories/index.md +228 -0
  81. package/wiki/references/base/repositories/mixins.md +331 -0
  82. package/wiki/references/base/repositories/relations.md +486 -0
  83. package/wiki/references/base/repositories.md +40 -549
  84. package/wiki/references/base/services.md +28 -4
  85. package/wiki/references/components/authentication.md +22 -2
  86. package/wiki/references/components/health-check.md +12 -0
  87. package/wiki/references/components/index.md +23 -0
  88. package/wiki/references/components/mail.md +687 -0
  89. package/wiki/references/components/request-tracker.md +16 -0
  90. package/wiki/references/components/socket-io.md +18 -0
  91. package/wiki/references/components/static-asset.md +14 -26
  92. package/wiki/references/components/swagger.md +17 -0
  93. package/wiki/references/configuration/environment-variables.md +427 -0
  94. package/wiki/references/configuration/index.md +73 -0
  95. package/wiki/references/helpers/cron.md +14 -0
  96. package/wiki/references/helpers/crypto.md +15 -0
  97. package/wiki/references/helpers/env.md +16 -0
  98. package/wiki/references/helpers/error.md +17 -0
  99. package/wiki/references/helpers/index.md +15 -0
  100. package/wiki/references/helpers/inversion.md +24 -4
  101. package/wiki/references/helpers/logger.md +19 -0
  102. package/wiki/references/helpers/network.md +11 -0
  103. package/wiki/references/helpers/queue.md +19 -0
  104. package/wiki/references/helpers/redis.md +21 -0
  105. package/wiki/references/helpers/socket-io.md +24 -5
  106. package/wiki/references/helpers/storage.md +18 -10
  107. package/wiki/references/helpers/testing.md +18 -0
  108. package/wiki/references/helpers/types.md +167 -0
  109. package/wiki/references/helpers/uid.md +167 -0
  110. package/wiki/references/helpers/worker-thread.md +16 -0
  111. package/wiki/references/index.md +177 -0
  112. package/wiki/references/quick-reference.md +634 -0
  113. package/wiki/references/src-details/boot.md +3 -3
  114. package/wiki/references/src-details/dev-configs.md +0 -4
  115. package/wiki/references/src-details/docs.md +2 -2
  116. package/wiki/references/src-details/index.md +86 -0
  117. package/wiki/references/src-details/inversion.md +1 -6
  118. package/wiki/references/src-details/mcp-server.md +3 -15
  119. package/wiki/references/utilities/index.md +86 -10
  120. package/wiki/references/utilities/jsx.md +577 -0
  121. package/wiki/references/utilities/request.md +0 -2
  122. package/wiki/references/utilities/statuses.md +740 -0
  123. package/wiki/changelogs/planned-transaction-support.md +0 -216
  124. package/wiki/get-started/best-practices/api-usage-examples.md +0 -266
  125. package/wiki/get-started/best-practices/architectural-patterns.md +0 -170
  126. package/wiki/get-started/best-practices/data-modeling.md +0 -177
  127. package/wiki/get-started/best-practices/deployment-strategies.md +0 -121
  128. package/wiki/get-started/best-practices/performance-optimization.md +0 -88
  129. package/wiki/get-started/best-practices/security-guidelines.md +0 -99
  130. package/wiki/get-started/core-concepts/components.md +0 -98
  131. package/wiki/get-started/core-concepts/persistent.md +0 -543
  132. package/wiki/get-started/index.md +0 -65
  133. package/wiki/get-started/philosophy.md +0 -296
  134. package/wiki/get-started/prerequisites.md +0 -113
@@ -0,0 +1,1399 @@
1
+ # Building an E-commerce API
2
+
3
+ This tutorial guides you through building a complete e-commerce backend with products, orders, cart, and payments. You'll learn advanced patterns for real-world applications.
4
+
5
+ **⏱️ Time to Complete:** ~2 hours
6
+
7
+ ## What You'll Build
8
+
9
+ - Product catalog with categories
10
+ - Shopping cart management
11
+ - Order processing
12
+ - Payment integration (Stripe)
13
+ - Inventory management
14
+
15
+ ## Prerequisites
16
+
17
+ - Completed [Building a CRUD API](./building-a-crud-api.md)
18
+ - PostgreSQL database running
19
+ - Basic understanding of [Dependency Injection](../core-concepts/dependency-injection.md)
20
+
21
+ ## 1. Project Setup
22
+
23
+ ### Initialize the Project
24
+
25
+ ```bash
26
+ mkdir ecommerce-api
27
+ cd ecommerce-api
28
+ bun init -y
29
+
30
+ # Install dependencies
31
+ bun add hono @hono/zod-openapi @venizia/ignis dotenv-flow
32
+ bun add drizzle-orm drizzle-zod pg stripe
33
+ bun add -d typescript @types/bun @venizia/dev-configs drizzle-kit @types/pg
34
+ ```
35
+
36
+ ### Project Structure
37
+
38
+ ```
39
+ ecommerce-api/
40
+ ├── src/
41
+ │ ├── index.ts
42
+ │ ├── application.ts
43
+ │ ├── models/
44
+ │ │ ├── product.model.ts
45
+ │ │ ├── category.model.ts
46
+ │ │ ├── cart.model.ts
47
+ │ │ ├── order.model.ts
48
+ │ │ └── index.ts
49
+ │ ├── repositories/
50
+ │ │ ├── product.repository.ts
51
+ │ │ ├── category.repository.ts
52
+ │ │ ├── cart.repository.ts
53
+ │ │ └── order.repository.ts
54
+ │ ├── services/
55
+ │ │ ├── product.service.ts
56
+ │ │ ├── cart.service.ts
57
+ │ │ ├── order.service.ts
58
+ │ │ └── payment.service.ts
59
+ │ ├── controllers/
60
+ │ │ ├── product.controller.ts
61
+ │ │ ├── cart.controller.ts
62
+ │ │ └── order.controller.ts
63
+ │ └── datasources/
64
+ │ └── postgres.datasource.ts
65
+ └── package.json
66
+ ```
67
+
68
+ ## 2. Database Models
69
+
70
+ Models in IGNIS combine Drizzle ORM schemas with Entity classes.
71
+
72
+ ### Category Model
73
+
74
+ ```typescript
75
+ // src/models/category.model.ts
76
+ import {
77
+ BaseEntity,
78
+ createRelations,
79
+ generateIdColumnDefs,
80
+ generateTzColumnDefs,
81
+ model,
82
+ TTableObject,
83
+ } from '@venizia/ignis';
84
+ import { pgTable, text, varchar } from 'drizzle-orm/pg-core';
85
+
86
+ export const categoryTable = pgTable('Category', {
87
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
88
+ ...generateTzColumnDefs(),
89
+ name: varchar('name', { length: 100 }).notNull(),
90
+ slug: varchar('slug', { length: 100 }).unique().notNull(),
91
+ description: text('description'),
92
+ parentId: text('parent_id'),
93
+ });
94
+
95
+ export const categoryRelations = createRelations({
96
+ source: categoryTable,
97
+ relations: [
98
+ { type: 'one', name: 'parent', target: () => categoryTable, fields: ['parentId'], references: ['id'] },
99
+ { type: 'many', name: 'children', target: () => categoryTable, fields: ['id'], references: ['parentId'] },
100
+ ],
101
+ });
102
+
103
+ export type TCategorySchema = typeof categoryTable;
104
+ export type TCategory = TTableObject<TCategorySchema>;
105
+
106
+ @model({ type: 'entity' })
107
+ export class Category extends BaseEntity<typeof Category.schema> {
108
+ static override schema = categoryTable;
109
+ static override relations = () => categoryRelations.definitions;
110
+ static override TABLE_NAME = 'Category';
111
+ }
112
+ ```
113
+
114
+ ### Product Model
115
+
116
+ ```typescript
117
+ // src/models/product.model.ts
118
+ import {
119
+ BaseEntity,
120
+ createRelations,
121
+ generateIdColumnDefs,
122
+ generateTzColumnDefs,
123
+ model,
124
+ TTableObject,
125
+ } from '@venizia/ignis';
126
+ import { pgTable, text, varchar, decimal, integer, boolean } from 'drizzle-orm/pg-core';
127
+ import { categoryTable, Category } from './category.model';
128
+
129
+ export const productTable = pgTable('Product', {
130
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
131
+ ...generateTzColumnDefs(),
132
+ name: varchar('name', { length: 255 }).notNull(),
133
+ description: text('description'),
134
+ price: decimal('price', { precision: 10, scale: 2 }).notNull(),
135
+ compareAtPrice: decimal('compare_at_price', { precision: 10, scale: 2 }),
136
+ sku: varchar('sku', { length: 100 }).unique(),
137
+ stock: integer('stock').default(0).notNull(),
138
+ isActive: boolean('is_active').default(true).notNull(),
139
+ categoryId: text('category_id'),
140
+ imageUrl: text('image_url'),
141
+ });
142
+
143
+ export const productRelations = createRelations({
144
+ source: productTable,
145
+ relations: [
146
+ { type: 'one', name: 'category', target: () => categoryTable, fields: ['categoryId'], references: ['id'] },
147
+ ],
148
+ });
149
+
150
+ export type TProductSchema = typeof productTable;
151
+ export type TProduct = TTableObject<TProductSchema>;
152
+
153
+ @model({ type: 'entity' })
154
+ export class Product extends BaseEntity<typeof Product.schema> {
155
+ static override schema = productTable;
156
+ static override relations = () => productRelations.definitions;
157
+ static override TABLE_NAME = 'Product';
158
+ }
159
+ ```
160
+
161
+ ### Cart Model
162
+
163
+ ```typescript
164
+ // src/models/cart.model.ts
165
+ import {
166
+ BaseEntity,
167
+ createRelations,
168
+ generateIdColumnDefs,
169
+ generateTzColumnDefs,
170
+ model,
171
+ TTableObject,
172
+ } from '@venizia/ignis';
173
+ import { pgTable, text, varchar, integer } from 'drizzle-orm/pg-core';
174
+ import { productTable } from './product.model';
175
+
176
+ export const cartTable = pgTable('Cart', {
177
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
178
+ ...generateTzColumnDefs(),
179
+ userId: text('user_id'),
180
+ sessionId: varchar('session_id', { length: 255 }),
181
+ });
182
+
183
+ export const cartItemTable = pgTable('CartItem', {
184
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
185
+ ...generateTzColumnDefs(),
186
+ cartId: text('cart_id').notNull(),
187
+ productId: text('product_id').notNull(),
188
+ quantity: integer('quantity').default(1).notNull(),
189
+ });
190
+
191
+ export const cartRelations = createRelations({
192
+ source: cartTable,
193
+ relations: [
194
+ { type: 'many', name: 'items', target: () => cartItemTable, fields: ['id'], references: ['cartId'] },
195
+ ],
196
+ });
197
+
198
+ export const cartItemRelations = createRelations({
199
+ source: cartItemTable,
200
+ relations: [
201
+ { type: 'one', name: 'cart', target: () => cartTable, fields: ['cartId'], references: ['id'] },
202
+ { type: 'one', name: 'product', target: () => productTable, fields: ['productId'], references: ['id'] },
203
+ ],
204
+ });
205
+
206
+ export type TCartSchema = typeof cartTable;
207
+ export type TCart = TTableObject<TCartSchema>;
208
+ export type TCartItemSchema = typeof cartItemTable;
209
+ export type TCartItem = TTableObject<TCartItemSchema>;
210
+
211
+ @model({ type: 'entity' })
212
+ export class Cart extends BaseEntity<typeof Cart.schema> {
213
+ static override schema = cartTable;
214
+ static override relations = () => cartRelations.definitions;
215
+ static override TABLE_NAME = 'Cart';
216
+ }
217
+
218
+ @model({ type: 'entity' })
219
+ export class CartItem extends BaseEntity<typeof CartItem.schema> {
220
+ static override schema = cartItemTable;
221
+ static override relations = () => cartItemRelations.definitions;
222
+ static override TABLE_NAME = 'CartItem';
223
+ }
224
+ ```
225
+
226
+ ### Order Model
227
+
228
+ ```typescript
229
+ // src/models/order.model.ts
230
+ import {
231
+ BaseEntity,
232
+ createRelations,
233
+ generateIdColumnDefs,
234
+ generateTzColumnDefs,
235
+ model,
236
+ TTableObject,
237
+ } from '@venizia/ignis';
238
+ import { pgTable, text, varchar, decimal, integer, jsonb } from 'drizzle-orm/pg-core';
239
+ import { productTable } from './product.model';
240
+
241
+ export const orderTable = pgTable('Order', {
242
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
243
+ ...generateTzColumnDefs(),
244
+ userId: text('user_id'),
245
+ email: varchar('email', { length: 255 }).notNull(),
246
+ status: varchar('status', { length: 50 }).default('pending').notNull(),
247
+ subtotal: decimal('subtotal', { precision: 10, scale: 2 }).notNull(),
248
+ tax: decimal('tax', { precision: 10, scale: 2 }).default('0').notNull(),
249
+ shipping: decimal('shipping', { precision: 10, scale: 2 }).default('0').notNull(),
250
+ total: decimal('total', { precision: 10, scale: 2 }).notNull(),
251
+ shippingAddress: jsonb('shipping_address'),
252
+ billingAddress: jsonb('billing_address'),
253
+ paymentIntentId: varchar('payment_intent_id', { length: 255 }),
254
+ });
255
+
256
+ export const orderItemTable = pgTable('OrderItem', {
257
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
258
+ orderId: text('order_id').notNull(),
259
+ productId: text('product_id').notNull(),
260
+ name: varchar('name', { length: 255 }).notNull(),
261
+ price: decimal('price', { precision: 10, scale: 2 }).notNull(),
262
+ quantity: integer('quantity').notNull(),
263
+ });
264
+
265
+ export const orderRelations = createRelations({
266
+ source: orderTable,
267
+ relations: [
268
+ { type: 'many', name: 'items', target: () => orderItemTable, fields: ['id'], references: ['orderId'] },
269
+ ],
270
+ });
271
+
272
+ export const orderItemRelations = createRelations({
273
+ source: orderItemTable,
274
+ relations: [
275
+ { type: 'one', name: 'order', target: () => orderTable, fields: ['orderId'], references: ['id'] },
276
+ { type: 'one', name: 'product', target: () => productTable, fields: ['productId'], references: ['id'] },
277
+ ],
278
+ });
279
+
280
+ export type TOrderSchema = typeof orderTable;
281
+ export type TOrder = TTableObject<TOrderSchema>;
282
+ export type TOrderItemSchema = typeof orderItemTable;
283
+ export type TOrderItem = TTableObject<TOrderItemSchema>;
284
+
285
+ @model({ type: 'entity' })
286
+ export class Order extends BaseEntity<typeof Order.schema> {
287
+ static override schema = orderTable;
288
+ static override relations = () => orderRelations.definitions;
289
+ static override TABLE_NAME = 'Order';
290
+ }
291
+
292
+ @model({ type: 'entity' })
293
+ export class OrderItem extends BaseEntity<typeof OrderItem.schema> {
294
+ static override schema = orderItemTable;
295
+ static override relations = () => orderItemRelations.definitions;
296
+ static override TABLE_NAME = 'OrderItem';
297
+ }
298
+ ```
299
+
300
+ ### Models Index
301
+
302
+ ```typescript
303
+ // src/models/index.ts
304
+ export * from './category.model';
305
+ export * from './product.model';
306
+ export * from './cart.model';
307
+ export * from './order.model';
308
+ ```
309
+
310
+ ## 3. DataSource
311
+
312
+ ```typescript
313
+ // src/datasources/postgres.datasource.ts
314
+ import {
315
+ BaseDataSource,
316
+ datasource,
317
+ TNodePostgresConnector,
318
+ ValueOrPromise,
319
+ } from '@venizia/ignis';
320
+ import { drizzle } from 'drizzle-orm/node-postgres';
321
+ import { Pool } from 'pg';
322
+
323
+ interface IDSConfigs {
324
+ host: string;
325
+ port: number;
326
+ database: string;
327
+ user: string;
328
+ password: string;
329
+ }
330
+
331
+ @datasource({ driver: 'node-postgres' })
332
+ export class PostgresDataSource extends BaseDataSource<TNodePostgresConnector, IDSConfigs> {
333
+ constructor() {
334
+ super({
335
+ name: PostgresDataSource.name,
336
+ config: {
337
+ host: process.env.APP_ENV_POSTGRES_HOST ?? 'localhost',
338
+ port: +(process.env.APP_ENV_POSTGRES_PORT ?? 5432),
339
+ database: process.env.APP_ENV_POSTGRES_DATABASE ?? 'ecommerce_db',
340
+ user: process.env.APP_ENV_POSTGRES_USERNAME ?? 'postgres',
341
+ password: process.env.APP_ENV_POSTGRES_PASSWORD ?? '',
342
+ },
343
+ });
344
+ }
345
+
346
+ override configure(): ValueOrPromise<void> {
347
+ const schema = this.getSchema();
348
+
349
+ this.logger.debug(
350
+ '[configure] Auto-discovered schema | Schema + Relations (%s): %o',
351
+ Object.keys(schema).length,
352
+ Object.keys(schema),
353
+ );
354
+
355
+ const client = new Pool(this.settings);
356
+ this.connector = drizzle({ client, schema });
357
+ }
358
+ }
359
+ ```
360
+
361
+ ## 4. Repositories
362
+
363
+ ### Product Repository
364
+
365
+ ```typescript
366
+ // src/repositories/product.repository.ts
367
+ import { Product } from '@/models/product.model';
368
+ import { PostgresDataSource } from '@/datasources/postgres.datasource';
369
+ import { DefaultCRUDRepository, repository } from '@venizia/ignis';
370
+
371
+ @repository({ model: Product, dataSource: PostgresDataSource })
372
+ export class ProductRepository extends DefaultCRUDRepository<typeof Product.schema> {}
373
+ ```
374
+
375
+ ### Category Repository
376
+
377
+ ```typescript
378
+ // src/repositories/category.repository.ts
379
+ import { Category } from '@/models/category.model';
380
+ import { PostgresDataSource } from '@/datasources/postgres.datasource';
381
+ import { DefaultCRUDRepository, repository } from '@venizia/ignis';
382
+
383
+ @repository({ model: Category, dataSource: PostgresDataSource })
384
+ export class CategoryRepository extends DefaultCRUDRepository<typeof Category.schema> {}
385
+ ```
386
+
387
+ ### Cart Repository
388
+
389
+ ```typescript
390
+ // src/repositories/cart.repository.ts
391
+ import { Cart, CartItem } from '@/models/cart.model';
392
+ import { PostgresDataSource } from '@/datasources/postgres.datasource';
393
+ import { DefaultCRUDRepository, repository, inject } from '@venizia/ignis';
394
+
395
+ @repository({ model: CartItem, dataSource: PostgresDataSource })
396
+ export class CartItemRepository extends DefaultCRUDRepository<typeof CartItem.schema> {}
397
+
398
+ @repository({ model: Cart, dataSource: PostgresDataSource })
399
+ export class CartRepository extends DefaultCRUDRepository<typeof Cart.schema> {
400
+ constructor(
401
+ // First parameter MUST be DataSource injection
402
+ @inject({ key: 'datasources.PostgresDataSource' })
403
+ dataSource: PostgresDataSource,
404
+
405
+ // From 2nd parameter, inject additional dependencies
406
+ @inject({ key: 'repositories.CartItemRepository' })
407
+ private _cartItemRepo: CartItemRepository,
408
+ ) {
409
+ super(dataSource);
410
+ }
411
+
412
+ async findCartItem(opts: { cartId: string; productId: string }) {
413
+ return this._cartItemRepo.findOne({
414
+ where: { cartId: opts.cartId, productId: opts.productId },
415
+ });
416
+ }
417
+
418
+ async addCartItem(opts: { cartId: string; productId: string; quantity: number }) {
419
+ return this._cartItemRepo.create(opts);
420
+ }
421
+
422
+ async updateCartItem(opts: { itemId: string; data: { quantity: number } }) {
423
+ return this._cartItemRepo.updateById(opts.itemId, opts.data);
424
+ }
425
+
426
+ async deleteCartItem(opts: { itemId: string }) {
427
+ return this._cartItemRepo.deleteById(opts.itemId);
428
+ }
429
+
430
+ async getCartItems(opts: { cartId: string }) {
431
+ return this._cartItemRepo.find({ where: { cartId: opts.cartId } });
432
+ }
433
+
434
+ async clearCart(opts: { cartId: string }) {
435
+ return this._cartItemRepo.deleteAll({ where: { cartId: opts.cartId } });
436
+ }
437
+ }
438
+ ```
439
+
440
+ ### Order Repository
441
+
442
+ ```typescript
443
+ // src/repositories/order.repository.ts
444
+ import { Order, OrderItem } from '@/models/order.model';
445
+ import { PostgresDataSource } from '@/datasources/postgres.datasource';
446
+ import { DefaultCRUDRepository, repository, inject } from '@venizia/ignis';
447
+
448
+ @repository({ model: OrderItem, dataSource: PostgresDataSource })
449
+ export class OrderItemRepository extends DefaultCRUDRepository<typeof OrderItem.schema> {}
450
+
451
+ @repository({ model: Order, dataSource: PostgresDataSource })
452
+ export class OrderRepository extends DefaultCRUDRepository<typeof Order.schema> {
453
+ constructor(
454
+ // First parameter MUST be DataSource injection
455
+ @inject({ key: 'datasources.PostgresDataSource' })
456
+ dataSource: PostgresDataSource,
457
+
458
+ // From 2nd parameter, inject additional dependencies
459
+ @inject({ key: 'repositories.OrderItemRepository' })
460
+ private _orderItemRepo: OrderItemRepository,
461
+ ) {
462
+ super(dataSource);
463
+ }
464
+
465
+ async createOrderItem(opts: {
466
+ orderId: string;
467
+ productId: string;
468
+ name: string;
469
+ price: string;
470
+ quantity: number;
471
+ }) {
472
+ return this._orderItemRepo.create(opts);
473
+ }
474
+
475
+ async getOrderItems(opts: { orderId: string }) {
476
+ return this._orderItemRepo.find({ where: { orderId: opts.orderId } });
477
+ }
478
+ }
479
+ ```
480
+
481
+ ## 5. Product Service with Inventory
482
+
483
+ ```typescript
484
+ // src/services/product.service.ts
485
+ import { injectable, inject } from '@venizia/ignis';
486
+ import { BaseService } from '@venizia/ignis';
487
+ import { ProductRepository } from '../repositories/product.repository';
488
+ import { getError } from '@venizia/ignis-helpers';
489
+
490
+ @injectable()
491
+ export class ProductService extends BaseService {
492
+ constructor(
493
+ @inject('repositories.ProductRepository')
494
+ private _productRepo: ProductRepository,
495
+ ) {
496
+ super({ scope: ProductService.name });
497
+ }
498
+
499
+ async getActiveProducts(opts: { categoryId?: string; limit?: number; offset?: number }) {
500
+ return this._productRepo.find({
501
+ where: {
502
+ isActive: true,
503
+ ...(opts.categoryId && { categoryId: opts.categoryId }),
504
+ },
505
+ orderBy: { createdAt: 'desc' },
506
+ limit: opts.limit ?? 20,
507
+ offset: opts.offset ?? 0,
508
+ });
509
+ }
510
+
511
+ async getProductById(opts: { id: string }) {
512
+ const product = await this._productRepo.findById(opts.id);
513
+ if (!product) {
514
+ throw getError({ statusCode: 404, message: 'Product not found' });
515
+ }
516
+ return product;
517
+ }
518
+
519
+ async checkStock(opts: { productId: string; quantity: number }): Promise<boolean> {
520
+ const product = await this.getProductById({ id: opts.productId });
521
+ return product.stock >= opts.quantity;
522
+ }
523
+
524
+ async reserveStock(opts: { productId: string; quantity: number }) {
525
+ const product = await this.getProductById({ id: opts.productId });
526
+
527
+ if (product.stock < opts.quantity) {
528
+ throw getError({
529
+ statusCode: 400,
530
+ message: `Insufficient stock. Available: ${product.stock}`,
531
+ });
532
+ }
533
+
534
+ await this._productRepo.updateById(opts.productId, {
535
+ stock: product.stock - opts.quantity,
536
+ });
537
+ }
538
+
539
+ async releaseStock(opts: { productId: string; quantity: number }) {
540
+ const product = await this.getProductById({ id: opts.productId });
541
+ await this._productRepo.updateById(opts.productId, {
542
+ stock: product.stock + opts.quantity,
543
+ });
544
+ }
545
+ }
546
+ ```
547
+
548
+ ## 6. Cart Service
549
+
550
+ ```typescript
551
+ // src/services/cart.service.ts
552
+ import { injectable, inject } from '@venizia/ignis';
553
+ import { BaseService } from '@venizia/ignis';
554
+ import { CartRepository } from '../repositories/cart.repository';
555
+ import { ProductService } from './product.service';
556
+ import { getError } from '@venizia/ignis-helpers';
557
+
558
+ interface ICartItem {
559
+ productId: string;
560
+ quantity: number;
561
+ }
562
+
563
+ @injectable()
564
+ export class CartService extends BaseService {
565
+ constructor(
566
+ @inject('repositories.CartRepository')
567
+ private _cartRepo: CartRepository,
568
+ @inject('services.ProductService')
569
+ private _productService: ProductService,
570
+ ) {
571
+ super({ scope: CartService.name });
572
+ }
573
+
574
+ async getOrCreateCart(opts: { userId?: string; sessionId?: string }) {
575
+ // Try to find existing cart
576
+ let cart = await this._cartRepo.findOne({
577
+ where: opts.userId
578
+ ? { userId: opts.userId }
579
+ : { sessionId: opts.sessionId },
580
+ });
581
+
582
+ if (!cart) {
583
+ cart = await this._cartRepo.create({
584
+ userId: opts.userId,
585
+ sessionId: opts.sessionId,
586
+ });
587
+ }
588
+
589
+ return cart;
590
+ }
591
+
592
+ async addItem(opts: { cartId: string; productId: string; quantity?: number }) {
593
+ const quantity = opts.quantity ?? 1;
594
+ // Validate product exists and has stock
595
+ const product = await this._productService.getProductById({ id: opts.productId });
596
+
597
+ if (!product.isActive) {
598
+ throw getError({ statusCode: 400, message: 'Product is not available' });
599
+ }
600
+
601
+ if (product.stock < quantity) {
602
+ throw getError({ statusCode: 400, message: 'Insufficient stock' });
603
+ }
604
+
605
+ // Check if item already in cart
606
+ const existingItem = await this._cartRepo.findCartItem({ cartId: opts.cartId, productId: opts.productId });
607
+
608
+ if (existingItem) {
609
+ // Update quantity
610
+ const newQuantity = existingItem.quantity + quantity;
611
+ if (product.stock < newQuantity) {
612
+ throw getError({ statusCode: 400, message: 'Insufficient stock for requested quantity' });
613
+ }
614
+ return this._cartRepo.updateCartItem({ itemId: existingItem.id, data: { quantity: newQuantity } });
615
+ }
616
+
617
+ // Add new item
618
+ return this._cartRepo.addCartItem({
619
+ cartId: opts.cartId,
620
+ productId: opts.productId,
621
+ quantity,
622
+ });
623
+ }
624
+
625
+ async updateItemQuantity(opts: { cartId: string; productId: string; quantity: number }) {
626
+ if (opts.quantity <= 0) {
627
+ return this.removeItem({ cartId: opts.cartId, productId: opts.productId });
628
+ }
629
+
630
+ const product = await this._productService.getProductById({ id: opts.productId });
631
+ if (product.stock < opts.quantity) {
632
+ throw getError({ statusCode: 400, message: 'Insufficient stock' });
633
+ }
634
+
635
+ const item = await this._cartRepo.findCartItem({ cartId: opts.cartId, productId: opts.productId });
636
+ if (!item) {
637
+ throw getError({ statusCode: 404, message: 'Item not in cart' });
638
+ }
639
+
640
+ return this._cartRepo.updateCartItem({ itemId: item.id, data: { quantity: opts.quantity } });
641
+ }
642
+
643
+ async removeItem(opts: { cartId: string; productId: string }) {
644
+ const item = await this._cartRepo.findCartItem({ cartId: opts.cartId, productId: opts.productId });
645
+ if (item) {
646
+ await this._cartRepo.deleteCartItem({ itemId: item.id });
647
+ }
648
+ }
649
+
650
+ async getCartWithItems(opts: { cartId: string }) {
651
+ const cart = await this._cartRepo.findById(opts.cartId);
652
+ if (!cart) {
653
+ throw getError({ statusCode: 404, message: 'Cart not found' });
654
+ }
655
+
656
+ const items = await this._cartRepo.getCartItems({ cartId: opts.cartId });
657
+
658
+ // Calculate totals
659
+ let subtotal = 0;
660
+ const itemsWithDetails = await Promise.all(
661
+ items.map(async (item) => {
662
+ const product = await this._productService.getProductById({ id: item.productId });
663
+ const itemTotal = Number(product.price) * item.quantity;
664
+ subtotal += itemTotal;
665
+
666
+ return {
667
+ ...item,
668
+ product,
669
+ itemTotal,
670
+ };
671
+ })
672
+ );
673
+
674
+ return {
675
+ ...cart,
676
+ items: itemsWithDetails,
677
+ subtotal,
678
+ itemCount: items.reduce((sum, item) => sum + item.quantity, 0),
679
+ };
680
+ }
681
+
682
+ async clearCart(opts: { cartId: string }) {
683
+ await this._cartRepo.clearCart({ cartId: opts.cartId });
684
+ }
685
+ }
686
+ ```
687
+
688
+ ## 7. Order Service with Payments
689
+
690
+ ```typescript
691
+ // src/services/order.service.ts
692
+ import { injectable, inject } from '@venizia/ignis';
693
+ import { BaseService } from '@venizia/ignis';
694
+ import { OrderRepository } from '../repositories/order.repository';
695
+ import { CartService } from './cart.service';
696
+ import { ProductService } from './product.service';
697
+ import { PaymentService } from './payment.service';
698
+ import { getError } from '@venizia/ignis-helpers';
699
+
700
+ interface ICreateOrderInput {
701
+ cartId: string;
702
+ email: string;
703
+ shippingAddress: {
704
+ name: string;
705
+ line1: string;
706
+ line2?: string;
707
+ city: string;
708
+ state: string;
709
+ postalCode: string;
710
+ country: string;
711
+ };
712
+ billingAddress?: typeof shippingAddress;
713
+ }
714
+
715
+ @injectable()
716
+ export class OrderService extends BaseService {
717
+ constructor(
718
+ @inject('repositories.OrderRepository')
719
+ private _orderRepo: OrderRepository,
720
+ @inject('services.CartService')
721
+ private _cartService: CartService,
722
+ @inject('services.ProductService')
723
+ private _productService: ProductService,
724
+ @inject('services.PaymentService')
725
+ private _paymentService: PaymentService,
726
+ ) {
727
+ super({ scope: OrderService.name });
728
+ }
729
+
730
+ async createOrder(opts: { input: ICreateOrderInput }) {
731
+ // Get cart with items
732
+ const cart = await this._cartService.getCartWithItems({ cartId: opts.input.cartId });
733
+
734
+ if (cart.items.length === 0) {
735
+ throw getError({ statusCode: 400, message: 'Cart is empty' });
736
+ }
737
+
738
+ // Validate stock for all items
739
+ for (const item of cart.items) {
740
+ const hasStock = await this._productService.checkStock({
741
+ productId: item.productId,
742
+ quantity: item.quantity,
743
+ });
744
+ if (!hasStock) {
745
+ throw getError({
746
+ statusCode: 400,
747
+ message: `Insufficient stock for ${item.product.name}`,
748
+ });
749
+ }
750
+ }
751
+
752
+ // Calculate totals
753
+ const subtotal = cart.subtotal;
754
+ const tax = subtotal * 0.1; // 10% tax
755
+ const shipping = subtotal > 100 ? 0 : 10; // Free shipping over $100
756
+ const total = subtotal + tax + shipping;
757
+
758
+ // Create payment intent
759
+ const paymentIntent = await this._paymentService.createPaymentIntent({
760
+ amount: Math.round(total * 100), // Stripe uses cents
761
+ currency: 'usd',
762
+ metadata: {
763
+ cartId: input.cartId,
764
+ email: input.email,
765
+ },
766
+ });
767
+
768
+ // Create order
769
+ const order = await this._orderRepo.create({
770
+ email: opts.input.email,
771
+ status: 'pending_payment',
772
+ subtotal: subtotal.toString(),
773
+ tax: tax.toString(),
774
+ shipping: shipping.toString(),
775
+ total: total.toString(),
776
+ shippingAddress: opts.input.shippingAddress,
777
+ billingAddress: opts.input.billingAddress ?? opts.input.shippingAddress,
778
+ paymentIntentId: paymentIntent.id,
779
+ });
780
+
781
+ // Create order items
782
+ for (const item of cart.items) {
783
+ await this._orderRepo.createOrderItem({
784
+ orderId: order.id,
785
+ productId: item.productId,
786
+ name: item.product.name,
787
+ price: item.product.price,
788
+ quantity: item.quantity,
789
+ });
790
+ }
791
+
792
+ return {
793
+ order,
794
+ clientSecret: paymentIntent.client_secret,
795
+ };
796
+ }
797
+
798
+ async confirmPayment(opts: { orderId: string; paymentIntentId: string }) {
799
+ const order = await this._orderRepo.findById(opts.orderId);
800
+
801
+ if (!order) {
802
+ throw getError({ statusCode: 404, message: 'Order not found' });
803
+ }
804
+
805
+ if (order.paymentIntentId !== opts.paymentIntentId) {
806
+ throw getError({ statusCode: 400, message: 'Invalid payment' });
807
+ }
808
+
809
+ // Verify payment with Stripe
810
+ const isSuccessful = await this._paymentService.verifyPayment({ paymentIntentId: opts.paymentIntentId });
811
+
812
+ if (!isSuccessful) {
813
+ throw getError({ statusCode: 400, message: 'Payment not confirmed' });
814
+ }
815
+
816
+ // Update order status
817
+ await this._orderRepo.updateById(opts.orderId, { status: 'paid' });
818
+
819
+ // Reserve stock for all items
820
+ const orderItems = await this._orderRepo.getOrderItems({ orderId: opts.orderId });
821
+ for (const item of orderItems) {
822
+ await this._productService.reserveStock({ productId: item.productId, quantity: item.quantity });
823
+ }
824
+
825
+ return this._orderRepo.findById(opts.orderId);
826
+ }
827
+
828
+ async getOrdersByUser(opts: { userId: string }) {
829
+ return this._orderRepo.find({
830
+ where: { userId: opts.userId },
831
+ orderBy: { createdAt: 'desc' },
832
+ });
833
+ }
834
+
835
+ async getOrderById(opts: { orderId: string }) {
836
+ const order = await this._orderRepo.findById(opts.orderId);
837
+ if (!order) {
838
+ throw getError({ statusCode: 404, message: 'Order not found' });
839
+ }
840
+
841
+ const items = await this._orderRepo.getOrderItems({ orderId: opts.orderId });
842
+ return { ...order, items };
843
+ }
844
+
845
+ async updateOrderStatus(opts: { orderId: string; status: string }) {
846
+ const validStatuses = ['pending_payment', 'paid', 'processing', 'shipped', 'delivered', 'cancelled'];
847
+
848
+ if (!validStatuses.includes(opts.status)) {
849
+ throw getError({ statusCode: 400, message: 'Invalid status' });
850
+ }
851
+
852
+ return this._orderRepo.updateById(opts.orderId, { status: opts.status });
853
+ }
854
+ }
855
+ ```
856
+
857
+ ## 8. Payment Service (Stripe)
858
+
859
+ ```typescript
860
+ // src/services/payment.service.ts
861
+ import { injectable } from '@venizia/ignis';
862
+ import { BaseService } from '@venizia/ignis';
863
+ import Stripe from 'stripe';
864
+ import { EnvHelper } from '@venizia/ignis-helpers';
865
+
866
+ @injectable()
867
+ export class PaymentService extends BaseService {
868
+ private _stripe: Stripe;
869
+
870
+ constructor() {
871
+ super({ scope: PaymentService.name });
872
+
873
+ const secretKey = EnvHelper.get('APP_ENV_STRIPE_SECRET_KEY');
874
+ this._stripe = new Stripe(secretKey, {
875
+ apiVersion: '2023-10-16',
876
+ });
877
+ }
878
+
879
+ async createPaymentIntent(opts: {
880
+ amount: number;
881
+ currency: string;
882
+ metadata?: Record<string, string>;
883
+ }) {
884
+ return this._stripe.paymentIntents.create({
885
+ amount: opts.amount,
886
+ currency: opts.currency,
887
+ metadata: opts.metadata,
888
+ automatic_payment_methods: {
889
+ enabled: true,
890
+ },
891
+ });
892
+ }
893
+
894
+ async verifyPayment(opts: { paymentIntentId: string }): Promise<boolean> {
895
+ const paymentIntent = await this._stripe.paymentIntents.retrieve(opts.paymentIntentId);
896
+ return paymentIntent.status === 'succeeded';
897
+ }
898
+
899
+ async refundPayment(opts: { paymentIntentId: string; amount?: number }) {
900
+ return this._stripe.refunds.create({
901
+ payment_intent: opts.paymentIntentId,
902
+ amount: opts.amount, // undefined = full refund
903
+ });
904
+ }
905
+
906
+ async createWebhookEvent(opts: { payload: string; signature: string }) {
907
+ const webhookSecret = EnvHelper.get('APP_ENV_STRIPE_WEBHOOK_SECRET');
908
+ return this._stripe.webhooks.constructEvent(opts.payload, opts.signature, webhookSecret);
909
+ }
910
+ }
911
+ ```
912
+
913
+ ## 9. Controllers
914
+
915
+ ### Product Controller
916
+
917
+ ```typescript
918
+ // src/controllers/product.controller.ts
919
+ import { z } from '@hono/zod-openapi';
920
+ import {
921
+ BaseController,
922
+ controller,
923
+ get,
924
+ inject,
925
+ HTTP,
926
+ jsonContent,
927
+ TRouteContext,
928
+ } from '@venizia/ignis';
929
+ import { ProductService } from '../services/product.service';
930
+
931
+ // Define route configs with PascalCase type and SCREAMING_SNAKE_CASE keys
932
+ const ProductRoutes = {
933
+ LIST: {
934
+ method: HTTP.Methods.GET,
935
+ path: '/',
936
+ request: {
937
+ query: z.object({
938
+ category: z.string().optional(),
939
+ limit: z.string().optional(),
940
+ offset: z.string().optional(),
941
+ }),
942
+ },
943
+ responses: {
944
+ [HTTP.ResultCodes.RS_2.Ok]: jsonContent({
945
+ description: 'List of products',
946
+ schema: z.object({
947
+ products: z.array(z.any()),
948
+ total: z.number(),
949
+ }),
950
+ }),
951
+ },
952
+ },
953
+ GET_BY_ID: {
954
+ method: HTTP.Methods.GET,
955
+ path: '/:id',
956
+ request: {
957
+ params: z.object({ id: z.string().uuid() }),
958
+ },
959
+ responses: {
960
+ [HTTP.ResultCodes.RS_2.Ok]: jsonContent({
961
+ description: 'Product details',
962
+ schema: z.any(),
963
+ }),
964
+ },
965
+ },
966
+ } as const;
967
+
968
+ type ProductRoutes = typeof ProductRoutes;
969
+
970
+ @controller({ path: '/products' })
971
+ export class ProductController extends BaseController {
972
+ constructor(
973
+ @inject('services.ProductService')
974
+ private _productService: ProductService,
975
+ ) {
976
+ super({ scope: ProductController.name, path: '/products' });
977
+ }
978
+
979
+ override binding() {}
980
+
981
+ @get({ configs: ProductRoutes.LIST })
982
+ async listProducts(c: TRouteContext<ProductRoutes['LIST']>) {
983
+ const { category, limit, offset } = c.req.valid('query');
984
+
985
+ const products = await this._productService.getActiveProducts({
986
+ categoryId: category,
987
+ limit: limit ? parseInt(limit) : undefined,
988
+ offset: offset ? parseInt(offset) : undefined,
989
+ });
990
+
991
+ return c.json({ products, total: products.length });
992
+ }
993
+
994
+ @get({ configs: ProductRoutes.GET_BY_ID })
995
+ async getProduct(c: TRouteContext<ProductRoutes['GET_BY_ID']>) {
996
+ const { id } = c.req.valid('param');
997
+ const product = await this._productService.getProductById({ id });
998
+ return c.json(product);
999
+ }
1000
+ }
1001
+ ```
1002
+
1003
+ ### Cart Controller
1004
+
1005
+ ```typescript
1006
+ // src/controllers/cart.controller.ts
1007
+ import { z } from '@hono/zod-openapi';
1008
+ import {
1009
+ BaseController,
1010
+ controller,
1011
+ get,
1012
+ post,
1013
+ put,
1014
+ del,
1015
+ inject,
1016
+ HTTP,
1017
+ jsonContent,
1018
+ TRouteContext,
1019
+ } from '@venizia/ignis';
1020
+ import { CartService } from '../services/cart.service';
1021
+
1022
+ const CartRoutes = {
1023
+ GET: {
1024
+ method: HTTP.Methods.GET,
1025
+ path: '/',
1026
+ responses: {
1027
+ [HTTP.ResultCodes.RS_2.Ok]: jsonContent({
1028
+ description: 'Current cart',
1029
+ schema: z.any(),
1030
+ }),
1031
+ },
1032
+ },
1033
+ ADD_ITEM: {
1034
+ method: HTTP.Methods.POST,
1035
+ path: '/items',
1036
+ request: {
1037
+ body: jsonContent({
1038
+ schema: z.object({
1039
+ productId: z.string().uuid(),
1040
+ quantity: z.number().int().positive().default(1),
1041
+ }),
1042
+ }),
1043
+ },
1044
+ responses: {
1045
+ [HTTP.ResultCodes.RS_2.Created]: jsonContent({
1046
+ description: 'Item added to cart',
1047
+ schema: z.any(),
1048
+ }),
1049
+ },
1050
+ },
1051
+ UPDATE_ITEM: {
1052
+ method: HTTP.Methods.PUT,
1053
+ path: '/items/:productId',
1054
+ request: {
1055
+ params: z.object({ productId: z.string().uuid() }),
1056
+ body: jsonContent({
1057
+ schema: z.object({
1058
+ quantity: z.number().int().min(0),
1059
+ }),
1060
+ }),
1061
+ },
1062
+ responses: {
1063
+ [HTTP.ResultCodes.RS_2.Ok]: jsonContent({
1064
+ description: 'Item quantity updated',
1065
+ schema: z.any(),
1066
+ }),
1067
+ },
1068
+ },
1069
+ REMOVE_ITEM: {
1070
+ method: HTTP.Methods.DELETE,
1071
+ path: '/items/:productId',
1072
+ request: {
1073
+ params: z.object({ productId: z.string().uuid() }),
1074
+ },
1075
+ responses: {
1076
+ [HTTP.ResultCodes.RS_2.NoContent]: {
1077
+ description: 'Item removed from cart',
1078
+ },
1079
+ },
1080
+ },
1081
+ } as const;
1082
+
1083
+ type CartRoutes = typeof CartRoutes;
1084
+
1085
+ @controller({ path: '/cart' })
1086
+ export class CartController extends BaseController {
1087
+ constructor(
1088
+ @inject('services.CartService')
1089
+ private _cartService: CartService,
1090
+ ) {
1091
+ super({ scope: CartController.name, path: '/cart' });
1092
+ }
1093
+
1094
+ override binding() {}
1095
+
1096
+ @get({ configs: CartRoutes.GET })
1097
+ async getCart(c: TRouteContext<CartRoutes['GET']>) {
1098
+ const sessionId = c.req.header('X-Session-ID') ?? 'guest';
1099
+ const cart = await this._cartService.getOrCreateCart({ sessionId });
1100
+ const cartWithItems = await this._cartService.getCartWithItems({ cartId: cart.id });
1101
+ return c.json(cartWithItems);
1102
+ }
1103
+
1104
+ @post({ configs: CartRoutes.ADD_ITEM })
1105
+ async addToCart(c: TRouteContext<CartRoutes['ADD_ITEM']>) {
1106
+ const sessionId = c.req.header('X-Session-ID') ?? 'guest';
1107
+ const { productId, quantity } = c.req.valid('json');
1108
+
1109
+ const cart = await this._cartService.getOrCreateCart({ sessionId });
1110
+ await this._cartService.addItem({ cartId: cart.id, productId, quantity });
1111
+
1112
+ const updatedCart = await this._cartService.getCartWithItems({ cartId: cart.id });
1113
+ return c.json(updatedCart, HTTP.ResultCodes.RS_2.Created);
1114
+ }
1115
+
1116
+ @put({ configs: CartRoutes.UPDATE_ITEM })
1117
+ async updateCartItem(c: TRouteContext<CartRoutes['UPDATE_ITEM']>) {
1118
+ const sessionId = c.req.header('X-Session-ID') ?? 'guest';
1119
+ const { productId } = c.req.valid('param');
1120
+ const { quantity } = c.req.valid('json');
1121
+
1122
+ const cart = await this._cartService.getOrCreateCart({ sessionId });
1123
+ await this._cartService.updateItemQuantity({ cartId: cart.id, productId, quantity });
1124
+
1125
+ const updatedCart = await this._cartService.getCartWithItems({ cartId: cart.id });
1126
+ return c.json(updatedCart);
1127
+ }
1128
+
1129
+ @del({ configs: CartRoutes.REMOVE_ITEM })
1130
+ async removeFromCart(c: TRouteContext<CartRoutes['REMOVE_ITEM']>) {
1131
+ const sessionId = c.req.header('X-Session-ID') ?? 'guest';
1132
+ const { productId } = c.req.valid('param');
1133
+
1134
+ const cart = await this._cartService.getOrCreateCart({ sessionId });
1135
+ await this._cartService.removeItem({ cartId: cart.id, productId });
1136
+
1137
+ return c.body(null, HTTP.ResultCodes.RS_2.NoContent);
1138
+ }
1139
+ }
1140
+ ```
1141
+
1142
+ ### Order Controller
1143
+
1144
+ ```typescript
1145
+ // src/controllers/order.controller.ts
1146
+ import { z } from '@hono/zod-openapi';
1147
+ import {
1148
+ BaseController,
1149
+ controller,
1150
+ get,
1151
+ post,
1152
+ inject,
1153
+ HTTP,
1154
+ jsonContent,
1155
+ TRouteContext,
1156
+ } from '@venizia/ignis';
1157
+ import { OrderService } from '../services/order.service';
1158
+
1159
+ const OrderRoutes = {
1160
+ CREATE: {
1161
+ method: HTTP.Methods.POST,
1162
+ path: '/',
1163
+ request: {
1164
+ body: jsonContent({
1165
+ schema: z.object({
1166
+ cartId: z.string().uuid(),
1167
+ email: z.string().email(),
1168
+ shippingAddress: z.object({
1169
+ name: z.string(),
1170
+ line1: z.string(),
1171
+ line2: z.string().optional(),
1172
+ city: z.string(),
1173
+ state: z.string(),
1174
+ postalCode: z.string(),
1175
+ country: z.string(),
1176
+ }),
1177
+ }),
1178
+ }),
1179
+ },
1180
+ responses: {
1181
+ [HTTP.ResultCodes.RS_2.Created]: jsonContent({
1182
+ description: 'Order created',
1183
+ schema: z.object({
1184
+ order: z.any(),
1185
+ clientSecret: z.string(),
1186
+ }),
1187
+ }),
1188
+ },
1189
+ },
1190
+ CONFIRM: {
1191
+ method: HTTP.Methods.POST,
1192
+ path: '/:id/confirm',
1193
+ request: {
1194
+ params: z.object({ id: z.string().uuid() }),
1195
+ body: jsonContent({
1196
+ schema: z.object({
1197
+ paymentIntentId: z.string(),
1198
+ }),
1199
+ }),
1200
+ },
1201
+ responses: {
1202
+ [HTTP.ResultCodes.RS_2.Ok]: jsonContent({
1203
+ description: 'Order confirmed',
1204
+ schema: z.any(),
1205
+ }),
1206
+ },
1207
+ },
1208
+ GET_BY_ID: {
1209
+ method: HTTP.Methods.GET,
1210
+ path: '/:id',
1211
+ request: {
1212
+ params: z.object({ id: z.string().uuid() }),
1213
+ },
1214
+ responses: {
1215
+ [HTTP.ResultCodes.RS_2.Ok]: jsonContent({
1216
+ description: 'Order details',
1217
+ schema: z.any(),
1218
+ }),
1219
+ },
1220
+ },
1221
+ } as const;
1222
+
1223
+ type OrderRoutes = typeof OrderRoutes;
1224
+
1225
+ @controller({ path: '/orders' })
1226
+ export class OrderController extends BaseController {
1227
+ constructor(
1228
+ @inject('services.OrderService')
1229
+ private _orderService: OrderService,
1230
+ ) {
1231
+ super({ scope: OrderController.name, path: '/orders' });
1232
+ }
1233
+
1234
+ override binding() {}
1235
+
1236
+ @post({ configs: OrderRoutes.CREATE })
1237
+ async createOrder(c: TRouteContext<OrderRoutes['CREATE']>) {
1238
+ const body = c.req.valid('json');
1239
+ const result = await this._orderService.createOrder({ input: body });
1240
+ return c.json(result, HTTP.ResultCodes.RS_2.Created);
1241
+ }
1242
+
1243
+ @post({ configs: OrderRoutes.CONFIRM })
1244
+ async confirmOrder(c: TRouteContext<OrderRoutes['CONFIRM']>) {
1245
+ const { id: orderId } = c.req.valid('param');
1246
+ const { paymentIntentId } = c.req.valid('json');
1247
+
1248
+ const order = await this._orderService.confirmPayment({ orderId, paymentIntentId });
1249
+ return c.json(order);
1250
+ }
1251
+
1252
+ @get({ configs: OrderRoutes.GET_BY_ID })
1253
+ async getOrder(c: TRouteContext<OrderRoutes['GET_BY_ID']>) {
1254
+ const { id: orderId } = c.req.valid('param');
1255
+ const order = await this._orderService.getOrderById({ orderId });
1256
+ return c.json(order);
1257
+ }
1258
+ }
1259
+ ```
1260
+
1261
+ ## 10. Application Setup
1262
+
1263
+ ```typescript
1264
+ // src/application.ts
1265
+ import { BaseApplication, IApplicationInfo } from '@venizia/ignis';
1266
+ import { HealthCheckComponent, SwaggerComponent } from '@venizia/ignis';
1267
+
1268
+ import { ProductController } from './controllers/product.controller';
1269
+ import { CartController } from './controllers/cart.controller';
1270
+ import { OrderController } from './controllers/order.controller';
1271
+
1272
+ import { ProductService } from './services/product.service';
1273
+ import { CartService } from './services/cart.service';
1274
+ import { OrderService } from './services/order.service';
1275
+ import { PaymentService } from './services/payment.service';
1276
+
1277
+ import { ProductRepository } from './repositories/product.repository';
1278
+ import { CartRepository } from './repositories/cart.repository';
1279
+ import { OrderRepository } from './repositories/order.repository';
1280
+
1281
+ import { PostgresDataSource } from './datasources/postgres.datasource';
1282
+
1283
+ export class EcommerceApp extends BaseApplication {
1284
+ getAppInfo(): IApplicationInfo {
1285
+ return { name: 'ecommerce-api', version: '1.0.0' };
1286
+ }
1287
+
1288
+ staticConfigure() {}
1289
+
1290
+ preConfigure() {
1291
+ // DataSources
1292
+ this.dataSource(PostgresDataSource);
1293
+
1294
+ // Repositories
1295
+ this.repository(ProductRepository);
1296
+ this.repository(CartRepository);
1297
+ this.repository(OrderRepository);
1298
+
1299
+ // Services
1300
+ this.service(ProductService);
1301
+ this.service(CartService);
1302
+ this.service(OrderService);
1303
+ this.service(PaymentService);
1304
+
1305
+ // Controllers
1306
+ this.controller(ProductController);
1307
+ this.controller(CartController);
1308
+ this.controller(OrderController);
1309
+
1310
+ // Components
1311
+ this.component(HealthCheckComponent);
1312
+ this.component(SwaggerComponent);
1313
+ }
1314
+
1315
+ postConfigure() {}
1316
+
1317
+ setupMiddlewares() {}
1318
+ }
1319
+ ```
1320
+
1321
+ ## 11. Running the Application
1322
+
1323
+ ### Environment Variables
1324
+
1325
+ ```bash
1326
+ # .env
1327
+ NODE_ENV=development
1328
+ APP_ENV_SERVER_HOST=0.0.0.0
1329
+ APP_ENV_SERVER_PORT=3000
1330
+
1331
+ # Database
1332
+ APP_ENV_POSTGRES_HOST=localhost
1333
+ APP_ENV_POSTGRES_PORT=5432
1334
+ APP_ENV_POSTGRES_USERNAME=ecommerce
1335
+ APP_ENV_POSTGRES_PASSWORD=password
1336
+ APP_ENV_POSTGRES_DATABASE=ecommerce_db
1337
+
1338
+ # Stripe
1339
+ APP_ENV_STRIPE_SECRET_KEY=sk_test_xxx
1340
+ APP_ENV_STRIPE_WEBHOOK_SECRET=whsec_xxx
1341
+ ```
1342
+
1343
+ ### Start the Server
1344
+
1345
+ ```bash
1346
+ bun run server:dev
1347
+ ```
1348
+
1349
+ ### Test the API
1350
+
1351
+ ```bash
1352
+ # Get products
1353
+ curl http://localhost:3000/api/products
1354
+
1355
+ # Add to cart
1356
+ curl -X POST http://localhost:3000/api/cart/items \
1357
+ -H "Content-Type: application/json" \
1358
+ -H "X-Session-ID: my-session" \
1359
+ -d '{"productId": "uuid-here", "quantity": 2}'
1360
+
1361
+ # Get cart
1362
+ curl http://localhost:3000/api/cart \
1363
+ -H "X-Session-ID: my-session"
1364
+
1365
+ # Create order
1366
+ curl -X POST http://localhost:3000/api/orders \
1367
+ -H "Content-Type: application/json" \
1368
+ -d '{
1369
+ "cartId": "cart-uuid",
1370
+ "email": "customer@example.com",
1371
+ "shippingAddress": {
1372
+ "name": "John Doe",
1373
+ "line1": "123 Main St",
1374
+ "city": "San Francisco",
1375
+ "state": "CA",
1376
+ "postalCode": "94102",
1377
+ "country": "US"
1378
+ }
1379
+ }'
1380
+ ```
1381
+
1382
+ ## Summary
1383
+
1384
+ You've built a complete e-commerce API with:
1385
+
1386
+ | Feature | Implementation |
1387
+ |---------|---------------|
1388
+ | Product Catalog | ProductService with filtering |
1389
+ | Shopping Cart | CartService with session handling |
1390
+ | Order Processing | OrderService with stock validation |
1391
+ | Payments | PaymentService with Stripe integration |
1392
+ | Inventory | Stock reservation on order confirmation |
1393
+
1394
+ ## Next Steps
1395
+
1396
+ - Add user authentication with [Auth Component](/references/components/authentication)
1397
+ - Add order notifications with [Mail Component](/references/components/mail)
1398
+ - Add real-time updates with [Socket.IO](./realtime-chat.md)
1399
+ - Deploy with [Deployment Guide](/best-practices/deployment-strategies)