create-nexwire 4.2.2 → 4.2.4
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/package.json +48 -23
- package/express/src/database/migrations/sqlite/0000_init.sql +47 -60
- package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -89
- package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
- package/express/src/database/schema.ts +2 -3
- 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 +35 -5
- package/express/src/framework/kernel.ts +54 -52
- package/express/src/framework/maker-cli/db/core.mjs +291 -288
- package/express/src/framework/maker-cli/index.mjs +2 -0
- package/express/src/framework/maker-cli/module/core.mjs +155 -87
- package/express/src/framework/maker-cli/module/index.mjs +13 -4
- 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 +76 -77
- package/express/src/framework/maker-cli/stubs/notification/job.ts.stub +11 -13
- package/express/src/framework/maker-cli/stubs/notification/model.ts.stub +29 -0
- package/express/src/framework/maker-cli/stubs/notification/provider.ts.stub +174 -0
- package/express/src/framework/maker-cli/stubs/notification/route.api.ts.stub +11 -10
- package/express/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +1 -1
- package/express/src/framework/maker-cli/stubs/notification/schema.plain.ts.stub +25 -25
- package/express/src/framework/maker-cli/stubs/notification/schema.ts.stub +25 -25
- 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/notification/index.ts +65 -47
- package/express/src/framework/providers/adapter.ts +139 -0
- package/express/src/framework/providers/contracts.ts +126 -0
- package/express/src/framework/providers/loader.ts +105 -0
- package/express/src/framework/providers/registry.ts +88 -0
- package/express/src/middlewares/auth-middleware.ts +65 -38
- package/express/src/providers/README.md +107 -0
- package/express/src/resources/src/main.ts +1 -1
- package/express/src/resources/src/pages/auth/forgetPassword.vue +1 -1
- package/express/src/resources/src/pages/auth/login.vue +1 -1
- package/express/src/resources/src/pages/auth/register.vue +1 -1
- package/express/src/resources/src/pages/auth/resetPassword.vue +1 -1
- package/express/src/resources/src/pages/auth/verifyEmail.vue +1 -1
- package/express/src/resources/src/pages/dashboard/index.vue +66 -66
- package/express/src/storage/database/nexwire.sqlite +0 -0
- package/hono/package.json +44 -19
- package/hono/src/database/migrations/sqlite/0000_init.sql +47 -60
- package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -89
- package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
- package/hono/src/database/schema.ts +2 -3
- 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 +59 -29
- package/hono/src/framework/kernel.ts +50 -48
- package/hono/src/framework/maker-cli/db/core.mjs +291 -288
- package/hono/src/framework/maker-cli/index.mjs +2 -0
- package/hono/src/framework/maker-cli/module/core.mjs +123 -52
- package/hono/src/framework/maker-cli/module/index.mjs +13 -4
- 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 +90 -85
- package/hono/src/framework/maker-cli/stubs/notification/job.ts.stub +10 -12
- package/hono/src/framework/maker-cli/stubs/notification/model.ts.stub +29 -0
- package/hono/src/framework/maker-cli/stubs/notification/provider.ts.stub +174 -0
- package/hono/src/framework/maker-cli/stubs/notification/route.api.ts.stub +11 -10
- package/hono/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +1 -1
- package/hono/src/framework/maker-cli/stubs/notification/schema.plain.ts.stub +25 -25
- package/hono/src/framework/maker-cli/stubs/notification/schema.ts.stub +25 -25
- 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/notification/index.ts +65 -47
- package/hono/src/framework/providers/adapter.ts +139 -0
- package/hono/src/framework/providers/contracts.ts +126 -0
- package/hono/src/framework/providers/loader.ts +105 -0
- package/hono/src/framework/providers/registry.ts +88 -0
- package/hono/src/middlewares/auth-middleware.ts +65 -38
- package/hono/src/providers/README.md +107 -0
- package/hono/src/resources/src/main.ts +1 -1
- package/hono/src/resources/src/pages/auth/forgetPassword.vue +1 -1
- package/hono/src/resources/src/pages/auth/login.vue +1 -1
- package/hono/src/resources/src/pages/auth/register.vue +1 -1
- package/hono/src/resources/src/pages/auth/resetPassword.vue +1 -1
- package/hono/src/resources/src/pages/auth/verifyEmail.vue +1 -1
- package/hono/src/resources/src/pages/dashboard/index.vue +66 -66
- package/hono/src/storage/database/nexwire.sqlite +0 -0
- package/package.json +10 -10
- package/src/index.js +14 -6
- package/express/src/modules/auth/database/models/notification.ts +0 -24
- package/hono/src/modules/auth/database/models/notification.ts +0 -24
|
@@ -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
|
},
|
|
@@ -68,6 +73,8 @@ const STUBS = {
|
|
|
68
73
|
},
|
|
69
74
|
routeApi: "notification/route.api.ts.stub",
|
|
70
75
|
routePlain: "notification/route.plain.ts.stub",
|
|
76
|
+
model: "notification/model.ts.stub",
|
|
77
|
+
provider: "notification/provider.ts.stub",
|
|
71
78
|
job: "notification/job.ts.stub"
|
|
72
79
|
},
|
|
73
80
|
test: {
|
|
@@ -157,6 +164,28 @@ function parsePanelPath(flags) {
|
|
|
157
164
|
return segments.join("/");
|
|
158
165
|
}
|
|
159
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Parse `--notifiable=<type>` into the owner class written into the generated
|
|
169
|
+
* controller. Accepts both `--notifiable=user` and `--notifiable user`, and
|
|
170
|
+
* defaults to `user` so the common case needs no flag.
|
|
171
|
+
*/
|
|
172
|
+
function parseNotifiable(flags) {
|
|
173
|
+
const matches = flags
|
|
174
|
+
.map((flag, index) => ({ flag, index }))
|
|
175
|
+
.filter(({ flag }) => typeof flag === "string" && flag.startsWith("--notifiable"));
|
|
176
|
+
if (matches.length === 0) return "user";
|
|
177
|
+
if (matches.length > 1) throw new Error("--notifiable can only be given once.");
|
|
178
|
+
|
|
179
|
+
const [{ flag, index }] = matches;
|
|
180
|
+
let raw = flag.slice("--notifiable".length).replace(/^=/, "").trim();
|
|
181
|
+
if (!raw && typeof flags[index + 1] === "string" && !flags[index + 1].startsWith("--")) raw = flags[index + 1].trim();
|
|
182
|
+
if (!raw) throw new Error("--notifiable needs a value, e.g. --notifiable=user");
|
|
183
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(raw)) {
|
|
184
|
+
throw new Error(`Invalid --notifiable value "${raw}". Use lowercase letters, numbers, dashes and underscores.`);
|
|
185
|
+
}
|
|
186
|
+
return raw;
|
|
187
|
+
}
|
|
188
|
+
|
|
160
189
|
/** True when the path is a directory holding at least one entry. */
|
|
161
190
|
async function dirHasFiles(target) {
|
|
162
191
|
try {
|
|
@@ -167,6 +196,24 @@ async function dirHasFiles(target) {
|
|
|
167
196
|
}
|
|
168
197
|
}
|
|
169
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Path of the notification store already shipped by any module, or "".
|
|
201
|
+
*
|
|
202
|
+
* The table/model/provider is a singleton: two modules shipping it would
|
|
203
|
+
* generate the same `notifications` table twice and register two providers for
|
|
204
|
+
* one contract. `module:make-notification` therefore writes the store only when
|
|
205
|
+
* no module owns it yet, and otherwise adds just another HTTP surface over it.
|
|
206
|
+
*/
|
|
207
|
+
async function findNotificationStore() {
|
|
208
|
+
const backendSrc = path.resolve(process.cwd(), "src");
|
|
209
|
+
const files = await glob("modules/**/database/models/notification.{ts,js}", {
|
|
210
|
+
cwd: backendSrc,
|
|
211
|
+
nodir: true,
|
|
212
|
+
windowsPathsNoEscape: true
|
|
213
|
+
});
|
|
214
|
+
return files.sort()[0] ? path.join(backendSrc, files.sort()[0]) : "";
|
|
215
|
+
}
|
|
216
|
+
|
|
170
217
|
/**
|
|
171
218
|
* Resolve the module a subcommand targets.
|
|
172
219
|
*
|
|
@@ -287,8 +334,8 @@ async function serviceFile(moduleName, controllerName, withModel) {
|
|
|
287
334
|
|
|
288
335
|
/** Build the full default scaffold for module:make. */
|
|
289
336
|
async function moduleFiles(moduleName, openApi) {
|
|
290
|
-
|
|
291
|
-
|
|
337
|
+
const name = leafName(moduleName);
|
|
338
|
+
const vars = moduleFileVars(moduleName, name);
|
|
292
339
|
const controllerStub = openApi ? STUBS.controller.openapi : STUBS.controller.plain;
|
|
293
340
|
return {
|
|
294
341
|
"facade.ts": await stub(STUBS.facade.named, vars),
|
|
@@ -304,15 +351,15 @@ async function moduleFiles(moduleName, openApi) {
|
|
|
304
351
|
* module, so the controller and schema it just created are known to exist.
|
|
305
352
|
*/
|
|
306
353
|
async function routeTemplate(moduleName, controllerName = moduleName) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
354
|
+
const controller = leafName(controllerName);
|
|
355
|
+
const routeStub = openApiEnabled() ? STUBS.route.api : STUBS.route.plain;
|
|
356
|
+
return await stub(routeStub, {
|
|
357
|
+
module: moduleName,
|
|
358
|
+
controller,
|
|
359
|
+
ClassName: pascal(controller),
|
|
360
|
+
ModuleClass: moduleLabel(moduleName)
|
|
361
|
+
});
|
|
362
|
+
}
|
|
316
363
|
|
|
317
364
|
/**
|
|
318
365
|
* Generate a barebone route file that wires to nothing. module:make-route uses
|
|
@@ -320,13 +367,13 @@ async function routeTemplate(moduleName, controllerName = moduleName) {
|
|
|
320
367
|
*/
|
|
321
368
|
async function standaloneRouteTemplate(moduleName, routeName) {
|
|
322
369
|
const routeStub = openApiEnabled() ? STUBS.route.standaloneApi : STUBS.route.standalonePlain;
|
|
323
|
-
return await stub(routeStub, {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
370
|
+
return await stub(routeStub, {
|
|
371
|
+
module: moduleName,
|
|
372
|
+
controller: routeName,
|
|
373
|
+
ClassName: pascal(routeName),
|
|
374
|
+
ModuleClass: moduleLabel(moduleName)
|
|
375
|
+
});
|
|
376
|
+
}
|
|
330
377
|
|
|
331
378
|
/** Generate a named model file for a module. */
|
|
332
379
|
async function namedModelTemplate(moduleName, name, dialect) {
|
|
@@ -506,7 +553,7 @@ export async function makeRoute(rawModule, rawControllerOrFlag, extraFlags = [])
|
|
|
506
553
|
try {
|
|
507
554
|
await fs.access(routePath);
|
|
508
555
|
exists = true;
|
|
509
|
-
} catch {}
|
|
556
|
+
} catch { }
|
|
510
557
|
if (exists && !force)
|
|
511
558
|
throw new Error(`Route file already exists: ${path.relative(process.cwd(), routePath)}. Re-run with --force to overwrite.`);
|
|
512
559
|
await fs.mkdir(path.dirname(routePath), { recursive: true });
|
|
@@ -514,32 +561,57 @@ export async function makeRoute(rawModule, rawControllerOrFlag, extraFlags = [])
|
|
|
514
561
|
console.log(`Route ready: ${path.relative(process.cwd(), routePath)}`);
|
|
515
562
|
}
|
|
516
563
|
|
|
517
|
-
/**
|
|
564
|
+
/**
|
|
565
|
+
* Generate the notification HTTP surface, plus the shared store the first time.
|
|
566
|
+
*
|
|
567
|
+
* The controller/schema/routes are always written: they are what lets a project
|
|
568
|
+
* expose `/api/notification/*` for a panel. The model, provider and mail job are
|
|
569
|
+
* written only when no module already ships the store, because the store is a
|
|
570
|
+
* singleton — a second copy would create a duplicate `notifications` table and
|
|
571
|
+
* register a second provider for the same contract.
|
|
572
|
+
*/
|
|
518
573
|
export async function makeNotificationModule(rawName = "notification", flags = []) {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
[`controllers/${leaf}.ts`]: await stub(STUBS.notification.controller, {
|
|
529
|
-
module: moduleName
|
|
530
|
-
}),
|
|
531
|
-
[`schemas/${leaf}.ts`]: await stub(openApi ? STUBS.notification.schema.openapi : STUBS.notification.schema.plain, {
|
|
532
|
-
module: moduleName
|
|
533
|
-
}),
|
|
534
|
-
"routes/index.ts": await stub(openApi ? STUBS.notification.routeApi : STUBS.notification.routePlain, { module: moduleName }),
|
|
535
|
-
[`jobs/${leaf}.ts`]: await stub(STUBS.notification.job, { module: moduleName })
|
|
536
|
-
});
|
|
574
|
+
const { moduleName, leaf } = resolveModuleRef(rawName, flags);
|
|
575
|
+
const root = moduleRoot(moduleName);
|
|
576
|
+
const force = hasFlag(flags, "--force") || hasFlag(flags, "--yes");
|
|
577
|
+
if (!force && (await dirHasFiles(root))) {
|
|
578
|
+
throw new Error(`Module already exists: src/modules/${moduleName}. Re-run with --force to overwrite its files.`);
|
|
579
|
+
}
|
|
580
|
+
const openApi = openApiEnabled();
|
|
581
|
+
const notifiable = parseNotifiable(flags);
|
|
582
|
+
const vars = { module: moduleName, leaf, notifiable };
|
|
537
583
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
584
|
+
const files = {
|
|
585
|
+
[`controllers/${leaf}.ts`]: await stub(STUBS.notification.controller, vars),
|
|
586
|
+
[`schemas/${leaf}.ts`]: await stub(openApi ? STUBS.notification.schema.openapi : STUBS.notification.schema.plain, vars),
|
|
587
|
+
"routes/index.ts": await stub(openApi ? STUBS.notification.routeApi : STUBS.notification.routePlain, vars)
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const existingStore = await findNotificationStore();
|
|
591
|
+
const ownsStore = existingStore ? path.resolve(existingStore).startsWith(root + path.sep) : false;
|
|
592
|
+
const writesStore = !existingStore || (force && ownsStore);
|
|
593
|
+
|
|
594
|
+
if (writesStore) {
|
|
595
|
+
files["database/models/notification.ts"] = await stub(STUBS.notification.model, vars);
|
|
596
|
+
files["providers/notification.ts"] = await stub(STUBS.notification.provider, vars);
|
|
597
|
+
files["jobs/mail.ts"] = await stub(STUBS.notification.job, vars);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
await writeFiles(root, files);
|
|
601
|
+
|
|
602
|
+
console.log(`Notification module ready: ${moduleName}`);
|
|
603
|
+
if (writesStore) {
|
|
604
|
+
console.log("Notification store included: model, provider, mail job");
|
|
605
|
+
const panelFlag = panelOf(moduleName) ? ` --path=${panelOf(moduleName)}` : "";
|
|
606
|
+
console.log(`Next: bun maker module:migrate ${leaf}${panelFlag}`);
|
|
607
|
+
} else {
|
|
608
|
+
console.log(`Notification store reused: ${path.relative(process.cwd(), existingStore)}`);
|
|
542
609
|
}
|
|
610
|
+
if (panelOf(moduleName)) console.log(`Panel: ${panelOf(moduleName)} -> /api/${moduleName}`);
|
|
611
|
+
console.log(`Notifiable type: ${notifiable}`);
|
|
612
|
+
console.log(`Route style: ${openApi ? "openapi" : "plain"}`);
|
|
613
|
+
console.log("See docs (packages/docs/guide/notification.md) for usage and UI integration.");
|
|
614
|
+
}
|
|
543
615
|
|
|
544
616
|
/** Soft-delete a module by moving it to storage trash. */
|
|
545
617
|
export async function deleteModule(rawName, flags = []) {
|
|
@@ -684,9 +756,7 @@ export async function makeController(rawModule, rawControllerOrFlag, extraFlags
|
|
|
684
756
|
const { name, flags } = parseNameArg(rawControllerOrFlag, extraFlags, "Controller name", leaf);
|
|
685
757
|
const root = await assertModuleExists(moduleName);
|
|
686
758
|
const lower = name.toLowerCase();
|
|
687
|
-
const hasService =
|
|
688
|
-
(await pathExists(path.join(root, "services", `${lower}.ts`))) ||
|
|
689
|
-
(await pathExists(legacyPath(root, "services", lower)));
|
|
759
|
+
const hasService = (await pathExists(path.join(root, "services", `${lower}.ts`))) || (await pathExists(legacyPath(root, "services", lower)));
|
|
690
760
|
const content = await controllerFile(moduleName, name, openApiEnabled(), hasService);
|
|
691
761
|
await writeFileSafe(path.join(root, `controllers/${lower}.ts`), content, flags, "Controller file");
|
|
692
762
|
}
|
|
@@ -710,8 +780,7 @@ export async function makeService(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
|
710
780
|
const { name, flags } = parseNameArg(rawNameOrFlag, extraFlags, "Service name", leaf);
|
|
711
781
|
const root = await assertModuleExists(moduleName);
|
|
712
782
|
const lower = name.toLowerCase();
|
|
713
|
-
const withModel =
|
|
714
|
-
hasFlag(flags, "--with-model") || (await pathExists(path.join(root, "database", "models", `${lower}.ts`)));
|
|
783
|
+
const withModel = hasFlag(flags, "--with-model") || (await pathExists(path.join(root, "database", "models", `${lower}.ts`)));
|
|
715
784
|
const content = await serviceFile(moduleName, name, withModel);
|
|
716
785
|
await writeFileSafe(path.join(root, `services/${lower}.ts`), content, flags, "Service file");
|
|
717
786
|
}
|
|
@@ -739,6 +808,31 @@ export async function makeLocalMiddleware(rawModule, rawNameOrFlag, extraFlags =
|
|
|
739
808
|
await writeFileSafe(path.join(root, `middlewares/${name}.ts`), content, flags, "Middleware file");
|
|
740
809
|
}
|
|
741
810
|
|
|
811
|
+
/**
|
|
812
|
+
* Generate a provider file for an existing module.
|
|
813
|
+
*
|
|
814
|
+
* The file lands in the module's providers/ folder, which the framework
|
|
815
|
+
* imports at boot just like src/providers/. That is what lets a module offer
|
|
816
|
+
* an implementation to the whole application without the application knowing
|
|
817
|
+
* anything about the module's internals.
|
|
818
|
+
*/
|
|
819
|
+
export async function makeModuleProvider(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
820
|
+
const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
|
|
821
|
+
const { name, flags } = parseNameArg(rawNameOrFlag, extraFlags, "Provider name", leaf);
|
|
822
|
+
const root = await assertModuleExists(moduleName);
|
|
823
|
+
const filePath = path.join(root, `providers/${name}.ts`);
|
|
824
|
+
if (hasFlag(flags, "--dry-run")) {
|
|
825
|
+
console.log(`Would create provider file: ${path.relative(process.cwd(), filePath)}`);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const content = await stub(STUBS.provider.module, {
|
|
829
|
+
module: moduleName,
|
|
830
|
+
name,
|
|
831
|
+
camel: camelCase(name)
|
|
832
|
+
});
|
|
833
|
+
await writeFileSafe(filePath, content, flags, "Provider file");
|
|
834
|
+
}
|
|
835
|
+
|
|
742
836
|
/** Generate a types file for an existing module. */
|
|
743
837
|
export async function makeType(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
744
838
|
const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
|
|
@@ -926,8 +1020,16 @@ export async function generateSchema(options = {}) {
|
|
|
926
1020
|
if (!options.silent) console.log(`Generated database schema with ${files.length} model file(s)`);
|
|
927
1021
|
}
|
|
928
1022
|
|
|
929
|
-
/**
|
|
930
|
-
|
|
1023
|
+
/**
|
|
1024
|
+
* Generate and run migrations that include a module.
|
|
1025
|
+
*
|
|
1026
|
+
* Drizzle diffs the schema against the last snapshot, so generating from a
|
|
1027
|
+
* module-only schema would emit `DROP TABLE` for every other module (auth
|
|
1028
|
+
* included). The full schema is used instead: it adds the module's tables and
|
|
1029
|
+
* keeps the rest of the database intact.
|
|
1030
|
+
*/
|
|
1031
|
+
export async function runModuleMigrate(rawModuleName, rawArgs = []) {
|
|
1032
|
+
const { moduleName } = resolveModuleRef(rawModuleName, rawArgs);
|
|
931
1033
|
await assertModuleExists(moduleName);
|
|
932
1034
|
const backendSrc = path.resolve(process.cwd(), "src");
|
|
933
1035
|
const files = await glob(`modules/${moduleName}/database/models/*.{ts,js}`, {
|
|
@@ -936,47 +1038,13 @@ async function generateModuleSchemaTemp(moduleName) {
|
|
|
936
1038
|
windowsPathsNoEscape: true
|
|
937
1039
|
});
|
|
938
1040
|
if (!files.length) throw new Error(`No model files found for module '${moduleName}'.`);
|
|
939
|
-
const exports = [];
|
|
940
|
-
const tempDir = path.resolve(process.cwd(), "src/storage/tmp");
|
|
941
|
-
await fs.mkdir(tempDir, { recursive: true });
|
|
942
|
-
// Flatten the panel path: "admin/testme" must not become a nested
|
|
943
|
-
// storage/tmp/schema.admin/testme.<ts>.ts path.
|
|
944
|
-
const slug = moduleName.replaceAll("/", "-");
|
|
945
|
-
const tempSchemaPath = path.join(tempDir, `schema.${slug}.${Date.now()}.ts`);
|
|
946
|
-
const tempSchemaDir = path.dirname(tempSchemaPath);
|
|
947
|
-
for (const file of files.sort()) {
|
|
948
|
-
const absoluteFile = path.join(backendSrc, file);
|
|
949
|
-
const relativePath = path
|
|
950
|
-
.relative(tempSchemaDir, absoluteFile)
|
|
951
|
-
.replace(/\\/g, "/")
|
|
952
|
-
.replace(/\.(ts|js)$/, ".ts");
|
|
953
|
-
const importPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
954
|
-
exports.push(`export * from "${importPath}";`);
|
|
955
|
-
}
|
|
956
|
-
await fs.writeFile(tempSchemaPath, `${exports.join("\n")}\n`);
|
|
957
|
-
return { tempSchemaPath, modelCount: files.length };
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
/** Generate and run migrations for a single module using a temporary schema. */
|
|
961
|
-
export async function runModuleMigrate(rawModuleName, rawArgs = []) {
|
|
962
|
-
const { moduleName } = resolveModuleRef(rawModuleName, rawArgs);
|
|
963
1041
|
await syncMigrationDialect();
|
|
964
1042
|
await ensureMigrationMeta();
|
|
965
1043
|
await ensureDatabaseDirectory();
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
try {
|
|
971
|
-
process.env.DRIZZLE_SCHEMA = drizzleSchemaPath;
|
|
972
|
-
await runNodeScript(packageScript("drizzle-kit", "bin.cjs"), await drizzleGenerateArgs());
|
|
973
|
-
await runNodeScript(packageScript("drizzle-kit", "bin.cjs"), ["migrate"]);
|
|
974
|
-
console.log(`Module migration complete: ${moduleName} (${modelCount} model file(s))`);
|
|
975
|
-
} finally {
|
|
976
|
-
if (previousSchema == null) delete process.env.DRIZZLE_SCHEMA;
|
|
977
|
-
else process.env.DRIZZLE_SCHEMA = previousSchema;
|
|
978
|
-
if (!keepTemp) await fs.rm(tempSchemaPath, { force: true }).catch(() => {});
|
|
979
|
-
}
|
|
1044
|
+
await generateSchema();
|
|
1045
|
+
await runNodeScript(packageScript("drizzle-kit", "bin.cjs"), await drizzleGenerateArgs());
|
|
1046
|
+
await runNodeScript(packageScript("drizzle-kit", "bin.cjs"), ["migrate"]);
|
|
1047
|
+
console.log(`Module migration complete: ${moduleName} (${files.length} model file(s))`);
|
|
980
1048
|
}
|
|
981
1049
|
|
|
982
1050
|
/** Generate a unit test file for a module. */
|
|
@@ -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)
|
|
@@ -38,8 +39,9 @@ export function registerModuleCommands(program, rawArgs) {
|
|
|
38
39
|
});
|
|
39
40
|
program
|
|
40
41
|
.command("module:make-notification [name]")
|
|
41
|
-
.description("Generate notification
|
|
42
|
+
.description("Generate the notification HTTP module, and the shared store the first time")
|
|
42
43
|
.option("--path <panel>", "Nest the module under a panel folder (e.g. --path=admin)")
|
|
44
|
+
.option("--notifiable <type>", "Owner type recorded with each notification (default: user)")
|
|
43
45
|
.option("--force", "Overwrite the files of a module that already exists")
|
|
44
46
|
.allowUnknownOption(true)
|
|
45
47
|
.action(async (name) => {
|
|
@@ -128,6 +130,14 @@ export function registerModuleCommands(program, rawArgs) {
|
|
|
128
130
|
.option("--path <panel>", "Address a module inside a panel folder (e.g. --path=admin)")
|
|
129
131
|
.allowUnknownOption(true)
|
|
130
132
|
.action(async (moduleName, name) => makeLocalMiddleware(moduleName, name, rawArgs.slice(name ? 3 : 2)));
|
|
133
|
+
program
|
|
134
|
+
.command("module:make-provider <module> [name]")
|
|
135
|
+
.description("Generate a provider file in the module's providers folder")
|
|
136
|
+
.option("--force", "Overwrite existing provider file")
|
|
137
|
+
.option("--dry-run", "Print what would be created without executing")
|
|
138
|
+
.option("--path <panel>", "Address a module inside a panel folder (e.g. --path=admin)")
|
|
139
|
+
.allowUnknownOption(true)
|
|
140
|
+
.action(async (moduleName, name) => makeModuleProvider(moduleName, name, rawArgs.slice(name ? 3 : 2)));
|
|
131
141
|
program
|
|
132
142
|
.command("module:make-type <module> [name]")
|
|
133
143
|
.description("Generate a types file for an existing module")
|
|
@@ -186,8 +196,7 @@ export function registerModuleCommands(program, rawArgs) {
|
|
|
186
196
|
});
|
|
187
197
|
program
|
|
188
198
|
.command("module:migrate <module>")
|
|
189
|
-
.description("Generate
|
|
190
|
-
.option("--keep-temp", "Keep temporary schema file after migration")
|
|
199
|
+
.description("Generate and run a migration that includes the module")
|
|
191
200
|
.option("--path <panel>", "Address a module inside a panel folder (e.g. --path=admin)")
|
|
192
201
|
.allowUnknownOption(true)
|
|
193
202
|
.action(async (moduleName) => runModuleMigrate(moduleName, rawArgs));
|
|
@@ -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,76 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
1
|
+
import type { Request, Response } from "express";
|
|
2
|
+
import { HttpStatusCodes, resolveNotificationProvider } from "@/framework/facade.js";
|
|
3
|
+
import type { NotifiableTarget } from "@/framework/providers/contracts.js";
|
|
4
|
+
|
|
5
|
+
/** Read the authenticated owner. A missing auth context means the request is anonymous. */
|
|
6
|
+
function targetFrom(res: Response): NotifiableTarget | null {
|
|
7
|
+
const auth = (res.locals as any)?.auth;
|
|
8
|
+
if (!auth || auth.id === undefined || auth.id === null) return null;
|
|
9
|
+
return { type: "{{notifiable}}", id: auth.id };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function unavailable(res: Response) {
|
|
13
|
+
return res.status(HttpStatusCodes.SERVICE_UNAVAILABLE).json({
|
|
14
|
+
message: "Notification provider not available. Restore src/modules/notification or register one in src/providers/."
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function unauthorized(res: Response) {
|
|
19
|
+
return res.status(HttpStatusCodes.UNAUTHORIZED).json({ message: "Unauthorized" });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const list = async (req: Request, res: Response) => {
|
|
23
|
+
const provider = await resolveNotificationProvider();
|
|
24
|
+
if (!provider) return unavailable(res);
|
|
25
|
+
|
|
26
|
+
const target = targetFrom(res);
|
|
27
|
+
if (!target) return unauthorized(res);
|
|
28
|
+
|
|
29
|
+
const page = Math.max(1, Number(req.query.page) || 1);
|
|
30
|
+
const perPage = Math.min(50, Math.max(1, Number(req.query.perPage) || 20));
|
|
31
|
+
|
|
32
|
+
return res.json(await provider.list(target, { page, perPage }));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const unreadCount = async (_req: Request, res: Response) => {
|
|
36
|
+
const provider = await resolveNotificationProvider();
|
|
37
|
+
if (!provider) return unavailable(res);
|
|
38
|
+
|
|
39
|
+
const target = targetFrom(res);
|
|
40
|
+
if (!target) return unauthorized(res);
|
|
41
|
+
|
|
42
|
+
return res.json({ count: await provider.unreadCount(target) });
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const markRead = async (req: Request, res: Response) => {
|
|
46
|
+
const provider = await resolveNotificationProvider();
|
|
47
|
+
if (!provider) return unavailable(res);
|
|
48
|
+
|
|
49
|
+
const target = targetFrom(res);
|
|
50
|
+
if (!target) return unauthorized(res);
|
|
51
|
+
|
|
52
|
+
await provider.markRead(target, String(req.params.id));
|
|
53
|
+
return res.json({ ok: true });
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const markAllRead = async (_req: Request, res: Response) => {
|
|
57
|
+
const provider = await resolveNotificationProvider();
|
|
58
|
+
if (!provider) return unavailable(res);
|
|
59
|
+
|
|
60
|
+
const target = targetFrom(res);
|
|
61
|
+
if (!target) return unauthorized(res);
|
|
62
|
+
|
|
63
|
+
await provider.markAllRead(target);
|
|
64
|
+
return res.json({ ok: true });
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const remove = async (req: Request, res: Response) => {
|
|
68
|
+
const provider = await resolveNotificationProvider();
|
|
69
|
+
if (!provider) return unavailable(res);
|
|
70
|
+
|
|
71
|
+
const target = targetFrom(res);
|
|
72
|
+
if (!target) return unauthorized(res);
|
|
73
|
+
|
|
74
|
+
await provider.remove(target, String(req.params.id));
|
|
75
|
+
return res.json({ ok: true });
|
|
76
|
+
};
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { shouldQueue } from "@/framework/
|
|
2
|
-
import { logger } from "@/framework/support/logger.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
});
|
|
13
|
-
|
|
1
|
+
import { mail, shouldQueue } from "@/framework/facade.js";
|
|
2
|
+
import { logger } from "@/framework/support/logger.js";
|
|
3
|
+
|
|
4
|
+
shouldQueue("notification:mail", "mail", async (job) => {
|
|
5
|
+
const { to, subject, html } = job.data;
|
|
6
|
+
|
|
7
|
+
await mail.sendMail({ to, subject, html });
|
|
8
|
+
logger.info("Notification mail sent", { to, subject });
|
|
9
|
+
|
|
10
|
+
return { ok: true };
|
|
11
|
+
});
|