create-rebe 1.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.env.example +4 -0
- package/template/README.md +63 -8
- package/template/SECURITY.md +39 -0
- package/template/app/routes/api.route.js +1 -1
- package/template/app/routes/register.route.js +1 -1
- package/template/app/routes/web.route.js +1 -1
- package/template/app/socket/register.socket.js +0 -2
- package/template/config/express.config.js +8 -0
- package/template/core/common/string.js +1 -1
- package/template/core/cron.core.js +1 -0
- package/template/core/database.core.js +30 -17
- package/template/core/error.core.js +8 -4
- package/template/core/express.core.js +16 -4
- package/template/core/hooks.core.js +10 -7
- package/template/core/migrator.core.js +201 -0
- package/template/core/modules.core.js +167 -0
- package/template/core/queue.core.js +1 -0
- package/template/core/routing.core.d.ts +273 -0
- package/template/core/routing.core.js +666 -0
- package/template/core/seeder.core.js +105 -0
- package/template/core/socket.core.js +1 -0
- package/template/database/migrations/.gitkeep +0 -0
- package/template/database/seeders/.gitkeep +0 -0
- package/template/docs/Database.md +14 -8
- package/template/docs/Express.md +5 -2
- package/template/docs/Make.md +46 -0
- package/template/docs/Migration.md +56 -0
- package/template/docs/Modules.md +96 -0
- package/template/docs/README.md +5 -0
- package/template/docs/Routing.md +116 -0
- package/template/docs/Seeder.md +54 -0
- package/template/eslint.config.js +52 -0
- package/template/package-lock.json +1068 -70
- package/template/package.json +15 -8
- package/template/scripts/cli/args.js +39 -0
- package/template/scripts/cli/bootstrap.js +16 -0
- package/template/scripts/cli/db.js +79 -0
- package/template/scripts/cli/help.js +58 -0
- package/template/scripts/cli/keys.js +100 -0
- package/template/scripts/cli/log.js +58 -0
- package/template/scripts/cli/make.js +249 -0
- package/template/scripts/cli/names.js +51 -0
- package/template/scripts/cli/templates.js +358 -0
- package/template/scripts/cli.js +75 -234
- package/template/tests/http.test.js +99 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
const logger = require('@core/logger.core')
|
|
7
|
+
const Database = require('@core/database.core')
|
|
8
|
+
const Modules = require('@core/modules.core')
|
|
9
|
+
|
|
10
|
+
const SEEDERS_DIR = path.resolve(process.cwd(), 'database', 'seeders')
|
|
11
|
+
|
|
12
|
+
// Seeder runner. Seeders are plain async functions used to populate data; unlike
|
|
13
|
+
// migrations they are not tracked and are meant to be re-runnable (write them to be
|
|
14
|
+
// idempotent — e.g. findOrCreate / upsert). Files live in database/seeders/ and/or a
|
|
15
|
+
// module's seeders/ dir. Files starting with "_" are ignored (base classes/helpers).
|
|
16
|
+
//
|
|
17
|
+
// module.exports = async ({ sequelize, Database, models }) => { ... }
|
|
18
|
+
class Seeder {
|
|
19
|
+
// Build the context handed to every seeder.
|
|
20
|
+
static async _context() {
|
|
21
|
+
const sequelize = await Database.connect()
|
|
22
|
+
return { sequelize, Database, Sequelize: Database.Sequelize, models: Object.fromEntries(Database.models) }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// basename -> absolute path across the flat dir and every module seeders dir.
|
|
26
|
+
static _discover() {
|
|
27
|
+
const dirs = [SEEDERS_DIR, ...Modules.dirs('seeders')]
|
|
28
|
+
const files = new Map()
|
|
29
|
+
|
|
30
|
+
for (const dir of dirs) {
|
|
31
|
+
if (!fs.existsSync(dir)) continue
|
|
32
|
+
for (const file of fs.readdirSync(dir)) {
|
|
33
|
+
if (!file.endsWith('.js') || file.startsWith('_')) continue
|
|
34
|
+
if (!files.has(file)) files.set(file, path.join(dir, file))
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return new Map([...files.entries()].sort(([a], [b]) => a.localeCompare(b)))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static _run(file, ctx) {
|
|
42
|
+
const raw = require(file)
|
|
43
|
+
const fn = raw && raw.default !== undefined ? raw.default : raw
|
|
44
|
+
if (typeof fn !== 'function') {
|
|
45
|
+
throw new Error(`Seeder "${path.basename(file)}" must export an async function`)
|
|
46
|
+
}
|
|
47
|
+
return fn(ctx)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Run every discovered seeder, in filename order.
|
|
51
|
+
static async seedAll() {
|
|
52
|
+
const ctx = await Seeder._context()
|
|
53
|
+
const all = Seeder._discover()
|
|
54
|
+
|
|
55
|
+
if (!all.size) {
|
|
56
|
+
logger.info('Seeders: nothing to seed')
|
|
57
|
+
return []
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ran = []
|
|
61
|
+
for (const [name, file] of all) {
|
|
62
|
+
logger.info(`Seeding: ${name}`)
|
|
63
|
+
await Seeder._run(file, ctx)
|
|
64
|
+
ran.push(name)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
logger.info(`Seeders: ${ran.length} run`)
|
|
68
|
+
return ran
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Run a single seeder. With no name, run database/seeders/index.js (the default
|
|
72
|
+
// "DatabaseSeeder" entry) when present, otherwise fall back to seeding all.
|
|
73
|
+
static async seed(name) {
|
|
74
|
+
const ctx = await Seeder._context()
|
|
75
|
+
|
|
76
|
+
if (!name) {
|
|
77
|
+
const indexFile = path.join(SEEDERS_DIR, 'index.js')
|
|
78
|
+
if (fs.existsSync(indexFile)) {
|
|
79
|
+
logger.info('Seeding: index.js')
|
|
80
|
+
await Seeder._run(indexFile, ctx)
|
|
81
|
+
return ['index.js']
|
|
82
|
+
}
|
|
83
|
+
return Seeder.seedAll()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const all = Seeder._discover()
|
|
87
|
+
// Match leniently: exact filename, or normalized (separators/case stripped) so
|
|
88
|
+
// `--class=UserSeeder` finds `user-seeder.js`.
|
|
89
|
+
const norm = (s) =>
|
|
90
|
+
s
|
|
91
|
+
.replace(/\.js$/, '')
|
|
92
|
+
.replace(/[^a-z0-9]/gi, '')
|
|
93
|
+
.toLowerCase()
|
|
94
|
+
const wanted = norm(name)
|
|
95
|
+
const target = all.get(name) || all.get(`${name}.js`) || [...all.entries()].find(([k]) => norm(k) === wanted)?.[1]
|
|
96
|
+
|
|
97
|
+
if (!target) throw new Error(`Seeder "${name}" not found in ${[SEEDERS_DIR, ...Modules.dirs('seeders')].join(', ')}`)
|
|
98
|
+
|
|
99
|
+
logger.info(`Seeding: ${path.basename(target)}`)
|
|
100
|
+
await Seeder._run(target, ctx)
|
|
101
|
+
return [path.basename(target)]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = Seeder
|
|
@@ -68,6 +68,7 @@ class Socket {
|
|
|
68
68
|
|
|
69
69
|
static async _loadHandlers() {
|
|
70
70
|
await Register.invoke(REGISTER_FILE, [Socket.io, Socket], { label: 'app/socket/register.socket.js' })
|
|
71
|
+
await require('@core/modules.core').invoke('socket', [Socket.io, Socket])
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
static broadcast(event, payload) {
|
|
File without changes
|
|
File without changes
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Database API
|
|
2
2
|
|
|
3
3
|
`@core/database.core` manages the Sequelize connection and model loading. It is
|
|
4
|
-
skipped entirely when `DB_ENABLED=false`.
|
|
5
|
-
|
|
4
|
+
skipped entirely when `DB_ENABLED=false`. Schema is managed by the migration runner
|
|
5
|
+
([Migration.md](./Migration.md)) and populated by seeders ([Seeder.md](./Seeder.md));
|
|
6
|
+
generate models/migrations/seeders with the CLI (see the [project README](../README.md#cli)).
|
|
6
7
|
|
|
7
8
|
```js
|
|
8
9
|
const Database = require('@core/database.core')
|
|
@@ -10,12 +11,17 @@ const Database = require('@core/database.core')
|
|
|
10
11
|
|
|
11
12
|
## Methods
|
|
12
13
|
|
|
13
|
-
| Method | Returns
|
|
14
|
-
| -------------- |
|
|
15
|
-
| `connect()` | Sequelize
|
|
16
|
-
| `disconnect()` | Promise
|
|
17
|
-
| `loadModels()` | `Map`
|
|
18
|
-
| `
|
|
14
|
+
| Method | Returns | Notes |
|
|
15
|
+
| -------------- | ---------- | --------------------------------------------------- |
|
|
16
|
+
| `connect()` | Sequelize | idempotent; `authenticate()` then `loadModels()` |
|
|
17
|
+
| `disconnect()` | Promise | closes the pool, clears models |
|
|
18
|
+
| `loadModels()` | `Map` | loads flat + module models, then wires associations |
|
|
19
|
+
| `modelDirs()` | `string[]` | `database/models` plus each module's `models/` dir |
|
|
20
|
+
| `model(name)` | Model | throws if the model is not registered |
|
|
21
|
+
|
|
22
|
+
Models are loaded from `database/models/*.js` **and** every module's `models/` dir
|
|
23
|
+
(see [Modules.md](./Modules.md)); a name collision logs a warning and the later
|
|
24
|
+
definition wins.
|
|
19
25
|
|
|
20
26
|
Static re-exports: `Database.Sequelize`, `Database.DataTypes`, `Database.Model`,
|
|
21
27
|
`Database.Op`.
|
package/template/docs/Express.md
CHANGED
|
@@ -26,11 +26,14 @@ These are wired by `bootstrap.core`; `upload()` is the one you call from routes.
|
|
|
26
26
|
3. `compression()`
|
|
27
27
|
4. `express.json` / `express.urlencoded` (limit = `EXPRESS_BODY_LIMIT`)
|
|
28
28
|
5. rate limit (when `EXPRESS_RATE_LIMIT_ENABLED`)
|
|
29
|
-
6. user middleware — `app/http/middlewares/register.middleware.js` `(app) => {}`
|
|
29
|
+
6. user middleware — `app/http/middlewares/register.middleware.js` `(app) => {}`, then module `middlewares` hooks
|
|
30
30
|
7. static — `./public` at `/`, uploads at `/uploads`
|
|
31
|
-
8. routes — `@routes/register.route` then `Routes.apply`
|
|
31
|
+
8. routes — `@routes/register.route` + module routes, then `Routes.apply` (see [Routing.md](./Routing.md))
|
|
32
32
|
9. `error.core` 404 + error handlers
|
|
33
33
|
|
|
34
|
+
The router (`@core/routing.core`) is internalized into the core as of 3.0.0 — there is
|
|
35
|
+
no external routing dependency.
|
|
36
|
+
|
|
34
37
|
`x-powered-by` is disabled. The full `storage/` root is **not** served — only
|
|
35
38
|
`/uploads` and `./public`.
|
|
36
39
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Scaffolding (`make:`) commands
|
|
2
|
+
|
|
3
|
+
The CLI generates code for every app layer. Run via `npm run cli -- <command>`. Names
|
|
4
|
+
accept any case (`UserProfile`, `user_profile`, `user-profile`) and are normalized:
|
|
5
|
+
PascalCase for classes, kebab-case for filenames, snake_case (pluralized) for tables.
|
|
6
|
+
|
|
7
|
+
**Every `make:` command supports `--module=<name>`** to target `modules/<name>/…`
|
|
8
|
+
instead of the flat app, and `--force` to overwrite.
|
|
9
|
+
|
|
10
|
+
| Command | Creates | Notes |
|
|
11
|
+
| ------------------------------------------------ | ---------------------------------------------------------- | --------------------------------------------------------------- |
|
|
12
|
+
| `make:model <Name> [--table=t] [--no-migration]` | `database/models/<name>.model.js` + create-table migration | `--no-migration` skips the migration |
|
|
13
|
+
| `make:migration <name>` | `database/migrations/<ts>_<name>.js` | blank up/down |
|
|
14
|
+
| `make:seed <Name>` | `database/seeders/<name>.js` | re-runnable seeder |
|
|
15
|
+
| `make:controller <Name> [--resource] [--api]` | `app/http/controllers/<name>.controller.js` | `--resource` = 7 RESTful actions; `--api` = without create/edit |
|
|
16
|
+
| `make:middleware <Name>` | `app/http/middlewares/<name>.middleware.js` | `handle({ req, res, next })` class |
|
|
17
|
+
| `make:validator <Name>` | `app/http/validators/<name>.validator.js` | `create…Schema` / `update…Schema` (zod) |
|
|
18
|
+
| `make:route <name>` | `app/routes/<name>.route.js` | `Routes.group(...)` file |
|
|
19
|
+
| `make:job <Name>` | `app/jobs/<name>.job.js` | `(Cron) => Cron.define(...)` |
|
|
20
|
+
| `make:queue <Name>` | `app/queue/<name>.queue.js` | `(Queue) => Queue.define(...)` |
|
|
21
|
+
| `make:socket <Name>` | `app/socket/<name>.socket.js` | `(io, Socket) => {}` |
|
|
22
|
+
| `make:hook [name]` | `app/hooks/<name>.hook.js` (or `register.hook.js`) | `{ before, after, shutdown }` |
|
|
23
|
+
| `make:resource <Name> [--api]` | controller + model + migration + validator + route | a wired vertical slice |
|
|
24
|
+
| `make:module <name>` | a full mini-app under `modules/<name>/` | see [Modules.md](./Modules.md) |
|
|
25
|
+
|
|
26
|
+
## Wiring notes
|
|
27
|
+
|
|
28
|
+
Some layers load through a single register file, so the CLI prints a one-line hint:
|
|
29
|
+
|
|
30
|
+
- **Routes** — flat `app/routes/<name>.route.js` must be required from
|
|
31
|
+
`app/routes/register.route.js`. Inside a **module**, route files in `routes/` are
|
|
32
|
+
**auto-loaded** (drop one in and it works).
|
|
33
|
+
- **Jobs / queue / socket** — flat files are invoked from the matching
|
|
34
|
+
`app/.../register.*.js`; module files are wired by the module entry.
|
|
35
|
+
|
|
36
|
+
## Examples
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm run cli -- make:resource Post # full CRUD slice (web)
|
|
40
|
+
npm run cli -- make:resource Post --api # JSON-only CRUD
|
|
41
|
+
npm run cli -- make:controller Billing --resource
|
|
42
|
+
npm run cli -- make:middleware EnsureAdmin
|
|
43
|
+
npm run cli -- make:module blog # full mini-app module
|
|
44
|
+
npm run cli -- make:resource Article --module=blog --api
|
|
45
|
+
npm run cli -- db:migrate # apply the generated migrations
|
|
46
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Migration API
|
|
2
|
+
|
|
3
|
+
`@core/migrator.core` is a lightweight schema-migration runner built on Sequelize's
|
|
4
|
+
`QueryInterface` — no extra dependencies. It replaces relying on `sync({ force, alter })`
|
|
5
|
+
for managing schema. Applied migrations are tracked in the `_migrations` table.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
const Migrator = require('@core/migrator.core')
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Migrations live in `database/migrations/*.js` and/or any module's `migrations/` dir.
|
|
12
|
+
Files are ordered **globally by filename**, so the `YYYYMMDDHHmmss_` timestamp prefix
|
|
13
|
+
the CLI generates gives deterministic order across the flat dir and every module.
|
|
14
|
+
Files whose name starts with `_` are ignored (helpers/partials).
|
|
15
|
+
|
|
16
|
+
## Writing a migration
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
// database/migrations/20260619101540_create_articles.js
|
|
20
|
+
module.exports = {
|
|
21
|
+
async up(queryInterface, Sequelize) {
|
|
22
|
+
await queryInterface.createTable('articles', {
|
|
23
|
+
id: { type: Sequelize.BIGINT.UNSIGNED, primaryKey: true, autoIncrement: true },
|
|
24
|
+
title: { type: Sequelize.STRING(190), allowNull: false, unique: true },
|
|
25
|
+
created_at: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.fn('NOW') },
|
|
26
|
+
updated_at: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.fn('NOW') },
|
|
27
|
+
})
|
|
28
|
+
},
|
|
29
|
+
async down(queryInterface) {
|
|
30
|
+
await queryInterface.dropTable('articles')
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Generate one with `make:model <Name>` (model + create-table migration) or
|
|
36
|
+
`make:migration <name>` (blank up/down) — see the CLI table in the
|
|
37
|
+
[project README](../README.md#cli).
|
|
38
|
+
|
|
39
|
+
## Methods
|
|
40
|
+
|
|
41
|
+
| Method | Returns | Notes |
|
|
42
|
+
| -------------------- | ---------- | ------------------------------------------------------------ |
|
|
43
|
+
| `up()` | `string[]` | run every pending migration under the next batch number |
|
|
44
|
+
| `down({ step = 1 })` | `string[]` | roll back the most recent batch (or the last `step` batches) |
|
|
45
|
+
| `rollbackAll()` | `Promise` | roll back every batch, one at a time |
|
|
46
|
+
| `status()` | `object[]` | `[{ name, applied, batch }]` for all discovered migrations |
|
|
47
|
+
| `dropAllTables()` | `string[]` | drop every table (FK checks toggled per dialect) |
|
|
48
|
+
|
|
49
|
+
## Batches
|
|
50
|
+
|
|
51
|
+
Each `up()` run records its migrations under a single incrementing `batch`. `down()`
|
|
52
|
+
rolls back the latest batch as a unit, so a group migrated together rolls back together.
|
|
53
|
+
`--step=N` on `db:rollback` extends this to the last `N` batches.
|
|
54
|
+
|
|
55
|
+
> Once migrations are the source of truth, keep `DB_FORCE` / `DB_ALTER` **off** in
|
|
56
|
+
> production — let migrations own the schema.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Modules API
|
|
2
|
+
|
|
3
|
+
`@core/modules.core` is an **opt-in** layer that groups one feature's pieces — models,
|
|
4
|
+
routes, migrations, seeders, jobs, queue workers, socket handlers — under
|
|
5
|
+
`modules/<name>/` behind a single entry file. It is fully additive: the flat layout
|
|
6
|
+
(`database/models`, `app/routes`, …) keeps working untouched, and a project with no
|
|
7
|
+
`modules/` directory pays nothing.
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
const Modules = require('@core/modules.core')
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Anatomy of a module
|
|
14
|
+
|
|
15
|
+
`make:module <name>` scaffolds a **full mini-app** mirroring the app layout:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
modules/
|
|
19
|
+
└── blog/
|
|
20
|
+
├── blog.module.js # entry (also accepts module.js / index.js)
|
|
21
|
+
├── http/
|
|
22
|
+
│ ├── controllers/ # static-class controllers
|
|
23
|
+
│ ├── middlewares/register.middleware.js # (app) => {}
|
|
24
|
+
│ └── validators/ # zod schemas
|
|
25
|
+
├── routes/ # *.js auto-loaded (drop in <name>.route.js)
|
|
26
|
+
├── jobs/register.job.js # (Cron) => {}
|
|
27
|
+
├── queue/register.queue.js # (Queue) => {}
|
|
28
|
+
├── socket/register.socket.js # (io, Socket) => {}
|
|
29
|
+
├── hooks/register.hook.js # { before, after, shutdown }
|
|
30
|
+
├── models/ # (sequelize, DataTypes) factories
|
|
31
|
+
├── migrations/ # up/down migration files
|
|
32
|
+
└── seeders/ # seeder files
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Target a module with `--module=<name>` on any `make:` command (e.g.
|
|
36
|
+
`make:resource Article --module=blog`); files land in the right subfolder. Route files
|
|
37
|
+
in `routes/` are auto-loaded, so a generated `<name>.route.js` works with no wiring.
|
|
38
|
+
|
|
39
|
+
## Entry file
|
|
40
|
+
|
|
41
|
+
Every field is optional. Directory fields resolve against the module folder; the
|
|
42
|
+
function/object fields mirror the flat `app/.../register.*.js` shapes.
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
// modules/blog/blog.module.js
|
|
46
|
+
module.exports = {
|
|
47
|
+
name: 'blog', // optional, defaults to the folder name
|
|
48
|
+
|
|
49
|
+
// scanned directories
|
|
50
|
+
models: './models', // dir scanned by database.core
|
|
51
|
+
migrations: './migrations', // dir scanned by migrator.core
|
|
52
|
+
seeders: './seeders', // dir scanned by seeder.core
|
|
53
|
+
routes: './routes', // file → required; dir → every *.js auto-loaded
|
|
54
|
+
|
|
55
|
+
// register hooks
|
|
56
|
+
middlewares: require('./http/middlewares/register.middleware'), // (app) => {}
|
|
57
|
+
jobs: require('./jobs/register.job'), // (Cron) => {}
|
|
58
|
+
queue: require('./queue/register.queue'), // (Queue) => {}
|
|
59
|
+
socket: require('./socket/register.socket'), // (io, Socket) => {}
|
|
60
|
+
hooks: require('./hooks/register.hook'), // { before, after, shutdown }
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The function hooks and `hooks` lifecycle object receive the exact same facades/context as
|
|
65
|
+
the flat `app/.../register.*.js` files and run **after** them during boot.
|
|
66
|
+
|
|
67
|
+
## How discovery wires in
|
|
68
|
+
|
|
69
|
+
| Contribution | Loaded by | When |
|
|
70
|
+
| ------------- | -------------------------------------- | --------------------- |
|
|
71
|
+
| `models` | `database.core.loadModels()` | DB connect |
|
|
72
|
+
| `migrations` | `migrator.core` | CLI `db:*` |
|
|
73
|
+
| `seeders` | `seeder.core` | CLI `db:seed*` |
|
|
74
|
+
| `routes` | `express.core` (after flat routes) | HTTP boot |
|
|
75
|
+
| `middlewares` | `express.core` (after flat middleware) | HTTP boot |
|
|
76
|
+
| `jobs` | `cron.core` (after flat register) | Cron start |
|
|
77
|
+
| `queue` | `queue.core` (after flat register) | Queue start |
|
|
78
|
+
| `socket` | `socket.core` (after flat register) | Socket attach |
|
|
79
|
+
| `hooks` | `hooks.core` (after the flat app hook) | before/after/shutdown |
|
|
80
|
+
|
|
81
|
+
Folders (and modules) whose name starts with `_` are skipped. A module with no entry
|
|
82
|
+
file is skipped with a warning. The core layer never imports module code statically —
|
|
83
|
+
modules are app-land, discovered here at runtime, so the dependency arrow stays
|
|
84
|
+
one-directional (`app → core`).
|
|
85
|
+
|
|
86
|
+
## Methods
|
|
87
|
+
|
|
88
|
+
| Method | Returns | Notes |
|
|
89
|
+
| ---------------------- | ---------- | ------------------------------------------------------------------------ |
|
|
90
|
+
| `discover()` | `object[]` | cached `[{ name, dir, def }]` for every module |
|
|
91
|
+
| `dirs(kind)` | `string[]` | absolute, existing dirs for `'models' \| 'migrations' \| 'seeders'` |
|
|
92
|
+
| `requireRoutes()` | `void` | load each module's routes (dir → every `*.js`, file → that file) |
|
|
93
|
+
| `invoke(kind, args)` | `Promise` | call each module's `'middlewares' \| 'jobs' \| 'queue' \| 'socket'` hook |
|
|
94
|
+
| `runHooks(stage, ctx)` | `Promise` | run each module's `hooks[stage]` (`before` \| `after` \| `shutdown`) |
|
|
95
|
+
| `list()` | `string[]` | module names |
|
|
96
|
+
| `reset()` | `void` | clear the discovery cache (tooling/tests) |
|
package/template/docs/README.md
CHANGED
|
@@ -14,11 +14,16 @@ Per-core API documentation. For project setup, structure, and the boot flow, see
|
|
|
14
14
|
| Redis (optional) | [Redis.md](./Redis.md) |
|
|
15
15
|
| Hooks | [Hooks.md](./Hooks.md) |
|
|
16
16
|
| Express (HTTP) | [Express.md](./Express.md) |
|
|
17
|
+
| Routing | [Routing.md](./Routing.md) |
|
|
17
18
|
| Socket.IO | [Socket.md](./Socket.md) |
|
|
18
19
|
| Validator | [Validator.md](./Validator.md) |
|
|
19
20
|
| Cron | [Cron.md](./Cron.md) |
|
|
20
21
|
| Queue | [Queue.md](./Queue.md) |
|
|
21
22
|
| Database | [Database.md](./Database.md) |
|
|
23
|
+
| Migration | [Migration.md](./Migration.md) |
|
|
24
|
+
| Seeder | [Seeder.md](./Seeder.md) |
|
|
25
|
+
| Modules (optional) | [Modules.md](./Modules.md) |
|
|
26
|
+
| Scaffolding (`make:`) | [Make.md](./Make.md) |
|
|
22
27
|
| Common (utils) | [Common.md](./Common.md) |
|
|
23
28
|
| Register (app→core bridge) | [Register.md](./Register.md) |
|
|
24
29
|
| Bootstrap | [Bootstrap.md](./Bootstrap.md) |
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Routing API
|
|
2
|
+
|
|
3
|
+
`@core/routing.core` is the Laravel-style router (internalized in 3.0.0 — previously the
|
|
4
|
+
external `@refkinscallv/express-routing` package). Define routes against the static
|
|
5
|
+
`Routes` class; `express.core` calls `Routes.apply(app, router)` during boot.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
const Routes = require('@core/routing.core')
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Every handler and `handle()`-based middleware receives the context
|
|
12
|
+
`{ req, res, next, error }` (`error` is only populated in the error handler).
|
|
13
|
+
|
|
14
|
+
## Defining routes
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
Routes.group('api', () => {
|
|
18
|
+
Routes.get('status', ({ res }) => res.json({ status: true, code: 200, message: 'OK', data: null, meta: null }))
|
|
19
|
+
Routes.post('users', UserController.store, [Validator.make(createUserSchema)])
|
|
20
|
+
Routes.get('users/:id', UserController.show).whereNumber('id').name('users.show')
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Method | Notes |
|
|
25
|
+
| ------------------------------------------------------------- | -------------------------------------------------- |
|
|
26
|
+
| `get/post/put/delete/patch/options/head(path, handler, mws?)` | register one route; returns a chainable handle |
|
|
27
|
+
| `add(methods, path, handler, mws?)` | one or many methods at once |
|
|
28
|
+
| `group(prefix, callback, mws?)` | nest routes under a URL prefix + shared middleware |
|
|
29
|
+
| `redirect(from, to, status=302)` | redirect route |
|
|
30
|
+
| `view(path, view, data?)` | render via the Express view engine |
|
|
31
|
+
|
|
32
|
+
Handlers may be an inline `({ req, res }) => …`, a `[Controller, 'method']` tuple, or a
|
|
33
|
+
controller method reference (`UserController.show`).
|
|
34
|
+
|
|
35
|
+
## Chainable registration
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
Routes.get('users/:id', handler)
|
|
39
|
+
.name('users.show') // for Routes.url()/route()
|
|
40
|
+
.whereNumber('id') // constrain :id to [0-9]+
|
|
41
|
+
.where('slug', '[a-z-]+') // custom regex (string or RegExp)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`whereNumber`, `whereAlpha`, `whereAlphaNumeric`, `whereUuid` are shorthands. A request
|
|
45
|
+
whose param fails the constraint falls through (`next('route')`) so a later route can match.
|
|
46
|
+
|
|
47
|
+
## Middleware
|
|
48
|
+
|
|
49
|
+
Middleware can be a plain Express function, a class/object with
|
|
50
|
+
`handle({ req, res, next, error })`, or a registered **alias/group** name (string).
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
// Named aliases & groups (Laravel-style)
|
|
54
|
+
Routes.registerMiddleware('auth', AuthMiddleware)
|
|
55
|
+
Routes.registerMiddleware({ admin: AdminMiddleware, guest: GuestMiddleware })
|
|
56
|
+
Routes.middlewareGroup('web', [SessionMw, CsrfMw])
|
|
57
|
+
|
|
58
|
+
// Scoped — accepts plain functions OR handle() classes:
|
|
59
|
+
Routes.middleware([AuthMiddleware, logFn], () => {
|
|
60
|
+
Routes.get('me', UserController.me)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Chaining — STRICT: only handle() classes/objects/aliases (no plain functions):
|
|
64
|
+
Routes.middleware(['auth']).get('me', UserController.me)
|
|
65
|
+
Routes.middleware([AuthMiddleware]).group('admin', () => {
|
|
66
|
+
/* ... */
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Per-route middleware is the optional 3rd argument: `Routes.get(path, handler, [Mw])`.
|
|
71
|
+
|
|
72
|
+
## Controllers & resources
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
// Auto-register every public method of a controller (methods starting with "_" are private):
|
|
76
|
+
// index -> /base · samplePath -> /base/sample-path · post_create -> POST /base/create
|
|
77
|
+
Routes.controller('users', UserController, { index: [AuthMiddleware] })
|
|
78
|
+
|
|
79
|
+
// The seven RESTful routes (only actions the controller implements are mounted):
|
|
80
|
+
Routes.resource('photos', PhotoController) // index/create/store/show/edit/update/destroy
|
|
81
|
+
Routes.apiResource('photos', PhotoController) // same, minus create/edit (HTML forms)
|
|
82
|
+
Routes.resource('photos', PhotoController, { only: ['index', 'show'], parameter: 'photo', middleware: [AuthMiddleware] })
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Generate these with `make:controller --resource` / `make:resource` (see [Make.md](./Make.md)).
|
|
86
|
+
|
|
87
|
+
## Named routes & URL generation
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
Routes.get('users/:id', handler).name('users.show')
|
|
91
|
+
Routes.url('users.show', { id: 5 }) // → /users/5
|
|
92
|
+
Routes.route('users.show', { id: 5, tab: 'a' }) // → /users/5?tab=a (alias of url())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Missing required params throw; extra keys become query string; values are
|
|
96
|
+
`encodeURIComponent`-escaped.
|
|
97
|
+
|
|
98
|
+
## App-level handlers
|
|
99
|
+
|
|
100
|
+
| Method | Purpose |
|
|
101
|
+
| -------------------------------- | ------------------------------------------------------------------------ |
|
|
102
|
+
| `errorHandler(handler)` | global error handler — receives `{ req, res, next, error }` |
|
|
103
|
+
| `fallback(handler)` | runs when no route matched (before the framework 404) |
|
|
104
|
+
| `maintenance(enabled, handler?)` | when on, every request gets 503 before routes |
|
|
105
|
+
| `allRoutes()` | `[{ methods, path, name, middlewareCount, handlerType }]` for inspection |
|
|
106
|
+
|
|
107
|
+
## Applying routes
|
|
108
|
+
|
|
109
|
+
`express.core` already calls this — you rarely call it directly:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
await Routes.apply(app) // mount on app
|
|
113
|
+
await Routes.apply(app, router) // mount on a router, then app.use(router)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Types ship at `core/routing.core.d.ts` for editor IntelliSense.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Seeder API
|
|
2
|
+
|
|
3
|
+
`@core/seeder.core` runs data seeders. Unlike migrations, seeders are **not tracked**
|
|
4
|
+
and are meant to be re-runnable — write them idempotently (`findOrCreate` / `upsert`).
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
const Seeder = require('@core/seeder.core')
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Seeders live in `database/seeders/*.js` and/or any module's `seeders/` dir. Files run
|
|
11
|
+
in filename order (prefix with numbers to control sequence). Files starting with `_`
|
|
12
|
+
are ignored (base classes/helpers).
|
|
13
|
+
|
|
14
|
+
## Writing a seeder
|
|
15
|
+
|
|
16
|
+
Each seeder exports an async function receiving a context object:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
// database/seeders/article-seeder.js
|
|
20
|
+
module.exports = async ({ Database, sequelize, Sequelize, models }) => {
|
|
21
|
+
const Article = Database.model('Article')
|
|
22
|
+
for (const title of ['Hello World', 'Second Post']) {
|
|
23
|
+
await Article.findOrCreate({ where: { title }, defaults: { title } })
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Context key | What it is |
|
|
29
|
+
| ----------- | ------------------------------------------------- |
|
|
30
|
+
| `Database` | the database core (`Database.model('Name')`) |
|
|
31
|
+
| `sequelize` | the live Sequelize instance |
|
|
32
|
+
| `Sequelize` | the Sequelize constructor (DataTypes, `Op`, …) |
|
|
33
|
+
| `models` | a plain object of all loaded models keyed by name |
|
|
34
|
+
|
|
35
|
+
Generate one with `make:seed <Name>`.
|
|
36
|
+
|
|
37
|
+
## Methods
|
|
38
|
+
|
|
39
|
+
| Method | Returns | Notes |
|
|
40
|
+
| ------------ | ---------- | ---------------------------------------------------------------------------- |
|
|
41
|
+
| `seedAll()` | `string[]` | run every discovered seeder, in filename order |
|
|
42
|
+
| `seed(name)` | `string[]` | run one seeder; matches filename leniently (`UserSeeder` ↔ `user-seeder.js`) |
|
|
43
|
+
|
|
44
|
+
With no `name`, `seed()` runs `database/seeders/index.js` (a "DatabaseSeeder" entry that
|
|
45
|
+
can call others) when present, otherwise falls back to running all seeders.
|
|
46
|
+
|
|
47
|
+
## CLI
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm run cli -- db:seed # default seeder (index.js) or all
|
|
51
|
+
npm run cli -- db:seed --class=ArticleSeeder
|
|
52
|
+
npm run cli -- db:seed --all # every seeder (alias: seed-all)
|
|
53
|
+
npm run cli -- db:reset --seed # drop → migrate → seed
|
|
54
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const js = require('@eslint/js')
|
|
4
|
+
const globals = require('globals')
|
|
5
|
+
const security = require('eslint-plugin-security')
|
|
6
|
+
const n = require('eslint-plugin-n').default
|
|
7
|
+
|
|
8
|
+
// Flat config (ESLint 10). Correctness via @eslint/js recommended + node best practices
|
|
9
|
+
// (eslint-plugin-n), security smells via eslint-plugin-security. Prettier owns
|
|
10
|
+
// formatting, so no stylistic rules here.
|
|
11
|
+
module.exports = [
|
|
12
|
+
{
|
|
13
|
+
ignores: ['node_modules/**', 'coverage/**', 'logs/**', 'storage/**', 'public/**', 'create-rebe/template/**', '**/*.d.ts'],
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
js.configs.recommended,
|
|
17
|
+
n.configs['flat/recommended-script'],
|
|
18
|
+
security.configs.recommended,
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
languageOptions: {
|
|
22
|
+
ecmaVersion: 2023,
|
|
23
|
+
sourceType: 'commonjs',
|
|
24
|
+
globals: { ...globals.node },
|
|
25
|
+
},
|
|
26
|
+
rules: {
|
|
27
|
+
// Register/handler callbacks routinely receive facade args they may not use
|
|
28
|
+
// (e.g. (io, Socket) => {}); don't flag unused args, only unused vars/imports.
|
|
29
|
+
'no-unused-vars': ['warn', { args: 'none', varsIgnorePattern: '^_' }],
|
|
30
|
+
'no-extend-native': 'error',
|
|
31
|
+
|
|
32
|
+
// The framework is a loader/scaffolder by design: it requires modules and
|
|
33
|
+
// reads files from computed paths, and shells out from the setup CLI. These
|
|
34
|
+
// security rules fire on that intentional behavior, so they are disabled to
|
|
35
|
+
// keep the genuine findings (eval, unsafe regex, timing, weak crypto) signal-rich.
|
|
36
|
+
'security/detect-non-literal-require': 'off',
|
|
37
|
+
'security/detect-non-literal-fs-filename': 'off',
|
|
38
|
+
'security/detect-child-process': 'off',
|
|
39
|
+
'security/detect-object-injection': 'off',
|
|
40
|
+
|
|
41
|
+
// module-alias (@core/*, @config/*) is resolved at runtime, not by eslint-plugin-n.
|
|
42
|
+
'n/no-missing-require': 'off',
|
|
43
|
+
'n/no-unpublished-require': 'off',
|
|
44
|
+
'n/no-process-exit': 'off',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
files: ['tests/**/*.js'],
|
|
50
|
+
languageOptions: { globals: { ...globals.jest } },
|
|
51
|
+
},
|
|
52
|
+
]
|