create-nexwire 4.0.0 → 4.1.0
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/README.md +5 -4
- package/express/.env +1 -1
- package/express/README.md +13 -7
- package/express/package.json +2 -2
- package/express/src/config/session.ts +1 -1
- 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/database/schema.ts +3 -3
- package/express/src/framework/cache/cache.ts +82 -81
- package/express/src/framework/database/seed.ts +1 -1
- package/express/src/framework/facade.ts +1 -24
- package/express/src/framework/http/app.ts +5 -5
- package/express/src/framework/http/openapi.ts +1 -1
- package/express/src/framework/http/router.ts +3 -18
- package/express/src/framework/http/types.ts +19 -0
- package/express/src/framework/http/validation.ts +18 -18
- package/express/src/framework/maker-cli/middleware/core.mjs +41 -47
- package/express/src/framework/maker-cli/module/core.mjs +920 -684
- package/express/src/framework/maker-cli/module/index.mjs +202 -140
- package/express/src/framework/maker-cli/runtime/core.mjs +1 -2
- package/express/src/framework/maker-cli/stubs/controller/openapi.ts.stub +40 -14
- package/express/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +40 -15
- package/express/src/framework/maker-cli/stubs/controller/plain.ts.stub +42 -16
- package/express/src/framework/maker-cli/stubs/controller/standalone.ts.stub +47 -0
- package/express/src/framework/maker-cli/stubs/facade/name.ts.stub +12 -0
- package/express/src/framework/maker-cli/stubs/helper/name.ts.stub +14 -0
- package/express/src/framework/maker-cli/stubs/middleware/name.local.ts.stub +15 -0
- package/express/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +9 -8
- package/express/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +9 -8
- package/express/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +9 -8
- package/express/src/framework/maker-cli/stubs/notification/route.api.ts.stub +50 -50
- package/express/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +11 -11
- package/express/src/framework/maker-cli/stubs/route/api.ts.stub +93 -79
- package/express/src/framework/maker-cli/stubs/route/plain.ts.stub +23 -10
- package/express/src/framework/maker-cli/stubs/route/standalone.api.ts.stub +113 -0
- package/express/src/framework/maker-cli/stubs/route/standalone.plain.ts.stub +37 -0
- package/express/src/framework/maker-cli/stubs/{controller/schema.plain.ts.stub → schema/name.plain.ts.stub} +13 -13
- package/express/src/framework/maker-cli/stubs/{controller/schema.ts.stub → schema/name.ts.stub} +32 -32
- package/express/src/framework/maker-cli/stubs/service/name.plain.ts.stub +67 -0
- package/express/src/framework/maker-cli/stubs/service/name.ts.stub +65 -0
- package/express/src/framework/maker-cli/stubs/type/name.ts.stub +22 -0
- package/express/src/framework/maker-cli/utils/naming.mjs +56 -13
- package/express/src/framework/modules/discover.ts +57 -54
- package/express/src/framework/notification/index.ts +1 -1
- package/express/src/framework/realtime/broadcast.ts +46 -46
- package/express/src/framework/server.ts +2 -1
- package/express/src/framework/session/session.ts +102 -98
- package/express/src/framework/support/mail.ts +48 -48
- package/express/src/framework/support/status-codes.ts +75 -0
- package/express/src/middlewares/auth-middleware.ts +1 -1
- package/express/src/modules/auth/__tests__/forgetpass.test.ts +200 -0
- package/express/src/modules/auth/__tests__/login.test.ts +171 -0
- package/express/src/modules/auth/__tests__/logout.test.ts +60 -0
- package/express/src/modules/auth/__tests__/logoutalldevices.test.ts +49 -0
- package/express/src/modules/auth/__tests__/me.test.ts +91 -0
- package/express/src/modules/auth/__tests__/refreshtoken.test.ts +305 -0
- package/express/src/modules/auth/__tests__/register.test.ts +291 -0
- package/express/src/modules/auth/__tests__/resetpass.test.ts +206 -0
- package/express/src/modules/auth/__tests__/verifyemail.test.ts +183 -0
- package/express/src/modules/auth/controllers/auth.ts +254 -0
- package/express/src/modules/auth/controllers/{role.controller.ts → role.ts} +25 -15
- package/{hono/src/modules/auth/database/models/notifications.ts → express/src/modules/auth/database/models/notification.ts} +1 -1
- package/express/src/modules/auth/database/models/role.ts +1 -1
- package/express/src/modules/auth/database/models/user.ts +1 -1
- package/express/src/modules/auth/facade.ts +12 -0
- package/express/src/modules/auth/{controllers/auth.helpers.ts → helpers/auth.ts} +3 -4
- package/express/src/modules/auth/jobs/registeruser.ts +23 -23
- package/express/src/modules/auth/routes/{api.ts → index.ts} +150 -150
- package/express/src/modules/auth/routes/role.ts +33 -33
- package/{hono/src/modules/auth/controllers/auth.schema.ts → express/src/modules/auth/schemas/auth.ts} +102 -102
- package/express/src/modules/auth/services/forgetpassword.ts +66 -0
- package/express/src/modules/auth/services/login.ts +63 -0
- package/express/src/modules/auth/services/logout.ts +25 -0
- package/express/src/modules/auth/services/logoutalldevices.ts +19 -0
- package/express/src/modules/auth/services/me.ts +31 -0
- package/express/src/modules/auth/services/refreshtoken.ts +110 -0
- package/express/src/modules/auth/services/register.ts +141 -0
- package/express/src/modules/auth/services/resetpassword.ts +69 -0
- package/express/src/modules/auth/services/verifyemail.ts +77 -0
- package/express/src/modules/auth/types/auth.ts +31 -0
- package/express/src/resources/src/assets/images/logo.png +0 -0
- package/express/src/resources/src/pages/auth/forgetPassword.vue +1 -0
- package/express/src/resources/src/pages/auth/login.vue +1 -1
- package/express/src/storage/database/nexwire.sqlite +0 -0
- package/hono/.env +1 -1
- package/hono/README.md +13 -7
- package/hono/package.json +1 -1
- package/hono/src/config/session.ts +1 -1
- 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/database/schema.ts +3 -3
- package/hono/src/framework/cache/cache.ts +82 -81
- package/hono/src/framework/database/seed.ts +1 -1
- package/hono/src/framework/http/app.ts +5 -5
- package/hono/src/framework/http/validation.ts +21 -21
- package/hono/src/framework/maker-cli/middleware/core.mjs +40 -46
- package/hono/src/framework/maker-cli/module/core.mjs +401 -168
- package/hono/src/framework/maker-cli/module/index.mjs +73 -11
- package/hono/src/framework/maker-cli/stubs/controller/openapi.ts.stub +28 -16
- package/hono/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +28 -17
- package/hono/src/framework/maker-cli/stubs/controller/plain.ts.stub +28 -16
- package/hono/src/framework/maker-cli/stubs/controller/standalone.ts.stub +56 -0
- package/hono/src/framework/maker-cli/stubs/facade/name.ts.stub +12 -0
- package/hono/src/framework/maker-cli/stubs/helper/name.ts.stub +14 -0
- package/hono/src/framework/maker-cli/stubs/middleware/name.local.ts.stub +15 -0
- package/hono/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +9 -8
- package/hono/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +9 -8
- package/hono/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +9 -8
- package/hono/src/framework/maker-cli/stubs/notification/route.api.ts.stub +49 -49
- package/hono/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +11 -11
- package/hono/src/framework/maker-cli/stubs/route/api.ts.stub +93 -79
- package/hono/src/framework/maker-cli/stubs/route/plain.ts.stub +23 -10
- package/hono/src/framework/maker-cli/stubs/route/standalone.api.ts.stub +112 -0
- package/hono/src/framework/maker-cli/stubs/route/standalone.plain.ts.stub +37 -0
- package/hono/src/framework/maker-cli/stubs/{controller/schema.plain.ts.stub → schema/name.plain.ts.stub} +13 -13
- package/hono/src/framework/maker-cli/stubs/{controller/schema.ts.stub → schema/name.ts.stub} +32 -32
- package/hono/src/framework/maker-cli/stubs/service/name.plain.ts.stub +67 -0
- package/hono/src/framework/maker-cli/stubs/service/name.ts.stub +65 -0
- package/hono/src/framework/maker-cli/stubs/type/name.ts.stub +22 -0
- package/hono/src/framework/maker-cli/utils/naming.mjs +56 -13
- package/hono/src/framework/modules/discover.ts +57 -54
- package/hono/src/framework/notification/index.ts +1 -1
- package/hono/src/framework/realtime/broadcast.ts +46 -46
- package/hono/src/framework/server.ts +1 -1
- package/hono/src/framework/session/session.ts +121 -116
- package/hono/src/middlewares/auth-middleware.ts +1 -1
- package/hono/src/modules/auth/__tests__/forgetpass.test.ts +170 -0
- package/hono/src/modules/auth/__tests__/login.test.ts +154 -0
- package/hono/src/modules/auth/__tests__/logout.test.ts +53 -0
- package/hono/src/modules/auth/__tests__/logoutalldevices.test.ts +34 -0
- package/hono/src/modules/auth/__tests__/me.test.ts +87 -0
- package/hono/src/modules/auth/__tests__/refreshtoken.test.ts +305 -0
- package/hono/src/modules/auth/__tests__/register.test.ts +258 -0
- package/hono/src/modules/auth/__tests__/resetpass.test.ts +214 -0
- package/hono/src/modules/auth/__tests__/verifyemail.test.ts +189 -0
- package/hono/src/modules/auth/controllers/auth.ts +238 -0
- package/hono/src/modules/auth/controllers/{role.controller.ts → role.ts} +25 -25
- package/{express/src/modules/auth/database/models/notifications.ts → hono/src/modules/auth/database/models/notification.ts} +1 -1
- package/hono/src/modules/auth/database/models/role.ts +1 -1
- package/hono/src/modules/auth/database/models/user.ts +1 -1
- package/hono/src/modules/auth/facade.ts +12 -0
- package/hono/src/modules/auth/{controllers/auth.helpers.ts → helpers/auth.ts} +5 -6
- package/hono/src/modules/auth/jobs/registeruser.ts +23 -23
- package/hono/src/modules/auth/routes/{api.ts → index.ts} +150 -150
- package/hono/src/modules/auth/routes/role.ts +33 -33
- package/{express/src/modules/auth/controllers/auth.schema.ts → hono/src/modules/auth/schemas/auth.ts} +102 -102
- package/hono/src/modules/auth/services/forgetpassword.ts +58 -0
- package/hono/src/modules/auth/services/login.ts +61 -0
- package/hono/src/modules/auth/services/logout.ts +24 -0
- package/hono/src/modules/auth/services/logoutalldevices.ts +19 -0
- package/hono/src/modules/auth/services/me.ts +31 -0
- package/hono/src/modules/auth/services/refreshtoken.ts +110 -0
- package/hono/src/modules/auth/services/register.ts +132 -0
- package/hono/src/modules/auth/services/resetpassword.ts +69 -0
- package/hono/src/modules/auth/services/verifyemail.ts +77 -0
- package/hono/src/modules/auth/types/auth.ts +31 -0
- package/hono/src/resources/src/assets/images/logo.png +0 -0
- package/hono/src/storage/database/nexwire.sqlite +0 -0
- package/package.json +1 -1
- package/express/src/framework/maker-cli/stubs/example/console.ts.stub +0 -34
- package/express/src/framework/maker-cli/stubs/example/controller.ts.stub +0 -418
- package/express/src/framework/maker-cli/stubs/example/job.ts.stub +0 -75
- package/express/src/framework/maker-cli/stubs/example/route.api.ts.stub +0 -207
- package/express/src/framework/maker-cli/stubs/example/route.plain.ts.stub +0 -18
- package/express/src/framework/maker-cli/stubs/example/schema.plain.ts.stub +0 -42
- package/express/src/framework/maker-cli/stubs/example/schema.ts.stub +0 -42
- package/express/src/modules/auth/controllers/auth.controller.ts +0 -358
- package/hono/src/framework/maker-cli/stubs/example/console.ts.stub +0 -34
- package/hono/src/framework/maker-cli/stubs/example/controller.ts.stub +0 -505
- package/hono/src/framework/maker-cli/stubs/example/job.ts.stub +0 -75
- package/hono/src/framework/maker-cli/stubs/example/route.api.ts.stub +0 -207
- package/hono/src/framework/maker-cli/stubs/example/route.plain.ts.stub +0 -18
- package/hono/src/framework/maker-cli/stubs/example/schema.plain.ts.stub +0 -42
- package/hono/src/framework/maker-cli/stubs/example/schema.ts.stub +0 -42
- package/hono/src/modules/auth/controllers/auth.controller.ts +0 -413
|
@@ -1,684 +1,920 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { glob } from "glob";
|
|
4
|
-
import { drizzleGenerateArgs, ensureDatabaseDirectory, ensureMigrationMeta, syncMigrationDialect } from "../db/core.mjs";
|
|
5
|
-
import { detectDialect, openApiEnabled } from "../utils/env-db.mjs";
|
|
6
|
-
import { writeFiles } from "../utils/file-ops.mjs";
|
|
7
|
-
import { hasFlag } from "../utils/flags.mjs";
|
|
8
|
-
import { assertName, pascal } from "../utils/naming.mjs";
|
|
9
|
-
import { packageScript, runNodeScript } from "../utils/process.mjs";
|
|
10
|
-
|
|
11
|
-
const stubsRoot = path.resolve(import.meta.dirname, "../stubs");
|
|
12
|
-
|
|
13
|
-
/** Template stubs used for module file generation. */
|
|
14
|
-
const STUBS = {
|
|
15
|
-
|
|
16
|
-
openapi: "
|
|
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
|
-
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
.
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (
|
|
402
|
-
console.log(
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
const
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
await
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { glob } from "glob";
|
|
4
|
+
import { drizzleGenerateArgs, ensureDatabaseDirectory, ensureMigrationMeta, syncMigrationDialect } from "../db/core.mjs";
|
|
5
|
+
import { detectDialect, openApiEnabled } from "../utils/env-db.mjs";
|
|
6
|
+
import { writeFiles } from "../utils/file-ops.mjs";
|
|
7
|
+
import { hasFlag } from "../utils/flags.mjs";
|
|
8
|
+
import { assertName, camelCase, pascal } from "../utils/naming.mjs";
|
|
9
|
+
import { packageScript, runNodeScript } from "../utils/process.mjs";
|
|
10
|
+
|
|
11
|
+
const stubsRoot = path.resolve(import.meta.dirname, "../stubs");
|
|
12
|
+
|
|
13
|
+
/** Template stubs used for module file generation. */
|
|
14
|
+
const STUBS = {
|
|
15
|
+
schema: {
|
|
16
|
+
openapi: "schema/name.ts.stub",
|
|
17
|
+
plain: "schema/name.plain.ts.stub"
|
|
18
|
+
},
|
|
19
|
+
controller: {
|
|
20
|
+
openapi: "controller/openapi.ts.stub",
|
|
21
|
+
openapiWithModel: "controller/openapi.with-model.ts.stub",
|
|
22
|
+
plain: "controller/plain.ts.stub",
|
|
23
|
+
standalone: "controller/standalone.ts.stub"
|
|
24
|
+
},
|
|
25
|
+
route: {
|
|
26
|
+
api: "route/api.ts.stub",
|
|
27
|
+
plain: "route/plain.ts.stub",
|
|
28
|
+
standaloneApi: "route/standalone.api.ts.stub",
|
|
29
|
+
standalonePlain: "route/standalone.plain.ts.stub"
|
|
30
|
+
},
|
|
31
|
+
model: {
|
|
32
|
+
named: {
|
|
33
|
+
mysql: "model/name.mysql.ts.stub",
|
|
34
|
+
postgresql: "model/name.postgresql.ts.stub",
|
|
35
|
+
sqlite: "model/name.sqlite.ts.stub"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
seeder: {
|
|
39
|
+
named: "seeder/name.ts.stub"
|
|
40
|
+
},
|
|
41
|
+
service: {
|
|
42
|
+
named: "service/name.ts.stub",
|
|
43
|
+
plain: "service/name.plain.ts.stub"
|
|
44
|
+
},
|
|
45
|
+
facade: {
|
|
46
|
+
named: "facade/name.ts.stub"
|
|
47
|
+
},
|
|
48
|
+
helper: {
|
|
49
|
+
named: "helper/name.ts.stub"
|
|
50
|
+
},
|
|
51
|
+
middleware: {
|
|
52
|
+
local: "middleware/name.local.ts.stub"
|
|
53
|
+
},
|
|
54
|
+
type: {
|
|
55
|
+
named: "type/name.ts.stub"
|
|
56
|
+
},
|
|
57
|
+
job: {
|
|
58
|
+
named: "job/name.ts.stub"
|
|
59
|
+
},
|
|
60
|
+
schedule: {
|
|
61
|
+
named: "schedule/name.ts.stub"
|
|
62
|
+
},
|
|
63
|
+
notification: {
|
|
64
|
+
controller: "notification/controller.ts.stub",
|
|
65
|
+
schema: {
|
|
66
|
+
openapi: "notification/schema.ts.stub",
|
|
67
|
+
plain: "notification/schema.plain.ts.stub"
|
|
68
|
+
},
|
|
69
|
+
routeApi: "notification/route.api.ts.stub",
|
|
70
|
+
routePlain: "notification/route.plain.ts.stub",
|
|
71
|
+
job: "notification/job.ts.stub"
|
|
72
|
+
},
|
|
73
|
+
test: {
|
|
74
|
+
unit: "test/unit.ts.stub"
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Module scaffolding handlers shared by maker module commands. */
|
|
79
|
+
|
|
80
|
+
/** Read a stub file from the stubs directory. */
|
|
81
|
+
async function readStubRaw(name) {
|
|
82
|
+
const file = path.join(stubsRoot, name);
|
|
83
|
+
return fs.readFile(file, "utf8");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Read a stub file and replace {{key}} placeholders, removing unfilled ones. */
|
|
87
|
+
async function stub(name, values = {}) {
|
|
88
|
+
let content = await readStubRaw(name);
|
|
89
|
+
for (const [key, value] of Object.entries(values)) {
|
|
90
|
+
content = content.replaceAll(`{{${key}}}`, String(value));
|
|
91
|
+
}
|
|
92
|
+
return content.replace(/\{\{[A-Z0-9_]+\}\}/g, "");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Resolve the canonical module root path inside src/modules. */
|
|
96
|
+
function moduleRoot(moduleName) {
|
|
97
|
+
const base = path.resolve(process.cwd(), "src/modules");
|
|
98
|
+
const root = path.resolve(base, moduleName);
|
|
99
|
+
if (root !== base && !root.startsWith(base + path.sep)) {
|
|
100
|
+
throw new Error(`Module path escapes src/modules: ${moduleName}`);
|
|
101
|
+
}
|
|
102
|
+
return root;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Leaf folder name of a module. `admin/post` -> `post`, `post` -> `post`.
|
|
107
|
+
* File names and class names always come from the leaf; imports come from the
|
|
108
|
+
* full path.
|
|
109
|
+
*/
|
|
110
|
+
function leafName(moduleName) {
|
|
111
|
+
const segments = String(moduleName).split("/").filter(Boolean);
|
|
112
|
+
return segments[segments.length - 1] || String(moduleName);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Panel prefix of a module, everything before the leaf. `admin/post` -> `admin`,
|
|
117
|
+
* `admin/reporting/deep` -> `admin/reporting`, `post` -> "".
|
|
118
|
+
*/
|
|
119
|
+
function panelOf(moduleName) {
|
|
120
|
+
const segments = String(moduleName).split("/").filter(Boolean);
|
|
121
|
+
return segments.slice(0, -1).join("/");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Scalar tag for a module. Panels become a nested tag group so Scalar renders
|
|
126
|
+
* `Admin / Post` instead of flattening everything under one panel.
|
|
127
|
+
*/
|
|
128
|
+
function moduleLabel(moduleName) {
|
|
129
|
+
return String(moduleName)
|
|
130
|
+
.split("/")
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.map((segment) => pascal(segment))
|
|
133
|
+
.join(" / ");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Parse `--path=<panel>` into a normalized module prefix.
|
|
138
|
+
* A panel is only a folder separator, so nested values are allowed:
|
|
139
|
+
* `--path=admin` and `--path=admin/reporting` both validate.
|
|
140
|
+
*/
|
|
141
|
+
function parsePanelPath(flags) {
|
|
142
|
+
const matches = flags.filter((flag) => flag.startsWith("--path"));
|
|
143
|
+
if (matches.length === 0) return "";
|
|
144
|
+
if (matches.length > 1) throw new Error("--path can only be given once.");
|
|
145
|
+
const raw = matches[0].slice("--path".length).replace(/^=/, "").trim();
|
|
146
|
+
if (!raw) throw new Error("--path needs a value, e.g. --path=admin");
|
|
147
|
+
const segments = raw
|
|
148
|
+
.split(/[\\/]+/)
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
.map((segment) => segment.toLowerCase());
|
|
151
|
+
if (segments.length === 0) throw new Error(`Invalid --path value: ${raw}`);
|
|
152
|
+
for (const segment of segments) {
|
|
153
|
+
if (!/^[a-z0-9][a-z0-9-_]*$/.test(segment)) {
|
|
154
|
+
throw new Error(`Invalid --path segment "${segment}". Use lowercase letters, numbers and dashes.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return segments.join("/");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** True when the path is a directory holding at least one entry. */
|
|
161
|
+
async function dirHasFiles(target) {
|
|
162
|
+
try {
|
|
163
|
+
const entries = await fs.readdir(target);
|
|
164
|
+
return entries.length > 0;
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Resolve the module a subcommand targets.
|
|
172
|
+
*
|
|
173
|
+
* `rawModule` is always the leaf name the developer typed. Subcommands accept the
|
|
174
|
+
* same `--path=<panel>` flag as `module:make`, so `panelme --path=admin` addresses
|
|
175
|
+
* src/modules/admin/panelme instead of failing with "module does not exist".
|
|
176
|
+
* Flags may arrive either in the positional slot or in extraFlags depending on
|
|
177
|
+
* whether a name was given, so every candidate string is scanned.
|
|
178
|
+
*/
|
|
179
|
+
function resolveModuleRef(rawModule, ...flagGroups) {
|
|
180
|
+
const raw = assertName(rawModule, "Module name");
|
|
181
|
+
const panel = parsePanelPath(flagGroups.flat().filter((value) => typeof value === "string"));
|
|
182
|
+
const moduleName = panel ? `${panel}/${raw}` : raw;
|
|
183
|
+
// Derive the leaf from the resolved path so `admin/post` passed directly as the
|
|
184
|
+
// module argument still yields `post` for filenames and default names.
|
|
185
|
+
return { moduleName, leaf: leafName(moduleName) };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Ensure a module directory exists, throwing a helpful error if not. */
|
|
189
|
+
async function assertModuleExists(moduleName) {
|
|
190
|
+
const root = moduleRoot(moduleName);
|
|
191
|
+
let stats;
|
|
192
|
+
try {
|
|
193
|
+
stats = await fs.stat(root);
|
|
194
|
+
} catch {
|
|
195
|
+
throw new Error(`Module does not exist: ${moduleName}. Create it first with: bun maker module:make ${moduleName}`);
|
|
196
|
+
}
|
|
197
|
+
if (!stats.isDirectory()) {
|
|
198
|
+
throw new Error(`Module path is not a directory: src/modules/${moduleName}`);
|
|
199
|
+
}
|
|
200
|
+
return root;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Build the placeholder substitution map shared by module file generators. */
|
|
204
|
+
function moduleFileVars(moduleName, controller) {
|
|
205
|
+
return {
|
|
206
|
+
module: moduleName,
|
|
207
|
+
controller,
|
|
208
|
+
ClassName: pascal(controller),
|
|
209
|
+
name: leafName(moduleName),
|
|
210
|
+
tableVariable: `${controller}s`
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Generate a controller file for a module.
|
|
216
|
+
* When a matching service exists the controller delegates to it; otherwise it
|
|
217
|
+
* falls back to the standalone shape so the file never imports a missing module.
|
|
218
|
+
*/
|
|
219
|
+
async function controllerFile(moduleName, controllerName, openApi, withService) {
|
|
220
|
+
const controller = controllerName.trim().toLowerCase();
|
|
221
|
+
const controllerStub = withService
|
|
222
|
+
? openApi
|
|
223
|
+
? STUBS.controller.openapi
|
|
224
|
+
: STUBS.controller.plain
|
|
225
|
+
: STUBS.controller.standalone;
|
|
226
|
+
return await stub(controllerStub, moduleFileVars(moduleName, controller));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Generate a schema file for a module. */
|
|
230
|
+
async function schemaFile(moduleName, controllerName, openApi) {
|
|
231
|
+
const controller = controllerName.trim().toLowerCase();
|
|
232
|
+
const schemaStub = openApi ? STUBS.schema.openapi : STUBS.schema.plain;
|
|
233
|
+
return await stub(schemaStub, moduleFileVars(moduleName, controller));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Generate a service file for a module. withModel picks the Drizzle-backed variant. */
|
|
237
|
+
async function serviceFile(moduleName, controllerName, withModel) {
|
|
238
|
+
const controller = controllerName.trim().toLowerCase();
|
|
239
|
+
const serviceStub = withModel ? STUBS.service.named : STUBS.service.plain;
|
|
240
|
+
return await stub(serviceStub, moduleFileVars(moduleName, controller));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Build the full default scaffold for module:make. */
|
|
244
|
+
async function moduleFiles(moduleName, openApi) {
|
|
245
|
+
const name = leafName(moduleName);
|
|
246
|
+
const vars = moduleFileVars(moduleName, name);
|
|
247
|
+
// The with-model controller variant only exists for the openapi style.
|
|
248
|
+
const controllerStub = openApi ? STUBS.controller.openapiWithModel : STUBS.controller.plain;
|
|
249
|
+
return {
|
|
250
|
+
"facade.ts": await stub(STUBS.facade.named, vars),
|
|
251
|
+
[`controllers/${name}.ts`]: await stub(controllerStub, vars),
|
|
252
|
+
[`schemas/${name}.ts`]: await schemaFile(moduleName, name, openApi),
|
|
253
|
+
// module:make also writes database/models/<name>.ts, so the service can query it.
|
|
254
|
+
[`services/${name}.ts`]: await serviceFile(moduleName, name, true)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Generate a wired route file. Only module:make uses this: it owns the whole
|
|
260
|
+
* module, so the controller and schema it just created are known to exist.
|
|
261
|
+
*/
|
|
262
|
+
async function routeTemplate(moduleName, controllerName = moduleName) {
|
|
263
|
+
const controller = leafName(controllerName);
|
|
264
|
+
const routeStub = openApiEnabled() ? STUBS.route.api : STUBS.route.plain;
|
|
265
|
+
return await stub(routeStub, {
|
|
266
|
+
module: moduleName,
|
|
267
|
+
controller,
|
|
268
|
+
ClassName: pascal(controller),
|
|
269
|
+
ModuleClass: moduleLabel(moduleName)
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Generate a barebone route file that wires to nothing. module:make-route uses
|
|
275
|
+
* this so the developer decides which controller and schema to attach.
|
|
276
|
+
*/
|
|
277
|
+
async function standaloneRouteTemplate(moduleName, routeName) {
|
|
278
|
+
const routeStub = openApiEnabled() ? STUBS.route.standaloneApi : STUBS.route.standalonePlain;
|
|
279
|
+
return await stub(routeStub, {
|
|
280
|
+
module: moduleName,
|
|
281
|
+
controller: routeName,
|
|
282
|
+
ClassName: pascal(routeName),
|
|
283
|
+
ModuleClass: moduleLabel(moduleName)
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Generate a named model file for a module. */
|
|
288
|
+
async function namedModelTemplate(moduleName, name, dialect) {
|
|
289
|
+
return await stub(STUBS.model.named[dialect], {
|
|
290
|
+
module: moduleName,
|
|
291
|
+
name,
|
|
292
|
+
controller: name,
|
|
293
|
+
ClassName: pascal(name),
|
|
294
|
+
tableName: `${name}s`,
|
|
295
|
+
tableVariable: `${name}s`
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Generate a seeder file for a module model. */
|
|
300
|
+
async function namedSeederTemplate(moduleName, modelName, className) {
|
|
301
|
+
return await stub(STUBS.seeder.named, {
|
|
302
|
+
module: moduleName,
|
|
303
|
+
name: modelName,
|
|
304
|
+
controller: className,
|
|
305
|
+
ClassName: pascal(className),
|
|
306
|
+
tableName: `${modelName}s`,
|
|
307
|
+
tableVariable: `${modelName}s`
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Check if a file path exists on disk. */
|
|
312
|
+
async function pathExists(filePath) {
|
|
313
|
+
try {
|
|
314
|
+
await fs.access(filePath);
|
|
315
|
+
return true;
|
|
316
|
+
} catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Modules scaffolded before 4.1.0 kept schemas and services inside controllers/
|
|
323
|
+
* and used suffixed filenames. Returns the first legacy path that still exists
|
|
324
|
+
* so lookups resolve for existing projects.
|
|
325
|
+
*/
|
|
326
|
+
function legacyPath(moduleRootPath, folder, baseName) {
|
|
327
|
+
if (folder === "schemas") {
|
|
328
|
+
return path.join(moduleRootPath, "controllers", `${baseName}.schema.ts`);
|
|
329
|
+
}
|
|
330
|
+
if (folder === "helpers") {
|
|
331
|
+
return path.join(moduleRootPath, "controllers", `${baseName}.helpers.ts`);
|
|
332
|
+
}
|
|
333
|
+
if (folder === "services") {
|
|
334
|
+
return path.join(moduleRootPath, "controllers", `${baseName}.service.ts`);
|
|
335
|
+
}
|
|
336
|
+
return path.join(moduleRootPath, "controllers", `${baseName}.controller.ts`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Write a file with safety checks (dry-run support, force overwrite protection). */
|
|
340
|
+
async function writeFileSafe(filePath, content, flags = [], label = "File") {
|
|
341
|
+
const dryRun = hasFlag(flags, "--dry-run");
|
|
342
|
+
const force = hasFlag(flags, "--force") || hasFlag(flags, "--yes");
|
|
343
|
+
if (dryRun) return;
|
|
344
|
+
const exists = await pathExists(filePath);
|
|
345
|
+
if (exists && !force) {
|
|
346
|
+
throw new Error(`${label} already exists: ${path.relative(process.cwd(), filePath)}. Re-run with --force to overwrite.`);
|
|
347
|
+
}
|
|
348
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
349
|
+
await fs.writeFile(filePath, content);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Generate a complete module scaffold (facade, schema, service, controller, route, model, seeder).
|
|
354
|
+
* Pass `--path=<panel>` to nest the module under a panel folder, e.g.
|
|
355
|
+
* `module:make post --path=admin` writes to src/modules/admin/post/.
|
|
356
|
+
*/
|
|
357
|
+
export async function makeModule(rawName, flags = []) {
|
|
358
|
+
const result = await createModule(rawName, flags);
|
|
359
|
+
reportModule(result);
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Create several modules in one run: `module:make post tag testme`.
|
|
365
|
+
*
|
|
366
|
+
* A name that cannot be created is reported and the batch continues, so one bad
|
|
367
|
+
* argument never costs you the modules that were fine. The caller decides the
|
|
368
|
+
* exit code from `failed`.
|
|
369
|
+
*/
|
|
370
|
+
export async function makeModules(rawNames, flags = []) {
|
|
371
|
+
const created = [];
|
|
372
|
+
const failed = [];
|
|
373
|
+
for (const rawName of rawNames) {
|
|
374
|
+
try {
|
|
375
|
+
created.push(await createModule(rawName, flags));
|
|
376
|
+
} catch (error) {
|
|
377
|
+
failed.push({ name: rawName, reason: error.message });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (created.length) {
|
|
382
|
+
console.log(`Module${created.length > 1 ? "s" : ""} ready: ${created.map((item) => item.moduleName).join(", ")}`);
|
|
383
|
+
const panel = created[0].panel;
|
|
384
|
+
if (panel) {
|
|
385
|
+
console.log(` panel ${panel} -> ${created.map((item) => `/api/${item.moduleName}`).join(", ")}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
for (const item of failed) {
|
|
389
|
+
console.log(` skipped ${item.name}: ${item.reason}`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return { created, failed };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Print the per-module summary for a single-module run. */
|
|
396
|
+
function reportModule({ moduleName, panel, leaf }) {
|
|
397
|
+
console.log(`Module ready: ${moduleName}`);
|
|
398
|
+
if (panel) console.log(`Panel: ${panel} -> /api/${moduleName}`);
|
|
399
|
+
console.log(`Database files: models/${leaf}.ts, seeders/${leaf}.ts`);
|
|
400
|
+
console.log(`Route style: ${openApiEnabled() ? "openapi" : "plain"}`);
|
|
401
|
+
if (panel) {
|
|
402
|
+
console.log(`Fix unresolved imports with: bun maker alias`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Scaffold one module.
|
|
408
|
+
*
|
|
409
|
+
* Refuses to touch a module folder that already has content. Without this guard
|
|
410
|
+
* writeFiles overwrites unconditionally, so re-running module:make on an
|
|
411
|
+
* existing module silently destroys the developer's edits to the controller,
|
|
412
|
+
* service, schema and route.
|
|
413
|
+
*/
|
|
414
|
+
async function createModule(rawName, flags = []) {
|
|
415
|
+
const leaf = assertName(rawName, "Module name");
|
|
416
|
+
const panel = parsePanelPath(flags);
|
|
417
|
+
const moduleName = panel ? `${panel}/${leaf}` : leaf;
|
|
418
|
+
const root = moduleRoot(moduleName);
|
|
419
|
+
const force = hasFlag(flags, "--force") || hasFlag(flags, "--yes");
|
|
420
|
+
if (!force && (await dirHasFiles(root))) {
|
|
421
|
+
throw new Error(`Module already exists: src/modules/${moduleName}. Re-run with --force to overwrite its files.`);
|
|
422
|
+
}
|
|
423
|
+
const openApi = openApiEnabled();
|
|
424
|
+
const files = await moduleFiles(moduleName, openApi);
|
|
425
|
+
const route = await routeTemplate(moduleName);
|
|
426
|
+
const dialect = detectDialect();
|
|
427
|
+
const model = await namedModelTemplate(moduleName, leaf, dialect);
|
|
428
|
+
const seeder = await namedSeederTemplate(moduleName, leaf, leaf);
|
|
429
|
+
await writeFiles(root, { ...files, "routes/index.ts": route });
|
|
430
|
+
await fs.mkdir(path.join(root, "database", "models"), { recursive: true });
|
|
431
|
+
await fs.mkdir(path.join(root, "database", "seeders"), { recursive: true });
|
|
432
|
+
await fs.writeFile(path.join(root, "database", "models", `${leaf}.ts`), model);
|
|
433
|
+
await fs.writeFile(path.join(root, "database", "seeders", `${leaf}.ts`), seeder);
|
|
434
|
+
return { moduleName, panel, leaf };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Generate a route file for an existing module. */
|
|
438
|
+
export async function makeRoute(rawModule, rawControllerOrFlag, extraFlags = []) {
|
|
439
|
+
const { moduleName, leaf } = resolveModuleRef(rawModule, rawControllerOrFlag, extraFlags);
|
|
440
|
+
const root = await assertModuleExists(moduleName);
|
|
441
|
+
const flags = [];
|
|
442
|
+
let routeName = leaf;
|
|
443
|
+
let controllerExplicit = false;
|
|
444
|
+
if (rawControllerOrFlag) {
|
|
445
|
+
if (rawControllerOrFlag.startsWith("--")) flags.push(rawControllerOrFlag);
|
|
446
|
+
else {
|
|
447
|
+
routeName = assertName(rawControllerOrFlag, "Route name");
|
|
448
|
+
controllerExplicit = true;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
flags.push(...extraFlags);
|
|
452
|
+
const route = await standaloneRouteTemplate(moduleName, routeName);
|
|
453
|
+
const routeFile = controllerExplicit ? `${routeName}.ts` : "index.ts";
|
|
454
|
+
const routePath = path.join(root, `routes/${routeFile}`);
|
|
455
|
+
const dryRun = hasFlag(flags, "--dry-run");
|
|
456
|
+
const force = hasFlag(flags, "--force") || hasFlag(flags, "--yes");
|
|
457
|
+
if (dryRun) return;
|
|
458
|
+
let exists = false;
|
|
459
|
+
try {
|
|
460
|
+
await fs.access(routePath);
|
|
461
|
+
exists = true;
|
|
462
|
+
} catch {}
|
|
463
|
+
if (exists && !force)
|
|
464
|
+
throw new Error(`Route file already exists: ${path.relative(process.cwd(), routePath)}. Re-run with --force to overwrite.`);
|
|
465
|
+
await fs.mkdir(path.dirname(routePath), { recursive: true });
|
|
466
|
+
await fs.writeFile(routePath, route);
|
|
467
|
+
console.log(`Route ready: ${path.relative(process.cwd(), routePath)}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Generate a notification module with controller, routes, and job. */
|
|
471
|
+
export async function makeNotificationModule(rawName = "notification", flags = []) {
|
|
472
|
+
const { moduleName, leaf } = resolveModuleRef(rawName, flags);
|
|
473
|
+
const root = moduleRoot(moduleName);
|
|
474
|
+
const force = hasFlag(flags, "--force") || hasFlag(flags, "--yes");
|
|
475
|
+
if (!force && (await dirHasFiles(root))) {
|
|
476
|
+
throw new Error(`Module already exists: src/modules/${moduleName}. Re-run with --force to overwrite its files.`);
|
|
477
|
+
}
|
|
478
|
+
const openApi = openApiEnabled();
|
|
479
|
+
|
|
480
|
+
await writeFiles(root, {
|
|
481
|
+
[`controllers/${leaf}.ts`]: await stub(STUBS.notification.controller, {
|
|
482
|
+
module: moduleName
|
|
483
|
+
}),
|
|
484
|
+
[`schemas/${leaf}.ts`]: await stub(openApi ? STUBS.notification.schema.openapi : STUBS.notification.schema.plain, {
|
|
485
|
+
module: moduleName
|
|
486
|
+
}),
|
|
487
|
+
"routes/index.ts": await stub(openApi ? STUBS.notification.routeApi : STUBS.notification.routePlain, { module: moduleName }),
|
|
488
|
+
[`jobs/${leaf}.ts`]: await stub(STUBS.notification.job, { module: moduleName })
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
console.log(`Notification module ready: ${moduleName}`);
|
|
492
|
+
if (panelOf(moduleName)) console.log(`Panel: ${panelOf(moduleName)} -> /api/${moduleName}`);
|
|
493
|
+
console.log(`Route style: ${openApi ? "openapi" : "plain"}`);
|
|
494
|
+
console.log("See docs (packages/docs/guide/notification.md) for Vue UI integration.");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Soft-delete a module by moving it to storage trash. */
|
|
498
|
+
export async function deleteModule(rawName, flags = []) {
|
|
499
|
+
const { moduleName: name, leaf } = resolveModuleRef(rawName, flags);
|
|
500
|
+
if (leaf === "notification") {
|
|
501
|
+
console.log("Use `bun maker module:delete-notification` to remove the notification module (removes UI files too).");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const modulesRoot = path.resolve(process.cwd(), "src/modules");
|
|
505
|
+
const modulePath = path.resolve(modulesRoot, name);
|
|
506
|
+
const relative = path.relative(modulesRoot, modulePath);
|
|
507
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("Unsafe module path.");
|
|
508
|
+
let stats;
|
|
509
|
+
try {
|
|
510
|
+
stats = await fs.stat(modulePath);
|
|
511
|
+
} catch {
|
|
512
|
+
throw new Error(`Module not found: ${name}`);
|
|
513
|
+
}
|
|
514
|
+
if (!stats.isDirectory()) throw new Error(`Module path is not a directory: src/modules/${name}`);
|
|
515
|
+
const trashRoot = path.resolve(process.cwd(), "src/storage/trash/modules");
|
|
516
|
+
const stamp = new Date().toISOString().replace(/[.:]/g, "-");
|
|
517
|
+
// Trash keeps the panel path so admin/post stays distinguishable from admin/user.
|
|
518
|
+
const trashPath = path.join(trashRoot, `${name.replaceAll("/", "-")}-${stamp}`);
|
|
519
|
+
const dryRun = hasFlag(flags, "--dry-run");
|
|
520
|
+
const confirmed = hasFlag(flags, "--yes") || hasFlag(flags, "--force");
|
|
521
|
+
if (dryRun) return;
|
|
522
|
+
if (!confirmed) throw new Error(`Refusing to delete module without confirmation. Re-run with: bun maker module:delete ${name} --yes`);
|
|
523
|
+
await fs.mkdir(trashRoot, { recursive: true });
|
|
524
|
+
try {
|
|
525
|
+
await fs.rename(modulePath, trashPath);
|
|
526
|
+
} catch (error) {
|
|
527
|
+
if (error?.code === "EPERM" || error?.code === "EXDEV") {
|
|
528
|
+
await fs.cp(modulePath, trashPath, { recursive: true });
|
|
529
|
+
await fs.rm(modulePath, { recursive: true, force: true });
|
|
530
|
+
} else {
|
|
531
|
+
throw error;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
console.log(`Moved module to trash: ${path.relative(process.cwd(), trashPath)}`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Remove a notification module (backend only — moves to trash). */
|
|
538
|
+
export async function deleteNotificationModule(rawName = "notification", flags = []) {
|
|
539
|
+
const { moduleName, leaf } = resolveModuleRef(rawName, flags);
|
|
540
|
+
const dryRun = hasFlag(flags, "--dry-run");
|
|
541
|
+
const confirmed = hasFlag(flags, "--yes") || hasFlag(flags, "--force");
|
|
542
|
+
|
|
543
|
+
const modulePath = moduleRoot(moduleName);
|
|
544
|
+
|
|
545
|
+
if (dryRun) {
|
|
546
|
+
const exists = await fs
|
|
547
|
+
.stat(modulePath)
|
|
548
|
+
.then(() => true)
|
|
549
|
+
.catch(() => false);
|
|
550
|
+
if (exists) console.log(`Would delete: ${path.relative(process.cwd(), modulePath)}`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (!confirmed) {
|
|
555
|
+
throw new Error(
|
|
556
|
+
`Refusing to delete notification module without confirmation. Re-run with: bun maker module:delete-notification ${leaf} --yes${panelOf(moduleName) ? ` --path=${panelOf(moduleName)}` : ""}`
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const exists = await fs
|
|
561
|
+
.stat(modulePath)
|
|
562
|
+
.then(() => true)
|
|
563
|
+
.catch(() => false);
|
|
564
|
+
if (!exists) {
|
|
565
|
+
console.log("Notification module not found — nothing to delete.");
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const trashRoot = path.resolve(process.cwd(), "src/storage/trash/modules");
|
|
570
|
+
const stamp = new Date().toISOString().replace(/[.:]/g, "-");
|
|
571
|
+
await fs.mkdir(trashRoot, { recursive: true });
|
|
572
|
+
|
|
573
|
+
const relPath = path.relative(process.cwd(), modulePath);
|
|
574
|
+
const trashPath = path.join(trashRoot, `${relPath.replace(/[/\\]/g, "-")}-${stamp}`);
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
await fs.rename(modulePath, trashPath);
|
|
578
|
+
} catch (error) {
|
|
579
|
+
if (error?.code === "EPERM" || error?.code === "EXDEV") {
|
|
580
|
+
await fs.cp(modulePath, trashPath, { recursive: true });
|
|
581
|
+
await fs.rm(modulePath, { recursive: true, force: true });
|
|
582
|
+
} else {
|
|
583
|
+
throw error;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
console.log(`Moved to trash: ${relPath}`);
|
|
587
|
+
console.log("Notification module deleted.");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** Permanently remove entries from module trash storage. */
|
|
591
|
+
export async function cleanModuleTrash(rawName, flags = []) {
|
|
592
|
+
const trashRoot = path.resolve(process.cwd(), "src/storage/trash/modules");
|
|
593
|
+
const dryRun = hasFlag(flags, "--dry-run");
|
|
594
|
+
const confirmed = hasFlag(flags, "--yes") || hasFlag(flags, "--force");
|
|
595
|
+
let entries = [];
|
|
596
|
+
try {
|
|
597
|
+
entries = await fs.readdir(trashRoot, { withFileTypes: true });
|
|
598
|
+
} catch {
|
|
599
|
+
if (dryRun) return;
|
|
600
|
+
console.log("No trash entries found");
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const targetName = rawName && !rawName.startsWith("--") ? assertName(rawName, "Module name") : "";
|
|
604
|
+
const matches = entries.map((e) => e.name).filter((name) => !targetName || name === targetName || name.startsWith(`${targetName}-`));
|
|
605
|
+
if (!matches.length) {
|
|
606
|
+
console.log(targetName ? `No trash entries found for module '${targetName}'` : "No trash entries found");
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (dryRun) return;
|
|
610
|
+
if (!confirmed)
|
|
611
|
+
throw new Error(
|
|
612
|
+
targetName
|
|
613
|
+
? `Refusing to clean trash for '${targetName}' without confirmation. Re-run with: bun maker module:trash:clean ${targetName} --yes`
|
|
614
|
+
: "Refusing to clean all trash without confirmation. Re-run with: bun maker module:trash:clean --yes"
|
|
615
|
+
);
|
|
616
|
+
for (const name of matches) await fs.rm(path.join(trashRoot, name), { recursive: true, force: true });
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Split "<module> <nameOrFlag> [extraFlags]" into a validated name plus flags.
|
|
621
|
+
* Shared by the per-file commands so they all accept --force/--dry-run the same way.
|
|
622
|
+
*/
|
|
623
|
+
function parseNameArg(rawNameOrFlag, extraFlags, kindLabel, fallback) {
|
|
624
|
+
const flags = [];
|
|
625
|
+
let name = fallback;
|
|
626
|
+
if (rawNameOrFlag) {
|
|
627
|
+
if (rawNameOrFlag.startsWith("--")) flags.push(rawNameOrFlag);
|
|
628
|
+
else name = assertName(rawNameOrFlag, kindLabel);
|
|
629
|
+
}
|
|
630
|
+
flags.push(...extraFlags);
|
|
631
|
+
return { name, flags };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Generate a controller for an existing module. */
|
|
635
|
+
export async function makeController(rawModule, rawControllerOrFlag, extraFlags = []) {
|
|
636
|
+
const { moduleName, leaf } = resolveModuleRef(rawModule, rawControllerOrFlag, extraFlags);
|
|
637
|
+
const { name, flags } = parseNameArg(rawControllerOrFlag, extraFlags, "Controller name", leaf);
|
|
638
|
+
const root = await assertModuleExists(moduleName);
|
|
639
|
+
const lower = name.toLowerCase();
|
|
640
|
+
const hasService =
|
|
641
|
+
(await pathExists(path.join(root, "services", `${lower}.ts`))) ||
|
|
642
|
+
(await pathExists(legacyPath(root, "services", lower)));
|
|
643
|
+
const content = await controllerFile(moduleName, name, openApiEnabled(), hasService);
|
|
644
|
+
await writeFileSafe(path.join(root, `controllers/${lower}.ts`), content, flags, "Controller file");
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** Generate a schema file for an existing module. */
|
|
648
|
+
export async function makeSchema(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
649
|
+
const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
|
|
650
|
+
const { name, flags } = parseNameArg(rawNameOrFlag, extraFlags, "Schema name", leaf);
|
|
651
|
+
const root = await assertModuleExists(moduleName);
|
|
652
|
+
const content = await schemaFile(moduleName, name, openApiEnabled());
|
|
653
|
+
await writeFileSafe(path.join(root, `schemas/${name.toLowerCase()}.ts`), content, flags, "Schema file");
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Generate a service file for an existing module.
|
|
658
|
+
* Pass --with-model when the module has database/models/<name>.ts so the
|
|
659
|
+
* service queries the table instead of returning the empty defaults.
|
|
660
|
+
*/
|
|
661
|
+
export async function makeService(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
662
|
+
const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
|
|
663
|
+
const { name, flags } = parseNameArg(rawNameOrFlag, extraFlags, "Service name", leaf);
|
|
664
|
+
const root = await assertModuleExists(moduleName);
|
|
665
|
+
const lower = name.toLowerCase();
|
|
666
|
+
const withModel =
|
|
667
|
+
hasFlag(flags, "--with-model") || (await pathExists(path.join(root, "database", "models", `${lower}.ts`)));
|
|
668
|
+
const content = await serviceFile(moduleName, name, withModel);
|
|
669
|
+
await writeFileSafe(path.join(root, `services/${lower}.ts`), content, flags, "Service file");
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** Generate a helper file for an existing module. */
|
|
673
|
+
export async function makeHelper(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
674
|
+
const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
|
|
675
|
+
const { name, flags } = parseNameArg(rawNameOrFlag, extraFlags, "Helper name", leaf);
|
|
676
|
+
const root = await assertModuleExists(moduleName);
|
|
677
|
+
const content = await stub(STUBS.helper.named, { module: moduleName, name, Name: pascal(name) });
|
|
678
|
+
await writeFileSafe(path.join(root, `helpers/${name}.ts`), content, flags, "Helper file");
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** Generate a module-local middleware file for an existing module. */
|
|
682
|
+
export async function makeLocalMiddleware(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
683
|
+
const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
|
|
684
|
+
const { name, flags } = parseNameArg(rawNameOrFlag, extraFlags, "Middleware name", leaf);
|
|
685
|
+
const root = await assertModuleExists(moduleName);
|
|
686
|
+
const content = await stub(STUBS.middleware.local, {
|
|
687
|
+
module: moduleName,
|
|
688
|
+
name,
|
|
689
|
+
camel: camelCase(name),
|
|
690
|
+
Name: pascal(name)
|
|
691
|
+
});
|
|
692
|
+
await writeFileSafe(path.join(root, `middlewares/${name}.ts`), content, flags, "Middleware file");
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** Generate a types file for an existing module. */
|
|
696
|
+
export async function makeType(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
697
|
+
const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
|
|
698
|
+
const { name, flags } = parseNameArg(rawNameOrFlag, extraFlags, "Type name", leaf);
|
|
699
|
+
const root = await assertModuleExists(moduleName);
|
|
700
|
+
const content = await stub(STUBS.type.named, { module: moduleName, name, Name: pascal(name) });
|
|
701
|
+
await writeFileSafe(path.join(root, `types/${name}.ts`), content, flags, "Type file");
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** Generate a model file for an existing module. */
|
|
705
|
+
export async function makeModel(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
706
|
+
const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
|
|
707
|
+
const flags = [];
|
|
708
|
+
let name = leaf;
|
|
709
|
+
if (rawNameOrFlag) {
|
|
710
|
+
if (rawNameOrFlag.startsWith("--")) flags.push(rawNameOrFlag);
|
|
711
|
+
else name = assertName(rawNameOrFlag, "Model name");
|
|
712
|
+
}
|
|
713
|
+
flags.push(...extraFlags);
|
|
714
|
+
const dialect = detectDialect();
|
|
715
|
+
const root = await assertModuleExists(moduleName);
|
|
716
|
+
const modelPath = path.join(root, `database/models/${name}.ts`);
|
|
717
|
+
await writeFileSafe(modelPath, await namedModelTemplate(moduleName, name, dialect), flags, "Model file");
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** Generate a seeder file for an existing module model. */
|
|
721
|
+
export async function makeSeeder(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
722
|
+
const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
|
|
723
|
+
const flags = [];
|
|
724
|
+
let name = leaf;
|
|
725
|
+
if (rawNameOrFlag) {
|
|
726
|
+
if (rawNameOrFlag.startsWith("--")) flags.push(rawNameOrFlag);
|
|
727
|
+
else name = assertName(rawNameOrFlag, "Seeder name");
|
|
728
|
+
}
|
|
729
|
+
flags.push(...extraFlags);
|
|
730
|
+
const root = await assertModuleExists(moduleName);
|
|
731
|
+
let modelName = name;
|
|
732
|
+
let modelPath = path.join(root, "database", "models", `${modelName}.ts`);
|
|
733
|
+
|
|
734
|
+
try {
|
|
735
|
+
await fs.access(modelPath);
|
|
736
|
+
} catch {
|
|
737
|
+
// Fall back to the module's own model. This must use the leaf, not
|
|
738
|
+
// moduleName: for a panel module moduleName is "admin/testme", so using it
|
|
739
|
+
// would probe database/models/admin/testme.ts and never match.
|
|
740
|
+
modelName = leaf;
|
|
741
|
+
modelPath = path.join(root, "database", "models", `${modelName}.ts`);
|
|
742
|
+
|
|
743
|
+
try {
|
|
744
|
+
await fs.access(modelPath);
|
|
745
|
+
console.log(`Model '${name}' not found, using module model '${modelName}' for seeder '${name}'.`);
|
|
746
|
+
} catch {
|
|
747
|
+
const panel = panelOf(moduleName);
|
|
748
|
+
const suggest = panel ? `${leaf} ${name} --path=${panel}` : `${leaf} ${name}`;
|
|
749
|
+
throw new Error(
|
|
750
|
+
`Model not found for seeder: src/modules/${moduleName}/database/models/${name}.ts. Create it first with: bun maker module:make-model ${suggest}`
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const seeder = await namedSeederTemplate(moduleName, modelName, name);
|
|
756
|
+
await writeFileSafe(path.join(root, `database/seeders/${name}.ts`), seeder, flags, "Seeder file");
|
|
757
|
+
|
|
758
|
+
console.log(`Seeder ready: ${moduleName}/${name}`);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** Generate a job file for an existing module. */
|
|
762
|
+
export async function makeJob(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
763
|
+
const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
|
|
764
|
+
const flags = [];
|
|
765
|
+
let name = leaf;
|
|
766
|
+
if (rawNameOrFlag) {
|
|
767
|
+
if (rawNameOrFlag.startsWith("--")) flags.push(rawNameOrFlag);
|
|
768
|
+
else name = assertName(rawNameOrFlag, "Job name");
|
|
769
|
+
}
|
|
770
|
+
flags.push(...extraFlags);
|
|
771
|
+
const root = await assertModuleExists(moduleName);
|
|
772
|
+
await writeFileSafe(path.join(root, `jobs/${name}.ts`), await stub(STUBS.job.named, { module: moduleName, name }), flags, "Job file");
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** Generate a schedule/console file for an existing module. */
|
|
776
|
+
export async function makeSchedule(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
777
|
+
const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
|
|
778
|
+
const flags = [];
|
|
779
|
+
let name = leaf;
|
|
780
|
+
if (rawNameOrFlag) {
|
|
781
|
+
if (rawNameOrFlag.startsWith("--")) flags.push(rawNameOrFlag);
|
|
782
|
+
else name = assertName(rawNameOrFlag, "Schedule name");
|
|
783
|
+
}
|
|
784
|
+
flags.push(...extraFlags);
|
|
785
|
+
const root = await assertModuleExists(moduleName);
|
|
786
|
+
await writeFileSafe(
|
|
787
|
+
path.join(root, `console/${name}.ts`),
|
|
788
|
+
await stub(STUBS.schedule.named, { module: moduleName, name }),
|
|
789
|
+
flags,
|
|
790
|
+
"Schedule file"
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/** List all discovered modules. */
|
|
795
|
+
export async function listModules() {
|
|
796
|
+
const modulesRoot = path.resolve(process.cwd(), "src/modules");
|
|
797
|
+
try {
|
|
798
|
+
const entries = await fs.readdir(modulesRoot, { withFileTypes: true });
|
|
799
|
+
const modules = entries
|
|
800
|
+
.filter((entry) => entry.isDirectory())
|
|
801
|
+
.map((entry) => entry.name)
|
|
802
|
+
.sort();
|
|
803
|
+
if (!modules.length) {
|
|
804
|
+
console.log("No modules found.");
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
console.log("Modules:");
|
|
808
|
+
for (const module of modules) console.log(` - ${module}`);
|
|
809
|
+
} catch {
|
|
810
|
+
console.log("No modules found.");
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/** Check that a module has at least one seeder file. */
|
|
815
|
+
export async function assertModuleHasSeeders(rawModule, flags = []) {
|
|
816
|
+
const { moduleName } = resolveModuleRef(rawModule, flags);
|
|
817
|
+
await assertModuleExists(moduleName);
|
|
818
|
+
const files = await glob(`${moduleName}/database/seeders/*.{ts,js}`, {
|
|
819
|
+
cwd: path.resolve(process.cwd(), "src/modules"),
|
|
820
|
+
nodir: true,
|
|
821
|
+
windowsPathsNoEscape: true
|
|
822
|
+
});
|
|
823
|
+
if (!files.length) throw new Error(`No seeders found for module '${moduleName}'.`);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/** Generate src/database/schema.ts by aggregating all module model exports. */
|
|
827
|
+
export async function generateSchema(options = {}) {
|
|
828
|
+
const backendSrc = path.resolve(process.cwd(), "src");
|
|
829
|
+
const files = await glob("modules/**/database/models/*.{ts,js}", {
|
|
830
|
+
cwd: backendSrc,
|
|
831
|
+
nodir: true,
|
|
832
|
+
windowsPathsNoEscape: true
|
|
833
|
+
});
|
|
834
|
+
const exports = [];
|
|
835
|
+
for (const file of files.sort()) {
|
|
836
|
+
const absoluteFile = path.join(backendSrc, file);
|
|
837
|
+
const outputDir = path.join(backendSrc, "database");
|
|
838
|
+
const relativePath = path
|
|
839
|
+
.relative(outputDir, absoluteFile)
|
|
840
|
+
.replace(/\\/g, "/")
|
|
841
|
+
.replace(/\.(ts|js)$/, ".js");
|
|
842
|
+
const importPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
843
|
+
exports.push(`export * from "${importPath}";`);
|
|
844
|
+
}
|
|
845
|
+
const output = path.join(backendSrc, "database/schema.ts");
|
|
846
|
+
await fs.mkdir(path.dirname(output), { recursive: true });
|
|
847
|
+
await fs.writeFile(output, `${exports.join("\n")}\n`);
|
|
848
|
+
if (!options.silent) console.log(`Generated database schema with ${files.length} model file(s)`);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** Generate a temporary schema file for a single module (used for module:generate/migrate). */
|
|
852
|
+
async function generateModuleSchemaTemp(moduleName) {
|
|
853
|
+
await assertModuleExists(moduleName);
|
|
854
|
+
const backendSrc = path.resolve(process.cwd(), "src");
|
|
855
|
+
const files = await glob(`modules/${moduleName}/database/models/*.{ts,js}`, {
|
|
856
|
+
cwd: backendSrc,
|
|
857
|
+
nodir: true,
|
|
858
|
+
windowsPathsNoEscape: true
|
|
859
|
+
});
|
|
860
|
+
if (!files.length) throw new Error(`No model files found for module '${moduleName}'.`);
|
|
861
|
+
const exports = [];
|
|
862
|
+
const tempDir = path.resolve(process.cwd(), "src/storage/tmp");
|
|
863
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
864
|
+
// Flatten the panel path: "admin/testme" must not become a nested
|
|
865
|
+
// storage/tmp/schema.admin/testme.<ts>.ts path.
|
|
866
|
+
const slug = moduleName.replaceAll("/", "-");
|
|
867
|
+
const tempSchemaPath = path.join(tempDir, `schema.${slug}.${Date.now()}.ts`);
|
|
868
|
+
const tempSchemaDir = path.dirname(tempSchemaPath);
|
|
869
|
+
for (const file of files.sort()) {
|
|
870
|
+
const absoluteFile = path.join(backendSrc, file);
|
|
871
|
+
const relativePath = path
|
|
872
|
+
.relative(tempSchemaDir, absoluteFile)
|
|
873
|
+
.replace(/\\/g, "/")
|
|
874
|
+
.replace(/\.(ts|js)$/, ".ts");
|
|
875
|
+
const importPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
876
|
+
exports.push(`export * from "${importPath}";`);
|
|
877
|
+
}
|
|
878
|
+
await fs.writeFile(tempSchemaPath, `${exports.join("\n")}\n`);
|
|
879
|
+
return { tempSchemaPath, modelCount: files.length };
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Generate and run migrations for a single module using a temporary schema. */
|
|
883
|
+
export async function runModuleMigrate(rawModuleName, rawArgs = []) {
|
|
884
|
+
const { moduleName } = resolveModuleRef(rawModuleName, rawArgs);
|
|
885
|
+
await syncMigrationDialect();
|
|
886
|
+
await ensureMigrationMeta();
|
|
887
|
+
await ensureDatabaseDirectory();
|
|
888
|
+
const keepTemp = rawArgs.includes("--keep-temp");
|
|
889
|
+
const { tempSchemaPath, modelCount } = await generateModuleSchemaTemp(moduleName);
|
|
890
|
+
const drizzleSchemaPath = `./${path.relative(process.cwd(), tempSchemaPath).replace(/\\/g, "/")}`;
|
|
891
|
+
const previousSchema = process.env.DRIZZLE_SCHEMA;
|
|
892
|
+
try {
|
|
893
|
+
process.env.DRIZZLE_SCHEMA = drizzleSchemaPath;
|
|
894
|
+
await runNodeScript(packageScript("drizzle-kit", "bin.cjs"), await drizzleGenerateArgs());
|
|
895
|
+
await runNodeScript(packageScript("drizzle-kit", "bin.cjs"), ["migrate"]);
|
|
896
|
+
console.log(`Module migration complete: ${moduleName} (${modelCount} model file(s))`);
|
|
897
|
+
} finally {
|
|
898
|
+
if (previousSchema == null) delete process.env.DRIZZLE_SCHEMA;
|
|
899
|
+
else process.env.DRIZZLE_SCHEMA = previousSchema;
|
|
900
|
+
if (!keepTemp) await fs.rm(tempSchemaPath, { force: true }).catch(() => {});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** Generate a unit test file for a module. */
|
|
905
|
+
export async function makeTest(rawModule, rawNameOrFlag, extraFlags = []) {
|
|
906
|
+
const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
|
|
907
|
+
const flags = [];
|
|
908
|
+
let name = leaf;
|
|
909
|
+
if (rawNameOrFlag) {
|
|
910
|
+
if (rawNameOrFlag.startsWith("--")) flags.push(rawNameOrFlag);
|
|
911
|
+
else name = assertName(rawNameOrFlag, "Test name");
|
|
912
|
+
}
|
|
913
|
+
flags.push(...extraFlags);
|
|
914
|
+
const root = await assertModuleExists(moduleName);
|
|
915
|
+
const testDir = path.join(root, "__tests__");
|
|
916
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
917
|
+
const testPath = path.join(testDir, `${name}.test.ts`);
|
|
918
|
+
await writeFileSafe(testPath, await stub(STUBS.test.unit, { module: moduleName, name }), flags, "Test file");
|
|
919
|
+
console.log(`Test ready: ${path.relative(process.cwd(), testPath)}`);
|
|
920
|
+
}
|