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.
- package/README.md +20 -8
- package/express/.env +1 -1
- package/express/.vscode/extensions.json +6 -0
- package/express/.vscode/settings.json +37 -26
- package/express/biome.json +99 -66
- package/express/package.json +31 -29
- package/express/src/config/database.ts +48 -29
- package/express/src/config/index.ts +83 -83
- package/express/src/config/openapi.ts +6 -24
- package/express/src/config/queue.ts +30 -30
- package/express/src/config/validate.ts +50 -40
- package/express/src/database/migrations/sqlite/0000_init.sql +47 -47
- package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
- package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
- package/express/src/database/schema.ts +2 -2
- package/express/src/framework/cache/cache.ts +40 -8
- package/express/src/framework/circuit-breaker/cb.ts +88 -60
- package/express/src/framework/database/connection.ts +52 -27
- package/express/src/framework/events/dispatcher.ts +61 -49
- package/express/src/framework/facade.ts +46 -3
- package/express/src/framework/http/app.ts +28 -22
- package/express/src/framework/http/logger.ts +27 -16
- package/express/src/framework/http/openapi.ts +9 -11
- package/express/src/framework/modules/routes.ts +26 -21
- package/express/src/framework/notification/channels/broadcast.ts +15 -0
- package/express/src/framework/notification/channels/mail.ts +40 -0
- package/express/src/framework/notification/contracts.ts +29 -0
- package/express/src/framework/notification/index.ts +19 -94
- package/express/src/framework/notification/registry.ts +15 -0
- package/express/src/framework/providers/adapter.ts +27 -17
- package/express/src/framework/queue/queue.ts +43 -44
- package/express/src/framework/realtime/broadcast.ts +29 -5
- package/express/src/framework/realtime/socket.ts +149 -119
- package/express/src/framework/redis/client.ts +148 -151
- package/express/src/framework/runtime/server-adapter.ts +39 -34
- package/express/src/framework/scheduler/lock.ts +128 -116
- package/express/src/framework/server.ts +33 -49
- package/express/src/framework/session/session.ts +69 -14
- package/express/src/framework/storage/storage.ts +744 -785
- package/express/src/framework/support/mail.ts +15 -7
- package/express/src/providers/notification.ts +4 -0
- package/express/src/resources/src/components/Checkbox.vue +32 -29
- package/express/src/resources/src/components/Input.vue +243 -223
- package/express/src/resources/src/components/InputGroup.vue +147 -145
- package/express/src/resources/src/components/InputPasswordToggle.vue +229 -213
- package/express/src/resources/src/components/Modal.vue +71 -55
- package/express/src/resources/src/components/Refresh.vue +19 -0
- package/express/src/resources/src/helpers/assets.ts +51 -0
- package/express/src/resources/src/layouts/Layout/Header.vue +124 -105
- package/express/src/resources/src/main.ts +3 -1
- package/express/src/resources/src/plugins/gum.ts +1 -1
- package/express/src/resources/src/plugins/pulse.ts +157 -147
- package/express/src/resources/src/stores/auth.ts +184 -164
- package/express/src/resources/vite.config.ts +54 -36
- package/express/src/storage/database/nexwire.sqlite +0 -0
- package/express/src/storage/logs/app.log +0 -0
- package/express/src/storage/logs/fatal.log +0 -0
- package/express/src/types/global.d.ts +32 -26
- package/hono/.env +1 -1
- package/hono/.vscode/extensions.json +6 -0
- package/hono/.vscode/settings.json +37 -26
- package/hono/biome.json +25 -2
- package/hono/package.json +25 -23
- package/hono/src/config/database.ts +48 -29
- package/hono/src/config/index.ts +83 -83
- package/hono/src/config/openapi.ts +6 -24
- package/hono/src/config/queue.ts +30 -30
- package/hono/src/config/validate.ts +50 -40
- package/hono/src/database/migrations/sqlite/0000_init.sql +47 -47
- package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
- package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
- package/hono/src/database/schema.ts +2 -2
- package/hono/src/framework/cache/cache.ts +40 -8
- package/hono/src/framework/circuit-breaker/cb.ts +88 -60
- package/hono/src/framework/database/connection.ts +52 -27
- package/hono/src/framework/events/dispatcher.ts +61 -49
- package/hono/src/framework/facade.ts +38 -1
- package/hono/src/framework/http/openapi.ts +7 -9
- package/hono/src/framework/notification/channels/broadcast.ts +15 -0
- package/hono/src/framework/notification/channels/mail.ts +40 -0
- package/hono/src/framework/notification/contracts.ts +29 -0
- package/hono/src/framework/notification/index.ts +19 -94
- package/hono/src/framework/notification/registry.ts +15 -0
- package/hono/src/framework/providers/adapter.ts +27 -17
- package/hono/src/framework/queue/queue.ts +43 -44
- package/hono/src/framework/realtime/broadcast.ts +29 -5
- package/hono/src/framework/realtime/socket.ts +217 -196
- package/hono/src/framework/redis/client.ts +148 -151
- package/hono/src/framework/scheduler/lock.ts +128 -116
- package/hono/src/framework/server.ts +27 -29
- package/hono/src/framework/session/session.ts +62 -14
- package/hono/src/framework/storage/storage.ts +744 -785
- package/hono/src/framework/support/mail.ts +56 -48
- package/hono/src/providers/notification.ts +4 -0
- package/hono/src/resources/src/components/Checkbox.vue +32 -29
- package/hono/src/resources/src/components/Input.vue +243 -234
- package/hono/src/resources/src/components/InputGroup.vue +147 -149
- package/hono/src/resources/src/components/InputPasswordToggle.vue +229 -222
- package/hono/src/resources/src/components/Modal.vue +71 -55
- package/hono/src/resources/src/components/Refresh.vue +19 -0
- package/hono/src/resources/src/helpers/assets.ts +51 -0
- package/hono/src/resources/src/layouts/Layout/Header.vue +124 -105
- package/hono/src/resources/src/main.ts +3 -1
- package/hono/src/resources/src/plugins/gum.ts +1 -1
- package/hono/src/resources/src/plugins/pulse.ts +157 -147
- package/hono/src/resources/src/stores/auth.ts +184 -164
- package/hono/src/resources/vite.config.ts +54 -36
- package/hono/src/storage/database/nexwire.sqlite +0 -0
- package/hono/src/storage/logs/app.log +0 -0
- package/hono/src/storage/logs/fatal.log +0 -0
- package/hono/src/types/global.d.ts +32 -26
- package/package.json +2 -2
- 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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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({
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
6
|
-
|
|
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 (
|
|
75
|
-
app.
|
|
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
|
-
|
|
19
|
-
|
|
25
|
+
// req.originalUrl ব্যবহার করুন, কারণ req.url রাউটারের ভেতরে বদলে যায়
|
|
26
|
+
const pathname = (req.originalUrl || req.url || "").split("?")[0];
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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 (
|
|
37
|
+
if (favicon) {
|
|
36
38
|
app.get("/favicon.ico", (_req, res) => {
|
|
37
|
-
const faviconPath = path.join(process.cwd(),
|
|
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
|
-
|
|
46
|
+
docsPath,
|
|
45
47
|
apiReference({
|
|
46
|
-
url:
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
+
};
|