create-rebe 1.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 (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -0
  3. package/bin/create-rebe.js +310 -0
  4. package/package.json +40 -0
  5. package/template/.env.example +141 -0
  6. package/template/.gitattributes +16 -0
  7. package/template/.nvmrc +1 -0
  8. package/template/.prettierignore +10 -0
  9. package/template/.prettierrc +12 -0
  10. package/template/LICENSE +21 -0
  11. package/template/README.md +150 -0
  12. package/template/SECURITY.md +133 -0
  13. package/template/app/hooks/register.hook.js +22 -0
  14. package/template/app/http/controllers/auth.controller.js +34 -0
  15. package/template/app/http/middlewares/auth.middleware.js +25 -0
  16. package/template/app/http/middlewares/register.middleware.js +18 -0
  17. package/template/app/http/validators/auth.validator.js +8 -0
  18. package/template/app/jobs/register.job.js +17 -0
  19. package/template/app/queue/register.queue.js +11 -0
  20. package/template/app/routes/api.route.js +18 -0
  21. package/template/app/routes/register.route.js +8 -0
  22. package/template/app/routes/web.route.js +7 -0
  23. package/template/app/socket/register.socket.js +15 -0
  24. package/template/config/app.config.js +12 -0
  25. package/template/config/bcrypt.config.js +7 -0
  26. package/template/config/cache.config.js +7 -0
  27. package/template/config/cors.config.js +37 -0
  28. package/template/config/cron.config.js +9 -0
  29. package/template/config/database.config.js +51 -0
  30. package/template/config/express.config.js +37 -0
  31. package/template/config/index.js +19 -0
  32. package/template/config/jwt.config.js +10 -0
  33. package/template/config/logger.config.js +10 -0
  34. package/template/config/mail.config.js +14 -0
  35. package/template/config/queue.config.js +11 -0
  36. package/template/config/redis.config.js +32 -0
  37. package/template/config/runtime.config.js +11 -0
  38. package/template/config/socket.config.js +15 -0
  39. package/template/config/storage.config.js +10 -0
  40. package/template/core/bootstrap.core.js +92 -0
  41. package/template/core/common/array.js +144 -0
  42. package/template/core/common/cache.js +100 -0
  43. package/template/core/common/collection.js +173 -0
  44. package/template/core/common/crypt.js +69 -0
  45. package/template/core/common/date.js +254 -0
  46. package/template/core/common/hash.js +61 -0
  47. package/template/core/common/object.js +155 -0
  48. package/template/core/common/path.js +80 -0
  49. package/template/core/common/storage.js +97 -0
  50. package/template/core/common/string.js +137 -0
  51. package/template/core/common/url.js +81 -0
  52. package/template/core/common.core.js +93 -0
  53. package/template/core/cron.core.js +141 -0
  54. package/template/core/database.core.js +113 -0
  55. package/template/core/error.core.js +83 -0
  56. package/template/core/express.core.js +161 -0
  57. package/template/core/hooks.core.js +47 -0
  58. package/template/core/jwt.core.js +81 -0
  59. package/template/core/logger.core.js +100 -0
  60. package/template/core/mailer.core.js +65 -0
  61. package/template/core/queue.core.js +226 -0
  62. package/template/core/redis.core.js +75 -0
  63. package/template/core/register.core.js +91 -0
  64. package/template/core/runtime.core.js +27 -0
  65. package/template/core/socket.core.js +93 -0
  66. package/template/core/validator.core.js +34 -0
  67. package/template/database/.gitkeep +0 -0
  68. package/template/database/models/post.model.js +26 -0
  69. package/template/database/models/user.model.js +30 -0
  70. package/template/docs/Bootstrap.md +50 -0
  71. package/template/docs/Common.md +48 -0
  72. package/template/docs/Cron.md +47 -0
  73. package/template/docs/Database.md +61 -0
  74. package/template/docs/Express.md +63 -0
  75. package/template/docs/Hooks.md +48 -0
  76. package/template/docs/Jwt.md +63 -0
  77. package/template/docs/Logger.md +60 -0
  78. package/template/docs/Mailer.md +50 -0
  79. package/template/docs/Queue.md +57 -0
  80. package/template/docs/README.md +32 -0
  81. package/template/docs/Redis.md +54 -0
  82. package/template/docs/Register.md +52 -0
  83. package/template/docs/Socket.md +55 -0
  84. package/template/docs/Validator.md +45 -0
  85. package/template/ecosystem.config.js +54 -0
  86. package/template/index.js +6 -0
  87. package/template/jest.config.js +27 -0
  88. package/template/jsconfig.json +37 -0
  89. package/template/logs/.gitkeep +0 -0
  90. package/template/nodemon.json +23 -0
  91. package/template/package-lock.json +7033 -0
  92. package/template/package.json +82 -0
  93. package/template/scripts/cli.js +258 -0
  94. package/template/storage/.gitkeep +0 -0
  95. package/template/tests/common.test.js +45 -0
  96. package/template/tests/crypt-hash-storage.test.js +44 -0
  97. package/template/tests/jwt.test.js +45 -0
  98. package/template/tests/register.test.js +55 -0
  99. package/template/tests/setup.js +11 -0
  100. package/template/tests/validator.test.js +65 -0
@@ -0,0 +1,75 @@
1
+ 'use strict'
2
+
3
+ const IORedis = require('ioredis')
4
+
5
+ const config = require('@config/index')
6
+ const logger = require('@core/logger.core')
7
+
8
+ // Standalone Redis connection (ioredis). A client is only created when
9
+ // REDIS_ENABLED=true; every consumer (cache/queue/socket/session) must gate its
10
+ // usage with isEnabledFor(feature) and provide an in-memory/default fallback, so
11
+ // the application runs unchanged whether or not Redis is present.
12
+ class Redis {
13
+ static _client = null
14
+
15
+ static async connect() {
16
+ if (!config.redis.enabled) {
17
+ logger.info('Redis is disabled (REDIS_ENABLED=false)')
18
+ return null
19
+ }
20
+ if (Redis._client) return Redis._client
21
+
22
+ const r = config.redis
23
+
24
+ Redis._client = new IORedis({
25
+ host: r.host,
26
+ port: r.port,
27
+ username: r.username || undefined,
28
+ password: r.password || undefined,
29
+ db: r.db,
30
+ keyPrefix: r.keyPrefix || undefined,
31
+ maxRetriesPerRequest: r.maxRetriesPerRequest,
32
+ connectTimeout: r.connectTimeout,
33
+ tls: r.tls ? {} : undefined,
34
+ lazyConnect: true,
35
+ })
36
+
37
+ // Without a listener, a runtime connection drop would emit an unhandled
38
+ // 'error' and crash the process.
39
+ Redis._client.on('error', (err) => logger.error('Redis client error', err))
40
+
41
+ try {
42
+ await Redis._client.connect()
43
+ await Redis._client.ping()
44
+ logger.info(`Redis connected (${r.host}:${r.port}/${r.db})`)
45
+ } catch (err) {
46
+ logger.error('Redis connection failed', err)
47
+ throw err
48
+ }
49
+
50
+ return Redis._client
51
+ }
52
+
53
+ // Return the live client. Throws when Redis is disabled or not yet connected —
54
+ // callers should guard with isEnabledFor() first.
55
+ static client() {
56
+ if (!config.redis.enabled) throw new Error('Redis is disabled (REDIS_ENABLED=false)')
57
+ if (!Redis._client) throw new Error('Redis is not connected; call Redis.connect() first')
58
+ return Redis._client
59
+ }
60
+
61
+ // True when Redis is enabled AND the given feature opts in (use.<feature>).
62
+ static isEnabledFor(feature) {
63
+ return Boolean(config.redis.enabled && config.redis.use[feature])
64
+ }
65
+
66
+ static async disconnect() {
67
+ if (Redis._client) {
68
+ await Redis._client.quit().catch(() => {})
69
+ Redis._client = null
70
+ logger.info('Redis connection closed')
71
+ }
72
+ }
73
+ }
74
+
75
+ module.exports = Redis
@@ -0,0 +1,91 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+
6
+ const logger = require('@core/logger.core')
7
+
8
+ // Safe bridge loader for application `register.*.js` entry points.
9
+ //
10
+ // The core layer never imports application code statically. Each core instead
11
+ // loads a single, well-known register file at runtime (Inversion of Control), so
12
+ // the dependency arrow stays one-directional: app -> core -> config -> .env.
13
+ //
14
+ // cron.core -> app/jobs/register.job.js module.exports = (Cron) => {}
15
+ // queue.core -> app/queue/register.queue.js module.exports = (Queue) => {}
16
+ // express.core -> app/http/middlewares/register.middleware.js (app) => {}
17
+ // express.core -> app/routes/register.route.js side-effect, exports Routes
18
+ // socket.core -> app/socket/register.socket.js (io, Socket) => {}
19
+ // hooks.core -> app/hooks/register.hook.js { before, after, shutdown }
20
+ //
21
+ // This loader centralises the contract: resolve under the project root, tolerate
22
+ // a missing optional file, isolate require()/execution failures, and validate the
23
+ // exported shape so a malformed register fails loudly with a clear message rather
24
+ // than a cryptic stack deep inside a core.
25
+ class Register {
26
+ // Resolve a path fragment against the project root (process.cwd()).
27
+ static path(...segments) {
28
+ return path.resolve(process.cwd(), ...segments)
29
+ }
30
+
31
+ // Require a register module and unwrap an ESM-style default export when present.
32
+ // Returns null when the file is absent and `optional` is true (the default).
33
+ static require(file, { optional = true, label } = {}) {
34
+ const name = label || file
35
+
36
+ if (!fs.existsSync(file)) {
37
+ if (optional) {
38
+ logger.warn(`Register not found, skipping: ${name}`)
39
+ return null
40
+ }
41
+ throw new Error(`Required register file not found: ${name}`)
42
+ }
43
+
44
+ try {
45
+ const mod = require(file)
46
+ return mod && mod.default !== undefined ? mod.default : mod
47
+ } catch (err) {
48
+ logger.error(`Failed to load register: ${name}`, err)
49
+ throw err
50
+ }
51
+ }
52
+
53
+ // Load and invoke a callable register (a function, or an object exposing
54
+ // register()). `args` are forwarded to the register (the core facade). Returns
55
+ // the register's return value, or null when the file is absent.
56
+ static async invoke(file, args = [], { optional = true, label } = {}) {
57
+ const name = label || file
58
+ const mod = Register.require(file, { optional, label: name })
59
+ if (mod == null) return null
60
+
61
+ const fn = typeof mod === 'function' ? mod : typeof mod.register === 'function' ? mod.register.bind(mod) : null
62
+ if (!fn) {
63
+ throw new Error(`Register "${name}" must export a function or an object with a register() method`)
64
+ }
65
+
66
+ return fn(...args)
67
+ }
68
+
69
+ // Load a register that must export a plain object (e.g. lifecycle hooks).
70
+ // Validates that every provided key in `allowed` is a function. Returns the
71
+ // object, or null when the file is absent.
72
+ static object(file, allowed = [], { optional = true, label } = {}) {
73
+ const name = label || file
74
+ const mod = Register.require(file, { optional, label: name })
75
+ if (mod == null) return null
76
+
77
+ if (typeof mod !== 'object') {
78
+ throw new Error(`Register "${name}" must export an object`)
79
+ }
80
+
81
+ for (const key of allowed) {
82
+ if (mod[key] !== undefined && typeof mod[key] !== 'function') {
83
+ throw new Error(`Register "${name}" key "${key}" must be a function`)
84
+ }
85
+ }
86
+
87
+ return mod
88
+ }
89
+ }
90
+
91
+ module.exports = Register
@@ -0,0 +1,27 @@
1
+ 'use strict'
2
+
3
+ const config = require('@config/index')
4
+
5
+ function bootRuntime() {
6
+ const { app, runtime } = config
7
+
8
+ process.env.TZ = app.timezone
9
+ process.env.NODE_ENV = app.env
10
+ process.env.UV_THREADPOOL_SIZE = String(runtime.uvThreadpoolSize)
11
+
12
+ process.setMaxListeners(runtime.maxListeners)
13
+ Error.stackTraceLimit = runtime.stackTraceLimit
14
+
15
+ if (!runtime.deprecationWarnings) {
16
+ process.removeAllListeners('warning')
17
+ }
18
+
19
+ if (runtime.bigintJson) {
20
+ // eslint-disable-next-line no-extend-native
21
+ BigInt.prototype.toJSON = function () {
22
+ return this.toString()
23
+ }
24
+ }
25
+ }
26
+
27
+ bootRuntime()
@@ -0,0 +1,93 @@
1
+ 'use strict'
2
+
3
+ const { Server } = require('socket.io')
4
+
5
+ const config = require('@config/index')
6
+ const logger = require('@core/logger.core')
7
+ const Register = require('@core/register.core')
8
+
9
+ const REGISTER_FILE = Register.path('app', 'socket', 'register.socket.js')
10
+
11
+ // Socket.IO attached to the Express http.Server (single port). Connection handlers
12
+ // live in app/socket/register.socket.js as (io, Socket) => {}.
13
+ class Socket {
14
+ static io = null
15
+
16
+ static async attach(httpServer) {
17
+ if (!config.socket.enabled) {
18
+ logger.info('Socket is disabled (SOCKET_ENABLED=false)')
19
+ return null
20
+ }
21
+ if (Socket.io) return Socket.io
22
+
23
+ Socket.io = new Server(httpServer, {
24
+ path: config.socket.path,
25
+ serveClient: config.socket.serveClient,
26
+ connectTimeout: config.socket.connectTimeout,
27
+ pingInterval: config.socket.pingInterval,
28
+ pingTimeout: config.socket.pingTimeout,
29
+ transports: config.socket.transports,
30
+ allowUpgrades: config.socket.allowUpgrades,
31
+ maxHttpBufferSize: config.socket.maxHttpBufferSize,
32
+ cors: config.cors.socket.cors,
33
+ })
34
+
35
+ await Socket._applyRedisAdapter()
36
+
37
+ Socket.io.on('connection', (socket) => {
38
+ logger.debug(`Socket connected: ${socket.id}`)
39
+ socket.on('disconnect', (reason) => logger.debug(`Socket disconnected: ${socket.id} (${reason})`))
40
+ })
41
+
42
+ await Socket._loadHandlers()
43
+
44
+ logger.info(`Socket.IO attached (path ${config.socket.path})`)
45
+ return Socket.io
46
+ }
47
+
48
+ // Multi-instance fan-out via Redis. Opt-in (REDIS_USE_SOCKET=true) and only when
49
+ // the optional adapter package is installed; otherwise the default in-memory
50
+ // adapter is used.
51
+ static async _applyRedisAdapter() {
52
+ const Redis = require('@core/redis.core')
53
+ if (!Redis.isEnabledFor('socket')) return
54
+
55
+ let createAdapter
56
+ try {
57
+ ;({ createAdapter } = require('@socket.io/redis-adapter'))
58
+ } catch {
59
+ logger.warn('REDIS_USE_SOCKET=true but @socket.io/redis-adapter is not installed; using default adapter')
60
+ return
61
+ }
62
+
63
+ const pub = Redis.client()
64
+ const sub = pub.duplicate()
65
+ Socket.io.adapter(createAdapter(pub, sub))
66
+ logger.info('Socket.IO using Redis adapter')
67
+ }
68
+
69
+ static async _loadHandlers() {
70
+ await Register.invoke(REGISTER_FILE, [Socket.io, Socket], { label: 'app/socket/register.socket.js' })
71
+ }
72
+
73
+ static broadcast(event, payload) {
74
+ if (Socket.io) Socket.io.emit(event, payload)
75
+ }
76
+
77
+ static toRoom(room, event, payload) {
78
+ if (Socket.io) Socket.io.to(room).emit(event, payload)
79
+ }
80
+
81
+ static close() {
82
+ return new Promise((resolve) => {
83
+ if (!Socket.io) return resolve()
84
+ Socket.io.close(() => {
85
+ Socket.io = null
86
+ logger.info('Socket.IO closed')
87
+ resolve()
88
+ })
89
+ })
90
+ }
91
+ }
92
+
93
+ module.exports = Socket
@@ -0,0 +1,34 @@
1
+ 'use strict'
2
+
3
+ // Adapt a Zod schema into express-routing middleware. Used per-route as:
4
+ // Routes.post('auth/login', AuthController.login, [Validator.make(loginSchema)])
5
+ //
6
+ // On failure it short-circuits with a 422 using the manual response envelope plus
7
+ // an `errors` array. On success the parsed value is stored on req.validated[source];
8
+ // for the body source it also replaces req.body (req.query/req.params are read-only
9
+ // in Express 5 and must not be reassigned).
10
+ class Validator {
11
+ static make(schema, source = 'body') {
12
+ return {
13
+ handle({ req, res, next }) {
14
+ const result = schema.safeParse(req[source])
15
+
16
+ if (!result.success) {
17
+ const errors = result.error.issues.map((issue) => ({
18
+ field: issue.path.join('.'),
19
+ message: issue.message,
20
+ }))
21
+ return res.status(422).json({ status: false, code: 422, message: 'Validation failed', data: null, meta: null, errors })
22
+ }
23
+
24
+ req.validated = req.validated || {}
25
+ req.validated[source] = result.data
26
+ if (source === 'body') req.body = result.data
27
+
28
+ next()
29
+ },
30
+ }
31
+ }
32
+ }
33
+
34
+ module.exports = Validator
File without changes
@@ -0,0 +1,26 @@
1
+ 'use strict'
2
+
3
+ // Sample model demonstrating an association (belongsTo User). Remove these sample
4
+ // files when you start modelling your own domain.
5
+ module.exports = (sequelize, DataTypes) => {
6
+ const Post = sequelize.define(
7
+ 'Post',
8
+ {
9
+ id: { type: DataTypes.BIGINT.UNSIGNED, primaryKey: true, autoIncrement: true },
10
+ user_id: { type: DataTypes.BIGINT.UNSIGNED, allowNull: false },
11
+ title: { type: DataTypes.STRING(200), allowNull: false },
12
+ body: { type: DataTypes.TEXT, allowNull: true },
13
+ published_at: { type: DataTypes.DATE, allowNull: true },
14
+ },
15
+ {
16
+ tableName: 'posts',
17
+ underscored: true,
18
+ },
19
+ )
20
+
21
+ Post.associate = (models) => {
22
+ if (models.User) Post.belongsTo(models.User, { foreignKey: 'user_id', as: 'author' })
23
+ }
24
+
25
+ return Post
26
+ }
@@ -0,0 +1,30 @@
1
+ 'use strict'
2
+
3
+ // Sample Sequelize model. database.core loads every file in database/models/*.js
4
+ // when DB_ENABLED=true (files prefixed with "_" are ignored). The factory receives
5
+ // (sequelize, DataTypes) and returns the model; associations are wired in a second
6
+ // pass via the static associate(models).
7
+ module.exports = (sequelize, DataTypes) => {
8
+ const User = sequelize.define(
9
+ 'User',
10
+ {
11
+ id: { type: DataTypes.BIGINT.UNSIGNED, primaryKey: true, autoIncrement: true },
12
+ name: { type: DataTypes.STRING(100), allowNull: false },
13
+ email: { type: DataTypes.STRING(190), allowNull: false, unique: true, validate: { isEmail: true } },
14
+ password: { type: DataTypes.STRING, allowNull: false },
15
+ },
16
+ {
17
+ tableName: 'users',
18
+ underscored: true,
19
+ // The password is excluded by default; use the withPassword scope for auth.
20
+ defaultScope: { attributes: { exclude: ['password'] } },
21
+ scopes: { withPassword: { attributes: {} } },
22
+ },
23
+ )
24
+
25
+ User.associate = (models) => {
26
+ if (models.Post) User.hasMany(models.Post, { foreignKey: 'user_id', as: 'posts' })
27
+ }
28
+
29
+ return User
30
+ }
@@ -0,0 +1,50 @@
1
+ # Bootstrap API
2
+
3
+ `@core/bootstrap.core` is the boot orchestrator. It wires every core in a
4
+ deterministic order and registers graceful shutdown. It is the only core called
5
+ directly from `index.js`.
6
+
7
+ ```js
8
+ // index.js
9
+ require('dotenv').config()
10
+ require('module-alias/register')
11
+ require('@core/runtime.core')
12
+ require('@core/bootstrap.core').run()
13
+ ```
14
+
15
+ ## Method
16
+
17
+ `run()` — async. Performs:
18
+
19
+ 1. `ErrorHandler.setExceptionHandler()` / `setRejectionHandler()`
20
+ 2. `Hooks.before({ config })`
21
+ 3. `Redis.connect()` — only if `REDIS_ENABLED=true`
22
+ 4. `Database.connect()` — only if `DB_ENABLED=true`
23
+ 5. `Express.create()` → `{ app, server }`
24
+ 6. `Socket.attach(server)`
25
+ 7. `Cron.start()` + `Queue.start()`
26
+ 8. `Express.listen()`
27
+ 9. `Hooks.after({ config })`
28
+ 10. registers SIGINT/SIGTERM handlers
29
+
30
+ If any startup step throws, the error is logged and the process exits with code 1.
31
+
32
+ ## Graceful shutdown
33
+
34
+ On SIGINT/SIGTERM (once), in order, each step isolated so one failure does not block
35
+ the rest:
36
+
37
+ ```
38
+ Hooks.shutdown -> Cron.stop -> Queue.stop -> Socket.close -> Express.close
39
+ -> Database.disconnect (if enabled) -> Redis.disconnect (if enabled) -> exit(0)
40
+ ```
41
+
42
+ Optional subsystems (Redis, Database) are required lazily, so a project that never
43
+ enables them does not load them.
44
+
45
+ ## Notes
46
+
47
+ - The server is created before `listen()` so the socket can attach to the same
48
+ `http.Server` (shared port).
49
+ - `Queue.stop()` drains in-process jobs best-effort (default 10s). PM2's
50
+ `kill_timeout` (15s) gives it headroom — see `ecosystem.config.js`.
@@ -0,0 +1,48 @@
1
+ # Common API
2
+
3
+ `@core/common.core` is the facade over shared utilities and the environment readers.
4
+ It is the only place besides `config/*` that reads `process.env`.
5
+
6
+ ```js
7
+ const Common = require('@core/common.core')
8
+ ```
9
+
10
+ ## Environment readers
11
+
12
+ | Method | Returns |
13
+ | ----------------------------------- | -------------------------------------------- |
14
+ | `getEnv(key, def=null)` | string |
15
+ | `getEnvInt(key, def=null)` | integer |
16
+ | `getEnvFloat(key, def=null)` | float |
17
+ | `getEnvBool(key, def=null)` | boolean (`true/1/yes/on/y/t/enable/enabled`) |
18
+ | `getEnvArray(key, def=[], sep=',')` | trimmed, non-empty array |
19
+ | `getEnvJson(key, def=null)` | parsed JSON or the default |
20
+
21
+ These are used inside `config/*.config.js`. Application and core code should read
22
+ values from `config`, not call these directly.
23
+
24
+ ## Generic helpers
25
+
26
+ | Method | Notes |
27
+ | ---------------- | -------------------------------------------------- |
28
+ | `sleep(ms)` | promise that resolves after `ms` |
29
+ | `isEmpty(value)` | true for null/undefined, empty string/array/object |
30
+ | `attempt(fn)` | `Promise<[err, result]>` — wraps try/catch |
31
+
32
+ ```js
33
+ const [err, result] = await Common.attempt(() => risky())
34
+ if (err) logger.warn(err)
35
+ ```
36
+
37
+ ## Utility namespaces
38
+
39
+ Pure utilities (no dependency on other cores), exposed as `Common.<Namespace>`:
40
+
41
+ `Arr`, `Str`, `Obj`, `Url`, `Path`, `Hash`, `Crypt`, `Collection`, `Date`, `Cache`,
42
+ `Storage` — implemented under `core/common/*`.
43
+
44
+ ```js
45
+ Common.Str.slug('Hello World')
46
+ Common.Arr.chunk([1, 2, 3, 4], 2)
47
+ Common.Cache.remember('key', 60, () => expensive())
48
+ ```
@@ -0,0 +1,47 @@
1
+ # Cron API
2
+
3
+ `@core/cron.core` schedules recurring jobs with `node-cron`. Jobs are declared in
4
+ `app/jobs/register.job.js`, which the core loads during boot.
5
+
6
+ ```js
7
+ const Cron = require('@core/cron.core')
8
+ ```
9
+
10
+ ## Methods
11
+
12
+ | Method | Notes |
13
+ | --------------------------------------------- | ---------------------------------------------------------------------------------- |
14
+ | `define(name, expression, handler, options?)` | validates the cron expression; `options.timezone`, `options.runOnInit` |
15
+ | `start()` | loads the register file, optionally prepares the history table, schedules all jobs |
16
+ | `runNow(name)` | run a job immediately |
17
+ | `remove(name)` | stop and unregister a job |
18
+ | `stop()` | stop all jobs |
19
+ | `list()` | `[{ name, expression, timezone }]` |
20
+
21
+ Expression format (node-cron): `second? minute hour day-of-month month day-of-week`.
22
+
23
+ ## Declaring jobs
24
+
25
+ ```js
26
+ // app/jobs/register.job.js
27
+ module.exports = (Cron) => {
28
+ Cron.define('cleanup-temp', '0 3 * * *', async () => {
29
+ // runs daily at 03:00
30
+ })
31
+ }
32
+ ```
33
+
34
+ Each run is wrapped in a guard, so one failure is logged and does not stop the
35
+ schedule.
36
+
37
+ ## Execution history (optional)
38
+
39
+ When `CRON_HISTORY=true` and the database is enabled, the core auto-creates a
40
+ `cron_runs` table and records each run (`running`/`completed`/`failed`) with duration.
41
+
42
+ ## Configuration
43
+
44
+ `CRON_ENABLED` (`true`), `CRON_TIMEZONE` (`Asia/Jakarta`), `CRON_HISTORY` (`true`).
45
+
46
+ > Single-instance only: with PM2 cluster mode every instance would fire the same
47
+ > schedule. See `ecosystem.config.js`.
@@ -0,0 +1,61 @@
1
+ # Database API
2
+
3
+ `@core/database.core` manages the Sequelize connection and model loading. It is
4
+ skipped entirely when `DB_ENABLED=false`. Model/migration generators are
5
+ intentionally out of scope for this framework.
6
+
7
+ ```js
8
+ const Database = require('@core/database.core')
9
+ ```
10
+
11
+ ## Methods
12
+
13
+ | Method | Returns | Notes |
14
+ | -------------- | --------- | ----------------------------------------------------- |
15
+ | `connect()` | Sequelize | idempotent; `authenticate()` then `loadModels()` |
16
+ | `disconnect()` | Promise | closes the pool, clears models |
17
+ | `loadModels()` | `Map` | loads `database/models/*.js`, then wires associations |
18
+ | `model(name)` | Model | throws if the model is not registered |
19
+
20
+ Static re-exports: `Database.Sequelize`, `Database.DataTypes`, `Database.Model`,
21
+ `Database.Op`.
22
+
23
+ ## Defining a model
24
+
25
+ ```js
26
+ // database/models/user.model.js
27
+ 'use strict'
28
+ module.exports = (sequelize, DataTypes) => {
29
+ const User = sequelize.define(
30
+ 'User',
31
+ {
32
+ email: { type: DataTypes.STRING, unique: true, allowNull: false },
33
+ password: { type: DataTypes.STRING, allowNull: false },
34
+ },
35
+ { tableName: 'users', underscored: true },
36
+ )
37
+
38
+ User.associate = (models) => {
39
+ // User.hasMany(models.Post)
40
+ }
41
+ return User
42
+ }
43
+ ```
44
+
45
+ Files starting with `_` are skipped. Associations run in a second pass after all
46
+ models load, so order does not matter.
47
+
48
+ ## Using a model
49
+
50
+ ```js
51
+ const Database = require('@core/database.core')
52
+ const User = Database.model('User')
53
+ const user = await User.findOne({ where: { email } })
54
+ ```
55
+
56
+ ## Configuration
57
+
58
+ `DB_ENABLED` is the master gate. Dialect, host/port, credentials, pool, retry, and
59
+ `define.*` options come from `config.db` — see `.env.example` for every variable.
60
+
61
+ > Never run `sync({ force })` or `fresh()` with `APP_ENV=production`.
@@ -0,0 +1,63 @@
1
+ # Express API
2
+
3
+ `@core/express.core` is the HTTP backbone. It builds the Express app and an
4
+ `http.Server` separately from listening, so `socket.core` can attach to the same
5
+ server before the port opens.
6
+
7
+ ```js
8
+ const Express = require('@core/express.core')
9
+ ```
10
+
11
+ ## Methods
12
+
13
+ | Method | Returns | Notes |
14
+ | ---------------- | -------------------------- | ----------------------------------------------------------- |
15
+ | `create()` | `Promise<{ app, server }>` | idempotent; builds the app + server, not listening yet |
16
+ | `listen()` | `Promise<server>` | listens on `APP_PORT`; rejects if `create()` was not called |
17
+ | `close()` | Promise | stops the server |
18
+ | `upload(field?)` | multer middleware/instance | `single(field)` when a field name is given |
19
+
20
+ These are wired by `bootstrap.core`; `upload()` is the one you call from routes.
21
+
22
+ ## Middleware order (built by `create()`)
23
+
24
+ 1. `helmet(config.express.helmet)`
25
+ 2. `config.cors.express`
26
+ 3. `compression()`
27
+ 4. `express.json` / `express.urlencoded` (limit = `EXPRESS_BODY_LIMIT`)
28
+ 5. rate limit (when `EXPRESS_RATE_LIMIT_ENABLED`)
29
+ 6. user middleware — `app/http/middlewares/register.middleware.js` `(app) => {}`
30
+ 7. static — `./public` at `/`, uploads at `/uploads`
31
+ 8. routes — `@routes/register.route` then `Routes.apply`
32
+ 9. `error.core` 404 + error handlers
33
+
34
+ `x-powered-by` is disabled. The full `storage/` root is **not** served — only
35
+ `/uploads` and `./public`.
36
+
37
+ ## File uploads
38
+
39
+ ```js
40
+ const Express = require('@core/express.core')
41
+
42
+ // single file under field "avatar"
43
+ Routes.post('upload', UploadController.store, [Express.upload('avatar')])
44
+
45
+ // in the controller, req.file holds the stored file
46
+ ```
47
+
48
+ Uploads are constrained by `STORAGE_UPLOAD_MAX_SIZE_MB` and
49
+ `STORAGE_UPLOAD_ALLOWED_TYPES`. Stored filenames are randomized to avoid path
50
+ traversal and collisions.
51
+
52
+ ## Configuration
53
+
54
+ `EXPRESS_BODY_LIMIT`, `EXPRESS_TRUST_PROXY`, `EXPRESS_RATE_LIMIT_*`, plus the helmet
55
+ block in `express.config.js`, CORS from `cors.config.js`, and storage from
56
+ `storage.config.js`. See `.env.example` for every variable and [SECURITY.md](../SECURITY.md).
57
+
58
+ ## Notes
59
+
60
+ - Behind a proxy/load balancer, set `EXPRESS_TRUST_PROXY=1` so client IPs and the
61
+ rate limiter work correctly.
62
+ - Uploaded files are served with `X-Content-Type-Options: nosniff`. SVG and other
63
+ active content can still carry script — see SECURITY.md before allowing them.