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
package/template/scripts/cli.js
CHANGED
|
@@ -1,258 +1,99 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
'use strict'
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// Display name is driven by APP_NAME (no hardcoded brand). Falls back to 'rebe'
|
|
13
|
-
// when .env is not present yet (e.g. before `setup`).
|
|
14
|
-
const APP_NAME = (process.env.APP_NAME || 'rebe').toUpperCase()
|
|
15
|
-
|
|
16
|
-
class Log {
|
|
17
|
-
static colors = {
|
|
18
|
-
reset: '\x1b[0m',
|
|
19
|
-
red: '\x1b[31m',
|
|
20
|
-
green: '\x1b[32m',
|
|
21
|
-
yellow: '\x1b[33m',
|
|
22
|
-
blue: '\x1b[34m',
|
|
23
|
-
cyan: '\x1b[36m',
|
|
24
|
-
white: '\x1b[37m',
|
|
25
|
-
gray: '\x1b[90m',
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
static color(color, message) {
|
|
29
|
-
return `${this.colors[color]}${message}${this.colors.reset}`
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
static info(message) {
|
|
33
|
-
console.log(this.color('blue', `[INFO] ${message}`))
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
static success(message) {
|
|
37
|
-
console.log(this.color('green', `[ OK ] ${message}`))
|
|
38
|
-
}
|
|
3
|
+
// Project CLI dispatcher. Thin by design: it parses the command name and routes to a
|
|
4
|
+
// handler in scripts/cli/. Three classes of command:
|
|
5
|
+
// • meta/keys — env + key generation (scripts/cli/keys.js, help.js)
|
|
6
|
+
// • make:* — file scaffolding, no runtime needed (scripts/cli/make.js)
|
|
7
|
+
// • db:* — touch the cores; the runtime (module-alias + .env) is prepared
|
|
8
|
+
// lazily and the DB connection is always closed afterwards.
|
|
39
9
|
|
|
40
|
-
|
|
41
|
-
console.log(this.color('yellow', `[WARN] ${message}`))
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
static error(message) {
|
|
45
|
-
console.log(this.color('red', `[FAIL] ${message}`))
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
static line() {
|
|
49
|
-
console.log(this.color('gray', '────────────────────────────────────────'))
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
static banner() {
|
|
53
|
-
const width = 38
|
|
54
|
-
const title = `${APP_NAME} CLI`
|
|
55
|
-
const padTotal = Math.max(0, width - title.length)
|
|
56
|
-
const left = Math.floor(padTotal / 2)
|
|
57
|
-
const centered = ' '.repeat(left) + title + ' '.repeat(padTotal - left)
|
|
10
|
+
require('dotenv').config()
|
|
58
11
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
12
|
+
const Log = require('./cli/log')
|
|
13
|
+
const { help } = require('./cli/help')
|
|
14
|
+
const keys = require('./cli/keys')
|
|
15
|
+
const make = require('./cli/make')
|
|
16
|
+
const db = require('./cli/db')
|
|
17
|
+
const { prepareRuntime } = require('./cli/bootstrap')
|
|
66
18
|
|
|
67
19
|
const command = process.argv[2]
|
|
68
20
|
const args = process.argv.slice(3)
|
|
69
21
|
|
|
70
|
-
|
|
71
|
-
|
|
22
|
+
// Commands that talk to the database. Each entry maps an alias to a db handler.
|
|
23
|
+
const DB_ROUTES = {
|
|
24
|
+
'db:migrate': db.migrate,
|
|
25
|
+
migrate: db.migrate,
|
|
26
|
+
'db:rollback': db.rollback,
|
|
27
|
+
rollback: db.rollback,
|
|
28
|
+
'db:reset': db.reset,
|
|
29
|
+
reset: db.reset,
|
|
30
|
+
'db:refresh': db.refresh,
|
|
31
|
+
refresh: db.refresh,
|
|
32
|
+
'db:fresh': db.fresh,
|
|
33
|
+
fresh: db.fresh,
|
|
34
|
+
'db:status': db.status,
|
|
35
|
+
status: db.status,
|
|
36
|
+
'db:wipe': db.wipe,
|
|
37
|
+
wipe: db.wipe,
|
|
38
|
+
'db:seed': db.seed,
|
|
39
|
+
seed: db.seed,
|
|
40
|
+
'db:seed-all': db.seedAll,
|
|
41
|
+
'seed-all': db.seedAll,
|
|
72
42
|
}
|
|
73
43
|
|
|
74
|
-
|
|
75
|
-
|
|
44
|
+
// Synchronous (no DB) commands.
|
|
45
|
+
const META_ROUTES = {
|
|
46
|
+
undefined: help,
|
|
47
|
+
help,
|
|
48
|
+
setup: () => keys.setup(args),
|
|
49
|
+
'key:generate': () => keys.generateAppKey(),
|
|
50
|
+
'key:jwt': () => keys.keyJwt(args),
|
|
51
|
+
'make:model': () => make.makeModel(args),
|
|
52
|
+
'make:migration': () => make.makeMigration(args),
|
|
53
|
+
'make:seed': () => make.makeSeed(args),
|
|
54
|
+
'make:controller': () => make.makeController(args),
|
|
55
|
+
'make:middleware': () => make.makeMiddleware(args),
|
|
56
|
+
'make:validator': () => make.makeValidator(args),
|
|
57
|
+
'make:route': () => make.makeRoute(args),
|
|
58
|
+
'make:job': () => make.makeJob(args),
|
|
59
|
+
'make:queue': () => make.makeQueue(args),
|
|
60
|
+
'make:socket': () => make.makeSocket(args),
|
|
61
|
+
'make:hook': () => make.makeHook(args),
|
|
62
|
+
'make:resource': () => make.makeResource(args),
|
|
63
|
+
'make:module': () => make.makeModule(args),
|
|
76
64
|
}
|
|
77
65
|
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const regex = new RegExp(`^${key}=.*$`, 'm')
|
|
89
|
-
|
|
90
|
-
if (regex.test(content)) {
|
|
91
|
-
content = content.replace(regex, `${key}=${value}`)
|
|
92
|
-
} else {
|
|
93
|
-
content += `\n${key}=${value}`
|
|
66
|
+
async function runDbCommand(handler) {
|
|
67
|
+
prepareRuntime()
|
|
68
|
+
const Database = require('@core/database.core')
|
|
69
|
+
try {
|
|
70
|
+
await handler(args)
|
|
71
|
+
} catch (err) {
|
|
72
|
+
Log.error(err.message)
|
|
73
|
+
process.exitCode = 1
|
|
74
|
+
} finally {
|
|
75
|
+
await Database.disconnect().catch(() => {})
|
|
94
76
|
}
|
|
95
|
-
|
|
96
|
-
fs.writeFileSync(envPath, content)
|
|
97
|
-
|
|
98
|
-
return value
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function help() {
|
|
102
|
-
Log.banner()
|
|
103
|
-
|
|
104
|
-
console.log(Log.color('yellow', 'Usage'))
|
|
105
|
-
Log.line()
|
|
106
|
-
|
|
107
|
-
console.log(' npm run cli -- setup')
|
|
108
|
-
console.log(' npm run cli -- help')
|
|
109
|
-
console.log(' npm run cli -- key:generate')
|
|
110
|
-
console.log(' npm run cli -- key:jwt')
|
|
111
|
-
console.log(' npm run cli -- key:jwt --refresh')
|
|
112
|
-
|
|
113
|
-
console.log()
|
|
114
|
-
|
|
115
|
-
console.log(Log.color('yellow', 'Commands'))
|
|
116
|
-
Log.line()
|
|
117
|
-
|
|
118
|
-
console.log(' setup')
|
|
119
|
-
console.log(Log.color('gray', ' Initialize project'))
|
|
120
|
-
|
|
121
|
-
console.log()
|
|
122
|
-
|
|
123
|
-
console.log(' help')
|
|
124
|
-
console.log(Log.color('gray', ' Show help information'))
|
|
125
|
-
|
|
126
|
-
console.log()
|
|
127
|
-
|
|
128
|
-
console.log(' key:generate')
|
|
129
|
-
console.log(Log.color('gray', ' Generate APP_KEY'))
|
|
130
|
-
|
|
131
|
-
console.log()
|
|
132
|
-
|
|
133
|
-
console.log(' key:jwt')
|
|
134
|
-
console.log(Log.color('gray', ' Generate JWT_SECRET'))
|
|
135
|
-
|
|
136
|
-
console.log()
|
|
137
|
-
|
|
138
|
-
console.log(' key:jwt --refresh')
|
|
139
|
-
console.log(Log.color('gray', ' Generate JWT_REFRESH_SECRET'))
|
|
140
|
-
|
|
141
|
-
console.log()
|
|
142
77
|
}
|
|
143
78
|
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const envPath = path.join(process.cwd(), '.env')
|
|
149
|
-
|
|
150
|
-
if (!fs.existsSync(examplePath)) {
|
|
151
|
-
Log.error('.env.example not found')
|
|
152
|
-
process.exit(1)
|
|
79
|
+
async function main() {
|
|
80
|
+
if (command in DB_ROUTES) {
|
|
81
|
+
await runDbCommand(DB_ROUTES[command])
|
|
82
|
+
return
|
|
153
83
|
}
|
|
154
84
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
Log.warn('.env already exists')
|
|
85
|
+
const meta = META_ROUTES[command === undefined ? 'undefined' : command]
|
|
86
|
+
if (meta) {
|
|
87
|
+
await meta()
|
|
88
|
+
return
|
|
160
89
|
}
|
|
161
90
|
|
|
162
|
-
try {
|
|
163
|
-
Log.info('Updating package.json dependencies...')
|
|
164
|
-
execSync('npx npm-check-updates -u', {
|
|
165
|
-
stdio: 'inherit',
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
Log.info('Installing dependencies...')
|
|
169
|
-
execSync('npm install', {
|
|
170
|
-
stdio: 'inherit',
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
generateAppKey()
|
|
174
|
-
generateJwtSecret()
|
|
175
|
-
generateJwtRefreshSecret()
|
|
176
|
-
|
|
177
|
-
Log.success('Setup completed')
|
|
178
|
-
} catch (error) {
|
|
179
|
-
Log.error(error.message)
|
|
180
|
-
process.exit(1)
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function generateAppKey() {
|
|
185
|
-
Log.banner()
|
|
186
|
-
|
|
187
|
-
const key = generateKey(32)
|
|
188
|
-
|
|
189
|
-
updateEnv('APP_KEY', key)
|
|
190
|
-
|
|
191
|
-
Log.success('APP_KEY generated')
|
|
192
|
-
console.log()
|
|
193
|
-
console.log(Log.color('cyan', key))
|
|
194
|
-
console.log()
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function generateJwtSecret() {
|
|
198
|
-
Log.banner()
|
|
199
|
-
|
|
200
|
-
const key = generateKey(64)
|
|
201
|
-
|
|
202
|
-
updateEnv('JWT_SECRET', key)
|
|
203
|
-
|
|
204
|
-
Log.success('JWT_SECRET generated')
|
|
205
|
-
console.log()
|
|
206
|
-
console.log(Log.color('cyan', key))
|
|
207
|
-
console.log()
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function generateJwtRefreshSecret() {
|
|
211
91
|
Log.banner()
|
|
212
|
-
|
|
213
|
-
const key = generateKey(64)
|
|
214
|
-
|
|
215
|
-
updateEnv('JWT_REFRESH_SECRET', key)
|
|
216
|
-
|
|
217
|
-
Log.success('JWT_REFRESH_SECRET generated')
|
|
92
|
+
Log.error(`Unknown command '${command}'`)
|
|
218
93
|
console.log()
|
|
219
|
-
|
|
94
|
+
Log.info('Run: npm run cli -- help')
|
|
220
95
|
console.log()
|
|
96
|
+
process.exit(1)
|
|
221
97
|
}
|
|
222
98
|
|
|
223
|
-
|
|
224
|
-
case undefined:
|
|
225
|
-
case 'help': {
|
|
226
|
-
help()
|
|
227
|
-
break
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
case 'setup': {
|
|
231
|
-
setup()
|
|
232
|
-
break
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
case 'key:generate': {
|
|
236
|
-
generateAppKey()
|
|
237
|
-
break
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
case 'key:jwt': {
|
|
241
|
-
if (args.includes('--refresh')) {
|
|
242
|
-
generateJwtRefreshSecret()
|
|
243
|
-
} else {
|
|
244
|
-
generateJwtSecret()
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
break
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
default: {
|
|
251
|
-
Log.banner()
|
|
252
|
-
Log.error(`Unknown command '${command}'`)
|
|
253
|
-
console.log()
|
|
254
|
-
Log.info(`Run: npm run cli -- help`)
|
|
255
|
-
console.log()
|
|
256
|
-
process.exit(1)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
99
|
+
main()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Set HTTP-layer env before any config/core module loads. Rate limiting is disabled
|
|
4
|
+
// so repeated requests in a test run are deterministic.
|
|
5
|
+
process.env.EXPRESS_RATE_LIMIT_ENABLED = 'false'
|
|
6
|
+
process.env.DB_ENABLED = 'false'
|
|
7
|
+
process.env.CORS_ORIGIN = '*'
|
|
8
|
+
|
|
9
|
+
const request = require('supertest')
|
|
10
|
+
const Routes = require('@core/routing.core')
|
|
11
|
+
const Express = require('@core/express.core')
|
|
12
|
+
|
|
13
|
+
// A plain-object resource controller registered before the app is built, so the
|
|
14
|
+
// router mounts it alongside the app's own routes when Express.create() runs.
|
|
15
|
+
const WidgetController = {
|
|
16
|
+
index: ({ res }) => res.json({ status: true, code: 200, message: 'list', data: [], meta: null }),
|
|
17
|
+
show: ({ req, res }) => res.json({ status: true, code: 200, message: 'one', data: { id: req.params.id }, meta: null }),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let app
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
Routes.apiResource('widgets', WidgetController)
|
|
24
|
+
app = (await Express.create()).app
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
afterAll(async () => {
|
|
28
|
+
await Express.close()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('HTTP — security & envelope', () => {
|
|
32
|
+
test('GET /api/status returns the standard envelope', async () => {
|
|
33
|
+
const res = await request(app).get('/api/status')
|
|
34
|
+
expect(res.status).toBe(200)
|
|
35
|
+
expect(res.body).toEqual({ status: true, code: 200, message: 'OK', data: null, meta: null })
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('security headers: helmet on, x-powered-by off', async () => {
|
|
39
|
+
const res = await request(app).get('/')
|
|
40
|
+
expect(res.headers['x-powered-by']).toBeUndefined()
|
|
41
|
+
expect(res.headers['content-security-policy']).toBeDefined()
|
|
42
|
+
expect(res.headers['x-content-type-options']).toBe('nosniff')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('404 uses the standard envelope (F-14)', async () => {
|
|
46
|
+
const res = await request(app).get('/api/does-not-exist')
|
|
47
|
+
expect(res.status).toBe(404)
|
|
48
|
+
expect(res.body.status).toBe(false)
|
|
49
|
+
expect(res.body.code).toBe(404)
|
|
50
|
+
expect(res.body).toHaveProperty('data', null)
|
|
51
|
+
expect(res.body).toHaveProperty('meta', null)
|
|
52
|
+
expect(res.body.message).toMatch(/Cannot GET/)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('HTTP — validation & auth', () => {
|
|
57
|
+
test('invalid login body returns 422 with errors[]', async () => {
|
|
58
|
+
const res = await request(app).post('/api/auth/login').send({ email: 'x', password: '1' })
|
|
59
|
+
expect(res.status).toBe(422)
|
|
60
|
+
expect(res.body.status).toBe(false)
|
|
61
|
+
expect(res.body.code).toBe(422)
|
|
62
|
+
expect(Array.isArray(res.body.errors)).toBe(true)
|
|
63
|
+
expect(res.body.errors.length).toBeGreaterThan(0)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('protected route is 401 without a token, 200 with one', async () => {
|
|
67
|
+
const noToken = await request(app).get('/api/auth/me')
|
|
68
|
+
expect(noToken.status).toBe(401)
|
|
69
|
+
|
|
70
|
+
const login = await request(app).post('/api/auth/login').send({ email: 'demo@example.com', password: 'password123' })
|
|
71
|
+
expect(login.status).toBe(200)
|
|
72
|
+
const token = login.body.data.accessToken
|
|
73
|
+
expect(typeof token).toBe('string')
|
|
74
|
+
|
|
75
|
+
const me = await request(app).get('/api/auth/me').set('Authorization', `Bearer ${token}`)
|
|
76
|
+
expect(me.status).toBe(200)
|
|
77
|
+
expect(me.body.data).toHaveProperty('sub')
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('HTTP — router resource', () => {
|
|
82
|
+
test('apiResource mounts index and show', async () => {
|
|
83
|
+
const index = await request(app).get('/widgets')
|
|
84
|
+
expect(index.status).toBe(200)
|
|
85
|
+
expect(index.body.data).toEqual([])
|
|
86
|
+
|
|
87
|
+
const show = await request(app).get('/widgets/42')
|
|
88
|
+
expect(show.status).toBe(200)
|
|
89
|
+
expect(show.body.data).toEqual({ id: '42' })
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('Router — named routes & url()', () => {
|
|
94
|
+
test('url() substitutes params and appends query', () => {
|
|
95
|
+
Routes.get('reports/:id', ({ res }) => res.end()).name('reports.show')
|
|
96
|
+
expect(Routes.url('reports.show', { id: 7 })).toBe('/reports/7')
|
|
97
|
+
expect(Routes.url('reports.show', { id: 7, tab: 'a' })).toBe('/reports/7?tab=a')
|
|
98
|
+
})
|
|
99
|
+
})
|