create-nexwire 4.2.2 → 4.2.3

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 (52) hide show
  1. package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  2. package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
  3. package/express/src/framework/database/app-schema.ts +37 -0
  4. package/express/src/framework/database/connection.ts +198 -196
  5. package/express/src/framework/database/seed.ts +35 -22
  6. package/express/src/framework/facade.ts +24 -5
  7. package/express/src/framework/kernel.ts +54 -52
  8. package/express/src/framework/maker-cli/index.mjs +2 -0
  9. package/express/src/framework/maker-cli/module/core.mjs +30 -0
  10. package/express/src/framework/maker-cli/module/index.mjs +10 -1
  11. package/express/src/framework/maker-cli/provider/core.mjs +49 -0
  12. package/express/src/framework/maker-cli/provider/index.mjs +12 -0
  13. package/express/src/framework/maker-cli/stubs/notification/controller.ts.stub +77 -77
  14. package/express/src/framework/maker-cli/stubs/provider/module-barrel.ts.stub +19 -0
  15. package/express/src/framework/maker-cli/stubs/provider/module.ts.stub +21 -0
  16. package/express/src/framework/maker-cli/stubs/provider/name.ts.stub +21 -0
  17. package/express/src/framework/maker-cli/utils/help.mjs +6 -1
  18. package/express/src/framework/notification/index.ts +63 -7
  19. package/express/src/framework/providers/adapter.ts +125 -0
  20. package/express/src/framework/providers/contracts.ts +56 -0
  21. package/express/src/framework/providers/loader.ts +105 -0
  22. package/express/src/framework/providers/registry.ts +62 -0
  23. package/express/src/middlewares/auth-middleware.ts +65 -38
  24. package/express/src/providers/README.md +107 -0
  25. package/express/src/storage/database/nexwire.sqlite +0 -0
  26. package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  27. package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
  28. package/hono/src/framework/database/app-schema.ts +37 -0
  29. package/hono/src/framework/database/connection.ts +198 -196
  30. package/hono/src/framework/database/seed.ts +35 -22
  31. package/hono/src/framework/facade.ts +48 -29
  32. package/hono/src/framework/kernel.ts +50 -48
  33. package/hono/src/framework/maker-cli/index.mjs +2 -0
  34. package/hono/src/framework/maker-cli/module/core.mjs +30 -0
  35. package/hono/src/framework/maker-cli/module/index.mjs +10 -1
  36. package/hono/src/framework/maker-cli/provider/core.mjs +49 -0
  37. package/hono/src/framework/maker-cli/provider/index.mjs +12 -0
  38. package/hono/src/framework/maker-cli/stubs/notification/controller.ts.stub +85 -85
  39. package/hono/src/framework/maker-cli/stubs/provider/module-barrel.ts.stub +19 -0
  40. package/hono/src/framework/maker-cli/stubs/provider/module.ts.stub +21 -0
  41. package/hono/src/framework/maker-cli/stubs/provider/name.ts.stub +21 -0
  42. package/hono/src/framework/maker-cli/utils/help.mjs +6 -1
  43. package/hono/src/framework/notification/index.ts +63 -7
  44. package/hono/src/framework/providers/adapter.ts +125 -0
  45. package/hono/src/framework/providers/contracts.ts +56 -0
  46. package/hono/src/framework/providers/loader.ts +105 -0
  47. package/hono/src/framework/providers/registry.ts +62 -0
  48. package/hono/src/middlewares/auth-middleware.ts +65 -38
  49. package/hono/src/providers/README.md +107 -0
  50. package/hono/src/storage/database/nexwire.sqlite +0 -0
  51. package/package.json +1 -1
  52. package/src/index.js +14 -6
