create-nexwire 4.2.1 → 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.
- 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/framework/database/app-schema.ts +37 -0
- package/express/src/framework/database/connection.ts +198 -196
- package/express/src/framework/database/seed.ts +35 -22
- package/express/src/framework/facade.ts +24 -5
- package/express/src/framework/kernel.ts +54 -52
- package/express/src/framework/maker-cli/index.mjs +2 -0
- package/express/src/framework/maker-cli/module/core.mjs +30 -0
- package/express/src/framework/maker-cli/module/index.mjs +10 -1
- package/express/src/framework/maker-cli/provider/core.mjs +49 -0
- package/express/src/framework/maker-cli/provider/index.mjs +12 -0
- package/express/src/framework/maker-cli/stubs/notification/controller.ts.stub +77 -77
- package/express/src/framework/maker-cli/stubs/provider/module-barrel.ts.stub +19 -0
- package/express/src/framework/maker-cli/stubs/provider/module.ts.stub +21 -0
- package/express/src/framework/maker-cli/stubs/provider/name.ts.stub +21 -0
- package/express/src/framework/maker-cli/utils/help.mjs +6 -1
- package/express/src/framework/maker-cli/utils/project-commands.mjs +73 -12
- package/express/src/framework/notification/index.ts +63 -7
- package/express/src/framework/providers/adapter.ts +125 -0
- package/express/src/framework/providers/contracts.ts +56 -0
- package/express/src/framework/providers/loader.ts +105 -0
- package/express/src/framework/providers/registry.ts +62 -0
- package/express/src/middlewares/auth-middleware.ts +65 -38
- package/express/src/providers/README.md +107 -0
- package/express/src/storage/database/nexwire.sqlite +0 -0
- 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/framework/database/app-schema.ts +37 -0
- package/hono/src/framework/database/connection.ts +198 -196
- package/hono/src/framework/database/seed.ts +35 -22
- package/hono/src/framework/facade.ts +48 -29
- package/hono/src/framework/kernel.ts +50 -48
- package/hono/src/framework/maker-cli/index.mjs +2 -0
- package/hono/src/framework/maker-cli/module/core.mjs +30 -0
- package/hono/src/framework/maker-cli/module/index.mjs +10 -1
- package/hono/src/framework/maker-cli/provider/core.mjs +49 -0
- package/hono/src/framework/maker-cli/provider/index.mjs +12 -0
- package/hono/src/framework/maker-cli/stubs/notification/controller.ts.stub +85 -85
- package/hono/src/framework/maker-cli/stubs/provider/module-barrel.ts.stub +19 -0
- package/hono/src/framework/maker-cli/stubs/provider/module.ts.stub +21 -0
- package/hono/src/framework/maker-cli/stubs/provider/name.ts.stub +21 -0
- package/hono/src/framework/maker-cli/utils/help.mjs +6 -1
- package/hono/src/framework/maker-cli/utils/project-commands.mjs +73 -12
- package/hono/src/framework/notification/index.ts +63 -7
- package/hono/src/framework/providers/adapter.ts +125 -0
- package/hono/src/framework/providers/contracts.ts +56 -0
- package/hono/src/framework/providers/loader.ts +105 -0
- package/hono/src/framework/providers/registry.ts +62 -0
- package/hono/src/middlewares/auth-middleware.ts +65 -38
- package/hono/src/providers/README.md +107 -0
- package/hono/src/storage/database/nexwire.sqlite +0 -0
- package/package.json +2 -2
- 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 {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
await
|
|
30
|
-
await
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
},
|
|
@@ -13,8 +13,16 @@ import { PROJECT_COMMAND_FLAG } from "./help.mjs";
|
|
|
13
13
|
* there would be overwritten on the next framework update - and `src/modules/`
|
|
14
14
|
* is swept by the route, job and seeder globs. The project root is the one
|
|
15
15
|
* place nothing else owns.
|
|
16
|
+
*
|
|
17
|
+
* Two names are read: commands.mjs (plain JavaScript, loaded by plain node)
|
|
18
|
+
* and commands.ts (TypeScript, loaded through tsx). The .mjs is the incumbent
|
|
19
|
+
* and wins if a project keeps both.
|
|
16
20
|
*/
|
|
17
21
|
export const PROJECT_COMMANDS_PATH = "maker/commands.mjs";
|
|
22
|
+
export const PROJECT_COMMANDS_TS_PATH = "maker/commands.ts";
|
|
23
|
+
|
|
24
|
+
/** Process-global so a re-evaluated module cannot register tsx twice. */
|
|
25
|
+
const TSX_REGISTERED = "__nexwireMakerTsxRegistered";
|
|
18
26
|
|
|
19
27
|
const PROJECT_COMMANDS_TEMPLATE = `/**
|
|
20
28
|
* Your project's maker commands.
|
|
@@ -36,8 +44,11 @@ const PROJECT_COMMANDS_TEMPLATE = `/**
|
|
|
36
44
|
* which one. Rename yours; the built-in stays.
|
|
37
45
|
* - An error in this file is reported, never swallowed. If your commands stop
|
|
38
46
|
* appearing, the fault is here rather than in your command code.
|
|
39
|
-
* -
|
|
40
|
-
*
|
|
47
|
+
* - This file runs as plain JavaScript under plain Node, so it cannot import
|
|
48
|
+
* your TypeScript source or use your tsconfig path alias. If a command
|
|
49
|
+
* needs either, name this file maker/commands.ts instead and delete this
|
|
50
|
+
* one - nexwire loads that file through tsx, with your tsconfig applied.
|
|
51
|
+
* For editor type support, create maker/tsconfig.json as shown in the docs.
|
|
41
52
|
*
|
|
42
53
|
* Replace or delete the example below once you have a command of your own.
|
|
43
54
|
*/
|
|
@@ -54,14 +65,47 @@ export default function register(program) {
|
|
|
54
65
|
}
|
|
55
66
|
`;
|
|
56
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Why: Plain node cannot import TypeScript, so maker/commands.ts needs tsx
|
|
70
|
+
* registered before it is imported. Registered at most once per process
|
|
71
|
+
* and never unregistered: two overlapping registrations corrupt the
|
|
72
|
+
* loader, and commands routinely import more TypeScript at action time,
|
|
73
|
+
* long after this function returns. The flag lives on globalThis because
|
|
74
|
+
* this module can be evaluated more than once in one process (vitest's
|
|
75
|
+
* per-file isolation) while the loader hooks are process-wide.
|
|
76
|
+
* When: Only when the project's file is TypeScript - maker/commands.mjs never
|
|
77
|
+
* pays for tsx being installed.
|
|
78
|
+
* Where: utils/project-commands.mjs.
|
|
79
|
+
*/
|
|
80
|
+
async function registerTsLoader(rel) {
|
|
81
|
+
if (globalThis[TSX_REGISTERED]) return;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const { register } = await import("tsx/esm/api");
|
|
85
|
+
register();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`${rel} needs tsx to load TypeScript, but it could not be registered: ${error?.message || error}. tsx ships as a devDependency of every nexwire project - run npm install, or rename the file to ${PROJECT_COMMANDS_PATH}.`,
|
|
89
|
+
{ cause: error }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
globalThis[TSX_REGISTERED] = true;
|
|
94
|
+
}
|
|
95
|
+
|
|
57
96
|
/**
|
|
58
97
|
* Why: Registers the commands a project defined for itself.
|
|
59
98
|
* When: Every maker invocation, right after the framework's own registrars.
|
|
60
99
|
* Where: Maker CLI entry flow, from utils/project-commands.mjs.
|
|
61
|
-
* How: Imports maker/commands.mjs
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
100
|
+
* How: Imports maker/commands.mjs - or maker/commands.ts, loaded through tsx,
|
|
101
|
+
* which resolves the project's tsconfig path aliases - resolved from
|
|
102
|
+
* process.cwd(), not from import.meta.dirname, which points inside
|
|
103
|
+
* node_modules and would find the framework's own directory instead of the
|
|
104
|
+
* project. If a project keeps both files the .mjs wins and the .ts file is
|
|
105
|
+
* skipped with a warning naming it, so neither file is ever silently dead:
|
|
106
|
+
* a project that already runs must not be re-registered by a file that
|
|
107
|
+
* appeared later, and a file that is skipped must say so. Absent is silent
|
|
108
|
+
* and free; broken is loud, because a developer's commands silently not
|
|
65
109
|
* existing is a far worse experience than an error message. A name that
|
|
66
110
|
* collides with a framework command is refused by commander itself, which
|
|
67
111
|
* is the right outcome: the built-in wins and the project cannot shadow a
|
|
@@ -70,10 +114,23 @@ export default function register(program) {
|
|
|
70
114
|
* asked for the name.
|
|
71
115
|
*/
|
|
72
116
|
export async function registerProjectCommands(program, rawArgs, cwd = process.cwd()) {
|
|
73
|
-
const
|
|
117
|
+
const mjsFile = path.resolve(cwd, PROJECT_COMMANDS_PATH);
|
|
118
|
+
const tsFile = path.resolve(cwd, PROJECT_COMMANDS_TS_PATH);
|
|
119
|
+
const hasMjs = fs.existsSync(mjsFile);
|
|
120
|
+
const hasTs = fs.existsSync(tsFile);
|
|
121
|
+
|
|
122
|
+
if (!hasMjs && !hasTs) {
|
|
123
|
+
return { loaded: false, file: mjsFile, added: [] };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const isTs = !hasMjs && hasTs;
|
|
127
|
+
const file = isTs ? tsFile : mjsFile;
|
|
128
|
+
const rel = isTs ? PROJECT_COMMANDS_TS_PATH : PROJECT_COMMANDS_PATH;
|
|
74
129
|
|
|
75
|
-
if (
|
|
76
|
-
|
|
130
|
+
if (hasMjs && hasTs) {
|
|
131
|
+
console.warn(
|
|
132
|
+
`${PROJECT_COMMANDS_TS_PATH} is ignored because ${PROJECT_COMMANDS_PATH} exists. Keep one - delete the file you are not using.`
|
|
133
|
+
);
|
|
77
134
|
}
|
|
78
135
|
|
|
79
136
|
// Not wrapped in a bare try/catch on purpose. A file that exists but fails to
|
|
@@ -81,18 +138,22 @@ export async function registerProjectCommands(program, rawArgs, cwd = process.cw
|
|
|
81
138
|
// command that is actually in this loader. The catches below only attach the
|
|
82
139
|
// file name, because an error from inside the project file otherwise reads as
|
|
83
140
|
// if it came from the framework.
|
|
141
|
+
if (isTs) {
|
|
142
|
+
await registerTsLoader(rel);
|
|
143
|
+
}
|
|
144
|
+
|
|
84
145
|
let module;
|
|
85
146
|
try {
|
|
86
147
|
module = await import(pathToFileURL(file).href);
|
|
87
148
|
} catch (error) {
|
|
88
|
-
throw new Error(`Could not load ${
|
|
149
|
+
throw new Error(`Could not load ${rel}: ${error?.message || error}`, { cause: error });
|
|
89
150
|
}
|
|
90
151
|
|
|
91
152
|
const register = module.default ?? module.register;
|
|
92
153
|
|
|
93
154
|
if (typeof register !== "function") {
|
|
94
155
|
throw new TypeError(
|
|
95
|
-
`${
|
|
156
|
+
`${rel} must export a default function (program, args) => void. Found: ${typeof register}.`
|
|
96
157
|
);
|
|
97
158
|
}
|
|
98
159
|
|
|
@@ -101,7 +162,7 @@ export async function registerProjectCommands(program, rawArgs, cwd = process.cw
|
|
|
101
162
|
try {
|
|
102
163
|
await register(program, rawArgs);
|
|
103
164
|
} catch (error) {
|
|
104
|
-
throw new Error(`Could not register commands from ${
|
|
165
|
+
throw new Error(`Could not register commands from ${rel}: ${error?.message || error}`, {
|
|
105
166
|
cause: error
|
|
106
167
|
});
|
|
107
168
|
}
|