create-rebe 1.0.0 → 3.0.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/package.json +1 -1
- package/template/.env.example +4 -0
- package/template/README.md +63 -8
- package/template/SECURITY.md +39 -0
- package/template/app/routes/api.route.js +1 -1
- package/template/app/routes/register.route.js +1 -1
- package/template/app/routes/web.route.js +1 -1
- package/template/app/socket/register.socket.js +0 -2
- package/template/config/express.config.js +8 -0
- package/template/core/common/string.js +1 -1
- package/template/core/cron.core.js +1 -0
- package/template/core/database.core.js +30 -17
- package/template/core/error.core.js +8 -4
- package/template/core/express.core.js +16 -4
- package/template/core/hooks.core.js +10 -7
- package/template/core/migrator.core.js +201 -0
- package/template/core/modules.core.js +167 -0
- package/template/core/queue.core.js +1 -0
- package/template/core/routing.core.d.ts +273 -0
- package/template/core/routing.core.js +666 -0
- package/template/core/seeder.core.js +105 -0
- package/template/core/socket.core.js +1 -0
- package/template/database/migrations/.gitkeep +0 -0
- package/template/database/seeders/.gitkeep +0 -0
- package/template/docs/Database.md +14 -8
- package/template/docs/Express.md +5 -2
- package/template/docs/Make.md +46 -0
- package/template/docs/Migration.md +56 -0
- package/template/docs/Modules.md +96 -0
- package/template/docs/README.md +5 -0
- package/template/docs/Routing.md +116 -0
- package/template/docs/Seeder.md +54 -0
- package/template/eslint.config.js +52 -0
- package/template/package-lock.json +1068 -70
- package/template/package.json +15 -8
- package/template/scripts/cli/args.js +39 -0
- package/template/scripts/cli/bootstrap.js +16 -0
- package/template/scripts/cli/db.js +79 -0
- package/template/scripts/cli/help.js +58 -0
- package/template/scripts/cli/keys.js +100 -0
- package/template/scripts/cli/log.js +58 -0
- package/template/scripts/cli/make.js +249 -0
- package/template/scripts/cli/names.js +51 -0
- package/template/scripts/cli/templates.js +358 -0
- package/template/scripts/cli.js +75 -234
- package/template/tests/http.test.js +99 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Name + timestamp helpers shared by the scaffolding commands.
|
|
4
|
+
|
|
5
|
+
// Split an arbitrary identifier (PascalCase, camelCase, snake_case, kebab-case,
|
|
6
|
+
// "space separated") into lowercase words.
|
|
7
|
+
function words(input) {
|
|
8
|
+
return String(input)
|
|
9
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
10
|
+
.replace(/[_\-./\\]+/g, ' ')
|
|
11
|
+
.trim()
|
|
12
|
+
.split(/\s+/)
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.map((w) => w.toLowerCase())
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toPascal(input) {
|
|
18
|
+
return words(input)
|
|
19
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
20
|
+
.join('')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toSnake(input) {
|
|
24
|
+
return words(input).join('_')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toKebab(input) {
|
|
28
|
+
return words(input).join('-')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Naive English pluralizer — enough for table names; override with --table when wrong.
|
|
32
|
+
function pluralize(word) {
|
|
33
|
+
if (/[^aeiou]y$/i.test(word)) return word.replace(/y$/i, 'ies')
|
|
34
|
+
if (/(s|x|z|ch|sh)$/i.test(word)) return `${word}es`
|
|
35
|
+
return `${word}s`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function tableName(input) {
|
|
39
|
+
const parts = words(input)
|
|
40
|
+
if (!parts.length) return ''
|
|
41
|
+
const last = parts.pop()
|
|
42
|
+
return [...parts, pluralize(last)].join('_')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Filesystem timestamp prefix: YYYYMMDDHHmmss (local time).
|
|
46
|
+
function timestamp(date = new Date()) {
|
|
47
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
48
|
+
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { words, toPascal, toSnake, toKebab, pluralize, tableName, timestamp }
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { toPascal, toKebab } = require('./names')
|
|
4
|
+
|
|
5
|
+
// Scaffolding templates. Each returns a file's contents as a string. Output mirrors
|
|
6
|
+
// the hand-written examples in database/models and the framework's code style
|
|
7
|
+
// (tabs, single quotes, no semicolons).
|
|
8
|
+
|
|
9
|
+
function modelTemplate(name, table) {
|
|
10
|
+
const Model = toPascal(name)
|
|
11
|
+
return `'use strict'
|
|
12
|
+
|
|
13
|
+
// Sequelize model. database.core loads every file in database/models/*.js (and each
|
|
14
|
+
// module's models/ dir) when DB_ENABLED=true; files prefixed with "_" are ignored.
|
|
15
|
+
// The factory receives (sequelize, DataTypes) and returns the model; associations
|
|
16
|
+
// are wired in a second pass via the static associate(models).
|
|
17
|
+
module.exports = (sequelize, DataTypes) => {
|
|
18
|
+
const ${Model} = sequelize.define(
|
|
19
|
+
'${Model}',
|
|
20
|
+
{
|
|
21
|
+
id: { type: DataTypes.BIGINT.UNSIGNED, primaryKey: true, autoIncrement: true },
|
|
22
|
+
// TODO: declare columns
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
tableName: '${table}',
|
|
26
|
+
underscored: true,
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
${Model}.associate = (models) => {
|
|
31
|
+
// e.g. ${Model}.belongsTo(models.User, { foreignKey: 'user_id', as: 'user' })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return ${Model}
|
|
35
|
+
}
|
|
36
|
+
`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createMigrationTemplate(table) {
|
|
40
|
+
return `'use strict'
|
|
41
|
+
|
|
42
|
+
// Auto-generated create-table migration. Edit the column definitions to match the
|
|
43
|
+
// model. Run with: npm run cli -- db:migrate / roll back: db:rollback
|
|
44
|
+
module.exports = {
|
|
45
|
+
async up(queryInterface, Sequelize) {
|
|
46
|
+
await queryInterface.createTable(
|
|
47
|
+
'${table}',
|
|
48
|
+
{
|
|
49
|
+
id: { type: Sequelize.BIGINT.UNSIGNED, primaryKey: true, autoIncrement: true },
|
|
50
|
+
// TODO: declare columns to match the model
|
|
51
|
+
created_at: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.fn('NOW') },
|
|
52
|
+
updated_at: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.fn('NOW') },
|
|
53
|
+
},
|
|
54
|
+
{ charset: 'utf8mb4' },
|
|
55
|
+
)
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async down(queryInterface) {
|
|
59
|
+
await queryInterface.dropTable('${table}')
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function blankMigrationTemplate(name) {
|
|
66
|
+
return `'use strict'
|
|
67
|
+
|
|
68
|
+
// Migration: ${name}
|
|
69
|
+
// up() applies the change, down() reverts it. queryInterface is the Sequelize
|
|
70
|
+
// QueryInterface; Sequelize exposes DataTypes (Sequelize.STRING, etc.).
|
|
71
|
+
module.exports = {
|
|
72
|
+
async up(queryInterface, Sequelize) {
|
|
73
|
+
// e.g. await queryInterface.addColumn('users', 'status', { type: Sequelize.STRING(20) })
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async down(queryInterface, Sequelize) {
|
|
77
|
+
// e.g. await queryInterface.removeColumn('users', 'status')
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function seederTemplate(name) {
|
|
84
|
+
return `'use strict'
|
|
85
|
+
|
|
86
|
+
// Seeder: ${name}
|
|
87
|
+
// Seeders are not tracked and may run repeatedly — write them idempotently
|
|
88
|
+
// (findOrCreate / upsert). Receives { sequelize, Database, Sequelize, models }.
|
|
89
|
+
// Run with: npm run cli -- db:seed --class=${name} / all: db:seed --all
|
|
90
|
+
module.exports = async ({ models, sequelize, Database }) => {
|
|
91
|
+
// const User = Database.model('User')
|
|
92
|
+
// await User.findOrCreate({ where: { email: 'admin@example.com' }, defaults: { name: 'Admin' } })
|
|
93
|
+
}
|
|
94
|
+
`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Full module entry — a self-contained mini-app. Discovered by modules.core at
|
|
98
|
+
// runtime; the flat layout (database/models, app/routes…) keeps working alongside it.
|
|
99
|
+
// Dir fields (models/migrations/seeders/routes) are scanned; the function/object
|
|
100
|
+
// fields (middlewares/jobs/queue/socket/hooks) mirror the app/.../register.*.js shapes.
|
|
101
|
+
// Remove any field — and its file — for a layer this module does not need.
|
|
102
|
+
function moduleEntryTemplate(name) {
|
|
103
|
+
return `'use strict'
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
name: '${name}',
|
|
107
|
+
|
|
108
|
+
// Scanned directories
|
|
109
|
+
models: './models', // (sequelize, DataTypes) factories
|
|
110
|
+
migrations: './migrations', // up/down migration files
|
|
111
|
+
seeders: './seeders', // seeder files
|
|
112
|
+
routes: './routes', // required for its routing side-effect
|
|
113
|
+
|
|
114
|
+
// Register hooks (same facades as app/.../register.*.js)
|
|
115
|
+
middlewares: require('./http/middlewares/register.middleware'), // (app) => {}
|
|
116
|
+
jobs: require('./jobs/register.job'), // (Cron) => {}
|
|
117
|
+
queue: require('./queue/register.queue'), // (Queue) => {}
|
|
118
|
+
socket: require('./socket/register.socket'), // (io, Socket) => {}
|
|
119
|
+
hooks: require('./hooks/register.hook'), // { before, after, shutdown }
|
|
120
|
+
}
|
|
121
|
+
`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Global middleware register — (app) => {}. Mirrors app/http/middlewares/register.middleware.js.
|
|
125
|
+
function appMiddlewareTemplate(name) {
|
|
126
|
+
return `'use strict'
|
|
127
|
+
|
|
128
|
+
// Global middleware for the "${name}" module. Receives the Express app, applied
|
|
129
|
+
// during boot after the security/parsing stack.
|
|
130
|
+
module.exports = (app) => {
|
|
131
|
+
// app.use((req, res, next) => next())
|
|
132
|
+
}
|
|
133
|
+
`
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function moduleRouteTemplate(name) {
|
|
137
|
+
return `'use strict'
|
|
138
|
+
|
|
139
|
+
// Routes for the "${name}" module. Uses the same Routes facade as app/routes.
|
|
140
|
+
const Routes = require('@core/routing.core')
|
|
141
|
+
|
|
142
|
+
Routes.group('${name}', () => {
|
|
143
|
+
Routes.get('', ({ res }) => {
|
|
144
|
+
res.json({ status: true, code: 200, message: '${name} module ready', data: null, meta: null })
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
module.exports = Routes
|
|
149
|
+
`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── App-layer scaffolds ──────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
function controllerTemplate(name, { resource = false, api = false } = {}) {
|
|
155
|
+
const Ctrl = `${toPascal(name)}Controller`
|
|
156
|
+
const ok = (message, data = 'null') => `res.json({ status: true, code: 200, message: '${message}', data: ${data}, meta: null })`
|
|
157
|
+
|
|
158
|
+
if (!resource) {
|
|
159
|
+
return `'use strict'
|
|
160
|
+
|
|
161
|
+
// HTTP controller. Static methods receive the context { req, res } and return the
|
|
162
|
+
// manual response envelope. Wire it in a route file, e.g.:
|
|
163
|
+
// Routes.get('${toKebab(name)}', ${Ctrl}.index)
|
|
164
|
+
class ${Ctrl} {
|
|
165
|
+
static async index({ req, res }) {
|
|
166
|
+
return ${ok('OK', '[]')}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = ${Ctrl}
|
|
171
|
+
`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const methods = []
|
|
175
|
+
methods.push(` // GET /${toKebab(name)}
|
|
176
|
+
static async index({ req, res }) {
|
|
177
|
+
return ${ok('OK', '[]')}
|
|
178
|
+
}`)
|
|
179
|
+
if (!api) {
|
|
180
|
+
methods.push(` // GET /${toKebab(name)}/create
|
|
181
|
+
static async create({ req, res }) {
|
|
182
|
+
return ${ok('OK')}
|
|
183
|
+
}`)
|
|
184
|
+
}
|
|
185
|
+
methods.push(` // POST /${toKebab(name)}
|
|
186
|
+
static async store({ req, res }) {
|
|
187
|
+
return res.status(201).json({ status: true, code: 201, message: 'Created', data: req.body, meta: null })
|
|
188
|
+
}`)
|
|
189
|
+
methods.push(` // GET /${toKebab(name)}/:id
|
|
190
|
+
static async show({ req, res }) {
|
|
191
|
+
return ${ok('OK', '{ id: req.params.id }')}
|
|
192
|
+
}`)
|
|
193
|
+
if (!api) {
|
|
194
|
+
methods.push(` // GET /${toKebab(name)}/:id/edit
|
|
195
|
+
static async edit({ req, res }) {
|
|
196
|
+
return ${ok('OK', '{ id: req.params.id }')}
|
|
197
|
+
}`)
|
|
198
|
+
}
|
|
199
|
+
methods.push(` // PUT|PATCH /${toKebab(name)}/:id
|
|
200
|
+
static async update({ req, res }) {
|
|
201
|
+
return ${ok('Updated', '{ id: req.params.id, ...req.body }')}
|
|
202
|
+
}`)
|
|
203
|
+
methods.push(` // DELETE /${toKebab(name)}/:id
|
|
204
|
+
static async destroy({ req, res }) {
|
|
205
|
+
return ${ok('Deleted')}
|
|
206
|
+
}`)
|
|
207
|
+
|
|
208
|
+
return `'use strict'
|
|
209
|
+
|
|
210
|
+
// RESTful resource controller. Register every action at once with:
|
|
211
|
+
// Routes.${api ? 'apiResource' : 'resource'}('${toKebab(name)}', ${Ctrl})
|
|
212
|
+
// Only the actions present here are mounted; rename/remove as needed.
|
|
213
|
+
class ${Ctrl} {
|
|
214
|
+
${methods.join('\n\n')}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = ${Ctrl}
|
|
218
|
+
`
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function middlewareTemplate(name) {
|
|
222
|
+
const Mw = toPascal(name)
|
|
223
|
+
return `'use strict'
|
|
224
|
+
|
|
225
|
+
// Route middleware. Implements handle({ req, res, next, error }); call next() to pass
|
|
226
|
+
// control on, or send a response to short-circuit. Use per-route or as an alias:
|
|
227
|
+
// Routes.get('path', Controller.action, [${Mw}])
|
|
228
|
+
// Routes.registerMiddleware('${toKebab(name)}', ${Mw})
|
|
229
|
+
class ${Mw} {
|
|
230
|
+
static handle({ req, res, next }) {
|
|
231
|
+
// TODO: guard logic
|
|
232
|
+
next()
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = ${Mw}
|
|
237
|
+
`
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function validatorTemplate(name) {
|
|
241
|
+
const base = toPascal(name)
|
|
242
|
+
return `'use strict'
|
|
243
|
+
|
|
244
|
+
const { z } = require('zod')
|
|
245
|
+
|
|
246
|
+
// Zod schemas. Apply per-route via the validator core:
|
|
247
|
+
// Routes.post('${toKebab(name)}', ${base}Controller.store, [Validator.make(create${base}Schema)])
|
|
248
|
+
const create${base}Schema = z.object({
|
|
249
|
+
// name: z.string().min(1),
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
const update${base}Schema = create${base}Schema.partial()
|
|
253
|
+
|
|
254
|
+
module.exports = { create${base}Schema, update${base}Schema }
|
|
255
|
+
`
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function routeTemplate(name) {
|
|
259
|
+
return moduleRouteTemplate(toKebab(name))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Resource route file wiring a controller to Routes.resource/apiResource. The
|
|
263
|
+
// controllerRequire path differs for flat (@http alias) vs module (relative) layouts.
|
|
264
|
+
function resourceRouteTemplate(name, { api = false, controllerRequire } = {}) {
|
|
265
|
+
const Ctrl = `${toPascal(name)}Controller`
|
|
266
|
+
const slug = toKebab(name)
|
|
267
|
+
return `'use strict'
|
|
268
|
+
|
|
269
|
+
const Routes = require('@core/routing.core')
|
|
270
|
+
const ${Ctrl} = require('${controllerRequire}')
|
|
271
|
+
|
|
272
|
+
// Registers index/show/store/update/destroy${api ? '' : '/create/edit'} for any of those
|
|
273
|
+
// actions the controller implements.
|
|
274
|
+
Routes.${api ? 'apiResource' : 'resource'}('${slug}', ${Ctrl})
|
|
275
|
+
|
|
276
|
+
module.exports = Routes
|
|
277
|
+
`
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function jobTemplate(name) {
|
|
281
|
+
return `'use strict'
|
|
282
|
+
|
|
283
|
+
const logger = require('@core/logger.core')
|
|
284
|
+
|
|
285
|
+
// Cron job module. Receives the Cron facade. Register with Cron.define(name, expr, fn).
|
|
286
|
+
// Expression: second? minute hour day-of-month month day-of-week
|
|
287
|
+
module.exports = (Cron) => {
|
|
288
|
+
Cron.define('${toKebab(name)}', '0 * * * *', async () => {
|
|
289
|
+
logger.debug('${toKebab(name)} tick')
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
`
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function queueTemplate(name) {
|
|
296
|
+
return `'use strict'
|
|
297
|
+
|
|
298
|
+
const logger = require('@core/logger.core')
|
|
299
|
+
|
|
300
|
+
// Queue worker module. Receives the Queue facade. Register handlers with
|
|
301
|
+
// Queue.define(name, handler, options); dispatch jobs with Queue.dispatch(name, payload).
|
|
302
|
+
module.exports = (Queue) => {
|
|
303
|
+
Queue.define('${toKebab(name)}', async (payload, job) => {
|
|
304
|
+
logger.debug(\`${toKebab(name)} processing job \${job.id}\`)
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
`
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function socketTemplate(name) {
|
|
311
|
+
return `'use strict'
|
|
312
|
+
|
|
313
|
+
const logger = require('@core/logger.core')
|
|
314
|
+
|
|
315
|
+
// Socket handlers. Receives (io, Socket). Register connection logic here.
|
|
316
|
+
module.exports = (io, Socket) => {
|
|
317
|
+
io.on('connection', (socket) => {
|
|
318
|
+
logger.debug(\`[${toKebab(name)}] socket connected: \${socket.id}\`)
|
|
319
|
+
|
|
320
|
+
socket.on('${toKebab(name)}:ping', (data, ack) => {
|
|
321
|
+
if (typeof ack === 'function') ack({ ok: true, data })
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
`
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function hookTemplate() {
|
|
329
|
+
return `'use strict'
|
|
330
|
+
|
|
331
|
+
// Lifecycle hooks. before() runs before the server starts, after() once it is ready,
|
|
332
|
+
// and shutdown() during graceful shutdown. Each receives { config }.
|
|
333
|
+
module.exports = {
|
|
334
|
+
async before({ config }) {},
|
|
335
|
+
async after({ config }) {},
|
|
336
|
+
async shutdown({ config }) {},
|
|
337
|
+
}
|
|
338
|
+
`
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
module.exports = {
|
|
342
|
+
modelTemplate,
|
|
343
|
+
createMigrationTemplate,
|
|
344
|
+
blankMigrationTemplate,
|
|
345
|
+
seederTemplate,
|
|
346
|
+
moduleEntryTemplate,
|
|
347
|
+
moduleRouteTemplate,
|
|
348
|
+
controllerTemplate,
|
|
349
|
+
middlewareTemplate,
|
|
350
|
+
validatorTemplate,
|
|
351
|
+
routeTemplate,
|
|
352
|
+
jobTemplate,
|
|
353
|
+
queueTemplate,
|
|
354
|
+
socketTemplate,
|
|
355
|
+
hookTemplate,
|
|
356
|
+
resourceRouteTemplate,
|
|
357
|
+
appMiddlewareTemplate,
|
|
358
|
+
}
|