create-nexwire 4.2.3 → 4.2.5

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 (87) hide show
  1. package/express/.vscode/extensions.json +6 -0
  2. package/express/.vscode/settings.json +37 -26
  3. package/express/biome.json +99 -66
  4. package/express/package.json +54 -27
  5. package/express/src/database/migrations/sqlite/0000_init.sql +47 -60
  6. package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -89
  7. package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
  8. package/express/src/database/schema.ts +0 -1
  9. package/express/src/framework/facade.ts +12 -1
  10. package/express/src/framework/maker-cli/db/core.mjs +291 -288
  11. package/express/src/framework/maker-cli/module/core.mjs +125 -87
  12. package/express/src/framework/maker-cli/module/index.mjs +3 -3
  13. package/express/src/framework/maker-cli/stubs/notification/controller.ts.stub +61 -62
  14. package/express/src/framework/maker-cli/stubs/notification/job.ts.stub +11 -13
  15. package/express/src/framework/maker-cli/stubs/notification/model.ts.stub +29 -0
  16. package/express/src/framework/maker-cli/stubs/notification/provider.ts.stub +174 -0
  17. package/express/src/framework/maker-cli/stubs/notification/route.api.ts.stub +11 -10
  18. package/express/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +1 -1
  19. package/express/src/framework/maker-cli/stubs/notification/schema.plain.ts.stub +25 -25
  20. package/express/src/framework/maker-cli/stubs/notification/schema.ts.stub +25 -25
  21. package/express/src/framework/notification/index.ts +49 -87
  22. package/express/src/framework/providers/adapter.ts +24 -10
  23. package/express/src/framework/providers/contracts.ts +80 -10
  24. package/express/src/framework/providers/registry.ts +27 -1
  25. package/express/src/resources/src/components/Checkbox.vue +32 -29
  26. package/express/src/resources/src/components/Input.vue +243 -223
  27. package/express/src/resources/src/components/InputGroup.vue +147 -145
  28. package/express/src/resources/src/components/InputPasswordToggle.vue +229 -213
  29. package/express/src/resources/src/components/Modal.vue +71 -55
  30. package/express/src/resources/src/components/Refresh.vue +19 -0
  31. package/express/src/resources/src/helpers/assets.ts +51 -0
  32. package/express/src/resources/src/main.ts +4 -2
  33. package/express/src/resources/src/pages/auth/forgetPassword.vue +1 -1
  34. package/express/src/resources/src/pages/auth/login.vue +1 -1
  35. package/express/src/resources/src/pages/auth/register.vue +1 -1
  36. package/express/src/resources/src/pages/auth/resetPassword.vue +1 -1
  37. package/express/src/resources/src/pages/auth/verifyEmail.vue +1 -1
  38. package/express/src/resources/src/pages/dashboard/index.vue +66 -66
  39. package/express/src/resources/src/plugins/gum.ts +1 -1
  40. package/express/src/resources/src/stores/auth.ts +184 -164
  41. package/express/src/resources/vite.config.ts +9 -2
  42. package/express/src/storage/database/nexwire.sqlite +0 -0
  43. package/hono/.vscode/extensions.json +6 -0
  44. package/hono/.vscode/settings.json +37 -26
  45. package/hono/biome.json +25 -2
  46. package/hono/package.json +50 -23
  47. package/hono/src/database/migrations/sqlite/0000_init.sql +47 -60
  48. package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -89
  49. package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
  50. package/hono/src/database/schema.ts +0 -1
  51. package/hono/src/framework/facade.ts +12 -1
  52. package/hono/src/framework/maker-cli/db/core.mjs +291 -288
  53. package/hono/src/framework/maker-cli/module/core.mjs +93 -52
  54. package/hono/src/framework/maker-cli/module/index.mjs +3 -3
  55. package/hono/src/framework/maker-cli/stubs/notification/controller.ts.stub +63 -58
  56. package/hono/src/framework/maker-cli/stubs/notification/job.ts.stub +10 -12
  57. package/hono/src/framework/maker-cli/stubs/notification/model.ts.stub +29 -0
  58. package/hono/src/framework/maker-cli/stubs/notification/provider.ts.stub +174 -0
  59. package/hono/src/framework/maker-cli/stubs/notification/route.api.ts.stub +11 -10
  60. package/hono/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +1 -1
  61. package/hono/src/framework/maker-cli/stubs/notification/schema.plain.ts.stub +25 -25
  62. package/hono/src/framework/maker-cli/stubs/notification/schema.ts.stub +25 -25
  63. package/hono/src/framework/notification/index.ts +49 -87
  64. package/hono/src/framework/providers/adapter.ts +24 -10
  65. package/hono/src/framework/providers/contracts.ts +80 -10
  66. package/hono/src/framework/providers/registry.ts +27 -1
  67. package/hono/src/resources/src/components/Checkbox.vue +32 -29
  68. package/hono/src/resources/src/components/Input.vue +243 -234
  69. package/hono/src/resources/src/components/InputGroup.vue +147 -149
  70. package/hono/src/resources/src/components/InputPasswordToggle.vue +229 -222
  71. package/hono/src/resources/src/components/Modal.vue +71 -55
  72. package/hono/src/resources/src/components/Refresh.vue +19 -0
  73. package/hono/src/resources/src/helpers/assets.ts +51 -0
  74. package/hono/src/resources/src/main.ts +4 -2
  75. package/hono/src/resources/src/pages/auth/forgetPassword.vue +1 -1
  76. package/hono/src/resources/src/pages/auth/login.vue +1 -1
  77. package/hono/src/resources/src/pages/auth/register.vue +1 -1
  78. package/hono/src/resources/src/pages/auth/resetPassword.vue +1 -1
  79. package/hono/src/resources/src/pages/auth/verifyEmail.vue +1 -1
  80. package/hono/src/resources/src/pages/dashboard/index.vue +66 -66
  81. package/hono/src/resources/src/plugins/gum.ts +1 -1
  82. package/hono/src/resources/src/stores/auth.ts +184 -164
  83. package/hono/src/resources/vite.config.ts +9 -2
  84. package/hono/src/storage/database/nexwire.sqlite +0 -0
  85. package/package.json +10 -10
  86. package/express/src/modules/auth/database/models/notification.ts +0 -24
  87. package/hono/src/modules/auth/database/models/notification.ts +0 -24
