@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,1261 @@
1
+ # Building a Real-Time Chat Application
2
+
3
+ This tutorial shows you how to build a real-time chat application with rooms, direct messages, typing indicators, and presence using Socket.IO.
4
+
5
+ **⏱️ Time to Complete:** ~75 minutes
6
+
7
+ ## What You'll Build
8
+
9
+ - Chat rooms (channels)
10
+ - Direct messages between users
11
+ - Typing indicators
12
+ - Online/offline presence
13
+ - Message history with pagination
14
+
15
+ ## Prerequisites
16
+
17
+ - Completed [Building a CRUD API](./building-a-crud-api.md)
18
+ - Understanding of [Socket.IO Component](/references/components/socket-io)
19
+ - Redis for pub/sub (optional but recommended for scaling)
20
+
21
+ ## 1. Project Setup
22
+
23
+ ```bash
24
+ mkdir chat-api
25
+ cd chat-api
26
+ bun init -y
27
+
28
+ # Install dependencies
29
+ bun add hono @hono/zod-openapi @venizia/ignis dotenv-flow
30
+ bun add drizzle-orm drizzle-zod pg socket.io
31
+ bun add -d typescript @types/bun @venizia/dev-configs drizzle-kit @types/pg
32
+ ```
33
+
34
+ ## 2. Database Models
35
+
36
+ Models in IGNIS combine Drizzle ORM schemas with Entity classes.
37
+
38
+ ### User Model
39
+
40
+ ```typescript
41
+ // src/models/user.model.ts
42
+ import {
43
+ BaseEntity,
44
+ createRelations,
45
+ generateIdColumnDefs,
46
+ generateTzColumnDefs,
47
+ model,
48
+ TTableObject,
49
+ } from '@venizia/ignis';
50
+ import { pgTable, varchar, text, timestamp, boolean } from 'drizzle-orm/pg-core';
51
+
52
+ export const userTable = pgTable('User', {
53
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
54
+ ...generateTzColumnDefs(),
55
+ username: varchar('username', { length: 50 }).unique().notNull(),
56
+ displayName: varchar('display_name', { length: 100 }),
57
+ avatar: text('avatar'),
58
+ isOnline: boolean('is_online').default(false).notNull(),
59
+ lastSeenAt: timestamp('last_seen_at'),
60
+ });
61
+
62
+ export const userRelations = createRelations({
63
+ source: userTable,
64
+ relations: [],
65
+ });
66
+
67
+ export type TUserSchema = typeof userTable;
68
+ export type TUser = TTableObject<TUserSchema>;
69
+
70
+ @model({ type: 'entity' })
71
+ export class User extends BaseEntity<typeof User.schema> {
72
+ static override schema = userTable;
73
+ static override relations = () => userRelations.definitions;
74
+ static override TABLE_NAME = 'User';
75
+ }
76
+ ```
77
+
78
+ ### Room Model
79
+
80
+ ```typescript
81
+ // src/models/room.model.ts
82
+ import {
83
+ BaseEntity,
84
+ createRelations,
85
+ generateIdColumnDefs,
86
+ generateTzColumnDefs,
87
+ model,
88
+ TTableObject,
89
+ } from '@venizia/ignis';
90
+ import { pgTable, varchar, text, timestamp, boolean } from 'drizzle-orm/pg-core';
91
+ import { userTable } from './user.model';
92
+
93
+ export const roomTable = pgTable('Room', {
94
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
95
+ ...generateTzColumnDefs(),
96
+ name: varchar('name', { length: 100 }).notNull(),
97
+ description: text('description'),
98
+ isPrivate: boolean('is_private').default(false).notNull(),
99
+ createdBy: text('created_by').notNull(),
100
+ });
101
+
102
+ export const roomMemberTable = pgTable('RoomMember', {
103
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
104
+ roomId: text('room_id').notNull(),
105
+ userId: text('user_id').notNull(),
106
+ role: varchar('role', { length: 20 }).default('member').notNull(),
107
+ joinedAt: timestamp('joined_at').defaultNow().notNull(),
108
+ lastReadAt: timestamp('last_read_at'),
109
+ });
110
+
111
+ export const roomRelations = createRelations({
112
+ source: roomTable,
113
+ relations: [
114
+ { type: 'one', name: 'creator', target: () => userTable, fields: ['createdBy'], references: ['id'] },
115
+ { type: 'many', name: 'members', target: () => roomMemberTable, fields: ['id'], references: ['roomId'] },
116
+ ],
117
+ });
118
+
119
+ export const roomMemberRelations = createRelations({
120
+ source: roomMemberTable,
121
+ relations: [
122
+ { type: 'one', name: 'room', target: () => roomTable, fields: ['roomId'], references: ['id'] },
123
+ { type: 'one', name: 'user', target: () => userTable, fields: ['userId'], references: ['id'] },
124
+ ],
125
+ });
126
+
127
+ export type TRoomSchema = typeof roomTable;
128
+ export type TRoom = TTableObject<TRoomSchema>;
129
+ export type TRoomMemberSchema = typeof roomMemberTable;
130
+ export type TRoomMember = TTableObject<TRoomMemberSchema>;
131
+
132
+ @model({ type: 'entity' })
133
+ export class Room extends BaseEntity<typeof Room.schema> {
134
+ static override schema = roomTable;
135
+ static override relations = () => roomRelations.definitions;
136
+ static override TABLE_NAME = 'Room';
137
+ }
138
+
139
+ @model({ type: 'entity' })
140
+ export class RoomMember extends BaseEntity<typeof RoomMember.schema> {
141
+ static override schema = roomMemberTable;
142
+ static override relations = () => roomMemberRelations.definitions;
143
+ static override TABLE_NAME = 'RoomMember';
144
+ }
145
+ ```
146
+
147
+ ### Message Model
148
+
149
+ ```typescript
150
+ // src/models/message.model.ts
151
+ import {
152
+ BaseEntity,
153
+ createRelations,
154
+ generateIdColumnDefs,
155
+ generateTzColumnDefs,
156
+ model,
157
+ TTableObject,
158
+ } from '@venizia/ignis';
159
+ import { pgTable, text, timestamp, varchar } from 'drizzle-orm/pg-core';
160
+ import { userTable } from './user.model';
161
+ import { roomTable } from './room.model';
162
+
163
+ export const messageTable = pgTable('Message', {
164
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
165
+ ...generateTzColumnDefs(),
166
+ roomId: text('room_id').notNull(),
167
+ senderId: text('sender_id').notNull(),
168
+ content: text('content').notNull(),
169
+ type: varchar('type', { length: 20 }).default('text').notNull(),
170
+ metadata: text('metadata'),
171
+ editedAt: timestamp('edited_at'),
172
+ deletedAt: timestamp('deleted_at'),
173
+ });
174
+
175
+ export const directMessageTable = pgTable('DirectMessage', {
176
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
177
+ ...generateTzColumnDefs(),
178
+ senderId: text('sender_id').notNull(),
179
+ receiverId: text('receiver_id').notNull(),
180
+ content: text('content').notNull(),
181
+ type: varchar('type', { length: 20 }).default('text').notNull(),
182
+ metadata: text('metadata'),
183
+ readAt: timestamp('read_at'),
184
+ deletedAt: timestamp('deleted_at'),
185
+ });
186
+
187
+ export const messageRelations = createRelations({
188
+ source: messageTable,
189
+ relations: [
190
+ { type: 'one', name: 'room', target: () => roomTable, fields: ['roomId'], references: ['id'] },
191
+ { type: 'one', name: 'sender', target: () => userTable, fields: ['senderId'], references: ['id'] },
192
+ ],
193
+ });
194
+
195
+ export const directMessageRelations = createRelations({
196
+ source: directMessageTable,
197
+ relations: [
198
+ { type: 'one', name: 'sender', target: () => userTable, fields: ['senderId'], references: ['id'] },
199
+ { type: 'one', name: 'receiver', target: () => userTable, fields: ['receiverId'], references: ['id'] },
200
+ ],
201
+ });
202
+
203
+ export type TMessageSchema = typeof messageTable;
204
+ export type TMessage = TTableObject<TMessageSchema>;
205
+ export type TDirectMessageSchema = typeof directMessageTable;
206
+ export type TDirectMessage = TTableObject<TDirectMessageSchema>;
207
+
208
+ @model({ type: 'entity' })
209
+ export class Message extends BaseEntity<typeof Message.schema> {
210
+ static override schema = messageTable;
211
+ static override relations = () => messageRelations.definitions;
212
+ static override TABLE_NAME = 'Message';
213
+ }
214
+
215
+ @model({ type: 'entity' })
216
+ export class DirectMessage extends BaseEntity<typeof DirectMessage.schema> {
217
+ static override schema = directMessageTable;
218
+ static override relations = () => directMessageRelations.definitions;
219
+ static override TABLE_NAME = 'DirectMessage';
220
+ }
221
+ ```
222
+
223
+ ### Models Index
224
+
225
+ ```typescript
226
+ // src/models/index.ts
227
+ export * from './user.model';
228
+ export * from './room.model';
229
+ export * from './message.model';
230
+ ```
231
+
232
+ ## 3. DataSource
233
+
234
+ ```typescript
235
+ // src/datasources/postgres.datasource.ts
236
+ import {
237
+ BaseDataSource,
238
+ datasource,
239
+ TNodePostgresConnector,
240
+ ValueOrPromise,
241
+ } from '@venizia/ignis';
242
+ import { drizzle } from 'drizzle-orm/node-postgres';
243
+ import { Pool } from 'pg';
244
+
245
+ interface IDSConfigs {
246
+ host: string;
247
+ port: number;
248
+ database: string;
249
+ user: string;
250
+ password: string;
251
+ }
252
+
253
+ @datasource({ driver: 'node-postgres' })
254
+ export class PostgresDataSource extends BaseDataSource<TNodePostgresConnector, IDSConfigs> {
255
+ constructor() {
256
+ super({
257
+ name: PostgresDataSource.name,
258
+ config: {
259
+ host: process.env.APP_ENV_POSTGRES_HOST ?? 'localhost',
260
+ port: +(process.env.APP_ENV_POSTGRES_PORT ?? 5432),
261
+ database: process.env.APP_ENV_POSTGRES_DATABASE ?? 'chat_db',
262
+ user: process.env.APP_ENV_POSTGRES_USERNAME ?? 'postgres',
263
+ password: process.env.APP_ENV_POSTGRES_PASSWORD ?? '',
264
+ },
265
+ });
266
+ }
267
+
268
+ override configure(): ValueOrPromise<void> {
269
+ const schema = this.getSchema();
270
+
271
+ this.logger.debug(
272
+ '[configure] Auto-discovered schema | Schema + Relations (%s): %o',
273
+ Object.keys(schema).length,
274
+ Object.keys(schema),
275
+ );
276
+
277
+ const client = new Pool(this.settings);
278
+ this.connector = drizzle({ client, schema });
279
+ }
280
+ }
281
+ ```
282
+
283
+ ## 4. Repositories
284
+
285
+ ### User Repository
286
+
287
+ ```typescript
288
+ // src/repositories/user.repository.ts
289
+ import { User } from '@/models/user.model';
290
+ import { PostgresDataSource } from '@/datasources/postgres.datasource';
291
+ import { DefaultCRUDRepository, repository } from '@venizia/ignis';
292
+
293
+ @repository({ model: User, dataSource: PostgresDataSource })
294
+ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {}
295
+ ```
296
+
297
+ ### Room Repository
298
+
299
+ ```typescript
300
+ // src/repositories/room.repository.ts
301
+ import { Room, RoomMember } from '@/models/room.model';
302
+ import { PostgresDataSource } from '@/datasources/postgres.datasource';
303
+ import { DefaultCRUDRepository, repository, inject } from '@venizia/ignis';
304
+
305
+ @repository({ model: RoomMember, dataSource: PostgresDataSource })
306
+ export class RoomMemberRepository extends DefaultCRUDRepository<typeof RoomMember.schema> {}
307
+
308
+ @repository({ model: Room, dataSource: PostgresDataSource })
309
+ export class RoomRepository extends DefaultCRUDRepository<typeof Room.schema> {
310
+ constructor(
311
+ // First parameter MUST be DataSource injection
312
+ @inject({ key: 'datasources.PostgresDataSource' })
313
+ dataSource: PostgresDataSource,
314
+
315
+ // From 2nd parameter, inject additional dependencies
316
+ @inject({ key: 'repositories.RoomMemberRepository' })
317
+ private _memberRepo: RoomMemberRepository,
318
+ ) {
319
+ super(dataSource);
320
+ }
321
+
322
+ async findByUser(opts: { userId: string }) {
323
+ const memberships = await this._memberRepo.find({
324
+ where: { userId: opts.userId },
325
+ include: { room: true },
326
+ });
327
+ return memberships.map(m => m.room);
328
+ }
329
+
330
+ async isMember(opts: { roomId: string; userId: string }): Promise<boolean> {
331
+ const member = await this._memberRepo.findOne({
332
+ where: { roomId: opts.roomId, userId: opts.userId },
333
+ });
334
+ return !!member;
335
+ }
336
+
337
+ async addMember(opts: { roomId: string; userId: string; role?: string }) {
338
+ return this._memberRepo.create({
339
+ roomId: opts.roomId,
340
+ userId: opts.userId,
341
+ role: opts.role ?? 'member',
342
+ });
343
+ }
344
+
345
+ async removeMember(opts: { roomId: string; userId: string }) {
346
+ return this._memberRepo.deleteAll({
347
+ where: { roomId: opts.roomId, userId: opts.userId },
348
+ });
349
+ }
350
+
351
+ async getMember(opts: { roomId: string; userId: string }) {
352
+ return this._memberRepo.findOne({
353
+ where: { roomId: opts.roomId, userId: opts.userId },
354
+ });
355
+ }
356
+
357
+ async getMembers(opts: { roomId: string }) {
358
+ return this._memberRepo.find({
359
+ where: { roomId: opts.roomId },
360
+ include: { user: true },
361
+ });
362
+ }
363
+ }
364
+ ```
365
+
366
+ ### Message Repository
367
+
368
+ ```typescript
369
+ // src/repositories/message.repository.ts
370
+ import { Message, DirectMessage } from '@/models/message.model';
371
+ import { PostgresDataSource } from '@/datasources/postgres.datasource';
372
+ import { DefaultCRUDRepository, repository, inject } from '@venizia/ignis';
373
+
374
+ @repository({ model: DirectMessage, dataSource: PostgresDataSource })
375
+ export class DirectMessageRepository extends DefaultCRUDRepository<typeof DirectMessage.schema> {}
376
+
377
+ @repository({ model: Message, dataSource: PostgresDataSource })
378
+ export class MessageRepository extends DefaultCRUDRepository<typeof Message.schema> {
379
+ constructor(
380
+ // First parameter MUST be DataSource injection
381
+ @inject({ key: 'datasources.PostgresDataSource' })
382
+ dataSource: PostgresDataSource,
383
+
384
+ // From 2nd parameter, inject additional dependencies
385
+ @inject({ key: 'repositories.DirectMessageRepository' })
386
+ private _dmRepo: DirectMessageRepository,
387
+ ) {
388
+ super(dataSource);
389
+ }
390
+
391
+ async createDirectMessage(opts: { senderId: string; receiverId: string; content: string }) {
392
+ return this._dmRepo.create(opts);
393
+ }
394
+
395
+ async findDirectMessages(opts: {
396
+ userId1: string;
397
+ userId2: string;
398
+ limit?: number;
399
+ before?: string;
400
+ }) {
401
+ return this._dmRepo.find({
402
+ where: {
403
+ or: [
404
+ { senderId: opts.userId1, receiverId: opts.userId2 },
405
+ { senderId: opts.userId2, receiverId: opts.userId1 },
406
+ ],
407
+ },
408
+ orderBy: { createdAt: 'desc' },
409
+ limit: opts.limit ?? 50,
410
+ });
411
+ }
412
+
413
+ async findConversations(opts: { userId: string }) {
414
+ // Get unique conversation partners
415
+ const sent = await this._dmRepo.find({
416
+ where: { senderId: opts.userId },
417
+ include: { receiver: true },
418
+ });
419
+ const received = await this._dmRepo.find({
420
+ where: { receiverId: opts.userId },
421
+ include: { sender: true },
422
+ });
423
+
424
+ // Combine and deduplicate
425
+ const partners = new Map();
426
+ sent.forEach(m => partners.set(m.receiverId, m.receiver));
427
+ received.forEach(m => partners.set(m.senderId, m.sender));
428
+
429
+ return Array.from(partners.values());
430
+ }
431
+ }
432
+ ```
433
+
434
+ ## 5. Socket.IO Events
435
+
436
+ Define your event types:
437
+
438
+ ```typescript
439
+ // src/types/socket.types.ts
440
+
441
+ // Client -> Server events
442
+ export interface ClientToServerEvents {
443
+ // Rooms
444
+ 'room:join': (roomId: string) => void;
445
+ 'room:leave': (roomId: string) => void;
446
+
447
+ // Messages
448
+ 'message:send': (data: { roomId: string; content: string; type?: string }) => void;
449
+ 'message:edit': (data: { messageId: string; content: string }) => void;
450
+ 'message:delete': (data: { messageId: string }) => void;
451
+
452
+ // Direct messages
453
+ 'dm:send': (data: { receiverId: string; content: string }) => void;
454
+
455
+ // Typing
456
+ 'typing:start': (roomId: string) => void;
457
+ 'typing:stop': (roomId: string) => void;
458
+
459
+ // Presence
460
+ 'presence:update': (status: 'online' | 'away' | 'busy') => void;
461
+ }
462
+
463
+ // Server -> Client events
464
+ export interface ServerToClientEvents {
465
+ // Messages
466
+ 'message:new': (message: Message) => void;
467
+ 'message:edited': (message: Message) => void;
468
+ 'message:deleted': (data: { messageId: string; roomId: string }) => void;
469
+
470
+ // Direct messages
471
+ 'dm:new': (message: DirectMessage) => void;
472
+
473
+ // Typing
474
+ 'typing:update': (data: { roomId: string; userId: string; isTyping: boolean }) => void;
475
+
476
+ // Presence
477
+ 'presence:changed': (data: { userId: string; status: string; lastSeenAt?: Date }) => void;
478
+
479
+ // Room
480
+ 'room:user-joined': (data: { roomId: string; user: User }) => void;
481
+ 'room:user-left': (data: { roomId: string; userId: string }) => void;
482
+
483
+ // Errors
484
+ 'error': (error: { code: string; message: string }) => void;
485
+ }
486
+
487
+ export interface InterServerEvents {
488
+ ping: () => void;
489
+ }
490
+
491
+ export interface SocketData {
492
+ userId: string;
493
+ username: string;
494
+ }
495
+ ```
496
+
497
+ ## 6. Chat Service
498
+
499
+ ```typescript
500
+ // src/services/chat.service.ts
501
+ import { injectable, inject } from '@venizia/ignis';
502
+ import { BaseService } from '@venizia/ignis';
503
+ import { MessageRepository } from '../repositories/message.repository';
504
+ import { RoomRepository } from '../repositories/room.repository';
505
+ import { UserRepository } from '../repositories/user.repository';
506
+ import { getError } from '@venizia/ignis-helpers';
507
+
508
+ @injectable()
509
+ export class ChatService extends BaseService {
510
+ constructor(
511
+ @inject('repositories.MessageRepository')
512
+ private _messageRepo: MessageRepository,
513
+ @inject('repositories.RoomRepository')
514
+ private _roomRepo: RoomRepository,
515
+ @inject('repositories.UserRepository')
516
+ private _userRepo: UserRepository,
517
+ ) {
518
+ super({ scope: ChatService.name });
519
+ }
520
+
521
+ // Room operations
522
+ async createRoom(opts: { name: string; description?: string; isPrivate?: boolean; createdBy: string }) {
523
+ const room = await this._roomRepo.create(opts);
524
+
525
+ // Add creator as admin
526
+ await this._roomRepo.addMember({ roomId: room.id, userId: opts.createdBy, role: 'admin' });
527
+
528
+ return room;
529
+ }
530
+
531
+ async joinRoom(opts: { roomId: string; userId: string }) {
532
+ const room = await this._roomRepo.findById(opts.roomId);
533
+ if (!room) {
534
+ throw getError({ statusCode: 404, message: 'Room not found' });
535
+ }
536
+
537
+ // Check if private room requires invitation
538
+ if (room.isPrivate) {
539
+ const isMember = await this._roomRepo.isMember({ roomId: opts.roomId, userId: opts.userId });
540
+ if (!isMember) {
541
+ throw getError({ statusCode: 403, message: 'Cannot join private room' });
542
+ }
543
+ } else {
544
+ // Auto-join public rooms
545
+ await this._roomRepo.addMember({ roomId: opts.roomId, userId: opts.userId, role: 'member' });
546
+ }
547
+
548
+ return room;
549
+ }
550
+
551
+ async leaveRoom(opts: { roomId: string; userId: string }) {
552
+ await this._roomRepo.removeMember({ roomId: opts.roomId, userId: opts.userId });
553
+ }
554
+
555
+ async getUserRooms(opts: { userId: string }) {
556
+ return this._roomRepo.findByUser({ userId: opts.userId });
557
+ }
558
+
559
+ // Message operations
560
+ async sendMessage(opts: { roomId: string; senderId: string; content: string; type?: string }) {
561
+ // Verify user is member of room
562
+ const isMember = await this._roomRepo.isMember({ roomId: opts.roomId, userId: opts.senderId });
563
+ if (!isMember) {
564
+ throw getError({ statusCode: 403, message: 'Not a member of this room' });
565
+ }
566
+
567
+ const message = await this._messageRepo.create({
568
+ roomId: opts.roomId,
569
+ senderId: opts.senderId,
570
+ content: opts.content,
571
+ type: opts.type ?? 'text',
572
+ });
573
+
574
+ // Get sender info for the response
575
+ const sender = await this._userRepo.findById(opts.senderId);
576
+
577
+ return {
578
+ ...message,
579
+ sender: {
580
+ id: sender.id,
581
+ username: sender.username,
582
+ displayName: sender.displayName,
583
+ avatar: sender.avatar,
584
+ },
585
+ };
586
+ }
587
+
588
+ async editMessage(opts: { messageId: string; userId: string; content: string }) {
589
+ const message = await this._messageRepo.findById(opts.messageId);
590
+
591
+ if (!message) {
592
+ throw getError({ statusCode: 404, message: 'Message not found' });
593
+ }
594
+
595
+ if (message.senderId !== opts.userId) {
596
+ throw getError({ statusCode: 403, message: 'Cannot edit others messages' });
597
+ }
598
+
599
+ return this._messageRepo.updateById(opts.messageId, {
600
+ content: opts.content,
601
+ editedAt: new Date(),
602
+ });
603
+ }
604
+
605
+ async deleteMessage(opts: { messageId: string; userId: string }) {
606
+ const message = await this._messageRepo.findById(opts.messageId);
607
+
608
+ if (!message) {
609
+ throw getError({ statusCode: 404, message: 'Message not found' });
610
+ }
611
+
612
+ if (message.senderId !== opts.userId) {
613
+ // Check if user is room admin
614
+ const member = await this._roomRepo.getMember({ roomId: message.roomId, userId: opts.userId });
615
+ if (!member || member.role !== 'admin') {
616
+ throw getError({ statusCode: 403, message: 'Cannot delete this message' });
617
+ }
618
+ }
619
+
620
+ return this._messageRepo.updateById(opts.messageId, {
621
+ deletedAt: new Date(),
622
+ });
623
+ }
624
+
625
+ async getMessages(opts: { roomId: string; limit?: number; before?: string }) {
626
+ const where: any = {
627
+ roomId: opts.roomId,
628
+ deletedAt: null,
629
+ };
630
+
631
+ if (opts.before) {
632
+ // Cursor-based pagination
633
+ const beforeMessage = await this._messageRepo.findById(opts.before);
634
+ if (beforeMessage) {
635
+ where.createdAt = { lt: beforeMessage.createdAt };
636
+ }
637
+ }
638
+
639
+ return this._messageRepo.find({
640
+ where,
641
+ orderBy: { createdAt: 'desc' },
642
+ limit: opts.limit ?? 50,
643
+ });
644
+ }
645
+
646
+ // Direct message operations
647
+ async sendDirectMessage(opts: { senderId: string; receiverId: string; content: string }) {
648
+ return this._messageRepo.createDirectMessage(opts);
649
+ }
650
+
651
+ async getDirectMessages(opts: { userId1: string; userId2: string; limit?: number; before?: string }) {
652
+ return this._messageRepo.findDirectMessages({
653
+ userId1: opts.userId1,
654
+ userId2: opts.userId2,
655
+ limit: opts.limit,
656
+ before: opts.before,
657
+ });
658
+ }
659
+
660
+ async getConversations(opts: { userId: string }) {
661
+ return this._messageRepo.findConversations({ userId: opts.userId });
662
+ }
663
+
664
+ // Presence operations
665
+ async setOnline(opts: { userId: string }) {
666
+ await this._userRepo.updateById(opts.userId, {
667
+ isOnline: true,
668
+ lastSeenAt: new Date(),
669
+ });
670
+ }
671
+
672
+ async setOffline(opts: { userId: string }) {
673
+ await this._userRepo.updateById(opts.userId, {
674
+ isOnline: false,
675
+ lastSeenAt: new Date(),
676
+ });
677
+ }
678
+
679
+ async getOnlineUsers(opts: { roomId: string }) {
680
+ const members = await this._roomRepo.getMembers({ roomId: opts.roomId });
681
+ return members.filter(m => m.user.isOnline);
682
+ }
683
+ }
684
+ ```
685
+
686
+ ## 7. Socket.IO Handler
687
+
688
+ ```typescript
689
+ // src/socket/chat.handler.ts
690
+ import { Server, Socket } from 'socket.io';
691
+ import { ChatService } from '../services/chat.service';
692
+ import { RedisHelper, LoggerFactory } from '@venizia/ignis-helpers';
693
+ import {
694
+ ClientToServerEvents,
695
+ ServerToClientEvents,
696
+ InterServerEvents,
697
+ SocketData,
698
+ } from '../types/socket.types';
699
+
700
+ type ChatSocket = Socket<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
701
+ type ChatServer = Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
702
+
703
+ export class ChatSocketHandler {
704
+ private _logger = LoggerFactory.getLogger(['ChatSocketHandler']);
705
+ private _typingTimeouts: Map<string, NodeJS.Timeout> = new Map();
706
+
707
+ constructor(
708
+ private _io: ChatServer,
709
+ private _chatService: ChatService,
710
+ ) {}
711
+
712
+ setupHandlers(socket: ChatSocket) {
713
+ const userId = socket.data.userId;
714
+ const username = socket.data.username;
715
+
716
+ this._logger.info('User connected', { userId, username, socketId: socket.id });
717
+
718
+ // Set user online
719
+ this._chatService.setOnline({ userId });
720
+ this.broadcastPresence({ userId, status: 'online' });
721
+
722
+ // Room handlers
723
+ socket.on('room:join', async (roomId) => {
724
+ try {
725
+ await this._chatService.joinRoom({ roomId, userId });
726
+ socket.join(`room:${roomId}`);
727
+
728
+ // Notify room members
729
+ socket.to(`room:${roomId}`).emit('room:user-joined', {
730
+ roomId,
731
+ user: { id: userId, username },
732
+ });
733
+
734
+ this._logger.info('User joined room', { userId, roomId });
735
+ } catch (error) {
736
+ socket.emit('error', { code: 'JOIN_FAILED', message: error.message });
737
+ }
738
+ });
739
+
740
+ socket.on('room:leave', async (roomId) => {
741
+ socket.leave(`room:${roomId}`);
742
+ socket.to(`room:${roomId}`).emit('room:user-left', { roomId, userId });
743
+ this._logger.info('User left room', { userId, roomId });
744
+ });
745
+
746
+ // Message handlers
747
+ socket.on('message:send', async (data) => {
748
+ try {
749
+ const message = await this._chatService.sendMessage({
750
+ roomId: data.roomId,
751
+ senderId: userId,
752
+ content: data.content,
753
+ type: data.type,
754
+ });
755
+
756
+ // Broadcast to room
757
+ this._io.to(`room:${data.roomId}`).emit('message:new', message);
758
+
759
+ // Clear typing indicator
760
+ this.clearTyping({ socket, roomId: data.roomId });
761
+ } catch (error) {
762
+ socket.emit('error', { code: 'SEND_FAILED', message: error.message });
763
+ }
764
+ });
765
+
766
+ socket.on('message:edit', async (data) => {
767
+ try {
768
+ const message = await this._chatService.editMessage({
769
+ messageId: data.messageId,
770
+ userId,
771
+ content: data.content,
772
+ });
773
+ this._io.to(`room:${message.roomId}`).emit('message:edited', message);
774
+ } catch (error) {
775
+ socket.emit('error', { code: 'EDIT_FAILED', message: error.message });
776
+ }
777
+ });
778
+
779
+ socket.on('message:delete', async (data) => {
780
+ try {
781
+ const message = await this._chatService.deleteMessage({ messageId: data.messageId, userId });
782
+ this._io.to(`room:${message.roomId}`).emit('message:deleted', {
783
+ messageId: data.messageId,
784
+ roomId: message.roomId,
785
+ });
786
+ } catch (error) {
787
+ socket.emit('error', { code: 'DELETE_FAILED', message: error.message });
788
+ }
789
+ });
790
+
791
+ // Direct message handlers
792
+ socket.on('dm:send', async (data) => {
793
+ try {
794
+ const message = await this._chatService.sendDirectMessage({
795
+ senderId: userId,
796
+ receiverId: data.receiverId,
797
+ content: data.content,
798
+ });
799
+
800
+ // Send to receiver (if online)
801
+ this._io.to(`user:${data.receiverId}`).emit('dm:new', message);
802
+
803
+ // Send back to sender
804
+ socket.emit('dm:new', message);
805
+ } catch (error) {
806
+ socket.emit('error', { code: 'DM_FAILED', message: error.message });
807
+ }
808
+ });
809
+
810
+ // Typing handlers
811
+ socket.on('typing:start', (roomId) => {
812
+ this.handleTyping({ socket, roomId, isTyping: true });
813
+ });
814
+
815
+ socket.on('typing:stop', (roomId) => {
816
+ this.handleTyping({ socket, roomId, isTyping: false });
817
+ });
818
+
819
+ // Presence handlers
820
+ socket.on('presence:update', (status) => {
821
+ this.broadcastPresence({ userId, status });
822
+ });
823
+
824
+ // Disconnect
825
+ socket.on('disconnect', () => {
826
+ this._logger.info('User disconnected', { userId, socketId: socket.id });
827
+ this._chatService.setOffline({ userId });
828
+ this.broadcastPresence({ userId, status: 'offline' });
829
+ });
830
+
831
+ // Join user's personal room for DMs
832
+ socket.join(`user:${userId}`);
833
+ }
834
+
835
+ private handleTyping(opts: { socket: ChatSocket; roomId: string; isTyping: boolean }) {
836
+ const userId = opts.socket.data.userId;
837
+ const key = `${opts.roomId}:${userId}`;
838
+
839
+ // Clear existing timeout
840
+ const existingTimeout = this._typingTimeouts.get(key);
841
+ if (existingTimeout) {
842
+ clearTimeout(existingTimeout);
843
+ }
844
+
845
+ // Broadcast typing status
846
+ opts.socket.to(`room:${opts.roomId}`).emit('typing:update', {
847
+ roomId: opts.roomId,
848
+ userId,
849
+ isTyping: opts.isTyping,
850
+ });
851
+
852
+ if (opts.isTyping) {
853
+ // Auto-stop typing after 3 seconds
854
+ const timeout = setTimeout(() => {
855
+ opts.socket.to(`room:${opts.roomId}`).emit('typing:update', {
856
+ roomId: opts.roomId,
857
+ userId,
858
+ isTyping: false,
859
+ });
860
+ this._typingTimeouts.delete(key);
861
+ }, 3000);
862
+
863
+ this._typingTimeouts.set(key, timeout);
864
+ }
865
+ }
866
+
867
+ private clearTyping(opts: { socket: ChatSocket; roomId: string }) {
868
+ const userId = opts.socket.data.userId;
869
+ const key = `${opts.roomId}:${userId}`;
870
+
871
+ const timeout = this._typingTimeouts.get(key);
872
+ if (timeout) {
873
+ clearTimeout(timeout);
874
+ this._typingTimeouts.delete(key);
875
+ }
876
+
877
+ opts.socket.to(`room:${opts.roomId}`).emit('typing:update', {
878
+ roomId: opts.roomId,
879
+ userId,
880
+ isTyping: false,
881
+ });
882
+ }
883
+
884
+ private broadcastPresence(opts: { userId: string; status: string }) {
885
+ this._io.emit('presence:changed', {
886
+ userId: opts.userId,
887
+ status: opts.status,
888
+ lastSeenAt: new Date(),
889
+ });
890
+ }
891
+ }
892
+ ```
893
+
894
+ ## 8. Application Setup
895
+
896
+ ```typescript
897
+ // src/application.ts
898
+ import { BaseApplication, IApplicationInfo, SocketIOComponent } from '@venizia/ignis';
899
+ import { EnvHelper } from '@venizia/ignis-helpers';
900
+ import { Server } from 'socket.io';
901
+ import { ChatSocketHandler } from './socket/chat.handler';
902
+ import { ChatService } from './services/chat.service';
903
+ import { verifyToken } from './middleware/auth';
904
+
905
+ export class ChatApp extends BaseApplication {
906
+ private _io: Server;
907
+ private _chatHandler: ChatSocketHandler;
908
+
909
+ getAppInfo(): IApplicationInfo {
910
+ return { name: 'chat-api', version: '1.0.0' };
911
+ }
912
+
913
+ staticConfigure() {}
914
+
915
+ preConfigure() {
916
+ // Register services and repositories
917
+ this.service(ChatService);
918
+ // ... other bindings
919
+
920
+ // Add Socket.IO component
921
+ this.component(SocketIOComponent);
922
+ }
923
+
924
+ postConfigure() {
925
+ this.setupSocketIO();
926
+ }
927
+
928
+ setupMiddlewares() {}
929
+
930
+ private setupSocketIO() {
931
+ const httpServer = this.getHttpServer();
932
+
933
+ this._io = new Server(httpServer, {
934
+ cors: {
935
+ origin: EnvHelper.get('APP_ENV_CORS_ORIGIN') ?? '*',
936
+ methods: ['GET', 'POST'],
937
+ },
938
+ });
939
+
940
+ // Authentication middleware
941
+ this._io.use(async (socket, next) => {
942
+ try {
943
+ const token = socket.handshake.auth.token;
944
+ if (!token) {
945
+ return next(new Error('Authentication required'));
946
+ }
947
+
948
+ const user = await verifyToken(token);
949
+ socket.data.userId = user.id;
950
+ socket.data.username = user.username;
951
+ next();
952
+ } catch (error) {
953
+ next(new Error('Invalid token'));
954
+ }
955
+ });
956
+
957
+ // Get chat service from container
958
+ const chatService = this.container.get<ChatService>('services.ChatService');
959
+ this._chatHandler = new ChatSocketHandler(this._io, chatService);
960
+
961
+ // Handle connections
962
+ this._io.on('connection', (socket) => {
963
+ this._chatHandler.setupHandlers(socket);
964
+ });
965
+ }
966
+ }
967
+ ```
968
+
969
+ ## 9. REST API for Chat History
970
+
971
+ ```typescript
972
+ // src/controllers/chat.controller.ts
973
+ import { z } from '@hono/zod-openapi';
974
+ import {
975
+ BaseController,
976
+ controller,
977
+ get,
978
+ post,
979
+ inject,
980
+ HTTP,
981
+ jsonContent,
982
+ TRouteContext,
983
+ } from '@venizia/ignis';
984
+ import { ChatService } from '../services/chat.service';
985
+
986
+ const ChatRoutes = {
987
+ GET_ROOMS: {
988
+ method: HTTP.Methods.GET,
989
+ path: '/rooms',
990
+ responses: {
991
+ [HTTP.ResultCodes.RS_2.Ok]: jsonContent({
992
+ description: 'User rooms',
993
+ schema: z.array(z.any()),
994
+ }),
995
+ },
996
+ },
997
+ CREATE_ROOM: {
998
+ method: HTTP.Methods.POST,
999
+ path: '/rooms',
1000
+ request: {
1001
+ body: jsonContent({
1002
+ schema: z.object({
1003
+ name: z.string().min(1).max(100),
1004
+ description: z.string().optional(),
1005
+ isPrivate: z.boolean().default(false),
1006
+ }),
1007
+ }),
1008
+ },
1009
+ responses: {
1010
+ [HTTP.ResultCodes.RS_2.Created]: jsonContent({
1011
+ description: 'Created room',
1012
+ schema: z.any(),
1013
+ }),
1014
+ },
1015
+ },
1016
+ GET_MESSAGES: {
1017
+ method: HTTP.Methods.GET,
1018
+ path: '/rooms/:roomId/messages',
1019
+ request: {
1020
+ params: z.object({ roomId: z.string().uuid() }),
1021
+ query: z.object({
1022
+ limit: z.string().optional(),
1023
+ before: z.string().optional(),
1024
+ }),
1025
+ },
1026
+ responses: {
1027
+ [HTTP.ResultCodes.RS_2.Ok]: jsonContent({
1028
+ description: 'Room messages',
1029
+ schema: z.array(z.any()),
1030
+ }),
1031
+ },
1032
+ },
1033
+ GET_CONVERSATIONS: {
1034
+ method: HTTP.Methods.GET,
1035
+ path: '/conversations',
1036
+ responses: {
1037
+ [HTTP.ResultCodes.RS_2.Ok]: jsonContent({
1038
+ description: 'User conversations',
1039
+ schema: z.array(z.any()),
1040
+ }),
1041
+ },
1042
+ },
1043
+ GET_DIRECT_MESSAGES: {
1044
+ method: HTTP.Methods.GET,
1045
+ path: '/dm/:userId',
1046
+ request: {
1047
+ params: z.object({ userId: z.string().uuid() }),
1048
+ query: z.object({
1049
+ limit: z.string().optional(),
1050
+ before: z.string().optional(),
1051
+ }),
1052
+ },
1053
+ responses: {
1054
+ [HTTP.ResultCodes.RS_2.Ok]: jsonContent({
1055
+ description: 'Direct messages',
1056
+ schema: z.array(z.any()),
1057
+ }),
1058
+ },
1059
+ },
1060
+ } as const;
1061
+
1062
+ type ChatRoutes = typeof ChatRoutes;
1063
+
1064
+ @controller({ path: '/chat' })
1065
+ export class ChatController extends BaseController {
1066
+ constructor(
1067
+ @inject('services.ChatService')
1068
+ private _chatService: ChatService,
1069
+ ) {
1070
+ super({ scope: ChatController.name, path: '/chat' });
1071
+ }
1072
+
1073
+ override binding() {}
1074
+
1075
+ @get({ configs: ChatRoutes.GET_ROOMS })
1076
+ async getRooms(c: TRouteContext<ChatRoutes['GET_ROOMS']>) {
1077
+ const userId = c.get('userId');
1078
+ const rooms = await this._chatService.getUserRooms({ userId });
1079
+ return c.json(rooms);
1080
+ }
1081
+
1082
+ @post({ configs: ChatRoutes.CREATE_ROOM })
1083
+ async createRoom(c: TRouteContext<ChatRoutes['CREATE_ROOM']>) {
1084
+ const userId = c.get('userId');
1085
+ const data = c.req.valid('json');
1086
+
1087
+ const room = await this._chatService.createRoom({
1088
+ ...data,
1089
+ createdBy: userId,
1090
+ });
1091
+
1092
+ return c.json(room, HTTP.ResultCodes.RS_2.Created);
1093
+ }
1094
+
1095
+ @get({ configs: ChatRoutes.GET_MESSAGES })
1096
+ async getMessages(c: TRouteContext<ChatRoutes['GET_MESSAGES']>) {
1097
+ const { roomId } = c.req.valid('param');
1098
+ const { limit, before } = c.req.valid('query');
1099
+
1100
+ const messages = await this._chatService.getMessages({
1101
+ roomId,
1102
+ limit: limit ? parseInt(limit) : undefined,
1103
+ before,
1104
+ });
1105
+
1106
+ return c.json(messages);
1107
+ }
1108
+
1109
+ @get({ configs: ChatRoutes.GET_CONVERSATIONS })
1110
+ async getConversations(c: TRouteContext<ChatRoutes['GET_CONVERSATIONS']>) {
1111
+ const userId = c.get('userId');
1112
+ const conversations = await this._chatService.getConversations({ userId });
1113
+ return c.json(conversations);
1114
+ }
1115
+
1116
+ @get({ configs: ChatRoutes.GET_DIRECT_MESSAGES })
1117
+ async getDirectMessages(c: TRouteContext<ChatRoutes['GET_DIRECT_MESSAGES']>) {
1118
+ const currentUserId = c.get('userId');
1119
+ const { userId: otherUserId } = c.req.valid('param');
1120
+ const { limit, before } = c.req.valid('query');
1121
+
1122
+ const messages = await this._chatService.getDirectMessages({
1123
+ userId1: currentUserId,
1124
+ userId2: otherUserId,
1125
+ limit: limit ? parseInt(limit) : undefined,
1126
+ before,
1127
+ });
1128
+
1129
+ return c.json(messages);
1130
+ }
1131
+ }
1132
+ ```
1133
+
1134
+ ## 10. Client Usage
1135
+
1136
+ ### JavaScript Client Example
1137
+
1138
+ ```typescript
1139
+ // client/chat-client.ts
1140
+ import { io, Socket } from 'socket.io-client';
1141
+
1142
+ interface Message {
1143
+ id: string;
1144
+ content: string;
1145
+ senderId: string;
1146
+ sender: { username: string; avatar?: string };
1147
+ createdAt: string;
1148
+ }
1149
+
1150
+ class ChatClient {
1151
+ private _socket: Socket;
1152
+
1153
+ constructor(opts: { serverUrl: string; token: string }) {
1154
+ this._socket = io(opts.serverUrl, {
1155
+ auth: { token: opts.token },
1156
+ });
1157
+
1158
+ this.setupListeners();
1159
+ }
1160
+
1161
+ private setupListeners() {
1162
+ this._socket.on('connect', () => {
1163
+ console.log('Connected to chat server');
1164
+ });
1165
+
1166
+ this._socket.on('message:new', (message: Message) => {
1167
+ console.log('New message:', message);
1168
+ // Update UI
1169
+ });
1170
+
1171
+ this._socket.on('typing:update', (data) => {
1172
+ console.log(`User ${data.userId} is ${data.isTyping ? 'typing' : 'stopped typing'}`);
1173
+ // Show/hide typing indicator
1174
+ });
1175
+
1176
+ this._socket.on('presence:changed', (data) => {
1177
+ console.log(`User ${data.userId} is now ${data.status}`);
1178
+ // Update online status
1179
+ });
1180
+
1181
+ this._socket.on('error', (error) => {
1182
+ console.error('Socket error:', error);
1183
+ });
1184
+ }
1185
+
1186
+ joinRoom(opts: { roomId: string }) {
1187
+ this._socket.emit('room:join', opts.roomId);
1188
+ }
1189
+
1190
+ leaveRoom(opts: { roomId: string }) {
1191
+ this._socket.emit('room:leave', opts.roomId);
1192
+ }
1193
+
1194
+ sendMessage(opts: { roomId: string; content: string }) {
1195
+ this._socket.emit('message:send', { roomId: opts.roomId, content: opts.content });
1196
+ }
1197
+
1198
+ sendDirectMessage(opts: { receiverId: string; content: string }) {
1199
+ this._socket.emit('dm:send', { receiverId: opts.receiverId, content: opts.content });
1200
+ }
1201
+
1202
+ startTyping(opts: { roomId: string }) {
1203
+ this._socket.emit('typing:start', opts.roomId);
1204
+ }
1205
+
1206
+ stopTyping(opts: { roomId: string }) {
1207
+ this._socket.emit('typing:stop', opts.roomId);
1208
+ }
1209
+
1210
+ disconnect() {
1211
+ this._socket.disconnect();
1212
+ }
1213
+ }
1214
+
1215
+ // Usage
1216
+ const chat = new ChatClient({ serverUrl: 'http://localhost:3000', token: 'your-jwt-token' });
1217
+
1218
+ chat.joinRoom({ roomId: 'room-uuid' });
1219
+ chat.sendMessage({ roomId: 'room-uuid', content: 'Hello everyone!' });
1220
+ ```
1221
+
1222
+ ## 11. Scaling with Redis
1223
+
1224
+ For production, use Redis for Socket.IO adapter:
1225
+
1226
+ ```typescript
1227
+ // src/application.ts
1228
+ import { createAdapter } from '@socket.io/redis-adapter';
1229
+ import { createClient } from 'redis';
1230
+ import { EnvHelper } from '@venizia/ignis-helpers';
1231
+
1232
+ private async setupSocketIO() {
1233
+ const pubClient = createClient({ url: EnvHelper.get('APP_ENV_REDIS_URL') });
1234
+ const subClient = pubClient.duplicate();
1235
+
1236
+ await Promise.all([pubClient.connect(), subClient.connect()]);
1237
+
1238
+ this._io.adapter(createAdapter(pubClient, subClient));
1239
+
1240
+ // ... rest of setup
1241
+ }
1242
+ ```
1243
+
1244
+ ## Summary
1245
+
1246
+ | Feature | Implementation |
1247
+ |---------|---------------|
1248
+ | Rooms | Socket.IO rooms + database |
1249
+ | Direct Messages | Personal rooms + database |
1250
+ | Typing Indicators | Socket events with auto-timeout |
1251
+ | Presence | Online status tracking |
1252
+ | History | REST API with pagination |
1253
+ | Scaling | Redis adapter for pub/sub |
1254
+
1255
+ ## Next Steps
1256
+
1257
+ - Add file/image sharing with [Storage Helper](../../references/helpers/storage.md)
1258
+ - Add push notifications
1259
+ - Implement read receipts
1260
+ - Add message reactions
1261
+ - Deploy with [Deployment Guide](../../best-practices/deployment-strategies.md)