@@ -1,52 +1,54 @@
1
- import { appConfig } from "@/config/index.js";
2
- import { validateConfig } from "@/config/validate.js";
3
- import { initDatabase } from "@/framework/database/connection.js";
4
- import { createHttpApp } from "@/framework/http/app.js";
5
- import { notFound, onError } from "@/framework/http/logger.js";
6
- import { ensurePublicDir, hasUiBuild, uiIndexMiddleware, uiStaticMiddleware } from "@/framework/http/static.js";
7
- import { registerModuleRoutes } from "@/framework/modules/routes.js";
8
- import { bootQueueJobs } from "@/framework/queue/queue.js";
9
- import { setupQueueDashboard } from "@/framework/queue/ui.js";
10
- import { initRedis } from "@/framework/redis/client.js";
11
- import { storage } from "@/framework/storage/storage.js";
12
-
13
- /**
14
- * Why: Assembles app kernel and boots all framework dependencies.
15
- * When: HTTP server startup.
16
- * Where: Called by server runtime entrypoint.
17
- * How: Initializes storage/db/redis/queues/events/routes and optional UI.
18
- */
19
- export async function createKernel() {
20
- validateConfig();
21
-
22
- await storage.init();
23
-
24
- await initRedis();
25
-
26
- const app = createHttpApp();
27
-
28
- await initDatabase();
29
- await bootQueueJobs();
30
- await registerModuleRoutes(app);
31
-
32
- const queueDashboard = await setupQueueDashboard();
33
- if (typeof queueDashboard.route === "function") {
34
- queueDashboard.route(app);
35
- } else {
36
- app.use(queueDashboard.basePath, queueDashboard.route);
37
- }
38
-
39
- if (appConfig.uiEnabled) {
40
- ensurePublicDir();
41
- }
42
-
43
- if (appConfig.uiEnabled && hasUiBuild()) {
44
- app.use(uiStaticMiddleware);
45
- app.get("*", uiIndexMiddleware);
46
- }
47
-
48
- app.use(notFound);
49
- app.use(onError);
50
-
51
- return { app, bullBoard: queueDashboard };
52
- }
1
+ import { appConfig } from "@/config/index.js";
2
+ import { validateConfig } from "@/config/validate.js";
3
+ import { initDatabase } from "@/framework/database/connection.js";
4
+ import { createHttpApp } from "@/framework/http/app.js";
5
+ import { notFound, onError } from "@/framework/http/logger.js";
6
+ import { ensurePublicDir, hasUiBuild, uiIndexMiddleware, uiStaticMiddleware } from "@/framework/http/static.js";
7
+ import { registerModuleRoutes } from "@/framework/modules/routes.js";
8
+ import { loadProviders } from "@/framework/providers/loader.js";
9
+ import { bootQueueJobs } from "@/framework/queue/queue.js";
10
+ import { setupQueueDashboard } from "@/framework/queue/ui.js";
11
+ import { initRedis } from "@/framework/redis/client.js";
12
+ import { storage } from "@/framework/storage/storage.js";
13
+
14
+ /**
15
+ * Why: Assembles app kernel and boots all framework dependencies.
16
+ * When: HTTP server startup.
17
+ * Where: Called by server runtime entrypoint.
18
+ * How: Initializes storage/db/redis/queues/events/routes and optional UI.
19
+ */
20
+ export async function createKernel() {
21
+ validateConfig();
22
+
23
+ await storage.init();
24
+
25
+ await initRedis();
26
+
27
+ const app = createHttpApp();
28
+
29
+ await initDatabase();
30
+ await loadProviders();
31
+ await bootQueueJobs();
32
+ await registerModuleRoutes(app);
33
+
34
+ const queueDashboard = await setupQueueDashboard();
35
+ if (typeof queueDashboard.route === "function") {
36
+ queueDashboard.route(app);
37
+ } else {
38
+ app.use(queueDashboard.basePath, queueDashboard.route);
39
+ }
40
+
41
+ if (appConfig.uiEnabled) {
42
+ ensurePublicDir();
43
+ }
44
+
45
+ if (appConfig.uiEnabled && hasUiBuild()) {
46
+ app.use(uiStaticMiddleware);
47
+ app.get("*", uiIndexMiddleware);
48
+ }
49
+
50
+ app.use(notFound);
51
+ app.use(onError);
52
+
53
+ return { app, bullBoard: queueDashboard };
54
+ }
@@ -5,6 +5,7 @@ import { config } from "dotenv";
5
5
  import { registerDbCommands } from "./db/index.mjs";
6
6
  import { registerMiddlewareCommands } from "./middleware/index.mjs";