@@ -0,0 +1,174 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { and, count, desc, eq, isNull } from "drizzle-orm";
3
+ import { db } from "@/framework/database/connection.js";
4
+ import type {
5
+ NotifiableTarget,
6
+ NotificationCreateInput,
7
+ NotificationListOptions,
8
+ NotificationPage,
9
+ NotificationProvider,
10
+ NotificationRecord
11
+ } from "@/framework/providers/contracts.js";
12
+ import { registerNotificationProvider } from "@/framework/providers/registry.js";
13
+ import { notifications } from "@/modules/{{module}}/database/models/notification.js";
14
+
15
+ /** Normalize a target into the polymorphic pair stored in the table. */
16
+ function normalizeTarget(target: NotifiableTarget) {
17
+ const type = String(target?.type || "user").trim() || "user";
18
+ if (target?.id === undefined || target?.id === null || target?.id === "") {
19
+ throw new Error("A notification target needs an id.");
20
+ }
21
+ return { type, id: String(target.id) };
22
+ }
23
+
24
+ /** Build the owner scope shared by every read and write. */
25
+ function scope(target: NotifiableTarget) {
26
+ const normalized = normalizeTarget(target);
27
+ return {
28
+ target: normalized,
29
+ where: and(eq(notifications.notifiableType, normalized.type), eq(notifications.notifiableId, normalized.id))
30
+ };
31
+ }
32
+
33
+ function toIso(value: Date | string | number | null | undefined): string | null {
34
+ if (value === null || value === undefined) return null;
35
+ const date = value instanceof Date ? value : new Date(value);
36
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
37
+ }
38
+
39
+ function parseData(value: unknown): Record<string, unknown> | null {
40
+ if (!value) return null;
41
+ if (typeof value === "object") return value as Record<string, unknown>;
42
+ try {
43
+ const parsed = JSON.parse(String(value));
44
+ return parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : null;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ /** Convert a DB row into the stable shape every consumer receives. */
51
+ function normalizeRow(row: typeof notifications.$inferSelect): NotificationRecord {
52
+ return {
53
+ id: String(row.id),
54
+ notifiableType: row.notifiableType,
55
+ notifiableId: row.notifiableId,
56
+ type: row.type,
57
+ title: row.title,
58
+ body: row.body ?? null,
59
+ data: parseData(row.data),
60
+ link: row.link ?? null,
61
+ readAt: toIso(row.readAt),
62
+ createdAt: toIso(row.createdAt) ?? new Date(0).toISOString()
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Why: Ships a working notification store so `notify()` needs no setup.
68
+ * When: Imported at boot by the provider loader (and lazily by the resolver).
69
+ * Where: `src/modules/notification/providers/notification.ts`.
70
+ * How: Drizzle-backed implementation of `NotificationProvider`. The primary
71
+ * key is a UUID string, so ids never depend on the auth table; numeric
72
+ * targets are accepted and stored as text by the contract.
73
+ */
74
+ export const notificationProvider: NotificationProvider = {
75
+ async create(target: NotifiableTarget, input: NotificationCreateInput): Promise<NotificationRecord> {
76
+ const { target: normalized } = scope(target);
77
+ const id = randomUUID();
78
+
79
+ await db.insert(notifications).values({
80
+ id,
81
+ notifiableType: normalized.type,
82
+ notifiableId: normalized.id,
83
+ type: input.type || "info",
84
+ title: input.title,
85
+ body: input.body ?? null,
86
+ link: input.link ?? null,
87
+ data: input.data ? JSON.stringify(input.data) : null
88
+ });
89
+
90
+ const [row] = await db.select().from(notifications).where(eq(notifications.id, id));
91
+ if (!row) throw new Error("Failed to create notification");
92
+ return normalizeRow(row);
93
+ },
94
+
95
+ async list(target: NotifiableTarget, options: NotificationListOptions = {}): Promise<NotificationPage> {
96
+ const { where } = scope(target);
97
+ const page = Math.max(1, Math.floor(Number(options.page) || 1));
98
+ const perPage = Math.min(100, Math.max(1, Math.floor(Number(options.perPage) || 20)));
99
+
100
+ const [totalRows, unreadRows, rows] = await Promise.all([
101
+ db.select({ count: count() }).from(notifications).where(where),
102
+ db.select({ count: count() }).from(notifications).where(and(where, isNull(notifications.readAt))),
103
+ db
104
+ .select()
105
+ .from(notifications)
106
+ .where(where)
107
+ .orderBy(desc(notifications.createdAt))
108
+ .limit(perPage)
109
+ .offset((page - 1) * perPage)
110
+ ]);
111
+
112
+ return {
113
+ data: rows.map(normalizeRow),
114
+ total: Number(totalRows[0]?.count ?? 0),
115
+ unread: Number(unreadRows[0]?.count ?? 0),
116
+ page,
117
+ perPage
118
+ };
119
+ },
120
+
121
+ async unreadCount(target: NotifiableTarget): Promise<number> {
122
+ const { where } = scope(target);
123
+ const rows = await db
124
+ .select({ count: count() })
125
+ .from(notifications)
126
+ .where(and(where, isNull(notifications.readAt)));
127
+ return Number(rows[0]?.count ?? 0);
128
+ },
129
+
130
+ async markRead(target: NotifiableTarget, id: number | string): Promise<boolean> {
131
+ const { where } = scope(target);
132
+ const notificationWhere = and(where, eq(notifications.id, String(id)));
133
+ const [existing] = await db.select({ id: notifications.id }).from(notifications).where(notificationWhere);
134
+ if (!existing) return false;
135
+
136
+ await db.update(notifications).set({ readAt: new Date() }).where(notificationWhere);
137
+ return true;
138
+ },
139
+
140
+ async markAllRead(target: NotifiableTarget): Promise<number> {
141
+ const { where } = scope(target);
142
+ const unreadWhere = and(where, isNull(notifications.readAt));
143
+ const rows = await db.select({ count: count() }).from(notifications).where(unreadWhere);
144
+ const unread = Number(rows[0]?.count ?? 0);
145
+ if (unread === 0) return 0;
146
+
147
+ await db.update(notifications).set({ readAt: new Date() }).where(unreadWhere);
148
+ return unread;
149
+ },
150
+
151
+ async remove(target: NotifiableTarget, id: number | string): Promise<boolean> {
152
+ const { where } = scope(target);
153
+ const notificationWhere = and(where, eq(notifications.id, String(id)));
154
+ const [existing] = await db.select({ id: notifications.id }).from(notifications).where(notificationWhere);
155
+ if (!existing) return false;
156
+
157
+ await db.delete(notifications).where(notificationWhere);
158
+ return true;
159
+ },
160
+
161
+ async deleteForTarget(target: NotifiableTarget): Promise<number> {
162
+ const { where } = scope(target);
163
+ const rows = await db.select({ count: count() }).from(notifications).where(where);
164
+ const total = Number(rows[0]?.count ?? 0);
165
+ if (total === 0) return 0;
166
+
167
+ await db.delete(notifications).where(where);
168
+ return total;
169
+ }
170
+ };
171
+
172
+ registerNotificationProvider(notificationProvider);
173
+
174
+ export default notificationProvider;
@@ -1,43 +1,45 @@
1
1
  import { createRoute, createRouter, HttpStatusCodes, jsonContent, z } from "@/framework/facade.js";
2
2
  import { authMiddleware } from "@/middlewares/auth-middleware.js";
3
- import { list, markAllRead, markRead, remove, unreadCount } from "../controllers/{{module}}.js";
4
- import { PaginatedNotificationsSchema, UnreadCountSchema } from "../schemas/{{module}}.js";
3
+ import { list, markAllRead, markRead, remove, unreadCount } from "../controllers/{{leaf}}.js";
4
+ import { PaginatedNotificationsSchema, UnreadCountSchema } from "../schemas/{{leaf}}.js";
5
+
6
+ const notificationId = z.object({ id: z.string().min(1) });
5
7
 
6
8
  const listRoute = createRoute({
7
9
  path: "/",
8
10
  method: "get",
9
11
  tags: ["Notifications"],
10
- responses: { [HttpStatusCodes.OK]: jsonContent(PaginatedNotificationsSchema, "Paginated notifications") },
12
+ responses: { [HttpStatusCodes.OK]: jsonContent(PaginatedNotificationsSchema, "Paginated notifications") }
11
13
  });
12
14
 
13
15
  const unreadCountRoute = createRoute({
14
16
  path: "/unread-count",
15
17
  method: "get",
16
18
  tags: ["Notifications"],
17
- responses: { [HttpStatusCodes.OK]: jsonContent(UnreadCountSchema, "Unread count") },
19
+ responses: { [HttpStatusCodes.OK]: jsonContent(UnreadCountSchema, "Unread count") }
18
20
  });
19
21
 
20
22
  const markReadRoute = createRoute({
21
23
  path: "/{id}/read",
22
24
  method: "patch",
23
25
  tags: ["Notifications"],
24
- request: { params: z.object({ id: z.coerce.number() }) },
25
- responses: { [HttpStatusCodes.OK]: jsonContent(z.object({ ok: z.boolean() }), "Marked as read") },
26
+ request: { params: notificationId },
27
+ responses: { [HttpStatusCodes.OK]: jsonContent(z.object({ ok: z.boolean() }), "Marked as read") }
26
28
  });
27
29
 
28
30
  const markAllReadRoute = createRoute({
29
31
  path: "/read-all",
30
32
  method: "patch",
31
33
  tags: ["Notifications"],
32
- responses: { [HttpStatusCodes.OK]: jsonContent(z.object({ ok: z.boolean() }), "All marked as read") },
34
+ responses: { [HttpStatusCodes.OK]: jsonContent(z.object({ ok: z.boolean() }), "All marked as read") }
33
35
  });
34
36
 
35
37
  const deleteRoute = createRoute({
36
38
  path: "/{id}",
37
39
  method: "delete",
38
40
  tags: ["Notifications"],
39
- request: { params: z.object({ id: z.coerce.number() }) },
40
- responses: { [HttpStatusCodes.OK]: jsonContent(z.object({ ok: z.boolean() }), "Deleted") },
41
+ request: { params: notificationId },
42
+ responses: { [HttpStatusCodes.OK]: jsonContent(z.object({ ok: z.boolean() }), "Deleted") }
41
43
  });
42
44
 
43
45
  export default createRouter()
@@ -47,4 +49,3 @@ export default createRouter()
47
49
  .api(markReadRoute, markRead)
48
50
  .api(markAllReadRoute, markAllRead)
49
51
  .api(deleteRoute, remove);
50
-
@@ -1,6 +1,6 @@
1
1
  import { group } from "@/framework/facade.js";
2
2
  import { authMiddleware } from "@/middlewares/auth-middleware.js";
3
- import { list, unreadCount, markRead, markAllRead, remove } from "../controllers/{{module}}.js";
3
+ import { list, unreadCount, markRead, markAllRead, remove } from "../controllers/{{leaf}}.js";
4
4
 
5
5
  export default group()
6
6
  .use(authMiddleware)
@@ -1,26 +1,26 @@
1
- import { z } from "@/framework/facade.js";
2
-
3
- export const NotificationSchema = z.object({
4
- id: z.number(),
5
- userId: z.number(),
6
- type: z.string(),
7
- title: z.string(),
8
- body: z.string().nullable(),
9
- data: z.any().nullable(),
10
- link: z.string().nullable(),
11
- readAt: z.string().nullable(),
12
- createdAt: z.string(),
13
- });
14
-
15
- export const UnreadCountSchema = z.object({
16
- count: z.number(),
17
- });
18
-
19
- export const PaginatedNotificationsSchema = z.object({
20
- data: z.array(NotificationSchema),
21
- total: z.number(),
22
- unread: z.number(),
23
- page: z.number(),
24
- perPage: z.number(),
25
- });
1
+ import { z } from "@/framework/facade.js";
26
2
 
3
+ export const NotificationSchema = z.object({
4
+ id: z.string(),
5
+ notifiableType: z.string(),
6
+ notifiableId: z.string(),
7
+ type: z.string(),
8
+ title: z.string(),
9
+ body: z.string().nullable(),
10
+ data: z.any().nullable(),
11
+ link: z.string().nullable(),
12
+ readAt: z.string().nullable(),
13
+ createdAt: z.string()
14
+ });
15
+
16
+ export const UnreadCountSchema = z.object({
17
+ count: z.number()
18
+ });
19
+
20
+ export const PaginatedNotificationsSchema = z.object({
21
+ data: z.array(NotificationSchema),
22
+ total: z.number(),
23
+ unread: z.number(),
24
+ page: z.number(),
25
+ perPage: z.number()
26
+ });
@@ -1,26 +1,26 @@
1
- import { z } from "zod";
2
-
3
- export const NotificationSchema = z.object({
4
- id: z.number(),
5
- userId: z.number(),
6
- type: z.string(),
7
- title: z.string(),
8
- body: z.string().nullable(),
9
- data: z.any().nullable(),
10
- link: z.string().nullable(),
11
- readAt: z.string().nullable(),
12
- createdAt: z.string(),
13
- });
14
-
15
- export const UnreadCountSchema = z.object({
16
- count: z.number(),
17
- });
18
-
19
- export const PaginatedNotificationsSchema = z.object({
20
- data: z.array(NotificationSchema),
21
- total: z.number(),
22
- unread: z.number(),
23
- page: z.number(),
24
- perPage: z.number(),
25
- });
1
+ import { z } from "@/framework/facade.js";
26
2
 
3
+ export const NotificationSchema = z.object({
4
+ id: z.string(),
5
+ notifiableType: z.string(),
6
+ notifiableId: z.string(),
7
+ type: z.string(),
8
+ title: z.string(),
9
+ body: z.string().nullable(),
10
+ data: z.any().nullable(),
11
+ link: z.string().nullable(),
12
+ readAt: z.string().nullable(),
13
+ createdAt: z.string()
14
+ });
15
+
16
+ export const UnreadCountSchema = z.object({
17
+ count: z.number()
18
+ });
19
+
20
+ export const PaginatedNotificationsSchema = z.object({
21
+ data: z.array(NotificationSchema),
22
+ total: z.number(),
23
+ unread: z.number(),
24
+ page: z.number(),
25
+ perPage: z.number()
26
+ });
@@ -1,27 +1,13 @@
1
- import { eq } from "drizzle-orm";
2
- import { db } from "@/framework/database/connection.js";
3
1
  import { dispatchEvent } from "@/framework/events/dispatcher.js";
4
- import { resolveAuthProvider } from "@/framework/providers/adapter.js";
2
+ import { resolveAuthProvider, resolveNotificationProvider } from "@/framework/providers/adapter.js";
3
+ import type { NotifiableTarget, NotificationRecord } from "@/framework/providers/contracts.js";
5
4
  import { redisClientIfReady } from "@/framework/redis/client.js";
6
5
  import { mail } from "@/framework/support/mail.js";
7
6
 
8
- let notifications: any = null;
9
- let modelsLoaded = false;
10
- let modelsLoadAttempted = false;
11
-
12
- async function loadNotificationModel() {
13
- if (modelsLoaded) return true;
14
- if (modelsLoadAttempted) return false;
15
- modelsLoadAttempted = true;
16
- try {
17
- const notificationMod = await import("@/modules/auth/database/models/notification.js").catch(() => null);
18
- if (notificationMod?.notifications) notifications = notificationMod.notifications;
19
- modelsLoaded = !!notifications;
20
- return modelsLoaded;
21
- } catch {
22
- return false;
23
- }
24
- }
7
+ let missingProviderWarned = false;
8
+
9
+ /** A numeric id (default auth), a string id (UUID/ULID), or an explicit target. */
10
+ export type NotificationTarget = number | string | NotifiableTarget;
25
11
 
26
12
  /** Extra JSON metadata attached to the notification (e.g. entity IDs, links). */
27
13
  export type NotificationOptions = {
@@ -46,38 +32,55 @@ export type NotificationOptions = {
46
32
  };
47
33
  };
48
34
 
49
- /** Create a notification and persist it to the database. Optionally broadcast via Socket.IO and/or send by email. */
50
- export async function notify(userId: number, options: NotificationOptions) {
51
- const ok = await loadNotificationModel();
52
- if (!ok || !notifications) {
53
- console.warn(
54
- "[notification] skipped: notification model not available (@/modules/auth/database/models/notification.js). Delete or replace auth/notification without breaking framework."
55
- );
35
+ /** Turn the public target argument into the polymorphic owner pair. */
36
+ function normalizeTarget(target: NotificationTarget): Required<Pick<NotifiableTarget, "id">> & { type: string } {
37
+ if (target !== null && typeof target === "object") {
38
+ return {
39
+ type: String(target.type || "user").trim() || "user",
40
+ id: target.id
41
+ };
42
+ }
43
+ return { type: "user", id: target as number | string };
44
+ }
45
+
46
+ /**
47
+ * Why: Persists a notification through the registered provider.
48
+ * When: A feature wants to record something for a user (or another notifiable).
49
+ * Where: Application code, through the framework facade.
50
+ * How: The provider owns storage; broadcasting and email stay here so a custom
51
+ * store cannot silently disable them. A missing provider is reported once
52
+ * and returns `null` instead of throwing, matching the framework's
53
+ * "optional module" behaviour.
54
+ */
55
+ export async function notify(target: NotificationTarget, options: NotificationOptions): Promise<NotificationRecord | null> {
56
+ const provider = await resolveNotificationProvider();
57
+ if (!provider) {
58
+ if (!missingProviderWarned) {
59
+ missingProviderWarned = true;
60
+ console.warn(
61
+ '[notification] skipped: no notification provider is registered. Run "bun maker module:make-notification notification" then "bun maker module:migrate notification", or register a provider in src/providers/.'
62
+ );
63
+ }
56
64
  return null;
57
65
  }
58
- const insertResult = await db.insert(notifications).values({
59
- userId,
60
- type: options.type || "info",
66
+
67
+ const normalizedTarget = normalizeTarget(target);
68
+ const normalized = await provider.create(normalizedTarget, {
69
+ type: options.type,
61
70
  title: options.title,
62
- body: options.body || null,
63
- link: options.link || null,
64
- data: options.data ? JSON.stringify(options.data) : null
71
+ body: options.body ?? null,
72
+ link: options.link ?? null,
73
+ data: options.data ?? null
65
74
  });
66
75
 
67
- const insertedId = Number((insertResult as any)[0]?.insertId ?? (insertResult as any).insertId ?? (insertResult as any).lastInsertRowid);
68
- if (!insertedId) throw new Error("Failed to create notification");
69
-
70
- const [row] = await db.select().from(notifications).where(eq(notifications.id, insertedId));
71
- const normalized = normalize(row);
72
-
73
76
  if (options.broadcast) {
74
77
  await dispatchEvent("notification.created", normalized, {
75
- broadcast: { users: [userId] }
78
+ broadcast: { users: [normalizedTarget.id] }
76
79
  });
77
80
  }
78
81
 
79
82
  if (options.mail) {
80
- await sendMailChannel(userId, normalized, options);
83
+ await sendMailChannel(normalizedTarget, normalized, options);
81
84
  }
82
85
 
83
86
  return normalized;
@@ -85,38 +88,18 @@ export async function notify(userId: number, options: NotificationOptions) {
85
88
 
86
89
  /** Deliver an email notification, either queued (if Redis is available) or direct. */
87
90
  async function sendMailChannel(
88
- userId: number,
89
- notification: { id: number; title: string; body: string | null },
91
+ target: { type: string; id: number | string },
92
+ notification: NotificationRecord,
90
93
  options: NotificationOptions
91
94
  ) {
92
- let to: string | null = (options.mail as any)?.to || null;
93
- let name: string | null = null;
95
+ let to: string | null = options.mail?.to || null;
94
96
 
95
97
  if (!to) {
96
98
  try {
97
99
  const provider = await resolveAuthProvider();
98
100
  if (provider?.getUserContact) {
99
- const contact = await provider.getUserContact(userId);
100
- if (contact?.email) {
101
- to = contact.email as string;
102
- name = (contact.name as string) || null;
103
- }
104
- }
105
- } catch {}
106
- }
107
-
108
- if (!to) {
109
- try {
110
- const userMod = await import("@/modules/auth/database/models/user.js").catch(() => null);
111
- if (userMod?.users) {
112
- const [user] = await db
113
- .select({ email: userMod.users.email, name: (userMod.users as any).name })
114
- .from(userMod.users)
115
- .where(eq(userMod.users.id, userId));
116
- if (user?.email) {
117
- to = user.email as string;
118
- name = (user as any).name || null;
119
- }
101
+ const contact = await provider.getUserContact(target.id);
102
+ if (contact?.email) to = contact.email as string;
120
103
  }
121
104
  } catch {}
122
105
  }
@@ -149,24 +132,3 @@ function defaultMailHtml(title: string, body: string | null) {
149
132
  function escapeHtml(str: string) {
150
133
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
151
134
  }
152
-
153
- /** Convert a DB row into the normalized response shape, parsing the `data` JSON column. */
154
- function normalize(row: any) {
155
- let parsed: unknown = null;
156
- if (row.data) {
157
- try {
158
- parsed = JSON.parse(row.data);
159
- } catch {}
160
- }
161
- return {
162
- id: row.id,
163
- userId: row.userId,
164
- type: row.type,
165
- title: row.title,
166
- body: row.body,
167
- data: parsed,
168
- link: row.link,
169
- readAt: row.readAt?.toISOString() || null,
170
- createdAt: row.createdAt.toISOString()
171
- };
172
- }
@@ -1,8 +1,8 @@
1
1
  import { eq } from "drizzle-orm";
2
2
  import { db } from "@/framework/database/connection.js";
3
- import type { AuthProvider } from "./contracts.js";
3
+ import type { AuthProvider, NotificationProvider } from "./contracts.js";
4
4
  import { loadProviders } from "./loader.js";
5
- import { getAuthProvider } from "./registry.js";
5
+ import { getAuthProvider, getNotificationProvider } from "./registry.js";
6
6
 
7
7
  let defaultProvider: AuthProvider | null = null;
8
8
  let defaultLoadAttempted = false;
@@ -26,16 +26,16 @@ async function loadDefaultProvider(): Promise<AuthProvider | null> {
26
26
  const refreshTokens = userMod.refreshTokens;
27
27
 
28
28
  defaultProvider = {
29
- async getUserById(id: number) {
29
+ async getUserById(id: number | string) {
30
30
  try {
31
31
  const user = await (db.query.users.findFirst as any)?.({
32
- where: eq(users.id, id),
32
+ where: eq(users.id, id as any),
33
33
  with: { role: true }
34
34
  });
35
35
  if (user) return user;
36
36
  } catch {}
37
37
  try {
38
- const rows = await db.select().from(users).where(eq(users.id, id));
38
+ const rows = await db.select().from(users).where(eq(users.id, id as any));
39
39
  return rows?.[0] ?? null;
40
40
  } catch {
41
41
  return null;
@@ -55,14 +55,14 @@ async function loadDefaultProvider(): Promise<AuthProvider | null> {
55
55
  return null;
56
56
  }
57
57
  },
58
- async deleteRefreshToken(tokenId: number) {
58
+ async deleteRefreshToken(tokenId: number | string) {
59
59
  try {
60
- await db.delete(refreshTokens).where(eq(refreshTokens.id, tokenId));
60
+ await db.delete(refreshTokens).where(eq(refreshTokens.id, tokenId as any));
61
61
  } catch {}
62
62
  },
63
- async revokeRefreshToken(tokenId: number) {
63
+ async revokeRefreshToken(tokenId: number | string) {
64
64
  try {
65
- await db.delete(refreshTokens).where(eq(refreshTokens.id, tokenId));
65
+ await db.delete(refreshTokens).where(eq(refreshTokens.id, tokenId as any));
66
66
  } catch {}
67
67
  },
68
68
  async getAuthContext(user: any, payload?: any) {
@@ -87,7 +87,7 @@ async function loadDefaultProvider(): Promise<AuthProvider | null> {
87
87
  ...user
88
88
  };
89
89
  },
90
- async getUserContact(userId: number) {
90
+ async getUserContact(userId: number | string) {
91
91
  try {
92
92
  const user = await this.getUserById(userId);
93
93
  if (!user) return null;
@@ -123,3 +123,17 @@ export async function resolveAuthProvider(): Promise<AuthProvider | null> {
123
123
  if (def) return def;
124
124
  return null;
125
125
  }
126
+
127
+ /**
128
+ * Why: Resolves the notification storage provider.
129
+ * When: `notify()` persists a notification; the HTTP handlers read it back.
130
+ * Where: Notification runtime and the generated notification controller.
131
+ * How: Provider files are loaded first, so a project can replace the store from
132
+ * `src/providers/` or a module provider folder. The notification module is
133
+ * opt-in, so an absent provider is a normal state: callers warn once
134
+ * (runtime) or answer 503 (HTTP) instead of throwing.
135
+ */
136
+ export async function resolveNotificationProvider(): Promise<NotificationProvider | null> {
137
+ await loadProviders();
138
+ return getNotificationProvider();
139
+ }