create-nexwire 4.2.4 → 4.3.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 (113) hide show
  1. package/README.md +20 -8
  2. package/express/.env +1 -1
  3. package/express/.vscode/extensions.json +6 -0
  4. package/express/.vscode/settings.json +37 -26
  5. package/express/biome.json +99 -66
  6. package/express/package.json +31 -29
  7. package/express/src/config/database.ts +48 -29
  8. package/express/src/config/index.ts +83 -83
  9. package/express/src/config/openapi.ts +6 -24
  10. package/express/src/config/queue.ts +30 -30
  11. package/express/src/config/validate.ts +50 -40
  12. package/express/src/database/migrations/sqlite/0000_init.sql +47 -47
  13. package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  14. package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
  15. package/express/src/database/schema.ts +2 -2
  16. package/express/src/framework/cache/cache.ts +40 -8
  17. package/express/src/framework/circuit-breaker/cb.ts +88 -60
  18. package/express/src/framework/database/connection.ts +52 -27
  19. package/express/src/framework/events/dispatcher.ts +61 -49
  20. package/express/src/framework/facade.ts +46 -3
  21. package/express/src/framework/http/app.ts +28 -22
  22. package/express/src/framework/http/logger.ts +27 -16
  23. package/express/src/framework/http/openapi.ts +9 -11
  24. package/express/src/framework/modules/routes.ts +26 -21
  25. package/express/src/framework/notification/channels/broadcast.ts +15 -0
  26. package/express/src/framework/notification/channels/mail.ts +40 -0
  27. package/express/src/framework/notification/contracts.ts +29 -0
  28. package/express/src/framework/notification/index.ts +19 -94
  29. package/express/src/framework/notification/registry.ts +15 -0
  30. package/express/src/framework/providers/adapter.ts +27 -17
  31. package/express/src/framework/queue/queue.ts +43 -44
  32. package/express/src/framework/realtime/broadcast.ts +29 -5
  33. package/express/src/framework/realtime/socket.ts +149 -119
  34. package/express/src/framework/redis/client.ts +148 -151
  35. package/express/src/framework/runtime/server-adapter.ts +39 -34
  36. package/express/src/framework/scheduler/lock.ts +128 -116
  37. package/express/src/framework/server.ts +33 -49
  38. package/express/src/framework/session/session.ts +69 -14
  39. package/express/src/framework/storage/storage.ts +744 -785
  40. package/express/src/framework/support/mail.ts +15 -7
  41. package/express/src/providers/notification.ts +4 -0
  42. package/express/src/resources/src/components/Checkbox.vue +32 -29
  43. package/express/src/resources/src/components/Input.vue +243 -223
  44. package/express/src/resources/src/components/InputGroup.vue +147 -145
  45. package/express/src/resources/src/components/InputPasswordToggle.vue +229 -213
  46. package/express/src/resources/src/components/Modal.vue +71 -55
  47. package/express/src/resources/src/components/Refresh.vue +19 -0
  48. package/express/src/resources/src/helpers/assets.ts +51 -0
  49. package/express/src/resources/src/layouts/Layout/Header.vue +124 -105
  50. package/express/src/resources/src/main.ts +3 -1
  51. package/express/src/resources/src/plugins/gum.ts +1 -1
  52. package/express/src/resources/src/plugins/pulse.ts +157 -147
  53. package/express/src/resources/src/stores/auth.ts +184 -164
  54. package/express/src/resources/vite.config.ts +54 -36
  55. package/express/src/storage/database/nexwire.sqlite +0 -0
  56. package/express/src/storage/logs/app.log +0 -0
  57. package/express/src/storage/logs/fatal.log +0 -0
  58. package/express/src/types/global.d.ts +32 -26
  59. package/hono/.env +1 -1
  60. package/hono/.vscode/extensions.json +6 -0
  61. package/hono/.vscode/settings.json +37 -26
  62. package/hono/biome.json +25 -2
  63. package/hono/package.json +25 -23
  64. package/hono/src/config/database.ts +48 -29
  65. package/hono/src/config/index.ts +83 -83
  66. package/hono/src/config/openapi.ts +6 -24
  67. package/hono/src/config/queue.ts +30 -30
  68. package/hono/src/config/validate.ts +50 -40
  69. package/hono/src/database/migrations/sqlite/0000_init.sql +47 -47
  70. package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  71. package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
  72. package/hono/src/database/schema.ts +2 -2
  73. package/hono/src/framework/cache/cache.ts +40 -8
  74. package/hono/src/framework/circuit-breaker/cb.ts +88 -60
  75. package/hono/src/framework/database/connection.ts +52 -27
  76. package/hono/src/framework/events/dispatcher.ts +61 -49
  77. package/hono/src/framework/facade.ts +38 -1
  78. package/hono/src/framework/http/openapi.ts +7 -9
  79. package/hono/src/framework/notification/channels/broadcast.ts +15 -0
  80. package/hono/src/framework/notification/channels/mail.ts +40 -0
  81. package/hono/src/framework/notification/contracts.ts +29 -0
  82. package/hono/src/framework/notification/index.ts +19 -94
  83. package/hono/src/framework/notification/registry.ts +15 -0
  84. package/hono/src/framework/providers/adapter.ts +27 -17
  85. package/hono/src/framework/queue/queue.ts +43 -44
  86. package/hono/src/framework/realtime/broadcast.ts +29 -5
  87. package/hono/src/framework/realtime/socket.ts +217 -196
  88. package/hono/src/framework/redis/client.ts +148 -151
  89. package/hono/src/framework/scheduler/lock.ts +128 -116
  90. package/hono/src/framework/server.ts +27 -29
  91. package/hono/src/framework/session/session.ts +62 -14
  92. package/hono/src/framework/storage/storage.ts +744 -785
  93. package/hono/src/framework/support/mail.ts +56 -48
  94. package/hono/src/providers/notification.ts +4 -0
  95. package/hono/src/resources/src/components/Checkbox.vue +32 -29
  96. package/hono/src/resources/src/components/Input.vue +243 -234
  97. package/hono/src/resources/src/components/InputGroup.vue +147 -149
  98. package/hono/src/resources/src/components/InputPasswordToggle.vue +229 -222
  99. package/hono/src/resources/src/components/Modal.vue +71 -55
  100. package/hono/src/resources/src/components/Refresh.vue +19 -0
  101. package/hono/src/resources/src/helpers/assets.ts +51 -0
  102. package/hono/src/resources/src/layouts/Layout/Header.vue +124 -105
  103. package/hono/src/resources/src/main.ts +3 -1
  104. package/hono/src/resources/src/plugins/gum.ts +1 -1
  105. package/hono/src/resources/src/plugins/pulse.ts +157 -147
  106. package/hono/src/resources/src/stores/auth.ts +184 -164
  107. package/hono/src/resources/vite.config.ts +54 -36
  108. package/hono/src/storage/database/nexwire.sqlite +0 -0
  109. package/hono/src/storage/logs/app.log +0 -0
  110. package/hono/src/storage/logs/fatal.log +0 -0
  111. package/hono/src/types/global.d.ts +32 -26
  112. package/package.json +2 -2
  113. package/src/index.js +139 -14
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { databaseConfig } from "@/config/index.js";
3
+ import { databaseConfig, detectDialectFrom } from "@/config/index.js";
4
4
  import { loadAppSchema } from "@/framework/database/app-schema.js";
