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.
Files changed (46) hide show
  1. package/package.json +1 -1
  2. package/template/.env.example +4 -0
  3. package/template/README.md +63 -8
  4. package/template/SECURITY.md +39 -0
  5. package/template/app/routes/api.route.js +1 -1
  6. package/template/app/routes/register.route.js +1 -1
  7. package/template/app/routes/web.route.js +1 -1
  8. package/template/app/socket/register.socket.js +0 -2
  9. package/template/config/express.config.js +8 -0
  10. package/template/core/common/string.js +1 -1
  11. package/template/core/cron.core.js +1 -0
  12. package/template/core/database.core.js +30 -17
  13. package/template/core/error.core.js +8 -4
  14. package/template/core/express.core.js +16 -4
  15. package/template/core/hooks.core.js +10 -7
  16. package/template/core/migrator.core.js +201 -0
  17. package/template/core/modules.core.js +167 -0
  18. package/template/core/queue.core.js +1 -0
  19. package/template/core/routing.core.d.ts +273 -0
  20. package/template/core/routing.core.js +666 -0
  21. package/template/core/seeder.core.js +105 -0
  22. package/template/core/socket.core.js +1 -0
  23. package/template/database/migrations/.gitkeep +0 -0
  24. package/template/database/seeders/.gitkeep +0 -0
  25. package/template/docs/Database.md +14 -8
  26. package/template/docs/Express.md +5 -2
  27. package/template/docs/Make.md +46 -0
  28. package/template/docs/Migration.md +56 -0
  29. package/template/docs/Modules.md +96 -0
  30. package/template/docs/README.md +5 -0
  31. package/template/docs/Routing.md +116 -0
  32. package/template/docs/Seeder.md +54 -0
  33. package/template/eslint.config.js +52 -0
  34. package/template/package-lock.json +1068 -70
  35. package/template/package.json +15 -8
  36. package/template/scripts/cli/args.js +39 -0
  37. package/template/scripts/cli/bootstrap.js +16 -0
  38. package/template/scripts/cli/db.js +79 -0
  39. package/template/scripts/cli/help.js +58 -0
  40. package/template/scripts/cli/keys.js +100 -0
  41. package/template/scripts/cli/log.js +58 -0
  42. package/template/scripts/cli/make.js +249 -0
  43. package/template/scripts/cli/names.js +51 -0
  44. package/template/scripts/cli/templates.js +358 -0
  45. package/template/scripts/cli.js +75 -234
  46. package/template/tests/http.test.js +99 -0
@@ -1,258 +1,99 @@
1
- #!/usr/bin/env node
2
-
3
1
  'use strict'
4
2
 
5
- require('dotenv').config()
6
-
7
- const fs = require('fs')
8
- const path = require('path')
9
- const crypto = require('crypto')
10
- const { execSync } = require('child_process')
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
- static warn(message) {
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
- console.log()
60
- console.log(this.color('cyan', `╔${''.repeat(width)}╗`))
61
- console.log(this.color('cyan', `║${centered}║`))
62
- console.log(this.color('cyan', `╚${'═'.repeat(width)}╝`))
63
- console.log()
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
- function generateKey(bytes = 32) {
71
- return crypto.randomBytes(bytes).toString('hex')
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
- function getEnvPath() {
75
- return path.join(process.cwd(), '.env')
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 updateEnv(key, value) {
79
- const envPath = getEnvPath()
80
-
81
- if (!fs.existsSync(envPath)) {
82
- Log.error('.env file not found')
83
- process.exit(1)
84
- }
85
-
86
- let content = fs.readFileSync(envPath, 'utf8')
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 setup() {
145
- Log.banner()
146
-
147
- const examplePath = path.join(process.cwd(), '.env.example')
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
- if (!fs.existsSync(envPath)) {
156
- fs.copyFileSync(examplePath, envPath)
157
- Log.success('.env created')
158
- } else {
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
- console.log(Log.color('cyan', key))
94
+ Log.info('Run: npm run cli -- help')
220
95
  console.log()
96
+ process.exit(1)
221
97
  }
222
98
 
223
- switch (command) {
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
+ })