create-nexgen 1.0.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/package.json +26 -0
- package/src/index.js +108 -0
- package/template/.dockerignore +14 -0
- package/template/.env +58 -0
- package/template/.env.example +59 -0
- package/template/.prettierignore +5 -0
- package/template/.prettierrc +8 -0
- package/template/README.md +447 -0
- package/template/drizzle.config.ts +29 -0
- package/template/eslint.config.js +52 -0
- package/template/gitignore-stub +24 -0
- package/template/package.json +96 -0
- package/template/public/assets/AuthLayout-CbswhpjJ.js +1 -0
- package/template/public/assets/Button-_7aQ7gHL.js +1 -0
- package/template/public/assets/Input-CLNJXmKc.css +1 -0
- package/template/public/assets/Input-z8GI8Aqo.js +1 -0
- package/template/public/assets/InputPasswordToggle-BxlzVGp3.js +1 -0
- package/template/public/assets/InputPasswordToggle-C77FI9Eg.css +1 -0
- package/template/public/assets/Layout-DotR1sQC.js +1 -0
- package/template/public/assets/Refresh-BdqsPPBC.js +1 -0
- package/template/public/assets/admin-ui-CU34rLdN.js +1 -0
- package/template/public/assets/bootstrap-icons-BeopsB42.woff +0 -0
- package/template/public/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
- package/template/public/assets/dashboard-CwybEyLc.js +1 -0
- package/template/public/assets/dashboard-Dc4d-Pi7.css +1 -0
- package/template/public/assets/forgetPassword-CKEJaXsq.js +1 -0
- package/template/public/assets/index-Bleyx5dm.js +64 -0
- package/template/public/assets/index-DUw8E6Yg.css +1 -0
- package/template/public/assets/login-DC7PTlQF.js +1 -0
- package/template/public/assets/realtime-test-BPQdrFym.css +1 -0
- package/template/public/assets/realtime-test-tQZ0rBEJ.js +1 -0
- package/template/public/assets/register-3O7Qs28C.js +1 -0
- package/template/public/assets/resetPassword-A5AzMWKs.js +1 -0
- package/template/public/assets/verifyEmail-DDBEQHOv.js +1 -0
- package/template/public/index.html +17 -0
- package/template/src/database/migrations/mysql/0000_init.sql +73 -0
- package/template/src/database/migrations/mysql/meta/0000_snapshot.json +484 -0
- package/template/src/database/migrations/mysql/meta/_journal.json +13 -0
- package/template/src/database/schema.ts +4 -0
- package/template/src/env.ts +107 -0
- package/template/src/framework/cache/cache.ts +81 -0
- package/template/src/framework/database/connection.ts +168 -0
- package/template/src/framework/database/optional-db-drivers.d.ts +9 -0
- package/template/src/framework/database/paginate.ts +200 -0
- package/template/src/framework/database/schema.ts +26 -0
- package/template/src/framework/database/seed.ts +33 -0
- package/template/src/framework/events/dispatcher.ts +57 -0
- package/template/src/framework/facade.ts +27 -0
- package/template/src/framework/http/app.ts +61 -0
- package/template/src/framework/http/cors.ts +19 -0
- package/template/src/framework/http/logger.ts +85 -0
- package/template/src/framework/http/openapi.ts +34 -0
- package/template/src/framework/http/ratelimiter.ts +13 -0
- package/template/src/framework/http/router.ts +76 -0
- package/template/src/framework/http/static.ts +33 -0
- package/template/src/framework/http/validation.ts +24 -0
- package/template/src/framework/kernel.ts +40 -0
- package/template/src/framework/maker-cli/src/index.mjs +51 -0
- package/template/src/framework/maker-cli/src/levels/level-1/env-db.mjs +57 -0
- package/template/src/framework/maker-cli/src/levels/level-1/file-ops.mjs +30 -0
- package/template/src/framework/maker-cli/src/levels/level-1/flags.mjs +16 -0
- package/template/src/framework/maker-cli/src/levels/level-1/help.mjs +24 -0
- package/template/src/framework/maker-cli/src/levels/level-1/naming.mjs +13 -0
- package/template/src/framework/maker-cli/src/levels/level-1/process.mjs +47 -0
- package/template/src/framework/maker-cli/src/levels/level-2/db/core.mjs +299 -0
- package/template/src/framework/maker-cli/src/levels/level-2/db/index.mjs +177 -0
- package/template/src/framework/maker-cli/src/levels/level-2/deploy/core.mjs +635 -0
- package/template/src/framework/maker-cli/src/levels/level-2/deploy/index.mjs +145 -0
- package/template/src/framework/maker-cli/src/levels/level-2/module/core.mjs +707 -0
- package/template/src/framework/maker-cli/src/levels/level-2/module/index.mjs +116 -0
- package/template/src/framework/maker-cli/src/levels/level-2/runtime/build-frontend.mjs +16 -0
- package/template/src/framework/maker-cli/src/levels/level-2/runtime/core.mjs +311 -0
- package/template/src/framework/maker-cli/src/levels/level-2/runtime/index.mjs +71 -0
- package/template/src/framework/maker-cli/stubs/controller/openapi.ts.stub +55 -0
- package/template/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +56 -0
- package/template/src/framework/maker-cli/stubs/controller/plain.ts.stub +57 -0
- package/template/src/framework/maker-cli/stubs/controller/schema.plain.ts.stub +13 -0
- package/template/src/framework/maker-cli/stubs/controller/schema.ts.stub +32 -0
- package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.bun.stub +49 -0
- package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.pnpm.stub +53 -0
- package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.stub +49 -0
- package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.yarn.stub +53 -0
- package/template/src/framework/maker-cli/stubs/deploy/README.stub +55 -0
- package/template/src/framework/maker-cli/stubs/deploy/compose/mysql.server.stub +29 -0
- package/template/src/framework/maker-cli/stubs/deploy/compose/postgres.server.stub +29 -0
- package/template/src/framework/maker-cli/stubs/deploy/compose/sqlite.stub +29 -0
- package/template/src/framework/maker-cli/stubs/deploy/env/mysql.server.stub +73 -0
- package/template/src/framework/maker-cli/stubs/deploy/env/postgres.server.stub +73 -0
- package/template/src/framework/maker-cli/stubs/deploy/env/sqlite.stub +72 -0
- package/template/src/framework/maker-cli/stubs/deploy/scripts/auto-migrate.sh.stub +15 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/README.stub +77 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/compose/noredis.stub +118 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/compose/redis.dev.stub +131 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/compose/redis.stub +129 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/env/local.example.stub +10 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/env/noredis.stub +24 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/env/redis.stub +24 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/README.stub +15 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/app.example.com.stub +12 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/pgadmin/servers.stub +13 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/redis/redis.conf.stub +6 -0
- package/template/src/framework/maker-cli/stubs/deploy/supervisor/noredis.stub +53 -0
- package/template/src/framework/maker-cli/stubs/deploy/supervisor/redis.stub +69 -0
- package/template/src/framework/maker-cli/stubs/deploy/workflow/local.json.stub +24 -0
- package/template/src/framework/maker-cli/stubs/deploy/workflow/remote.json.stub +20 -0
- package/template/src/framework/maker-cli/stubs/example/console.ts.stub +33 -0
- package/template/src/framework/maker-cli/stubs/example/controller.ts.stub +503 -0
- package/template/src/framework/maker-cli/stubs/example/job.ts.stub +74 -0
- package/template/src/framework/maker-cli/stubs/example/route.api.ts.stub +206 -0
- package/template/src/framework/maker-cli/stubs/example/schema.ts.stub +41 -0
- package/template/src/framework/maker-cli/stubs/job/name.ts.stub +24 -0
- package/template/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +8 -0
- package/template/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +8 -0
- package/template/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +8 -0
- package/template/src/framework/maker-cli/stubs/notification/NotificationBell.vue.stub +218 -0
- package/template/src/framework/maker-cli/stubs/notification/controller.ts.stub +85 -0
- package/template/src/framework/maker-cli/stubs/notification/index.vue.stub +211 -0
- package/template/src/framework/maker-cli/stubs/notification/job.ts.stub +12 -0
- package/template/src/framework/maker-cli/stubs/notification/route.api.ts.stub +49 -0
- package/template/src/framework/maker-cli/stubs/notification/schema.ts.stub +25 -0
- package/template/src/framework/maker-cli/stubs/route/api.ts.stub +79 -0
- package/template/src/framework/maker-cli/stubs/route/plain.ts.stub +10 -0
- package/template/src/framework/maker-cli/stubs/schedule/name.ts.stub +35 -0
- package/template/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -0
- package/template/src/framework/modules/discover.ts +54 -0
- package/template/src/framework/modules/routes.ts +26 -0
- package/template/src/framework/notification/index.ts +109 -0
- package/template/src/framework/queue/clear.ts +20 -0
- package/template/src/framework/queue/queue.ts +213 -0
- package/template/src/framework/queue/ui.ts +104 -0
- package/template/src/framework/queue/worker.ts +33 -0
- package/template/src/framework/realtime/broadcast.ts +27 -0
- package/template/src/framework/realtime/index.ts +1 -0
- package/template/src/framework/realtime/socket-cookie.ts +65 -0
- package/template/src/framework/realtime/socket.ts +132 -0
- package/template/src/framework/realtime/types.ts +6 -0
- package/template/src/framework/realtime/ui.ts +16 -0
- package/template/src/framework/redis/client.ts +126 -0
- package/template/src/framework/scheduler/lock.ts +124 -0
- package/template/src/framework/scheduler/run.ts +26 -0
- package/template/src/framework/scheduler/scheduler.ts +82 -0
- package/template/src/framework/server.ts +147 -0
- package/template/src/framework/session/session.ts +116 -0
- package/template/src/framework/storage/storage.ts +743 -0
- package/template/src/framework/support/cookie.ts +78 -0
- package/template/src/framework/support/jwt.ts +45 -0
- package/template/src/framework/support/lifecycle.ts +35 -0
- package/template/src/framework/support/logger.ts +102 -0
- package/template/src/framework/support/mail.ts +43 -0
- package/template/src/framework/support/password.ts +23 -0
- package/template/src/framework/support/url.ts +25 -0
- package/template/src/middlewares/auth-middleware.ts +98 -0
- package/template/src/middlewares/role-middleware.ts +24 -0
- package/template/src/modules/auth/controllers/auth.controller.ts +445 -0
- package/template/src/modules/auth/controllers/auth.helpers.ts +110 -0
- package/template/src/modules/auth/controllers/auth.schema.ts +102 -0
- package/template/src/modules/auth/controllers/role.controller.ts +25 -0
- package/template/src/modules/auth/database/models/notifications.ts +22 -0
- package/template/src/modules/auth/database/models/role.ts +14 -0
- package/template/src/modules/auth/database/models/user.ts +46 -0
- package/template/src/modules/auth/database/seeders/role.ts +19 -0
- package/template/src/modules/auth/database/seeders/user.ts +33 -0
- package/template/src/modules/auth/jobs/forgetpass.ts +18 -0
- package/template/src/modules/auth/jobs/registeruser.ts +31 -0
- package/template/src/modules/auth/jobs/verifyemail.ts +18 -0
- package/template/src/modules/auth/routes/api.ts +151 -0
- package/template/src/modules/auth/routes/role.ts +39 -0
- package/template/src/modules/welcome/controllers/welcome.controller.ts +14 -0
- package/template/src/modules/welcome/controllers/welcome.schema.ts +6 -0
- package/template/src/modules/welcome/database/models/welcome.ts +6 -0
- package/template/src/modules/welcome/routes/api.ts +20 -0
- package/template/src/resources/index.html +16 -0
- package/template/src/resources/src/App.vue +5 -0
- package/template/src/resources/src/assets/css/styles.css +14934 -0
- package/template/src/resources/src/assets/css/styles.css.map +1 -0
- package/template/src/resources/src/assets/images/favicon/favicon.ico +0 -0
- package/template/src/resources/src/assets/images/favicon/favicon1.ico +0 -0
- package/template/src/resources/src/assets/images/logo-1.png +0 -0
- package/template/src/resources/src/assets/images/logo-dark-sm.png +0 -0
- package/template/src/resources/src/assets/images/logo-dark.png +0 -0
- package/template/src/resources/src/assets/images/logo-dark1.png +0 -0
- package/template/src/resources/src/assets/images/logo-sm.png +0 -0
- package/template/src/resources/src/assets/images/logo1.png +0 -0
- package/template/src/resources/src/assets/images/logo2.png +0 -0
- package/template/src/resources/src/assets/scss/custom.css +217 -0
- package/template/src/resources/src/assets/scss/custom.css.map +1 -0
- package/template/src/resources/src/assets/scss/custom.scss +1100 -0
- package/template/src/resources/src/components/Button.vue +35 -0
- package/template/src/resources/src/components/Checkbox.vue +29 -0
- package/template/src/resources/src/components/FloatButton.vue +36 -0
- package/template/src/resources/src/components/Href.vue +32 -0
- package/template/src/resources/src/components/Input.vue +227 -0
- package/template/src/resources/src/components/InputGroup.vue +153 -0
- package/template/src/resources/src/components/InputPasswordToggle.vue +226 -0
- package/template/src/resources/src/components/Modal.vue +102 -0
- package/template/src/resources/src/components/Pagebar.vue +28 -0
- package/template/src/resources/src/components/Refresh.vue +26 -0
- package/template/src/resources/src/components/Select.vue +390 -0
- package/template/src/resources/src/components/Spinner.vue +42 -0
- package/template/src/resources/src/components/Switch.vue +65 -0
- package/template/src/resources/src/components/TextArea.vue +121 -0
- package/template/src/resources/src/components/Toast.vue +56 -0
- package/template/src/resources/src/components/datatable/DataTableSkeleton.vue +99 -0
- package/template/src/resources/src/components/datatable/Pagination.vue +161 -0
- package/template/src/resources/src/components/datatable/SelectOpption.vue +54 -0
- package/template/src/resources/src/components/datatable/index.vue +237 -0
- package/template/src/resources/src/composables/useAuth.ts +52 -0
- package/template/src/resources/src/composables/useBrowserDetect.ts +5 -0
- package/template/src/resources/src/composables/useDialog.ts +5 -0
- package/template/src/resources/src/composables/useGum.ts +3 -0
- package/template/src/resources/src/composables/usePulse.ts +5 -0
- package/template/src/resources/src/env.d.ts +20 -0
- package/template/src/resources/src/helpers/nformatter.ts +10 -0
- package/template/src/resources/src/helpers/utils.ts +68 -0
- package/template/src/resources/src/layouts/AuthLayout.vue +20 -0
- package/template/src/resources/src/layouts/Layout/Footer.vue +23 -0
- package/template/src/resources/src/layouts/Layout/Header.vue +90 -0
- package/template/src/resources/src/layouts/Layout/Sidebar.vue +137 -0
- package/template/src/resources/src/layouts/Layout/index.vue +76 -0
- package/template/src/resources/src/main.ts +27 -0
- package/template/src/resources/src/pages/auth/forgetPassword.vue +76 -0
- package/template/src/resources/src/pages/auth/login.vue +93 -0
- package/template/src/resources/src/pages/auth/register.vue +130 -0
- package/template/src/resources/src/pages/auth/resetPassword.vue +119 -0
- package/template/src/resources/src/pages/auth/verifyEmail.vue +60 -0
- package/template/src/resources/src/pages/dashboard/index.vue +76 -0
- package/template/src/resources/src/plugins/axios.ts +33 -0
- package/template/src/resources/src/plugins/browserDetect.ts +55 -0
- package/template/src/resources/src/plugins/dialog.ts +167 -0
- package/template/src/resources/src/plugins/gum.ts +343 -0
- package/template/src/resources/src/plugins/pulse.ts +141 -0
- package/template/src/resources/src/plugins/routeProgress.ts +87 -0
- package/template/src/resources/src/router/index.ts +85 -0
- package/template/src/resources/src/stores/admin-ui.ts +148 -0
- package/template/src/resources/src/stores/auth.ts +151 -0
- package/template/src/resources/tsconfig.json +19 -0
- package/template/src/resources/vite.config.ts +43 -0
- package/template/src/storage/logs/app.log +20179 -0
- package/template/src/storage/logs/fatal.log +727 -0
- package/template/tsconfig.json +20 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { glob } from "glob";
|
|
4
|
+
import { detectDialect, openApiEnabled } from "../../level-1/env-db.mjs";
|
|
5
|
+
import { hasFlag } from "../../level-1/flags.mjs";
|
|
6
|
+
import { assertName, pascal } from "../../level-1/naming.mjs";
|
|
7
|
+
import { writeFileAlways, writeFiles } from "../../level-1/file-ops.mjs";
|
|
8
|
+
import { packageScript, runNodeScript } from "../../level-1/process.mjs";
|
|
9
|
+
import { drizzleGenerateArgs, ensureDatabaseDirectory, ensureMigrationMeta, syncMigrationDialect } from "../db/core.mjs";
|
|
10
|
+
|
|
11
|
+
const stubsRoot = path.resolve(import.meta.dirname, "../../../../stubs");
|
|
12
|
+
|
|
13
|
+
/** Template stubs used for module file generation. */
|
|
14
|
+
const STUBS = {
|
|
15
|
+
controller: {
|
|
16
|
+
openapi: "controller/openapi.ts.stub",
|
|
17
|
+
openapiWithModel: "controller/openapi.with-model.ts.stub",
|
|
18
|
+
plain: "controller/plain.ts.stub",
|
|
19
|
+
schema: {
|
|
20
|
+
openapi: "controller/schema.ts.stub",
|
|
21
|
+
plain: "controller/schema.plain.ts.stub"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
route: {
|
|
25
|
+
api: "route/api.ts.stub",
|
|
26
|
+
plain: "route/plain.ts.stub"
|
|
27
|
+
},
|
|
28
|
+
model: {
|
|
29
|
+
named: {
|
|
30
|
+
mysql: "model/name.mysql.ts.stub",
|
|
31
|
+
postgresql: "model/name.postgresql.ts.stub",
|
|
32
|
+
sqlite: "model/name.sqlite.ts.stub"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
seeder: {
|
|
36
|
+
named: "seeder/name.ts.stub"
|
|
37
|
+
},
|
|
38
|
+
example: {
|
|
39
|
+
schema: "example/schema.ts.stub",
|
|
40
|
+
controller: "example/controller.ts.stub",
|
|
41
|
+
routeApi: "example/route.api.ts.stub",
|
|
42
|
+
job: "example/job.ts.stub",
|
|
43
|
+
console: "example/console.ts.stub"
|
|
44
|
+
},
|
|
45
|
+
job: {
|
|
46
|
+
named: "job/name.ts.stub"
|
|
47
|
+
},
|
|
48
|
+
schedule: {
|
|
49
|
+
named: "schedule/name.ts.stub"
|
|
50
|
+
},
|
|
51
|
+
notification: {
|
|
52
|
+
controller: "notification/controller.ts.stub",
|
|
53
|
+
schema: "notification/schema.ts.stub",
|
|
54
|
+
routeApi: "notification/route.api.ts.stub",
|
|
55
|
+
job: "notification/job.ts.stub",
|
|
56
|
+
bell: "notification/NotificationBell.vue.stub",
|
|
57
|
+
page: "notification/index.vue.stub"
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Module scaffolding handlers shared by maker module commands. */
|
|
62
|
+
|
|
63
|
+
/** Read a stub file from the stubs directory. */
|
|
64
|
+
async function readStubRaw(name) {
|
|
65
|
+
const file = path.join(stubsRoot, name);
|
|
66
|
+
return fs.readFile(file, "utf8");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Read a stub file and replace {{key}} placeholders, removing unfilled ones. */
|
|
70
|
+
async function stub(name, values = {}) {
|
|
71
|
+
let content = await readStubRaw(name);
|
|
72
|
+
for (const [key, value] of Object.entries(values)) {
|
|
73
|
+
content = content.replaceAll(`{{${key}}}`, String(value));
|
|
74
|
+
}
|
|
75
|
+
return content.replace(/\{\{[A-Z0-9_]+\}\}/g, "");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Resolve the canonical module root path inside src/modules. */
|
|
79
|
+
function moduleRoot(moduleName) {
|
|
80
|
+
return path.resolve(process.cwd(), "src/modules", moduleName);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Ensure a module directory exists, throwing a helpful error if not. */
|
|
84
|
+
async function assertModuleExists(moduleName) {
|
|
85
|
+
const root = moduleRoot(moduleName);
|
|
86
|
+
let stats;
|
|
87
|
+
try {
|
|
88
|
+
stats = await fs.stat(root);
|
|
89
|
+
} catch {
|
|
90
|
+
throw new Error(`Module does not exist: ${moduleName}. Create it first with: bun maker module:make ${moduleName}`);
|
|
91
|
+
}
|
|
92
|
+
if (!stats.isDirectory()) {
|
|
93
|
+
throw new Error(`Module path is not a directory: src/modules/${moduleName}`);
|
|
94
|
+
}
|
|
95
|
+
return root;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Generate controller and schema file content for a module. */
|
|
99
|
+
async function controllerFiles(moduleName, controllerName = moduleName, options = {}) {
|
|
100
|
+
const { includeModelImport = false } = options;
|
|
101
|
+
const controller = controllerName.trim().toLowerCase();
|
|
102
|
+
const openApi = openApiEnabled();
|
|
103
|
+
const controllerStub = openApi
|
|
104
|
+
? includeModelImport
|
|
105
|
+
? STUBS.controller.openapiWithModel
|
|
106
|
+
: STUBS.controller.openapi
|
|
107
|
+
: STUBS.controller.plain;
|
|
108
|
+
const schemaStub = openApi ? STUBS.controller.schema.openapi : STUBS.controller.schema.plain;
|
|
109
|
+
return {
|
|
110
|
+
[`controllers/${controller}.schema.ts`]: await stub(schemaStub, {
|
|
111
|
+
module: moduleName,
|
|
112
|
+
controller,
|
|
113
|
+
ClassName: pascal(controller),
|
|
114
|
+
name: moduleName
|
|
115
|
+
}),
|
|
116
|
+
[`controllers/${controller}.controller.ts`]: await stub(controllerStub, {
|
|
117
|
+
module: moduleName,
|
|
118
|
+
controller,
|
|
119
|
+
name: moduleName,
|
|
120
|
+
tableVariable: `${controller}s`
|
|
121
|
+
})
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Generate a route file for a module. */
|
|
126
|
+
async function routeTemplate(moduleName, controllerName = moduleName) {
|
|
127
|
+
const controller = controllerName.trim().toLowerCase();
|
|
128
|
+
const routeStub = openApiEnabled() ? STUBS.route.api : STUBS.route.plain;
|
|
129
|
+
return await stub(routeStub, { module: moduleName, controller, ClassName: pascal(controller) });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Generate a named model file for a module. */
|
|
133
|
+
async function namedModelTemplate(moduleName, name, dialect) {
|
|
134
|
+
return await stub(STUBS.model.named[dialect], {
|
|
135
|
+
module: moduleName,
|
|
136
|
+
name,
|
|
137
|
+
controller: name,
|
|
138
|
+
ClassName: pascal(name),
|
|
139
|
+
tableName: `${name}s`,
|
|
140
|
+
tableVariable: `${name}s`
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Generate a seeder file for a module model. */
|
|
145
|
+
async function namedSeederTemplate(moduleName, modelName, className) {
|
|
146
|
+
return await stub(STUBS.seeder.named, {
|
|
147
|
+
module: moduleName,
|
|
148
|
+
name: modelName,
|
|
149
|
+
controller: className,
|
|
150
|
+
ClassName: pascal(className),
|
|
151
|
+
tableName: `${modelName}s`,
|
|
152
|
+
tableVariable: `${modelName}s`
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Comment out every line of content (used for seeders generated without --force). */
|
|
157
|
+
function asCommentedSeeder(content) {
|
|
158
|
+
return content.split("\n").map((line) => (line ? `// ${line}` : "//")).join("\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Check if a file path exists on disk. */
|
|
162
|
+
async function pathExists(filePath) {
|
|
163
|
+
try {
|
|
164
|
+
await fs.access(filePath);
|
|
165
|
+
return true;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Resolve the best controller name for a route. Prefers explicit name, falls back to most recently modified controller. */
|
|
172
|
+
async function resolveRouteControllerName(moduleRootPath, moduleName, preferredName = "") {
|
|
173
|
+
const preferred = preferredName ? preferredName.trim().toLowerCase() : "";
|
|
174
|
+
if (preferred) {
|
|
175
|
+
const preferredController = path.join(
|
|
176
|
+
moduleRootPath,
|
|
177
|
+
"controllers",
|
|
178
|
+
`${preferred}.controller.ts`
|
|
179
|
+
);
|
|
180
|
+
const preferredSchema = path.join(moduleRootPath, "controllers", `${preferred}.schema.ts`);
|
|
181
|
+
if ((await pathExists(preferredController)) && (await pathExists(preferredSchema))) return preferred;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const controllerDir = path.join(moduleRootPath, "controllers");
|
|
185
|
+
let entries = [];
|
|
186
|
+
try {
|
|
187
|
+
entries = await fs.readdir(controllerDir, { withFileTypes: true });
|
|
188
|
+
} catch {
|
|
189
|
+
return moduleName;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let latest = "";
|
|
193
|
+
let latestMtime = -1;
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
if (!entry.isFile() || !entry.name.endsWith(".controller.ts")) continue;
|
|
196
|
+
const baseName = entry.name.replace(/\.controller\.ts$/, "");
|
|
197
|
+
const schemaPath = path.join(controllerDir, `${baseName}.schema.ts`);
|
|
198
|
+
if (!(await pathExists(schemaPath))) continue;
|
|
199
|
+
const fullPath = path.join(controllerDir, entry.name);
|
|
200
|
+
const stats = await fs.stat(fullPath);
|
|
201
|
+
const mtime = stats.mtimeMs || 0;
|
|
202
|
+
if (mtime > latestMtime) {
|
|
203
|
+
latest = baseName;
|
|
204
|
+
latestMtime = mtime;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return latest || moduleName;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Write a file with safety checks (dry-run support, force overwrite protection). */
|
|
212
|
+
async function writeFileSafe(filePath, content, flags = [], label = "File") {
|
|
213
|
+
const dryRun = hasFlag(flags, "--dry-run");
|
|
214
|
+
const force = hasFlag(flags, "--force") || hasFlag(flags, "--yes");
|
|
215
|
+
if (dryRun) return;
|
|
216
|
+
const exists = await pathExists(filePath);
|
|
217
|
+
if (exists && !force) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`${label} already exists: ${path.relative(process.cwd(), filePath)}. Re-run with --force to overwrite.`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
223
|
+
await fs.writeFile(filePath, content);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Generate a complete module scaffold (controller, route, models, seeders). */
|
|
227
|
+
export async function makeModule(rawName) {
|
|
228
|
+
const name = assertName(rawName, "Module name");
|
|
229
|
+
const root = path.resolve(process.cwd(), "src/modules", name);
|
|
230
|
+
const controller = await controllerFiles(name, name, { includeModelImport: true });
|
|
231
|
+
const route = await routeTemplate(name);
|
|
232
|
+
const dialect = detectDialect();
|
|
233
|
+
const model = await namedModelTemplate(name, name, dialect);
|
|
234
|
+
const seeder = await namedSeederTemplate(name, name, name);
|
|
235
|
+
await writeFiles(root, { ...controller, "routes/api.ts": route });
|
|
236
|
+
await fs.mkdir(path.join(root, "database", "models"), { recursive: true });
|
|
237
|
+
await fs.mkdir(path.join(root, "database", "seeders"), { recursive: true });
|
|
238
|
+
await fs.writeFile(path.join(root, "database", "models", `${name}.ts`), model);
|
|
239
|
+
await fs.writeFile(
|
|
240
|
+
path.join(root, "database", "seeders", `${name}.ts`),
|
|
241
|
+
asCommentedSeeder(seeder)
|
|
242
|
+
);
|
|
243
|
+
console.log(`Module ready: ${name}`);
|
|
244
|
+
console.log("Database files: models/{module}.ts, seeders/{module}.ts (commented)");
|
|
245
|
+
console.log(`Route style: ${openApiEnabled() ? "openapi" : "plain"}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Generate a one-shot example module with queue/broadcast/scheduler cases. */
|
|
249
|
+
export async function makeExampleModule(rawName = "example") {
|
|
250
|
+
const moduleName = assertName(rawName || "example", "Module name");
|
|
251
|
+
const root = path.resolve(process.cwd(), "src/modules", moduleName);
|
|
252
|
+
await writeFiles(root, {
|
|
253
|
+
[`controllers/${moduleName}.schema.ts`]: await stub(STUBS.example.schema, { module: moduleName }),
|
|
254
|
+
[`controllers/${moduleName}.controller.ts`]: await stub(STUBS.example.controller, { module: moduleName }),
|
|
255
|
+
"routes/api.ts": await stub(STUBS.example.routeApi, { module: moduleName }),
|
|
256
|
+
[`jobs/${moduleName}.ts`]: await stub(STUBS.example.job, { module: moduleName }),
|
|
257
|
+
[`console/${moduleName}.ts`]: await stub(STUBS.example.console, { module: moduleName })
|
|
258
|
+
});
|
|
259
|
+
await fs.mkdir(path.join(root, "database", "models"), { recursive: true });
|
|
260
|
+
await fs.mkdir(path.join(root, "database", "seeders"), { recursive: true });
|
|
261
|
+
console.log(`Example module ready: ${moduleName}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Generate a route file for an existing module. */
|
|
265
|
+
export async function makeRoute(rawModule, rawControllerOrFlag, extraFlags = []) {
|
|
266
|
+
const moduleName = assertName(rawModule, "Module name");
|
|
267
|
+
const root = await assertModuleExists(moduleName);
|
|
268
|
+
const flags = [];
|
|
269
|
+
let routeName = moduleName;
|
|
270
|
+
let controllerExplicit = false;
|
|
271
|
+
if (rawControllerOrFlag) {
|
|
272
|
+
if (rawControllerOrFlag.startsWith("--")) flags.push(rawControllerOrFlag);
|
|
273
|
+
else {
|
|
274
|
+
routeName = assertName(rawControllerOrFlag, "Route name");
|
|
275
|
+
controllerExplicit = true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
flags.push(...extraFlags);
|
|
279
|
+
const controllerName = await resolveRouteControllerName(root, moduleName, routeName);
|
|
280
|
+
const route = await routeTemplate(moduleName, controllerName);
|
|
281
|
+
const routeFile = controllerExplicit ? `${routeName}.ts` : "api.ts";
|
|
282
|
+
const routePath = path.join(root, `routes/${routeFile}`);
|
|
283
|
+
const dryRun = hasFlag(flags, "--dry-run");
|
|
284
|
+
const force = hasFlag(flags, "--force") || hasFlag(flags, "--yes");
|
|
285
|
+
if (dryRun) return;
|
|
286
|
+
let exists = false;
|
|
287
|
+
try { await fs.access(routePath); exists = true; } catch { }
|
|
288
|
+
if (exists && !force) throw new Error(`Route file already exists: ${path.relative(process.cwd(), routePath)}. Re-run with --force to overwrite.`);
|
|
289
|
+
await fs.mkdir(path.dirname(routePath), { recursive: true });
|
|
290
|
+
await fs.writeFile(routePath, route);
|
|
291
|
+
console.log(`Route ready: ${path.relative(process.cwd(), routePath)}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Add a notification child route under dashlayout in the Vue router. */
|
|
295
|
+
async function addNotificationRoute(moduleName) {
|
|
296
|
+
const filePath = path.resolve(process.cwd(), "src/resources/src/router/index.ts");
|
|
297
|
+
let content;
|
|
298
|
+
try { content = await fs.readFile(filePath, "utf-8"); } catch { return; }
|
|
299
|
+
if (content.includes(`path: "/${moduleName}s"`)) return;
|
|
300
|
+
|
|
301
|
+
const block = `\
|
|
302
|
+
{
|
|
303
|
+
path: "/${moduleName}s",
|
|
304
|
+
name: "${moduleName}s",
|
|
305
|
+
component: () => import("@/pages/${moduleName}s/index.vue"),
|
|
306
|
+
meta: { requiresAuth: true }
|
|
307
|
+
},
|
|
308
|
+
]`;
|
|
309
|
+
|
|
310
|
+
if (!content.includes(" ]")) return;
|
|
311
|
+
content = content.replace(" ]", block);
|
|
312
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
313
|
+
console.log(` + Added route /${moduleName}s → src/resources/src/router/index.ts`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Remove a notification child route from the Vue router. */
|
|
317
|
+
async function removeNotificationRoute(moduleName) {
|
|
318
|
+
const filePath = path.resolve(process.cwd(), "src/resources/src/router/index.ts");
|
|
319
|
+
let content;
|
|
320
|
+
try { content = await fs.readFile(filePath, "utf-8"); } catch { return; }
|
|
321
|
+
|
|
322
|
+
const route = `\
|
|
323
|
+
{
|
|
324
|
+
path: "/${moduleName}s",
|
|
325
|
+
name: "${moduleName}s",
|
|
326
|
+
component: () => import("@/pages/${moduleName}s/index.vue"),
|
|
327
|
+
meta: { requiresAuth: true }
|
|
328
|
+
},\n`;
|
|
329
|
+
|
|
330
|
+
if (!content.includes(route.trim())) return;
|
|
331
|
+
content = content.replace(route, "");
|
|
332
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
333
|
+
console.log(` - Removed route /${moduleName}s from src/resources/src/router/index.ts`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Add NotificationBell import and component to Header.vue. */
|
|
337
|
+
async function addNotificationBellToHeader() {
|
|
338
|
+
const filePath = path.resolve(process.cwd(), "src/resources/src/layouts/Layout/Header.vue");
|
|
339
|
+
let content;
|
|
340
|
+
try { content = await fs.readFile(filePath, "utf-8"); } catch { return; }
|
|
341
|
+
|
|
342
|
+
if (content.includes("NotificationBell")) return;
|
|
343
|
+
|
|
344
|
+
content = content.replace(
|
|
345
|
+
`import { authUser } from "@/composables/useAuth";`,
|
|
346
|
+
`import { authUser } from "@/composables/useAuth";\nimport NotificationBell from "@/components/NotificationBell.vue";`
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
content = content.replace(
|
|
350
|
+
`<div class="btn-group order-4 order-sm-3">`,
|
|
351
|
+
`<NotificationBell class="order-2 order-sm-3" />\n <div class="btn-group order-4 order-sm-3">`
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
355
|
+
console.log(` + Added NotificationBell → src/resources/src/layouts/Layout/Header.vue`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Remove NotificationBell import and component from Header.vue. */
|
|
359
|
+
async function removeNotificationBellFromHeader() {
|
|
360
|
+
const filePath = path.resolve(process.cwd(), "src/resources/src/layouts/Layout/Header.vue");
|
|
361
|
+
let content;
|
|
362
|
+
try { content = await fs.readFile(filePath, "utf-8"); } catch { return; }
|
|
363
|
+
|
|
364
|
+
const importLine = `import NotificationBell from "@/components/NotificationBell.vue";\n`;
|
|
365
|
+
const componentLine = `<NotificationBell class="order-2 order-sm-3" />\n `;
|
|
366
|
+
|
|
367
|
+
content = content.replace(importLine, "");
|
|
368
|
+
content = content.replace(componentLine, "");
|
|
369
|
+
|
|
370
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
371
|
+
console.log(` - Removed NotificationBell from src/resources/src/layouts/Layout/Header.vue`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Generate a notification module with controller, routes, job, and frontend files. */
|
|
375
|
+
export async function makeNotificationModule(rawName = "notification") {
|
|
376
|
+
const moduleName = assertName(rawName, "Module name");
|
|
377
|
+
const root = path.resolve(process.cwd(), "src/modules", moduleName);
|
|
378
|
+
|
|
379
|
+
await writeFiles(root, {
|
|
380
|
+
[`controllers/${moduleName}.controller.ts`]: await stub(STUBS.notification.controller, { module: moduleName }),
|
|
381
|
+
[`controllers/${moduleName}.schema.ts`]: await stub(STUBS.notification.schema, { module: moduleName }),
|
|
382
|
+
"routes/api.ts": await stub(STUBS.notification.routeApi, { module: moduleName }),
|
|
383
|
+
[`jobs/${moduleName}.ts`]: await stub(STUBS.notification.job, { module: moduleName }),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
await writeFileAlways(
|
|
387
|
+
path.resolve(process.cwd(), `src/resources/src/components/NotificationBell.vue`),
|
|
388
|
+
await stub(STUBS.notification.bell, { module: moduleName })
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const pageDir = path.resolve(process.cwd(), `src/resources/src/pages/${moduleName}s`);
|
|
392
|
+
await writeFileAlways(
|
|
393
|
+
path.join(pageDir, "index.vue"),
|
|
394
|
+
await stub(STUBS.notification.page, { module: moduleName })
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
await addNotificationRoute(moduleName);
|
|
398
|
+
await addNotificationBellToHeader();
|
|
399
|
+
|
|
400
|
+
console.log(`Notification module ready: ${moduleName}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Soft-delete a module by moving it to storage trash. */
|
|
404
|
+
export async function deleteModule(rawName, flags = []) {
|
|
405
|
+
const name = assertName(rawName, "Module name");
|
|
406
|
+
if (name === "notification") {
|
|
407
|
+
console.log("Use `bun maker module:delete-notification` to remove the notification module (removes frontend files too).");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const modulesRoot = path.resolve(process.cwd(), "src/modules");
|
|
411
|
+
const modulePath = path.resolve(modulesRoot, name);
|
|
412
|
+
const relative = path.relative(modulesRoot, modulePath);
|
|
413
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("Unsafe module path.");
|
|
414
|
+
let stats; try { stats = await fs.stat(modulePath); } catch { throw new Error(`Module not found: ${name}`); }
|
|
415
|
+
if (!stats.isDirectory()) throw new Error(`Module path is not a directory: src/modules/${name}`);
|
|
416
|
+
const trashRoot = path.resolve(process.cwd(), "src/storage/trash/modules");
|
|
417
|
+
const stamp = new Date().toISOString().replace(/[.:]/g, "-");
|
|
418
|
+
const trashPath = path.join(trashRoot, `${name}-${stamp}`);
|
|
419
|
+
const dryRun = hasFlag(flags, "--dry-run");
|
|
420
|
+
const confirmed = hasFlag(flags, "--yes") || hasFlag(flags, "--force");
|
|
421
|
+
if (dryRun) return;
|
|
422
|
+
if (!confirmed) throw new Error(`Refusing to delete module without confirmation. Re-run with: bun maker module:delete ${name} --yes`);
|
|
423
|
+
await fs.mkdir(trashRoot, { recursive: true });
|
|
424
|
+
try {
|
|
425
|
+
await fs.rename(modulePath, trashPath);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (error?.code === "EPERM" || error?.code === "EXDEV") {
|
|
428
|
+
await fs.cp(modulePath, trashPath, { recursive: true });
|
|
429
|
+
await fs.rm(modulePath, { recursive: true, force: true });
|
|
430
|
+
} else {
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
console.log(`Moved module to trash: ${path.relative(process.cwd(), trashPath)}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Remove a notification module and all its frontend files. */
|
|
438
|
+
export async function deleteNotificationModule(rawName = "notification", flags = []) {
|
|
439
|
+
const moduleName = assertName(rawName, "Module name");
|
|
440
|
+
const dryRun = hasFlag(flags, "--dry-run");
|
|
441
|
+
const confirmed = hasFlag(flags, "--yes") || hasFlag(flags, "--force");
|
|
442
|
+
|
|
443
|
+
const tasks = [
|
|
444
|
+
{ path: path.resolve(process.cwd(), "src/modules", moduleName), label: "Module" },
|
|
445
|
+
{ path: path.resolve(process.cwd(), "src/resources/src/components/NotificationBell.vue"), label: "Bell component" },
|
|
446
|
+
{ path: path.resolve(process.cwd(), `src/resources/src/pages/${moduleName}s`), label: "Notifications page" },
|
|
447
|
+
];
|
|
448
|
+
|
|
449
|
+
const trashRoot = path.resolve(process.cwd(), "src/storage/trash/modules");
|
|
450
|
+
const stamp = new Date().toISOString().replace(/[.:]/g, "-");
|
|
451
|
+
|
|
452
|
+
if (dryRun) {
|
|
453
|
+
for (const task of tasks) {
|
|
454
|
+
const exists = await fs.stat(task.path).then(() => true).catch(() => false);
|
|
455
|
+
if (exists) console.log(`Would delete: ${path.relative(process.cwd(), task.path)}`);
|
|
456
|
+
}
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (!confirmed) {
|
|
461
|
+
throw new Error(
|
|
462
|
+
`Refusing to delete notification module without confirmation. Re-run with: bun maker module:delete-notification --yes`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
await fs.mkdir(trashRoot, { recursive: true });
|
|
467
|
+
|
|
468
|
+
for (const task of tasks) {
|
|
469
|
+
const exists = await fs.stat(task.path).then(() => true).catch(() => false);
|
|
470
|
+
if (!exists) continue;
|
|
471
|
+
|
|
472
|
+
const relPath = path.relative(process.cwd(), task.path);
|
|
473
|
+
const trashPath = path.join(trashRoot, `${relPath.replace(/[/\\]/g, "-")}-${stamp}`);
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
await fs.rename(task.path, trashPath);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
if (error?.code === "EPERM" || error?.code === "EXDEV") {
|
|
479
|
+
await fs.cp(task.path, trashPath, { recursive: true });
|
|
480
|
+
await fs.rm(task.path, { recursive: true, force: true });
|
|
481
|
+
} else {
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
console.log(`Moved to trash: ${relPath}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
await removeNotificationRoute(moduleName);
|
|
489
|
+
await removeNotificationBellFromHeader();
|
|
490
|
+
|
|
491
|
+
console.log(`Notification module deleted: ${moduleName}`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Permanently remove entries from module trash storage. */
|
|
495
|
+
export async function cleanModuleTrash(rawName, flags = []) {
|
|
496
|
+
const trashRoot = path.resolve(process.cwd(), "src/storage/trash/modules");
|
|
497
|
+
const dryRun = hasFlag(flags, "--dry-run");
|
|
498
|
+
const confirmed = hasFlag(flags, "--yes") || hasFlag(flags, "--force");
|
|
499
|
+
let entries = [];
|
|
500
|
+
try { entries = await fs.readdir(trashRoot, { withFileTypes: true }); } catch { if (dryRun) return; console.log("No trash entries found"); return; }
|
|
501
|
+
const targetName = rawName && !rawName.startsWith("--") ? assertName(rawName, "Module name") : "";
|
|
502
|
+
const matches = entries.map((e) => e.name).filter((name) => !targetName || name === targetName || name.startsWith(`${targetName}-`));
|
|
503
|
+
if (!matches.length) { console.log(targetName ? `No trash entries found for module '${targetName}'` : "No trash entries found"); return; }
|
|
504
|
+
if (dryRun) return;
|
|
505
|
+
if (!confirmed) throw new Error(targetName ? `Refusing to clean trash for '${targetName}' without confirmation. Re-run with: bun maker module:trash:clean ${targetName} --yes` : "Refusing to clean all trash without confirmation. Re-run with: bun maker module:trash:clean --yes");
|
|
506
|
+
for (const name of matches) await fs.rm(path.join(trashRoot, name), { recursive: true, force: true });
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Generate a controller for an existing module. */
|
|
510
|
+
export async function makeController(rawModule, rawControllerOrFlag, extraFlags = []) {
|
|
511
|
+
const moduleName = assertName(rawModule, "Module name");
|
|
512
|
+
const flags = [];
|
|
513
|
+
let controllerName = moduleName;
|
|
514
|
+
if (rawControllerOrFlag) {
|
|
515
|
+
if (rawControllerOrFlag.startsWith("--")) flags.push(rawControllerOrFlag);
|
|
516
|
+
else controllerName = assertName(rawControllerOrFlag, "Controller name");
|
|
517
|
+
}
|
|
518
|
+
flags.push(...extraFlags);
|
|
519
|
+
const root = await assertModuleExists(moduleName);
|
|
520
|
+
const files = await controllerFiles(moduleName, controllerName);
|
|
521
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
522
|
+
await writeFileSafe(path.join(root, relativePath), content, flags, "Controller file");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Generate a model file for an existing module. */
|
|
527
|
+
export async function makeModel(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
528
|
+
const moduleName = assertName(rawModule, "Module name");
|
|
529
|
+
const flags = [];
|
|
530
|
+
let name = moduleName;
|
|
531
|
+
if (rawNameOrFlag) {
|
|
532
|
+
if (rawNameOrFlag.startsWith("--")) flags.push(rawNameOrFlag);
|
|
533
|
+
else name = assertName(rawNameOrFlag, "Model name");
|
|
534
|
+
}
|
|
535
|
+
flags.push(...extraFlags);
|
|
536
|
+
const dialect = detectDialect();
|
|
537
|
+
const root = await assertModuleExists(moduleName);
|
|
538
|
+
const modelPath = path.join(root, `database/models/${name}.ts`);
|
|
539
|
+
await writeFileSafe(
|
|
540
|
+
modelPath,
|
|
541
|
+
await namedModelTemplate(moduleName, name, dialect),
|
|
542
|
+
flags,
|
|
543
|
+
"Model file"
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Generate a seeder file for an existing module model. */
|
|
548
|
+
export async function makeSeeder(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
549
|
+
const moduleName = assertName(rawModule, "Module name");
|
|
550
|
+
const flags = [];
|
|
551
|
+
let name = moduleName;
|
|
552
|
+
if (rawNameOrFlag) {
|
|
553
|
+
if (rawNameOrFlag.startsWith("--")) flags.push(rawNameOrFlag);
|
|
554
|
+
else name = assertName(rawNameOrFlag, "Seeder name");
|
|
555
|
+
}
|
|
556
|
+
flags.push(...extraFlags);
|
|
557
|
+
const root = await assertModuleExists(moduleName);
|
|
558
|
+
let modelName = name;
|
|
559
|
+
let modelPath = path.join(root, "database", "models", `${modelName}.ts`);
|
|
560
|
+
|
|
561
|
+
try {
|
|
562
|
+
await fs.access(modelPath);
|
|
563
|
+
} catch {
|
|
564
|
+
modelName = moduleName;
|
|
565
|
+
modelPath = path.join(root, "database", "models", `${modelName}.ts`);
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
await fs.access(modelPath);
|
|
569
|
+
console.log(
|
|
570
|
+
`Model '${name}' not found, using module model '${modelName}' for seeder '${name}'.`
|
|
571
|
+
);
|
|
572
|
+
} catch {
|
|
573
|
+
throw new Error(
|
|
574
|
+
`Model not found for seeder: src/modules/${moduleName}/database/models/${name}.ts. Create it first with: bun maker module:make-model ${moduleName} ${name}`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
await writeFileSafe(
|
|
580
|
+
path.join(root, `database/seeders/${name}.ts`),
|
|
581
|
+
await namedSeederTemplate(moduleName, modelName, name),
|
|
582
|
+
flags,
|
|
583
|
+
"Seeder file"
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
console.log(`Seeder ready: ${moduleName}/${name}`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Generate a job file for an existing module. */
|
|
590
|
+
export async function makeJob(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
591
|
+
const moduleName = assertName(rawModule, "Module name");
|
|
592
|
+
const flags = [];
|
|
593
|
+
let name = moduleName;
|
|
594
|
+
if (rawNameOrFlag) {
|
|
595
|
+
if (rawNameOrFlag.startsWith("--")) flags.push(rawNameOrFlag);
|
|
596
|
+
else name = assertName(rawNameOrFlag, "Job name");
|
|
597
|
+
}
|
|
598
|
+
flags.push(...extraFlags);
|
|
599
|
+
const root = await assertModuleExists(moduleName);
|
|
600
|
+
await writeFileSafe(
|
|
601
|
+
path.join(root, `jobs/${name}.ts`),
|
|
602
|
+
await stub(STUBS.job.named, { module: moduleName, name }),
|
|
603
|
+
flags,
|
|
604
|
+
"Job file"
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** Generate a schedule/console file for an existing module. */
|
|
609
|
+
export async function makeSchedule(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
610
|
+
const moduleName = assertName(rawModule, "Module name");
|
|
611
|
+
const flags = [];
|
|
612
|
+
let name = moduleName;
|
|
613
|
+
if (rawNameOrFlag) {
|
|
614
|
+
if (rawNameOrFlag.startsWith("--")) flags.push(rawNameOrFlag);
|
|
615
|
+
else name = assertName(rawNameOrFlag, "Schedule name");
|
|
616
|
+
}
|
|
617
|
+
flags.push(...extraFlags);
|
|
618
|
+
const root = await assertModuleExists(moduleName);
|
|
619
|
+
await writeFileSafe(
|
|
620
|
+
path.join(root, `console/${name}.ts`),
|
|
621
|
+
await stub(STUBS.schedule.named, { module: moduleName, name }),
|
|
622
|
+
flags,
|
|
623
|
+
"Schedule file"
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** List all discovered modules. */
|
|
628
|
+
export async function listModules() {
|
|
629
|
+
const modulesRoot = path.resolve(process.cwd(), "src/modules");
|
|
630
|
+
try {
|
|
631
|
+
const entries = await fs.readdir(modulesRoot, { withFileTypes: true });
|
|
632
|
+
const modules = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
633
|
+
if (!modules.length) { console.log("No modules found."); return; }
|
|
634
|
+
console.log("Modules:");
|
|
635
|
+
for (const module of modules) console.log(` - ${module}`);
|
|
636
|
+
} catch {
|
|
637
|
+
console.log("No modules found.");
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Check that a module has at least one seeder file. */
|
|
642
|
+
export async function assertModuleHasSeeders(moduleName) {
|
|
643
|
+
await assertModuleExists(moduleName);
|
|
644
|
+
const files = await glob(`${moduleName}/database/seeders/*.{ts,js}`, { cwd: path.resolve(process.cwd(), "src/modules"), nodir: true, windowsPathsNoEscape: true });
|
|
645
|
+
if (!files.length) throw new Error(`No seeders found for module '${moduleName}'.`);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** Generate src/database/schema.ts by aggregating all module model exports. */
|
|
649
|
+
export async function generateSchema(options = {}) {
|
|
650
|
+
const backendSrc = path.resolve(process.cwd(), "src");
|
|
651
|
+
const files = await glob("modules/**/database/models/*.{ts,js}", { cwd: backendSrc, nodir: true, windowsPathsNoEscape: true });
|
|
652
|
+
const exports = [];
|
|
653
|
+
for (const file of files.sort()) {
|
|
654
|
+
const absoluteFile = path.join(backendSrc, file);
|
|
655
|
+
const outputDir = path.join(backendSrc, "database");
|
|
656
|
+
const relativePath = path.relative(outputDir, absoluteFile).replace(/\\/g, "/").replace(/\.(ts|js)$/, ".js");
|
|
657
|
+
const importPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
658
|
+
exports.push(`export * from "${importPath}";`);
|
|
659
|
+
}
|
|
660
|
+
const output = path.join(backendSrc, "database/schema.ts");
|
|
661
|
+
await fs.mkdir(path.dirname(output), { recursive: true });
|
|
662
|
+
await fs.writeFile(output, `${exports.join("\n")}\n`);
|
|
663
|
+
if (!options.silent) console.log(`Generated database schema with ${files.length} model file(s)`);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** Generate a temporary schema file for a single module (used for module:generate/migrate). */
|
|
667
|
+
async function generateModuleSchemaTemp(moduleName) {
|
|
668
|
+
await assertModuleExists(moduleName);
|
|
669
|
+
const backendSrc = path.resolve(process.cwd(), "src");
|
|
670
|
+
const files = await glob(`modules/${moduleName}/database/models/*.{ts,js}`, { cwd: backendSrc, nodir: true, windowsPathsNoEscape: true });
|
|
671
|
+
if (!files.length) throw new Error(`No model files found for module '${moduleName}'.`);
|
|
672
|
+
const exports = [];
|
|
673
|
+
const tempDir = path.resolve(process.cwd(), "src/storage/tmp");
|
|
674
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
675
|
+
const tempSchemaPath = path.join(tempDir, `schema.${moduleName}.${Date.now()}.ts`);
|
|
676
|
+
const tempSchemaDir = path.dirname(tempSchemaPath);
|
|
677
|
+
for (const file of files.sort()) {
|
|
678
|
+
const absoluteFile = path.join(backendSrc, file);
|
|
679
|
+
const relativePath = path.relative(tempSchemaDir, absoluteFile).replace(/\\/g, "/").replace(/\.(ts|js)$/, ".ts");
|
|
680
|
+
const importPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
681
|
+
exports.push(`export * from "${importPath}";`);
|
|
682
|
+
}
|
|
683
|
+
await fs.writeFile(tempSchemaPath, `${exports.join("\n")}\n`);
|
|
684
|
+
return { tempSchemaPath, modelCount: files.length };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Generate and run migrations for a single module using a temporary schema. */
|
|
688
|
+
export async function runModuleMigrate(rawModuleName, rawArgs = []) {
|
|
689
|
+
const moduleName = assertName(rawModuleName, "Module name");
|
|
690
|
+
await syncMigrationDialect();
|
|
691
|
+
await ensureMigrationMeta();
|
|
692
|
+
await ensureDatabaseDirectory();
|
|
693
|
+
const keepTemp = rawArgs.includes("--keep-temp");
|
|
694
|
+
const { tempSchemaPath, modelCount } = await generateModuleSchemaTemp(moduleName);
|
|
695
|
+
const drizzleSchemaPath = `./${path.relative(process.cwd(), tempSchemaPath).replace(/\\/g, "/")}`;
|
|
696
|
+
const previousSchema = process.env.DRIZZLE_SCHEMA;
|
|
697
|
+
try {
|
|
698
|
+
process.env.DRIZZLE_SCHEMA = drizzleSchemaPath;
|
|
699
|
+
await runNodeScript(packageScript("drizzle-kit", "bin.cjs"), await drizzleGenerateArgs());
|
|
700
|
+
await runNodeScript(packageScript("drizzle-kit", "bin.cjs"), ["migrate"]);
|
|
701
|
+
console.log(`Module migration complete: ${moduleName} (${modelCount} model file(s))`);
|
|
702
|
+
} finally {
|
|
703
|
+
if (previousSchema == null) delete process.env.DRIZZLE_SCHEMA;
|
|
704
|
+
else process.env.DRIZZLE_SCHEMA = previousSchema;
|
|
705
|
+
if (!keepTemp) await fs.rm(tempSchemaPath, { force: true }).catch(() => { });
|
|
706
|
+
}
|
|
707
|
+
}
|