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,48 @@
1
+ # Hooks API
2
+
3
+ `@core/hooks.core` runs application lifecycle hooks at fixed points during boot and
4
+ shutdown. Handlers live in `app/hooks/register.hook.js`.
5
+
6
+ ```js
7
+ const Hooks = require('@core/hooks.core')
8
+ ```
9
+
10
+ ## Stages
11
+
12
+ | Stage | When | Failure behavior |
13
+ | ---------- | --------------------------------------- | -------------------------- |
14
+ | `before` | config loaded, services not started yet | thrown error aborts boot |
15
+ | `after` | HTTP server is listening | thrown error aborts boot |
16
+ | `shutdown` | SIGINT/SIGTERM received | logged; shutdown continues |
17
+
18
+ Each handler receives a context object (`{ config }`).
19
+
20
+ ## Methods
21
+
22
+ | Method | Notes |
23
+ | ---------------------------------------------- | ------------------------------ |
24
+ | `before(ctx)` / `after(ctx)` / `shutdown(ctx)` | run a single stage |
25
+ | `run(stage, ctx)` | run an arbitrary stage by name |
26
+
27
+ These are invoked by `bootstrap.core`; you rarely call them directly.
28
+
29
+ ## register.hook.js
30
+
31
+ ```js
32
+ 'use strict'
33
+
34
+ module.exports = {
35
+ async before(ctx) {
36
+ // warm caches, assert external services are reachable
37
+ },
38
+ async after(ctx) {
39
+ // emit "ready", register health probes
40
+ },
41
+ async shutdown(ctx) {
42
+ // flush buffers, close third-party clients
43
+ },
44
+ }
45
+ ```
46
+
47
+ The file is optional — if absent, all stages are no-ops. Every declared key must be
48
+ a function or the loader throws a clear error.
@@ -0,0 +1,63 @@
1
+ # Jwt API
2
+
3
+ `@core/jwt.core` wraps `jsonwebtoken` with separate access and refresh secrets. The
4
+ signing algorithm is pinned to **HS256** on both sign and verify, which rejects
5
+ forged tokens that use `none` or a swapped algorithm.
6
+
7
+ ```js
8
+ const Jwt = require('@core/jwt.core')
9
+ ```
10
+
11
+ ## Methods
12
+
13
+ | Method | Returns | Notes |
14
+ | -------------------------------- | ----------------------------------------------------- | ------------------------------------------ |
15
+ | `sign(payload, options?)` | token string | uses `JWT_SECRET`, `expiresIn` from config |
16
+ | `verify(token, options?)` | decoded payload | throws on invalid/expired |
17
+ | `signRefresh(payload, options?)` | token string | uses `JWT_REFRESH_SECRET` |
18
+ | `verifyRefresh(token, options?)` | decoded payload | throws on invalid/expired |
19
+ | `issue(payload)` | `{ accessToken, refreshToken, tokenType, expiresIn }` | issues a pair |
20
+ | `tryVerify(token)` | `{ valid, payload, error }` | non-throwing |
21
+ | `decode(token, options?)` | payload \| null | no signature check |
22
+ | `expiresAt(token)` | `Date` \| null | from `exp` claim |
23
+ | `isExpired(token)` | boolean | true when no `exp` or already past |
24
+ | `fromHeader(authHeader)` | token \| null | parses `Bearer <token>` |
25
+
26
+ `sign`/`verify` throw if the corresponding secret is not configured.
27
+
28
+ ## Examples
29
+
30
+ Issue tokens on login:
31
+
32
+ ```js
33
+ const tokens = Jwt.issue({ sub: user.id, email: user.email })
34
+ res.json({ status: true, code: 200, message: 'OK', data: tokens, meta: null })
35
+ ```
36
+
37
+ Protect a route (see `app/http/middlewares/auth.middleware.js`):
38
+
39
+ ```js
40
+ const token = Jwt.fromHeader(req.headers.authorization)
41
+ const { valid, payload } = Jwt.tryVerify(token)
42
+ if (!valid) return res.status(401).json({ status: false, code: 401, message: 'Invalid token', data: null, meta: null })
43
+ req.user = payload
44
+ ```
45
+
46
+ Refresh flow:
47
+
48
+ ```js
49
+ const { sub } = Jwt.verifyRefresh(refreshToken)
50
+ const fresh = Jwt.issue({ sub })
51
+ ```
52
+
53
+ ## Configuration
54
+
55
+ `JWT_SECRET`, `JWT_EXPIRES_IN` (`1d`), `JWT_REFRESH_SECRET`,
56
+ `JWT_REFRESH_EXPIRES_IN` (`7d`). Generate secrets with
57
+ `npm run cli -- key:jwt` and `npm run cli -- key:jwt --refresh`.
58
+
59
+ ## Security notes
60
+
61
+ - Keep the access TTL short; use refresh tokens for renewal.
62
+ - Put only non-sensitive claims in the payload (it is base64, not encrypted).
63
+ - Rotating `JWT_SECRET` invalidates all existing access tokens.
@@ -0,0 +1,60 @@
1
+ # Logger API
2
+
3
+ `@core/logger.core` exports a ready-to-use Winston instance (not a class). It writes
4
+ a colored console line plus daily-rotating files (`combined-%DATE%.log` and
5
+ `error-%DATE%.log`) under `LOG_FILE` (default `logs/`).
6
+
7
+ ```js
8
+ const logger = require('@core/logger.core')
9
+ ```
10
+
11
+ ## Levels (lowest → highest priority)
12
+
13
+ ```js
14
+ logger.silly('most verbose, development only')
15
+ logger.debug('debug info, query params, ...')
16
+ logger.verbose('more detail than info')
17
+ logger.http('request/response logging')
18
+ logger.info('normal application flow')
19
+ logger.warn('non-critical anomaly')
20
+ logger.error('critical failure')
21
+ ```
22
+
23
+ ## Metadata and errors
24
+
25
+ ```js
26
+ logger.info('User login', { userId: 1, ip: '127.0.0.1' })
27
+
28
+ // Pass an Error so the stack is captured (errors({ stack: true }) is enabled):
29
+ logger.error(new Error('Something went wrong'))
30
+ logger.error('DB connect failed', new Error('ECONNREFUSED'))
31
+
32
+ // Dynamic level:
33
+ logger.log({ level: 'info', message: 'custom level call' })
34
+ ```
35
+
36
+ ## What is shown
37
+
38
+ Output depends on `LOG_LEVEL`:
39
+
40
+ ```
41
+ LOG_LEVEL=silly -> everything
42
+ LOG_LEVEL=debug -> debug and above (no silly)
43
+ LOG_LEVEL=http -> http and above
44
+ LOG_LEVEL=info -> info, warn, error
45
+ LOG_LEVEL=warn -> warn, error
46
+ LOG_LEVEL=error -> error only
47
+ ```
48
+
49
+ In production (`APP_ENV=production`) a console filter additionally blocks `error`,
50
+ `warn`, and `debug` from the console, so the console effectively shows `info`,
51
+ `http`, and `verbose`. All levels are still written to the rotating log files.
52
+
53
+ ## Configuration
54
+
55
+ | Env | Default | Meaning |
56
+ | --------------- | ------- | ------------------------------- |
57
+ | `LOG_LEVEL` | `debug` | minimum level emitted |
58
+ | `LOG_FILE` | `logs` | output directory |
59
+ | `LOG_MAX_SIZE` | `20m` | rotate when a file exceeds this |
60
+ | `LOG_MAX_FILES` | `14d` | retention window |
@@ -0,0 +1,50 @@
1
+ # Mailer API
2
+
3
+ `@core/mailer.core` wraps `nodemailer`. The SMTP transport is created lazily and
4
+ cached. When `MAIL_ENABLED=false`, `send()` is a safe no-op so application code never
5
+ has to branch on the toggle.
6
+
7
+ ```js
8
+ const Mailer = require('@core/mailer.core')
9
+ ```
10
+
11
+ ## Methods
12
+
13
+ | Method | Returns | Notes |
14
+ | --------------- | --------------------------------------- | --------------------------------------------------------------- |
15
+ | `send(message)` | nodemailer info, or `{ skipped: true }` | `from` defaults from config |
16
+ | `verify()` | boolean | verifies the SMTP connection; logs and returns false on failure |
17
+
18
+ `message` is a standard nodemailer payload: `{ to, subject, html, text, cc, bcc,
19
+ attachments, from? }`.
20
+
21
+ ## Examples
22
+
23
+ ```js
24
+ await Mailer.send({
25
+ to: 'user@example.com',
26
+ subject: 'Welcome',
27
+ html: '<h1>Hello</h1>',
28
+ })
29
+ ```
30
+
31
+ Pair with the queue so requests are not blocked by SMTP latency:
32
+
33
+ ```js
34
+ // app/queue/register.queue.js
35
+ module.exports = (Queue) => {
36
+ Queue.define('emails', async (payload) => Mailer.send(payload), { concurrency: 2 })
37
+ }
38
+
39
+ // anywhere:
40
+ require('@core/queue.core').dispatch('emails', { to, subject, html })
41
+ ```
42
+
43
+ ## Configuration
44
+
45
+ `MAIL_ENABLED`, `MAIL_HOST`, `MAIL_PORT` (`587`), `MAIL_SECURE` (`false`),
46
+ `MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_FROM_ADDRESS`, `MAIL_FROM_NAME`.
47
+
48
+ When `MAIL_USERNAME` is empty, the transport is created without auth (useful for
49
+ local relays / Mailtrap test inboxes). Set `MAIL_SECURE=true` for implicit TLS
50
+ (port 465); leave it false for STARTTLS (port 587).
@@ -0,0 +1,57 @@
1
+ # Queue API
2
+
3
+ `@core/queue.core` is an in-process job queue with per-queue concurrency and
4
+ exponential-backoff retries. Persistence to the database is optional (jobs are
5
+ recovered on restart). Workers are declared in `app/queue/register.queue.js`.
6
+
7
+ ```js
8
+ const Queue = require('@core/queue.core')
9
+ ```
10
+
11
+ ## Methods
12
+
13
+ | Method | Returns | Notes |
14
+ | ------------------------------------ | --------------------------------- | -------------------------------------------------------- |
15
+ | `define(name, handler, options?)` | Queue | `options.concurrency`, `maxRetries`, `retryDelay` |
16
+ | `dispatch(name, payload?, options?)` | jobId | `options.delay` ms; `options.maxRetries` |
17
+ | `start()` | Promise | loads workers, prepares the table, recovers pending jobs |
18
+ | `stop(timeoutMs=10000)` | Promise | best-effort drain |
19
+ | `stats()` | `{ [name]: { pending, active } }` | queue depth snapshot |
20
+
21
+ `handler(payload, job)` runs each job; throwing triggers a retry with backoff
22
+ `retryDelay * 2^(attempt-1)` until `maxRetries` is exceeded.
23
+
24
+ ## Example
25
+
26
+ ```js
27
+ // app/queue/register.queue.js
28
+ module.exports = (Queue) => {
29
+ Queue.define(
30
+ 'emails',
31
+ async (payload) => {
32
+ await require('@core/mailer.core').send(payload)
33
+ },
34
+ { concurrency: 2, maxRetries: 3 },
35
+ )
36
+ }
37
+
38
+ // dispatch from anywhere:
39
+ const Queue = require('@core/queue.core')
40
+ Queue.dispatch('emails', { to, subject, html })
41
+ Queue.dispatch('emails', { to, subject, html }, { delay: 5000 }) // after 5s
42
+ ```
43
+
44
+ ## Persistence (optional)
45
+
46
+ When `QUEUE_PERSIST=true` and the database is enabled, the core auto-creates a
47
+ `queue_jobs` table and recovers `pending`/`processing` jobs on the next boot. Without
48
+ a database, the queue is purely in-memory and jobs are lost on restart.
49
+
50
+ ## Configuration
51
+
52
+ `QUEUE_ENABLED` (`true`), `QUEUE_CONCURRENCY` (`5`), `QUEUE_MAX_RETRIES` (`3`),
53
+ `QUEUE_RETRY_DELAY` (`1000`), `QUEUE_PERSIST` (`true`).
54
+
55
+ > The `define`/`dispatch` contract is identical whether the backend is in-process or
56
+ > (with `REDIS_USE_QUEUE`) a shared store — application code does not change when the
57
+ > backend is swapped.
@@ -0,0 +1,32 @@
1
+ # rebe — API Reference
2
+
3
+ Per-core API documentation. For project setup, structure, and the boot flow, see the
4
+ [project README](../README.md); for the security model and audit, see
5
+ [SECURITY.md](../SECURITY.md).
6
+
7
+ ## Cores
8
+
9
+ | Core | Doc |
10
+ | -------------------------- | ------------------------------ |
11
+ | Logger | [Logger.md](./Logger.md) |
12
+ | JWT | [Jwt.md](./Jwt.md) |
13
+ | Mailer | [Mailer.md](./Mailer.md) |
14
+ | Redis (optional) | [Redis.md](./Redis.md) |
15
+ | Hooks | [Hooks.md](./Hooks.md) |
16
+ | Express (HTTP) | [Express.md](./Express.md) |
17
+ | Socket.IO | [Socket.md](./Socket.md) |
18
+ | Validator | [Validator.md](./Validator.md) |
19
+ | Cron | [Cron.md](./Cron.md) |
20
+ | Queue | [Queue.md](./Queue.md) |
21
+ | Database | [Database.md](./Database.md) |
22
+ | Common (utils) | [Common.md](./Common.md) |
23
+ | Register (app→core bridge) | [Register.md](./Register.md) |
24
+ | Bootstrap | [Bootstrap.md](./Bootstrap.md) |
25
+
26
+ ## Conventions
27
+
28
+ - Every API handler returns the manual envelope `{ status, code, message, data, meta }`
29
+ (validation failures add `errors`).
30
+ - Cores are static classes; import via aliases (`@core/...`, `@config/...`).
31
+ - Configuration lives in `config/*` and is read from `.env` — see `.env.example` for
32
+ every variable and its default.
@@ -0,0 +1,54 @@
1
+ # Redis API
2
+
3
+ `@core/redis.core` is a standalone `ioredis` connection. A client is created **only**
4
+ when `REDIS_ENABLED=true`. Other features (cache, queue, socket, session) opt in via
5
+ `REDIS_USE_*` and must provide a fallback when Redis is off, so the app runs
6
+ unchanged with or without Redis.
7
+
8
+ ```js
9
+ const Redis = require('@core/redis.core')
10
+ ```
11
+
12
+ ## Methods
13
+
14
+ | Method | Returns | Notes |
15
+ | ----------------------- | -------------- | ---------------------------------------------------------------- |
16
+ | `connect()` | client \| null | no-op (returns null) when disabled; idempotent; pings on connect |
17
+ | `client()` | ioredis client | throws when disabled or not yet connected |
18
+ | `isEnabledFor(feature)` | boolean | `enabled && use[feature]` (`cache`/`queue`/`socket`/`session`) |
19
+ | `disconnect()` | Promise | closes the connection (called on shutdown) |
20
+
21
+ `connect()` and `disconnect()` are wired into `bootstrap.core`; you normally only
22
+ call `isEnabledFor()` and `client()`.
23
+
24
+ ## Example: optional cache with fallback
25
+
26
+ ```js
27
+ const Redis = require('@core/redis.core')
28
+ const Common = require('@core/common.core')
29
+
30
+ async function getCached(key, ttl, producer) {
31
+ if (Redis.isEnabledFor('cache')) {
32
+ const hit = await Redis.client().get(key)
33
+ if (hit) return JSON.parse(hit)
34
+ const value = await producer()
35
+ await Redis.client().set(key, JSON.stringify(value), 'EX', ttl)
36
+ return value
37
+ }
38
+ // Fallback: in-memory cache (core/common/cache).
39
+ return Common.Cache.remember(key, ttl, producer)
40
+ }
41
+ ```
42
+
43
+ ## Multi-instance socket
44
+
45
+ With `REDIS_USE_SOCKET=true`, `socket.core` uses `@socket.io/redis-adapter` (install
46
+ it separately) for cross-instance broadcasts. Without the package, it logs a warning
47
+ and keeps the default in-memory adapter.
48
+
49
+ ## Configuration
50
+
51
+ `REDIS_ENABLED`, `REDIS_HOST`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`,
52
+ `REDIS_DB`, `REDIS_KEY_PREFIX`, `REDIS_TLS`, `REDIS_MAX_RETRIES`,
53
+ `REDIS_CONNECT_TIMEOUT`, and the per-feature toggles `REDIS_USE_{CACHE,QUEUE,SOCKET,
54
+ SESSION}` (effective only when `REDIS_ENABLED=true`).
@@ -0,0 +1,52 @@
1
+ # Register API
2
+
3
+ `@core/register.core` is the hardened loader for the app→core bridge. The core layer
4
+ never imports application code statically; each core loads a single `register.*.js`
5
+ entry point at runtime through this loader (Inversion of Control). The loader keeps
6
+ the dependency arrow one-directional and turns malformed registers into clear errors
7
+ instead of cryptic stacks deep inside a core.
8
+
9
+ ```js
10
+ const Register = require('@core/register.core')
11
+ ```
12
+
13
+ ## Methods
14
+
15
+ | Method | Returns | Notes |
16
+ | ----------------------------------------- | ----------------------- | ------------------------------------------------------------------------ |
17
+ | `path(...segments)` | absolute path | resolves under the project root (`process.cwd()`) |
18
+ | `require(file, { optional=true, label })` | module \| null | unwraps a `default` export; null when an optional file is absent |
19
+ | `invoke(file, args=[], opts)` | register result \| null | calls a function or `{ register }` export with `args` |
20
+ | `object(file, allowed=[], opts)` | object \| null | requires a plain object; validates that each `allowed` key is a function |
21
+
22
+ Behavior:
23
+
24
+ - Missing **optional** file → logs a warning and returns null. Missing **required**
25
+ file (`optional: false`) → throws.
26
+ - A `require()` failure is logged and rethrown.
27
+ - `invoke` rejects an export that is neither a function nor `{ register() }`.
28
+ - `object` rejects a non-object export or a non-function handler key.
29
+
30
+ ## How the cores use it
31
+
32
+ | Core | File | Call |
33
+ | ------------ | --------------------------------------------- | ------------------------------------------------------ |
34
+ | cron.core | `app/jobs/register.job.js` | `Register.invoke(file, [Cron])` |
35
+ | queue.core | `app/queue/register.queue.js` | `Register.invoke(file, [Queue])` |
36
+ | express.core | `app/http/middlewares/register.middleware.js` | `Register.require(file)` → `(app)` |
37
+ | socket.core | `app/socket/register.socket.js` | `Register.invoke(file, [io, Socket])` |
38
+ | hooks.core | `app/hooks/register.hook.js` | `Register.object(file, ['before','after','shutdown'])` |
39
+
40
+ ## Authoring a register
41
+
42
+ Keep registers thin: wire application code to the core facade, no heavy logic.
43
+
44
+ ```js
45
+ // app/jobs/register.job.js
46
+ module.exports = (Cron) => {
47
+ Cron.define('heartbeat', '*/30 * * * *', async () => {})
48
+ }
49
+ ```
50
+
51
+ The `{ register(facade) {} }` object form is also accepted, but the direct function
52
+ form is preferred for consistency.
@@ -0,0 +1,55 @@
1
+ # Socket API
2
+
3
+ `@core/socket.core` attaches Socket.IO to the Express `http.Server` (single port).
4
+ Connection handlers live in `app/socket/register.socket.js`.
5
+
6
+ ```js
7
+ const Socket = require('@core/socket.core')
8
+ ```
9
+
10
+ ## Methods
11
+
12
+ | Method | Returns | Notes |
13
+ | ------------------------------ | --------------------- | ---------------------------------------------------- |
14
+ | `attach(httpServer)` | `Promise<io \| null>` | returns null when `SOCKET_ENABLED=false`; idempotent |
15
+ | `broadcast(event, payload)` | void | emit to all clients |
16
+ | `toRoom(room, event, payload)` | void | emit to a room |
17
+ | `close()` | Promise | close the server |
18
+
19
+ `attach`/`close` are wired by `bootstrap.core`; you use `broadcast`/`toRoom` and the
20
+ register file.
21
+
22
+ ## Connection handlers
23
+
24
+ ```js
25
+ // app/socket/register.socket.js
26
+ module.exports = (io, Socket) => {
27
+ io.on('connection', (socket) => {
28
+ socket.on('join', (room) => socket.join(room))
29
+ socket.on('ping', (payload) => socket.emit('pong', { received: payload, at: Date.now() }))
30
+ })
31
+ }
32
+ ```
33
+
34
+ Emit from elsewhere (e.g. a controller or job):
35
+
36
+ ```js
37
+ const Socket = require('@core/socket.core')
38
+ Socket.broadcast('notice', { message: 'hello' })
39
+ Socket.toRoom('room:42', 'update', { id: 42 })
40
+ ```
41
+
42
+ ## Multi-instance (optional)
43
+
44
+ With `REDIS_USE_SOCKET=true` and `@socket.io/redis-adapter` installed, the core wires
45
+ the Redis adapter so rooms and broadcasts work across instances. Without the package
46
+ it logs a warning and uses the default in-memory adapter.
47
+
48
+ ## Configuration
49
+
50
+ `SOCKET_ENABLED`, `SOCKET_PATH`, ping/transport options, and CORS from
51
+ `cors.config.js` (`SOCKET_CORS_ORIGIN`, `SOCKET_CORS_CREDENTIALS`). See `.env.example`
52
+ for every variable.
53
+
54
+ > CORS for sockets is enforced at the handshake. Lock `SOCKET_CORS_ORIGIN` down in
55
+ > production rather than leaving it `*`.
@@ -0,0 +1,45 @@
1
+ # Validator API
2
+
3
+ `@core/validator.core` turns a Zod schema into express-routing middleware. On
4
+ failure it short-circuits with a 422 using the standard envelope plus an `errors`
5
+ array; on success it stores the parsed value and continues.
6
+
7
+ ```js
8
+ const Validator = require('@core/validator.core')
9
+ ```
10
+
11
+ ## Method
12
+
13
+ `make(schema, source = 'body')` → `{ handle({ req, res, next }) }`
14
+
15
+ - `source` is one of `'body'`, `'query'`, `'params'`.
16
+ - On success: `req.validated[source]` holds the parsed data. For `source === 'body'`
17
+ it also replaces `req.body` (in Express 5 `req.query`/`req.params` are read-only and
18
+ must not be reassigned — read the parsed copy from `req.validated`).
19
+ - On failure: responds `422 { status:false, code:422, message:'Validation failed',
20
+ data:null, meta:null, errors:[{ field, message }] }`.
21
+
22
+ ## Example
23
+
24
+ ```js
25
+ // app/http/validators/auth.validator.js
26
+ const { z } = require('zod')
27
+ module.exports.loginSchema = z.object({
28
+ email: z.string().email(),
29
+ password: z.string().min(6),
30
+ })
31
+
32
+ // app/routes/api.route.js
33
+ Routes.post('auth/login', AuthController.login, [Validator.make(loginSchema)])
34
+
35
+ // query validation:
36
+ Routes.get('users', UserController.index, [Validator.make(listQuerySchema, 'query')])
37
+ // -> read parsed values from req.validated.query
38
+ ```
39
+
40
+ ## Notes
41
+
42
+ - Validate at the route boundary; do not re-validate in the controller when a schema
43
+ exists.
44
+ - `errors[].field` is the dotted Zod path (e.g. `address.city`); `message` is the
45
+ Zod message.
@@ -0,0 +1,54 @@
1
+ // PM2 process definition for the backend.
2
+ //
3
+ // IMPORTANT: run as a SINGLE fork instance (instances: 1, exec_mode: 'fork').
4
+ // The app keeps state in-process — Socket.IO rooms, the in-process job queue,
5
+ // import-progress tracking, and node-cron schedules. Cluster mode would
6
+ // duplicate cron ticks, split socket rooms, and break queue/progress state.
7
+ //
8
+ // The backend serves BOTH the JSON API and any static assets from ./public, so
9
+ // this one process is all you need in production. `.env` is loaded by the app
10
+ // itself (index.js → dotenv), so PM2 does not inject it.
11
+ //
12
+ // The PM2 process name is read from APP_NAME (fallback 'rebe').
13
+ //
14
+ // Usage (from the project root):
15
+ // pm2 start ecosystem.config.js
16
+ // pm2 logs <APP_NAME>
17
+ // pm2 restart <APP_NAME>
18
+ require('dotenv').config()
19
+
20
+ module.exports = {
21
+ apps: [
22
+ {
23
+ name: process.env.APP_NAME || 'rebe',
24
+ cwd: __dirname, // process.cwd() must be the project root (relative paths: app, public, storage, logs)
25
+ script: 'index.js', // same entry as `npm start` (node .)
26
+ exec_mode: 'fork',
27
+ instances: 1,
28
+
29
+ autorestart: true,
30
+ watch: false, // never watch in prod; restarts would drop sockets/queue
31
+ max_memory_restart: '1G',
32
+
33
+ // Graceful shutdown: the app handles SIGINT/SIGTERM and waits for the
34
+ // queue to drain (up to ~10s). Give it headroom before PM2 force-kills.
35
+ kill_timeout: 15000,
36
+
37
+ // Treat the app as "online" only after it has been up a few seconds
38
+ // (avoids crash-loop flapping being counted as successful starts).
39
+ min_uptime: '10s',
40
+ max_restarts: 10,
41
+
42
+ env: {
43
+ NODE_ENV: 'production',
44
+ },
45
+
46
+ // Logs (PM2 stdout/stderr). The app also writes its own winston logs
47
+ // under ./logs via LOG_FILE.
48
+ time: true,
49
+ merge_logs: true,
50
+ out_file: 'logs/pm2-out.log',
51
+ error_file: 'logs/pm2-error.log',
52
+ },
53
+ ],
54
+ };
@@ -0,0 +1,6 @@
1
+ 'use strict'
2
+
3
+ require('dotenv').config()
4
+ require('module-alias/register')
5
+ require('@core/runtime.core')
6
+ require('@core/bootstrap.core').run()
@@ -0,0 +1,27 @@
1
+ 'use strict'
2
+
3
+ // Jest resolves the module aliases via moduleNameMapper (module-alias is a runtime
4
+ // shim and does not apply under Jest). Keep this map in sync with
5
+ // package.json._moduleAliases and jsconfig.json.
6
+ module.exports = {
7
+ testEnvironment: 'node',
8
+ setupFiles: ['<rootDir>/tests/setup.js'],
9
+ testMatch: ['<rootDir>/tests/**/*.test.js'],
10
+ moduleNameMapper: {
11
+ '^@config$': '<rootDir>/config/index.js',
12
+ '^@config/(.*)$': '<rootDir>/config/$1',
13
+ '^@core/(.*)$': '<rootDir>/core/$1',
14
+ '^@app/(.*)$': '<rootDir>/app/$1',
15
+ '^@http/(.*)$': '<rootDir>/app/http/$1',
16
+ '^@routes/(.*)$': '<rootDir>/app/routes/$1',
17
+ '^@jobs/(.*)$': '<rootDir>/app/jobs/$1',
18
+ '^@queue/(.*)$': '<rootDir>/app/queue/$1',
19
+ '^@socket/(.*)$': '<rootDir>/app/socket/$1',
20
+ '^@hooks/(.*)$': '<rootDir>/app/hooks/$1',
21
+ '^@database/(.*)$': '<rootDir>/database/$1',
22
+ '^@storage/(.*)$': '<rootDir>/storage/$1',
23
+ },
24
+ // Winston's rotating-file transport keeps timers alive; force a clean exit.
25
+ forceExit: true,
26
+ clearMocks: true,
27
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "compilerOptions": {
3
+ "paths": {
4
+ "@config/*": ["./config/*"],
5
+ "@core/*": ["./core/*"],
6
+ "@app/*": ["./app/*"],
7
+ "@http/*": ["./app/http/*"],
8
+ "@routes/*": ["./app/routes/*"],
9
+ "@jobs/*": ["./app/jobs/*"],
10
+ "@queue/*": ["./app/queue/*"],
11
+ "@socket/*": ["./app/socket/*"],
12
+ "@hooks/*": ["./app/hooks/*"],
13
+ "@database/*": ["./database/*"],
14
+ "@storage/*": ["./storage/*"]
15
+ },
16
+ "module": "commonjs",
17
+ "moduleResolution": "node",
18
+ "ignoreDeprecations": "6.0",
19
+ "target": "ES2023",
20
+ "lib": ["ES2023"]
21
+ },
22
+ "include": [
23
+ "*.js",
24
+ "config/**/*",
25
+ "core/**/*",
26
+ "app/**/*",
27
+ "database/**/*",
28
+ "storage/**/*"
29
+ ],
30
+ "exclude": [
31
+ "node_modules",
32
+ "dist",
33
+ "coverage",
34
+ "logs",
35
+ "create-rebe"
36
+ ]
37
+ }
File without changes