7
7
  import { registerModuleCommands } from "./module/index.mjs";
8
+ import { registerProviderCommands } from "./provider/index.mjs";
8
9
  import { registerRuntimeCommands } from "./runtime/index.mjs";
9
10
  import { configureGroupedHelp, makerCommandPrefix, showHelp } from "./utils/help.mjs";
10
11
  import { registerProjectCommands } from "./utils/project-commands.mjs";
@@ -42,6 +43,7 @@ try {
42
43
 
43
44
  registerMiddlewareCommands(program, args);
44
45
  registerModuleCommands(program, args);
46
+ registerProviderCommands(program, args);
45
47
  registerDeployCommands(program, args);
46
48
  registerDbCommands(program, args);
47
49
  registerRuntimeCommands(program, args);
@@ -45,6 +45,11 @@ const STUBS = {
45
45
  facade: {
46
46
  named: "facade/name.ts.stub"
47
47
  },
48
+ provider: {
49
+ named: "provider/name.ts.stub",
50
+ module: "provider/module.ts.stub",
51
+ moduleBarrel: "provider/module-barrel.ts.stub"
52
+ },
48
53
  helper: {
49
54
  named: "helper/name.ts.stub"
50
55
  },
@@ -739,6 +744,31 @@ export async function makeLocalMiddleware(rawModule, rawNameOrFlag, extraFlags =
739
744
  await writeFileSafe(path.join(root, `middlewares/${name}.ts`), content, flags, "Middleware file");
740
745
  }
741
746
 
747
+ /**
748
+ * Generate a provider file for an existing module.
749
+ *
750
+ * The file lands in the module's providers/ folder, which the framework
751
+ * imports at boot just like src/providers/. That is what lets a module offer
752
+ * an implementation to the whole application without the application knowing
753
+ * anything about the module's internals.
754
+ */
755
+ export async function makeModuleProvider(rawModule, rawNameOrFlag, extraFlags = []) {
756
+ const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
757
+ const { name, flags } = parseNameArg(rawNameOrFlag, extraFlags, "Provider name", leaf);
758
+ const root = await assertModuleExists(moduleName);
759
+ const filePath = path.join(root, `providers/${name}.ts`);
760
+ if (hasFlag(flags, "--dry-run")) {
761
+ console.log(`Would create provider file: ${path.relative(process.cwd(), filePath)}`);
762
+ return;
763
+ }
764
+ const content = await stub(STUBS.provider.module, {
765
+ module: moduleName,
766
+ name,
767
+ camel: camelCase(name)
768
+ });
769
+ await writeFileSafe(filePath, content, flags, "Provider file");
770
+ }
771
+
742
772
  /** Generate a types file for an existing module. */
743
773
  export async function makeType(rawModule, rawNameOrFlag, extraFlags = []) {
744
774
  const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
@@ -12,6 +12,7 @@ import {
12
12
  makeLocalMiddleware,
13
13
  makeModel,
14
14
  makeModule,
15
+ makeModuleProvider,
15
16
  makeModules,
16
17
  makeNotificationModule,
17
18
  makeRoute,
@@ -28,7 +29,7 @@ import {
28
29
  export function registerModuleCommands(program, rawArgs) {
29
30
  program
30
31
  .command("module:make <names...>")
31
- .description("Create one or more modules with facade, schema, service, controller, route, model, and seeder folders")
32
+ .description("Create one or more modules with facade, providers, schema, service, controller, route, model, and seeder folders")
32
33
  .option("--path <panel>", "Nest the module(s) under a panel folder (e.g. --path=admin)")
33
34
  .option("--force", "Overwrite the files of modules that already exist")
34
35
  .allowUnknownOption(true)
@@ -128,6 +129,14 @@ export function registerModuleCommands(program, rawArgs) {
128
129
  .option("--path <panel>", "Address a module inside a panel folder (e.g. --path=admin)")
129
130
  .allowUnknownOption(true)
130
131
  .action(async (moduleName, name) => makeLocalMiddleware(moduleName, name, rawArgs.slice(name ? 3 : 2)));
132
+ program
133
+ .command("module:make-provider <module> [name]")
134
+ .description("Generate a provider file in the module's providers folder")
135
+ .option("--force", "Overwrite existing provider file")
136
+ .option("--dry-run", "Print what would be created without executing")
137
+ .option("--path <panel>", "Address a module inside a panel folder (e.g. --path=admin)")
138
+ .allowUnknownOption(true)
139
+ .action(async (moduleName, name) => makeModuleProvider(moduleName, name, rawArgs.slice(name ? 3 : 2)));
131
140
  program
132
141
  .command("module:make-type <module> [name]")
133
142
  .description("Generate a types file for an existing module")
@@ -0,0 +1,49 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { hasFlag } from "../utils/flags.mjs";
4
+ import { assertName, camelCase } from "../utils/naming.mjs";
5
+
6
+ const stubsRoot = path.resolve(import.meta.dirname, "../stubs");
7
+
8
+ /** Read a stub file and replace {{key}} placeholders, removing unfilled ones. */
9
+ async function stub(name, values = {}) {
10
+ let content = await fs.readFile(path.join(stubsRoot, name), "utf8");
11
+ for (const [key, value] of Object.entries(values)) {
12
+ content = content.replaceAll(`{{${key}}}`, String(value));
13
+ }
14
+ return content.replace(/\{\{[A-Z0-9_]+\}\}/g, "");
15
+ }
16
+
17
+ /**
18
+ * Why: Generates the app-wide provider file.
19
+ * When: A developer wants to plug an implementation into the whole app.
20
+ * Where: src/providers/<name>.ts - imported automatically at boot.
21
+ * How: Writes the stub and prints how to reach the provider from anywhere.
22
+ */
23
+ export async function makeProvider(rawName, flags = []) {
24
+ const name = assertName(rawName, "Provider name");
25
+ const camel = camelCase(name);
26
+ const dryRun = hasFlag(flags, "--dry-run");
27
+ const force = hasFlag(flags, "--force") || hasFlag(flags, "--yes");
28
+
29
+ const filePath = path.resolve(process.cwd(), "src/providers", `${name}.ts`);
30
+ if (dryRun) {
31
+ console.log(`Would create provider file: ${path.relative(process.cwd(), filePath)}`);
32
+ return;
33
+ }
34
+
35
+ let exists = false;
36
+ try {
37
+ await fs.access(filePath);
38
+ exists = true;
39
+ } catch {}
40
+ if (exists && !force) {
41
+ throw new Error(`Provider already exists: ${path.relative(process.cwd(), filePath)}. Re-run with --force to overwrite.`);
42
+ }
43
+
44
+ const content = await stub("provider/name.ts.stub", { name, camel });
45
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
46
+ await fs.writeFile(filePath, content);
47
+ console.log(`Provider ready: ${path.relative(process.cwd(), filePath)}`);
48
+ console.log(`It is imported at boot. Reach it with: getProvider("${name}")`);
49
+ }
@@ -0,0 +1,12 @@
1
+ import { makeProvider } from "./core.mjs";
2
+
3
+ /** Register provider:* subcommands on the CLI program. */
4
+ export function registerProviderCommands(program, rawArgs) {
5
+ program
6
+ .command("provider:make <name>")
7
+ .description("Generate a provider file in src/providers")
8
+ .option("--force", "Overwrite existing provider file")
9
+ .option("--dry-run", "Print what would be created without executing")
10
+ .allowUnknownOption(true)
11
+ .action(async (name) => makeProvider(name, rawArgs.filter((arg) => arg.startsWith("--"))));
12
+ }
@@ -1,77 +1,77 @@
1
- import { and, count, desc, eq, sql } from "drizzle-orm";
2
- import type { Request, Response } from "express";
3
- import { db } from "@/framework/facade.js";
4
- import { notifications } from "@/modules/auth/database/models/notifications.js";
5
-
6
- type AuthRequest = Request & { auth: { id: number } };
7
-
8
- export const list = async (req: AuthRequest, res: Response) => {
9
- const userId = req.auth.id;
10
- const page = Math.max(1, Number(req.query.page) || 1);
11
- const perPage = Math.min(50, Math.max(1, Number(req.query.perPage) || 20));
12
-
13
- const unread = await db.select({ count: count() }).from(notifications)
14
- .where(and(eq(notifications.userId, userId), sql`${notifications.readAt} IS NULL`));
15
- const total = await db.select({ count: count() }).from(notifications)
16
- .where(eq(notifications.userId, userId));
17
- const rows = await db.select().from(notifications)
18
- .where(eq(notifications.userId, userId))
19
- .orderBy(desc(notifications.createdAt))
20
- .limit(perPage)
21
- .offset((page - 1) * perPage);
22
-
23
- res.json({
24
- data: rows.map(normalize),
25
- total: total[0].count,
26
- unread: unread[0].count,
27
- page,
28
- perPage
29
- });
30
- };
31
-
32
- export const unreadCount = async (req: AuthRequest, res: Response) => {
33
- const userId = req.auth.id;
34
- const rows = await db.select({ count: count() }).from(notifications)
35
- .where(and(eq(notifications.userId, userId), sql`${notifications.readAt} IS NULL`));
36
- res.json({ count: rows[0].count });
37
- };
38
-
39
- export const markRead = async (req: AuthRequest, res: Response) => {
40
- const id = Number(req.params.id);
41
- await db.update(notifications)
42
- .set({ readAt: sql`now()` })
43
- .where(and(eq(notifications.id, id), eq(notifications.userId, req.auth.id)));
44
- res.json({ ok: true });
45
- };
46
-
47
- export const markAllRead = async (req: AuthRequest, res: Response) => {
48
- await db.update(notifications)
49
- .set({ readAt: sql`now()` })
50
- .where(and(eq(notifications.userId, req.auth.id), sql`${notifications.readAt} IS NULL`));
51
- res.json({ ok: true });
52
- };
53
-
54
- export const remove = async (req: AuthRequest, res: Response) => {
55
- const id = Number(req.params.id);
56
- await db.delete(notifications)
57
- .where(and(eq(notifications.id, id), eq(notifications.userId, req.auth.id)));
58
- res.json({ ok: true });
59
- };
60
-
61
- function normalize(row: typeof notifications.$inferSelect) {
62
- let parsed: unknown = null;
63
- if (row.data) {
64
- try { parsed = JSON.parse(row.data); } catch {}
65
- }
66
- return {
67
- id: row.id,
68
- userId: row.userId,
69
- type: row.type,
70
- title: row.title,
71
- body: row.body,
72
- data: parsed,
73
- link: row.link,
74
- readAt: row.readAt?.toISOString() || null,
75
- createdAt: row.createdAt.toISOString(),
76
- };
77
- }
1
+ import { and, count, desc, eq, sql } from "drizzle-orm";
2
+ import type { Request, Response } from "express";
3
+ import { db } from "@/framework/facade.js";
4
+ import { notifications } from "@/modules/auth/database/models/notification.js";
5
+
6
+ type AuthRequest = Request & { auth: { id: number } };
7
+
8
+ export const list = async (req: AuthRequest, res: Response) => {
9
+ const userId = req.auth.id;
10
+ const page = Math.max(1, Number(req.query.page) || 1);
11
+ const perPage = Math.min(50, Math.max(1, Number(req.query.perPage) || 20));
12
+
13
+ const unread = await db.select({ count: count() }).from(notifications)
14
+ .where(and(eq(notifications.userId, userId), sql`${notifications.readAt} IS NULL`));
15
+ const total = await db.select({ count: count() }).from(notifications)
16
+ .where(eq(notifications.userId, userId));
17
+ const rows = await db.select().from(notifications)
18
+ .where(eq(notifications.userId, userId))
19
+ .orderBy(desc(notifications.createdAt))
20
+ .limit(perPage)
21
+ .offset((page - 1) * perPage);
22
+
23
+ res.json({
24
+ data: rows.map(normalize),
25
+ total: total[0].count,
26
+ unread: unread[0].count,
27
+ page,
28
+ perPage
29
+ });
30
+ };
31
+
32
+ export const unreadCount = async (req: AuthRequest, res: Response) => {
33
+ const userId = req.auth.id;
34
+ const rows = await db.select({ count: count() }).from(notifications)
35
+ .where(and(eq(notifications.userId, userId), sql`${notifications.readAt} IS NULL`));
36
+ res.json({ count: rows[0].count });
37
+ };
38
+
39
+ export const markRead = async (req: AuthRequest, res: Response) => {
40
+ const id = Number(req.params.id);
41
+ await db.update(notifications)
42
+ .set({ readAt: sql`now()` })
43
+ .where(and(eq(notifications.id, id), eq(notifications.userId, req.auth.id)));
44
+ res.json({ ok: true });
45
+ };
46
+
47
+ export const markAllRead = async (req: AuthRequest, res: Response) => {
48
+ await db.update(notifications)
49
+ .set({ readAt: sql`now()` })
50
+ .where(and(eq(notifications.userId, req.auth.id), sql`${notifications.readAt} IS NULL`));
51
+ res.json({ ok: true });
52
+ };
53
+
54
+ export const remove = async (req: AuthRequest, res: Response) => {
55
+ const id = Number(req.params.id);
56
+ await db.delete(notifications)
57
+ .where(and(eq(notifications.id, id), eq(notifications.userId, req.auth.id)));
58
+ res.json({ ok: true });
59
+ };
60
+
61
+ function normalize(row: typeof notifications.$inferSelect) {
62
+ let parsed: unknown = null;
63
+ if (row.data) {
64
+ try { parsed = JSON.parse(row.data); } catch {}
65
+ }
66
+ return {
67
+ id: row.id,
68
+ userId: row.userId,
69
+ type: row.type,
70
+ title: row.title,
71
+ body: row.body,
72
+ data: parsed,
73
+ link: row.link,
74
+ readAt: row.readAt?.toISOString() || null,
75
+ createdAt: row.createdAt.toISOString(),
76
+ };
77
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Providers for the {{module}} module.
3
+ *
4
+ * This file is imported automatically at boot, so whatever it registers is
5
+ * available to the whole application. Use this to plug implementations (e.g.
6
+ * auth, payments) into the app - not to export the module's API to other
7
+ * modules (that is what facade.ts is for):
8
+ *
9
+ * import { registerProvider } from "@/framework/facade.js";
10
+ *
11
+ * registerProvider("{{module}}", { ... });
12
+ *
13
+ * A module with more than one provider keeps them in providers/*.ts instead -
14
+ * those files are imported at boot too.
15
+ *
16
+ * Rule: a module never imports another module's provider implementation.
17
+ */
18
+
19
+ export {};
@@ -0,0 +1,21 @@
1
+ import { registerProvider } from "@/framework/facade.js";
2
+
3
+ /**
4
+ * Why: Plugs the {{name}} implementation into the application.
5
+ * When: Imported automatically at boot from each module's providers folder.
6
+ * Where: src/modules/{{module}}/providers/{{name}}.ts
7
+ *
8
+ * Reach it from anywhere in the app:
9
+ *
10
+ * import { getProvider } from "@/framework/facade.js";
11
+ * const {{camel}} = getProvider<typeof {{camel}}Provider>("{{name}}");
12
+ *
13
+ * Rule: a provider never imports another module's provider implementation.
14
+ */
15
+ export const {{camel}}Provider = {
16
+ async handle(input: unknown) {
17
+ return input;
18
+ }
19
+ };
20
+
21
+ registerProvider("{{name}}", {{camel}}Provider);
@@ -0,0 +1,21 @@
1
+ import { registerProvider } from "@/framework/facade.js";
2
+
3
+ /**
4
+ * Why: Plugs the {{name}} implementation into the application.
5
+ * When: Imported automatically at boot from src/providers/.
6
+ * Where: src/providers/{{name}}.ts
7
+ *
8
+ * Reach it from anywhere in the app:
9
+ *
10
+ * import { getProvider } from "@/framework/facade.js";
11
+ * const {{camel}} = getProvider<typeof {{camel}}Provider>("{{name}}");
12
+ *
13
+ * Rule: a provider never imports another provider's implementation.
14
+ */
15
+ export const {{camel}}Provider = {
16
+ async handle(input: unknown) {
17
+ return input;
18
+ }
19
+ };
20
+
21
+ registerProvider("{{name}}", {{camel}}Provider);
@@ -89,7 +89,11 @@ const COMMAND_GROUPS = [
89
89
  // `module:make-middleware` writes inside one module and applies only to it.
90
90
  // In the same section the two look interchangeable, which is exactly the
91
91
  // mistake a developer is about to make.
92
- commands: ["middleware:make"]
92
+ //
93
+ // `provider:make` follows the same split: it writes to src/providers and
94
+ // applies to the whole app, `module:make-provider` writes inside one
95
+ // module and applies only to it.
96
+ commands: ["middleware:make", "provider:make"]
93
97
  },
94
98
  {
95
99
  title: "Generators - modules",
@@ -107,6 +111,7 @@ const COMMAND_GROUPS = [
107
111
  "module:make-test",
108
112
  "module:make-helper",
109
113
  "module:make-middleware",
114
+ "module:make-provider",
110
115
  "module:make-type"
111
116
  ]
112
117
  },
@@ -1,10 +1,27 @@
1
1
  import { eq } from "drizzle-orm";
2
2
  import { db } from "@/framework/database/connection.js";
3
3
  import { dispatchEvent } from "@/framework/events/dispatcher.js";
4
+ import { resolveAuthProvider } from "@/framework/providers/adapter.js";
4
5
  import { redisClientIfReady } from "@/framework/redis/client.js";
5
6
  import { mail } from "@/framework/support/mail.js";
6
- import { notifications } from "@/modules/auth/database/models/notification.js";
7
- import { users } from "@/modules/auth/database/models/user.js";
7
+
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
+ }
8
25
 
9
26
  /** Extra JSON metadata attached to the notification (e.g. entity IDs, links). */
10
27
  export type NotificationOptions = {
@@ -22,6 +39,8 @@ export type NotificationOptions = {
22
39
  broadcast?: boolean;
23
40
  /** Email delivery options */
24
41
  mail?: {
42
+ /** Explicit recipient. Skips the auth provider contact lookup. */
43
+ to?: string;
25
44
  subject?: string;
26
45
  html?: string;
27
46
  };
@@ -29,6 +48,13 @@ export type NotificationOptions = {
29
48
 
30
49
  /** Create a notification and persist it to the database. Optionally broadcast via Socket.IO and/or send by email. */
31
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
+ );
56
+ return null;
57
+ }
32
58
  const insertResult = await db.insert(notifications).values({
33
59
  userId,
34
60
  type: options.type || "info",
@@ -63,9 +89,39 @@ async function sendMailChannel(
63
89
  notification: { id: number; title: string; body: string | null },
64
90
  options: NotificationOptions
65
91
  ) {
66
- const [user] = await db.select({ email: users.email, name: users.name }).from(users).where(eq(users.id, userId));
92
+ let to: string | null = (options.mail as any)?.to || null;
93
+ let name: string | null = null;
94
+
95
+ if (!to) {
96
+ try {
97
+ const provider = await resolveAuthProvider();
98
+ 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
+ }
120
+ }
121
+ } catch {}
122
+ }
67
123
 
68
- if (!user?.email) return;
124
+ if (!to) return;
69
125
 
70
126
  const subject = options.mail?.subject || notification.title;
71
127
  const html = options.mail?.html || defaultMailHtml(notification.title, notification.body);
@@ -74,14 +130,14 @@ async function sendMailChannel(
74
130
  await dispatchEvent(
75
131
  "notification:mail",
76
132
  {
77
- to: user.email,
133
+ to,
78
134
  subject,
79
135
  html
80
136
  },
81
137
  { queue: "mail" }
82
138
  );
83
139
  } else {
84
- await mail.sendMail({ to: user.email, subject, html });
140
+ await mail.sendMail({ to, subject, html });
85
141
  }
86
142
  }
87
143
 
@@ -95,7 +151,7 @@ function escapeHtml(str: string) {
95
151
  }
96
152
 
97
153
  /** Convert a DB row into the normalized response shape, parsing the `data` JSON column. */
98
- function normalize(row: typeof notifications.$inferSelect) {
154
+ function normalize(row: any) {
99
155
  let parsed: unknown = null;
100
156
  if (row.data) {
101
157
  try {