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.
- package/LICENSE +21 -0
- package/README.md +54 -0
- package/bin/create-rebe.js +310 -0
- package/package.json +40 -0
- package/template/.env.example +141 -0
- package/template/.gitattributes +16 -0
- package/template/.nvmrc +1 -0
- package/template/.prettierignore +10 -0
- package/template/.prettierrc +12 -0
- package/template/LICENSE +21 -0
- package/template/README.md +150 -0
- package/template/SECURITY.md +133 -0
- package/template/app/hooks/register.hook.js +22 -0
- package/template/app/http/controllers/auth.controller.js +34 -0
- package/template/app/http/middlewares/auth.middleware.js +25 -0
- package/template/app/http/middlewares/register.middleware.js +18 -0
- package/template/app/http/validators/auth.validator.js +8 -0
- package/template/app/jobs/register.job.js +17 -0
- package/template/app/queue/register.queue.js +11 -0
- package/template/app/routes/api.route.js +18 -0
- package/template/app/routes/register.route.js +8 -0
- package/template/app/routes/web.route.js +7 -0
- package/template/app/socket/register.socket.js +15 -0
- package/template/config/app.config.js +12 -0
- package/template/config/bcrypt.config.js +7 -0
- package/template/config/cache.config.js +7 -0
- package/template/config/cors.config.js +37 -0
- package/template/config/cron.config.js +9 -0
- package/template/config/database.config.js +51 -0
- package/template/config/express.config.js +37 -0
- package/template/config/index.js +19 -0
- package/template/config/jwt.config.js +10 -0
- package/template/config/logger.config.js +10 -0
- package/template/config/mail.config.js +14 -0
- package/template/config/queue.config.js +11 -0
- package/template/config/redis.config.js +32 -0
- package/template/config/runtime.config.js +11 -0
- package/template/config/socket.config.js +15 -0
- package/template/config/storage.config.js +10 -0
- package/template/core/bootstrap.core.js +92 -0
- package/template/core/common/array.js +144 -0
- package/template/core/common/cache.js +100 -0
- package/template/core/common/collection.js +173 -0
- package/template/core/common/crypt.js +69 -0
- package/template/core/common/date.js +254 -0
- package/template/core/common/hash.js +61 -0
- package/template/core/common/object.js +155 -0
- package/template/core/common/path.js +80 -0
- package/template/core/common/storage.js +97 -0
- package/template/core/common/string.js +137 -0
- package/template/core/common/url.js +81 -0
- package/template/core/common.core.js +93 -0
- package/template/core/cron.core.js +141 -0
- package/template/core/database.core.js +113 -0
- package/template/core/error.core.js +83 -0
- package/template/core/express.core.js +161 -0
- package/template/core/hooks.core.js +47 -0
- package/template/core/jwt.core.js +81 -0
- package/template/core/logger.core.js +100 -0
- package/template/core/mailer.core.js +65 -0
- package/template/core/queue.core.js +226 -0
- package/template/core/redis.core.js +75 -0
- package/template/core/register.core.js +91 -0
- package/template/core/runtime.core.js +27 -0
- package/template/core/socket.core.js +93 -0
- package/template/core/validator.core.js +34 -0
- package/template/database/.gitkeep +0 -0
- package/template/database/models/post.model.js +26 -0
- package/template/database/models/user.model.js +30 -0
- package/template/docs/Bootstrap.md +50 -0
- package/template/docs/Common.md +48 -0
- package/template/docs/Cron.md +47 -0
- package/template/docs/Database.md +61 -0
- package/template/docs/Express.md +63 -0
- package/template/docs/Hooks.md +48 -0
- package/template/docs/Jwt.md +63 -0
- package/template/docs/Logger.md +60 -0
- package/template/docs/Mailer.md +50 -0
- package/template/docs/Queue.md +57 -0
- package/template/docs/README.md +32 -0
- package/template/docs/Redis.md +54 -0
- package/template/docs/Register.md +52 -0
- package/template/docs/Socket.md +55 -0
- package/template/docs/Validator.md +45 -0
- package/template/ecosystem.config.js +54 -0
- package/template/index.js +6 -0
- package/template/jest.config.js +27 -0
- package/template/jsconfig.json +37 -0
- package/template/logs/.gitkeep +0 -0
- package/template/nodemon.json +23 -0
- package/template/package-lock.json +7033 -0
- package/template/package.json +82 -0
- package/template/scripts/cli.js +258 -0
- package/template/storage/.gitkeep +0 -0
- package/template/tests/common.test.js +45 -0
- package/template/tests/crypt-hash-storage.test.js +44 -0
- package/template/tests/jwt.test.js +45 -0
- package/template/tests/register.test.js +55 -0
- package/template/tests/setup.js +11 -0
- 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,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
|