5
5
 
6
6
  export type Dialect = "sqlite" | "mysql" | "postgresql";
@@ -20,10 +20,7 @@ async function optionalImport<T = any>(name: string): Promise<T> {
20
20
  * How: Checks DATABASE_URL prefix and maps to sqlite/mysql/postgresql.
21
21
  */
22
22
  export function detectDialect(): Dialect {
23
- const url = databaseConfig.url.toLowerCase();
24
- if (url.startsWith("mysql")) return "mysql";
25
- if (url.startsWith("postgres")) return "postgresql";
26
- return "sqlite";
23
+ return detectDialectFrom(databaseConfig.url);
27
24
  }
28
25
 
29
26
  /**
@@ -36,9 +33,9 @@ export async function initDatabase() {
36
33
  if (databaseInstance) return databaseInstance;
37
34
 
38
35
  dialect = detectDialect();
39
-
40
36
  const schema = await loadAppSchema();
41
37
 
38
+ // --- SQLite (libsql) ---
42
39
  if (dialect === "sqlite") {
43
40
  let drizzleSqlite: any;
44
41
  let createClient: any;
@@ -51,13 +48,24 @@ export async function initDatabase() {
51
48
  throw new Error("Missing sqlite dependencies. Install with: bun add drizzle-orm @libsql/client");
52
49
  }
53
50
 
54
- const file = path.resolve(process.cwd(), databaseConfig.url.replace(/^sqlite:/, ""));
55
- await fs.mkdir(path.dirname(file), { recursive: true });
56
- pool = createClient({ url: `file:${file.replace(/\\/g, "/")}` });
51
+ // databaseConfig.url is guaranteed to be "file:..."
52
+ const url = databaseConfig.url;
53
+
54
+ // Ensure the directory exists (skip for :memory: or absolute URLs)
55
+ if (!url.includes(":memory:")) {
56
+ const rawPath = url.replace(/^file:/i, "");
57
+ if (!rawPath.startsWith("//")) {
58
+ const file = path.resolve(process.cwd(), rawPath);
59
+ await fs.mkdir(path.dirname(file), { recursive: true });
60
+ }
61
+ }
62
+
63
+ pool = createClient({ url });
57
64
  databaseInstance = drizzleSqlite(pool, { schema });
58
65
  return databaseInstance;
59
66
  }
60
67
 
68
+ // --- MySQL ---
61
69
  if (dialect === "mysql") {
62
70
  let drizzleMysql: any;
63
71
  let mysql: any;
@@ -67,11 +75,19 @@ export async function initDatabase() {
67
75
  throw new Error("Missing mysql dependencies. Install with: bun add drizzle-orm mysql2");
68
76
  }
69
77
 
70
- pool = mysql.createPool({ uri: databaseConfig.url, connectionLimit: 10, enableKeepAlive: true });
78
+ pool = mysql.createPool({
79
+ uri: databaseConfig.url,
80
+ connectionLimit: databaseConfig.pool.max,
81
+ connectTimeout: databaseConfig.pool.connectTimeout,
82
+ enableKeepAlive: databaseConfig.pool.keepAlive,
83
+ ...(databaseConfig.ssl ? { ssl: {} } : {})
84
+ });
85
+
71
86
  databaseInstance = drizzleMysql(pool, { schema, mode: "default" });
72
87
  return databaseInstance;
73
88
  }
74
89
 
90
+ // --- PostgreSQL ---
75
91
  let drizzlePg: any;
76
92
  let postgres: any;
77
93
  try {
@@ -83,7 +99,14 @@ export async function initDatabase() {
83
99
  throw new Error("Missing postgres dependencies. Install with: bun add drizzle-orm postgres");
84
100
  }
85
101
 
86
- pool = postgres(databaseConfig.url);
102
+ pool = postgres(databaseConfig.url, {
103
+ max: databaseConfig.pool.max,
104
+ idle_timeout: Math.floor(databaseConfig.pool.idleTimeout / 1000),
105
+ connect_timeout: Math.floor(databaseConfig.pool.connectTimeout / 1000),
106
+ prepare: databaseConfig.prepare,
107
+ ...(databaseConfig.ssl ? { ssl: "require" as const } : {})
108
+ });
109
+
87
110
  databaseInstance = drizzlePg(pool, { schema });
88
111
  return databaseInstance;
89
112
  }
@@ -121,23 +144,20 @@ export async function closeDatabase() {
121
144
 
122
145
  const activeDialect = databaseDialect();
123
146
 
124
- if (activeDialect === "sqlite") {
125
- pool.close?.();
126
- pool = undefined;
127
- databaseInstance = undefined;
128
- return;
129
- }
130
-
131
- if (activeDialect === "mysql") {
132
- await pool.end();
147
+ try {
148
+ if (activeDialect === "sqlite") {
149
+ await pool.close?.();
150
+ } else if (activeDialect === "mysql") {
151
+ await pool.end();
152
+ } else {
153
+ await pool.end();
154
+ }
155
+ } catch (error) {
156
+ console.error("Error closing database pool:", error);
157
+ } finally {
133
158
  pool = undefined;
134
159
  databaseInstance = undefined;
135
- return;
136
160
  }
137
-
138
- await pool.end();
139
- pool = undefined;
140
- databaseInstance = undefined;
141
161
  }
142
162
 
143
163
  /**
@@ -146,7 +166,7 @@ export async function closeDatabase() {
146
166
  * Where: Locking, schema, and shutdown helpers.
147
167
  * How: Uses initialized dialect or falls back to detection.
148
168
  */
149
- export function databaseDialect() {
169
+ export function databaseDialect(): Dialect {
150
170
  return dialect || detectDialect();
151
171
  }
152
172
 
@@ -185,12 +205,17 @@ export const db = new Proxy(
185
205
  {
186
206
  get(_target, property) {
187
207
  const instance = database();
208
+
188
209
  if (property === "execute") {
189
210
  return async (query: unknown, params?: unknown) => {
190
- const raw = instance.execute ? await instance.execute(query, params) : await instance.all(query);
211
+ if (!instance.execute) {
212
+ throw new Error("The current driver does not support raw execute.");
213
+ }
214
+ const raw = await instance.execute(query, params);
191
215
  return normalizeExecuteResult(raw);
192
216
  };
193
217
  }
218
+
194
219
  const value = instance[property as keyof typeof instance];
195
220
  return typeof value === "function" ? value.bind(instance) : value;
196
221
  }
@@ -1,49 +1,61 @@
1
- import { queueJob } from "@/framework/queue/queue.js";
2
- import { type BroadcastOptions, broadcast } from "@/framework/realtime/broadcast.js";
3
-
4
- type CommandHandler = (payload: any) => Promise<any>;
5
-
6
- const commandHandlers = new Map<string, CommandHandler>();
7
-
8
- /**
9
- * Why: Registers in-memory command handler for synchronous dispatch.
10
- * When: Module startup files register command handlers.
11
- * Where: Job/command style module code.
12
- * How: Stores handler in a process-level map by command name.
13
- */
14
- export function command(name: string, handler: CommandHandler) {
15
- commandHandlers.set(name, handler);
16
- }
17
-
18
- /**
19
- * Why: Executes command immediately or queues it for async processing.
20
- * When: Application logic needs command pattern behavior.
21
- * Where: Controllers, jobs, and service layers.
22
- * How: Runs local handler unless async requested; otherwise enqueues.
23
- */
24
- export async function dispatchCommand(name: string, payload: any, options: { async?: boolean; queue?: string } = {}) {
25
- if (!options.async) {
26
- const handler = commandHandlers.get(name);
27
- if (handler) return await handler(payload);
28
- }
29
-
30
- return await queueJob(name, payload, { queue: options.queue || "default" });
31
- }
32
-
33
- /**
34
- * Why: Dispatches domain events with optional queueing and websocket fan-out.
35
- * When: Domain actions need side effects or realtime notifications.
36
- * Where: Controllers/jobs across modules.
37
- * How: Broadcasts via Socket.IO and optionally enqueues job.
38
- */
39
- export async function dispatchEvent(name: string, payload: any, options: { queue?: boolean | string; broadcast?: BroadcastOptions } = {}) {
40
- if (options.broadcast) {
41
- broadcast(name, payload, options.broadcast);
42
- }
43
-
44
- if (options.queue) {
45
- return await queueJob(name, payload, {
46
- queue: typeof options.queue === "string" ? options.queue : "default"
47
- });
48
- }
49
- }
1
+ import { queueJob } from "@/framework/queue/queue.js";
2
+ import { type BroadcastOptions, broadcast } from "@/framework/realtime/broadcast.js";
3
+ import { logger } from "@/framework/support/logger.js";
4
+
5
+ type CommandHandler = (payload: any) => Promise<any>;
6
+
7
+ const commandHandlers = new Map<string, CommandHandler>();
8
+
9
+ /**
10
+ * Why: Registers in-memory command handler for synchronous dispatch.
11
+ * When: Module startup files register command handlers.
12
+ * Where: Job/command style module code.
13
+ * How: Stores handler in a process-level map by command name.
14
+ */
15
+ export function command(name: string, handler: CommandHandler) {
16
+ commandHandlers.set(name, handler);
17
+ }
18
+
19
+ /**
20
+ * Why: Executes command immediately or queues it for async processing.
21
+ * When: Application logic needs command pattern behavior.
22
+ * Where: Controllers, jobs, and service layers.
23
+ * How: Runs local handler unless async requested; otherwise enqueues.
24
+ */
25
+ export async function dispatchCommand(name: string, payload: any, options: { async?: boolean; queue?: string } = {}) {
26
+ if (!options.async) {
27
+ const handler = commandHandlers.get(name);
28
+ if (handler) return await handler(payload);
29
+ }
30
+
31
+ return await queueJob(name, payload, { queue: options.queue || "default" });
32
+ }
33
+
34
+ /**
35
+ * Why: Dispatches domain events with optional queueing and websocket fan-out.
36
+ * When: Domain actions need side effects or realtime notifications.
37
+ * Where: Controllers/jobs across modules.
38
+ * How: Broadcasts via Socket.IO and optionally enqueues job.
39
+ */
40
+ export async function dispatchEvent(name: string, payload: any, options: { queue?: boolean | string; broadcast?: BroadcastOptions } = {}) {
41
+ // 1. Handle Broadcast safely
42
+ if (options.broadcast) {
43
+ try {
44
+ // Await the broadcast so the circuit breaker and redis publish complete safely
45
+ await broadcast(name, payload, options.broadcast);
46
+ } catch (error) {
47
+ logger.error(`Failed to broadcast event: ${name}`, { error });
48
+ }
49
+ }
50
+
51
+ // 2. Handle Queue safely
52
+ if (options.queue) {
53
+ try {
54
+ return await queueJob(name, payload, {
55
+ queue: typeof options.queue === "string" ? options.queue : "default"
56
+ });
57
+ } catch (error) {
58
+ logger.error(`Failed to queue event: ${name}`, { error });
59
+ }
60
+ }
61
+ }
@@ -1,15 +1,47 @@
1
+ /**
2
+ * Why: Central public API surface for framework consumers.
3
+ * When: Modules/middlewares need framework utilities.
4
+ * Where: App-layer imports should target this file.
5
+ * How: Re-exports stable, reusable helpers while hiding setup internals.
6
+ */
7
+
1
8
  export * as lodash from "lodash-es";
9
+ // ==========================================
10
+ // 1. External & Core Utilities
11
+ // ==========================================
2
12
  export { z } from "zod";
13
+ // ==========================================
14
+ // 6. Cache & Session
15
+ // ==========================================
3
16
  export { cache } from "@/framework/cache/cache.js";
17
+ // ==========================================
18
+ // 2. Database
19
+ // ==========================================
4
20
  export { database, db } from "@/framework/database/connection.js";
5
- export type { PaginatedResult } from "@/framework/database/paginate.js";
6
- export { paginate, paginateModel, paginateQuery, paginateTable } from "@/framework/database/paginate.js";
21
+ export { type PaginatedResult, paginate, paginateModel, paginateQuery, paginateTable } from "@/framework/database/paginate.js";
22
+ // ==========================================
23
+ // 4. Events & Queue
24
+ // ==========================================
7
25
  export { command, dispatchCommand, dispatchEvent } from "@/framework/events/dispatcher.js";
8
26
  export { registerOpenApiRoute } from "@/framework/http/openapi.js";
9
27
  export type { RouteConfig } from "@/framework/http/router.js";
28
+ // ==========================================
29
+ // 3. HTTP & Routing
30
+ // ==========================================
10
31
  export { createRoute, createRouter, group, jsonContent } from "@/framework/http/router.js";
11
32
  export { validate } from "@/framework/http/validation.js";
33
+ export { broadcastChannel } from "@/framework/notification/channels/broadcast.js";
34
+ export { mailChannel } from "@/framework/notification/channels/mail.js";
35
+ export type { NotificationChannel, NotificationOptions } from "@/framework/notification/contracts.js";
36
+ // ==========================================
37
+ // 10. Notification Engine (Delivery Channels)
38
+ // ==========================================
12
39
  export { notify } from "@/framework/notification/index.js";
40
+ export { registerChannel } from "@/framework/notification/registry.js";
41
+ // ==========================================
42
+ // 9. Providers (Storage/Auth)
43
+ // ==========================================
44
+ export { resolveAuthProvider, resolveNotificationProvider } from "@/framework/providers/adapter.js";
13
45
  export type {
14
46
  AuthProvider,
15
47
  AuthProviderRefreshToken,
@@ -23,7 +55,6 @@ export type {
23
55
  NotificationProvider,
24
56
  NotificationRecord
25
57
  } from "@/framework/providers/contracts.js";
26
- export { resolveAuthProvider, resolveNotificationProvider } from "@/framework/providers/adapter.js";
27
58
  export { loadProviders } from "@/framework/providers/loader.js";
28
59
  export {
29
60
  clearAuthProvider,
@@ -41,12 +72,24 @@ export {
41
72
  registerProvider
42
73
  } from "@/framework/providers/registry.js";
43
74
  export { queue, queueJob, shouldQueue } from "@/framework/queue/queue.js";
75
+ // ==========================================
76
+ // 5. Realtime
77
+ // ==========================================
44
78
  export { broadcast } from "@/framework/realtime/index.js";
45
79
  export { defineSchedule } from "@/framework/scheduler/scheduler.js";
46
80
  export { session } from "@/framework/session/session.js";
81
+ // ==========================================
82
+ // 7. Storage & Mail
83
+ // ==========================================
47
84
  export { storage } from "@/framework/storage/storage.js";
85
+ // ==========================================
86
+ // 8. Security, Auth & Uploads
87
+ // ==========================================
48
88
  export { cookie } from "@/framework/support/cookie.js";
49
89
  export { jwt } from "@/framework/support/jwt.js";
90
+ // ==========================================
91
+ // 11. Utilities
92
+ // ==========================================
50
93
  export { logger } from "@/framework/support/logger.js";
51
94
  export { mail } from "@/framework/support/mail.js";
52
95
  export { password } from "@/framework/support/password.js";
@@ -30,6 +30,30 @@ export function createHttpApp() {
30
30
  next();
31
31
  });
32
32
 
33
+ if (appConfig.openApiEnabled) {
34
+ configureOpenApi(app);
35
+ registerOpenApiRoute(
36
+ createRoute({
37
+ path: "/health",
38
+ method: "get",
39
+ tags: ["System"],
40
+ responses: {
41
+ 200: jsonContent(z.object({ message: z.string() }), "Application health status")
42
+ }
43
+ })
44
+ );
45
+
46
+ app.get("/health", (_req, res) => {
47
+ res.json({ message: "Application is healthy" });
48
+ });
49
+ }
50
+
51
+ if (loggingConfig.httpRequests) {
52
+ app.use(loggerMiddleware);
53
+ }
54
+
55
+ app.use(rateLimiterMiddleware);
56
+
33
57
  app.get("/ready", (_req, res) => {
34
58
  const checks: Record<string, string> = {};
35
59
  let allOk = true;
@@ -71,30 +95,12 @@ export function createHttpApp() {
71
95
  res.type("text/plain").status(200).send(output);
72
96
  });
73
97
 
74
- if (loggingConfig.httpRequests) {
75
- app.use(loggerMiddleware);
76
- }
77
-
78
- app.use(rateLimiterMiddleware);
79
-
80
- if (appConfig.openApiEnabled) {
81
- configureOpenApi(app);
82
- registerOpenApiRoute(
83
- createRoute({
84
- path: "/health",
85
- method: "get",
86
- tags: ["System"],
87
- responses: {
88
- 200: jsonContent(z.object({ message: z.string() }), "Application health status")
89
- }
90
- })
91
- );
98
+ if (!appConfig.openApiEnabled) {
99
+ app.get("/health", (_req, res) => {
100
+ res.json({ message: "Application is healthy" });
101
+ });
92
102
  }
93
103
 
94
- app.get("/health", (_req, res) => {
95
- res.json({ message: "Application is healthy" });
96
- });
97
-
98
104
  if (!hasUiBuild()) {
99
105
  app.get("/", (_req, res) => {
100
106
  res.json({ name: appConfig.name, ok: true });
@@ -1,12 +1,19 @@
1
1
  import type { ErrorRequestHandler, RequestHandler } from "express";
2
2
  import createError from "http-errors";
3
3
  import { logger } from "@/framework/support/logger.js";
4
+ import { openApiConfig } from "@/config/index.js";
4
5
 
5
6
  function requestIp(req: Parameters<RequestHandler>[0]) {
6
7
  const forwarded = req.headers["x-forwarded-for"];
7
8
  return String(req.headers["cf-connecting-ip"] || forwarded || req.headers["x-real-ip"] || req.socket.remoteAddress || "");
8
9
  }
9
10
 
11
+ // এই ফাংশনটি Express-এর জন্য তৈরি করা হয়েছে, তাই এটি কিছু অতিরিক্ত পাথও বাদ দেয় যা Hono-তে প্রযোজ্য নয়।
12
+ function shouldSkipHttpLog(pathname: string) {
13
+ // /queues, /queues/..., /favicon.ico বাদ দিন
14
+ return pathname.startsWith("/queues") || pathname === "/favicon.ico" || pathname === openApiConfig.scalar.specUrl || pathname === openApiConfig.scalar.docsPath;
15
+ }
16
+
10
17
  export const loggerMiddleware: RequestHandler = (req, res, next) => {
11
18
  const startedAt = Date.now();
12
19
  const requestId = crypto.randomUUID();
@@ -15,19 +22,22 @@ export const loggerMiddleware: RequestHandler = (req, res, next) => {
15
22
  res.setHeader("X-Request-ID", requestId);
16
23
 
17
24
  res.on("finish", () => {
18
- const pathname = (req.url ?? "").split("?")[0];
19
- if (pathname === "/health" || pathname.startsWith("/queues")) return;
25
+ // req.originalUrl ব্যবহার করুন, কারণ req.url রাউটারের ভেতরে বদলে যায়
26
+ const pathname = (req.originalUrl || req.url || "").split("?")[0];
20
27
 
21
- logger.info("HTTP Request", {
22
- requestId,
23
- method: req.method,
24
- path: pathname,
25
- url: req.originalUrl,
26
- status: res.statusCode,
27
- duration_ms: Date.now() - startedAt,
28
- ip: requestIp(req),
29
- userAgent: req.headers["user-agent"] || null
30
- });
28
+ // shouldSkipHttpLog চেক করুন
29
+ if (!shouldSkipHttpLog(pathname)) {
30
+ logger.info("HTTP Request", {
31
+ requestId,
32
+ method: req.method,
33
+ path: pathname,
34
+ url: req.originalUrl,
35
+ status: res.statusCode,
36
+ duration_ms: Date.now() - startedAt,
37
+ ip: requestIp(req),
38
+ userAgent: req.headers["user-agent"] || null
39
+ });
40
+ }
31
41
  });
32
42
 
33
43
  next();
@@ -45,6 +55,7 @@ export const onError: ErrorRequestHandler = (error, _req, res, _next) => {
45
55
  status,
46
56
  issues: error.error.issues
47
57
  });
58
+
48
59
  res.status(status).json({
49
60
  success: false,
50
61
  error: {
@@ -52,18 +63,18 @@ export const onError: ErrorRequestHandler = (error, _req, res, _next) => {
52
63
  issues: error.error.issues
53
64
  }
54
65
  });
66
+
55
67
  return;
56
68
  }
57
69
 
58
70
  const message = status === 500 && !error?.expose ? "Internal server error" : (error?.message ?? "Internal server error");
59
71
  const stack = process.env.NODE_ENV === "production" ? undefined : error?.stack;
72
+
60
73
  logger.error("Request failed", {
61
74
  status,
62
75
  message,
63
76
  stack: error?.stack
64
77
  });
65
- res.status(status).json({
66
- message,
67
- ...(stack ? { stack } : {})
68
- });
78
+
79
+ res.status(status).json({ message, ...(stack ? { stack } : {}) });
69
80
  };
@@ -28,27 +28,25 @@ export function createOpenApiDocument() {
28
28
  }
29
29
 
30
30
  export function configureOpenApi(app: Express) {
31
- app.get(openApiConfig.scalar.specUrl, (_req, res) => {
31
+ const { specUrl, docsPath, favicon, ...scalarOptions } = openApiConfig.scalar;
32
+
33
+ app.get(specUrl, (_req, res) => {
32
34
  res.json(createOpenApiDocument());
33
35
  });
34
36
 
35
- if (openApiConfig.scalar.favicon) {
37
+ if (favicon) {
36
38
  app.get("/favicon.ico", (_req, res) => {
37
- const faviconPath = path.join(process.cwd(), openApiConfig.scalar.favicon);
39
+ const faviconPath = path.join(process.cwd(), favicon);
38
40
  if (!fs.existsSync(faviconPath)) return res.status(404).end();
39
41
  res.type("image/x-icon").sendFile(faviconPath);
40
42
  });
41
43
  }
42
44
 
43
45
  app.get(
44
- openApiConfig.scalar.docsPath,
46
+ docsPath,
45
47
  apiReference({
46
- url: openApiConfig.scalar.specUrl,
47
- layout: openApiConfig.scalar.layout,
48
- theme: openApiConfig.scalar.theme,
49
- pageTitle: openApiConfig.scalar.pageTitle,
50
- defaultHttpClient: openApiConfig.scalar.defaultHttpClient,
51
- defaultOpenAllTags: openApiConfig.scalar.defaultOpenAllTags
52
- } as any)
48
+ url: specUrl, // Map specUrl to url
49
+ ...scalarOptions // Pass EVERYTHING else automatically!
50
+ } as any) // The `as any` might still be needed if Express types are slightly behind
53
51
  );
54
52
  }
@@ -1,21 +1,26 @@
1
- import type { Express } from "express";
2
- import { registerRouterOpenApi } from "@/framework/http/router.js";
3
- import { discoverModuleFiles, importFile, moduleNameFromPath } from "@/framework/modules/discover.js";
4
-
5
- export async function registerModuleRoutes(app: Express) {
6
- const files = await discoverModuleFiles("**/routes/*.{ts,js}");
7
-
8
- for (const file of files) {
9
- const route = await importFile(file);
10
-
11
- if (!route.default) {
12
- throw new Error(`Route file ${file} has no default export`);
13
- }
14
-
15
- const prefix = `/api/${moduleNameFromPath(file)}`;
16
- registerRouterOpenApi(route.default, prefix);
17
- app.use(prefix, route.default);
18
- }
19
-
20
- return files.length;
21
- }
1
+ import type { Express } from "express";
2
+ import { registerRouterOpenApi } from "@/framework/http/router.js";
3
+ import { discoverModuleFiles, importFile, moduleNameFromPath } from "@/framework/modules/discover.js";
4
+
5
+ export async function registerModuleRoutes(app: Express) {
6
+ const files = await discoverModuleFiles("**/routes/*.{ts,js}");
7
+
8
+ for (const file of files) {
9
+ const route = await importFile(file);
10
+
11
+ if (!route.default) {
12
+ throw new Error(`Route file ${file} has no default export`);
13
+ }
14
+
15
+ // Optional Guard: Ensure it's an Express Router
16
+ if (typeof route.default !== "function" || !route.default.stack) {
17
+ throw new Error(`Route file ${file} must export an express.Router() instance as default`);
18
+ }
19
+
20
+ const prefix = `/api/${moduleNameFromPath(file)}`;
21
+ registerRouterOpenApi(route.default, prefix);
22
+ app.use(prefix, route.default);
23
+ }
24
+
25
+ return files.length;
26
+ }
@@ -0,0 +1,15 @@
1
+ import { dispatchEvent } from "@/framework/events/dispatcher.js";
2
+ import type { NotificationChannel } from "@/framework/notification/contracts.js";
3
+
4
+ export const broadcastChannel: NotificationChannel = {
5
+ name: "broadcast",
6
+ async send(target, notification, options) {
7
+ if (!options.broadcast) return false;
8
+
9
+ await dispatchEvent("notification.created", notification, {
10
+ broadcast: { users: [target.id] }
11
+ });
12
+
13
+ return true;
14
+ }
15
+ };
@@ -0,0 +1,40 @@
1
+ import { mail } from "@/framework/support/mail.js";
2
+ import { resolveAuthProvider } from "@/framework/providers/adapter.js";
3
+ import { redisClientIfReady } from "@/framework/redis/client.js";
4
+ import { dispatchEvent } from "@/framework/events/dispatcher.js";
5
+ import type { NotificationChannel } from "@/framework/notification/contracts.js";
6
+
7
+ function escapeHtml(str: string) {
8
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
9
+ }
10
+
11
+ export const mailChannel: NotificationChannel = {
12
+ name: "mail",
13
+ async send(target, notification, options) {
14
+ if (!options.mail) return false;
15
+
16
+ let to = options.mail.to || null;
17
+ if (!to) {
18
+ try {
19
+ const provider = await resolveAuthProvider();
20
+ if (provider?.getUserContact) {
21
+ const contact = await provider.getUserContact(target.id);
22
+ if (contact?.email) to = contact.email as string;
23
+ }
24
+ } catch { }
25
+ }
26
+
27
+ if (!to) return false;
28
+
29
+ const subject = options.mail.subject || notification.title;
30
+ const html = options.mail.html || `<h2>${escapeHtml(notification.title)}</h2>${notification.body ? `<p>${escapeHtml(notification.body)}</p>` : ""}`;
31
+
32
+ if (redisClientIfReady()) {
33
+ await dispatchEvent("notification:mail", { to, subject, html }, { queue: "mail" });
34
+ } else {
35
+ await mail.sendMail({ to, subject, html });
36
+ }
37
+
38
+ return true;
39
+ }
40
+ };