maxserver 0.0.15 → 0.0.17
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/README.md +99 -82
- package/bin/init.js +3 -3
- package/package.json +1 -1
- package/src/getAddress.js +25 -24
- package/src/index.js +31 -27
- package/src/routeLoader.js +80 -72
- package/src/setup.js +8 -5
- package/templates/env +6 -22
- package/templates/server.js +4 -5
package/README.md
CHANGED
|
@@ -11,58 +11,63 @@
|
|
|
11
11
|
I am simplifying and improving things, that it will work for everyone plugn play.
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
Ready node server setup based on **Fastify** to
|
|
14
|
+
Ready node server setup based on **Fastify** to speed up backend development.
|
|
15
|
+
maxserver stands for **maximized simplicity** and **minimum boilerplate**.
|
|
15
16
|
|
|
16
17
|
- **Auto Routes**: auto imports and registers routes and schemas
|
|
17
18
|
- **Auto Docs**: auto generates docs based on schemas
|
|
18
|
-
- **Preconfigures
|
|
19
|
+
- **Preconfigures essentials**: jwt auth, cors, helmet
|
|
19
20
|
- **Auto Connect MongoDB** (optional)
|
|
20
21
|
- **Dev server**
|
|
21
22
|
<br><br>
|
|
22
23
|
|
|
23
|
-
- Dependencies: original fastify packages + scalar/fastify-api-reference (doc generator)
|
|
24
|
-
- The source is simple and short. Everyone shall be able to read, understand and modify if needed.
|
|
25
|
-
|
|
26
24
|
|
|
27
25
|
## Install
|
|
28
26
|
|
|
29
27
|
### Setup ready project
|
|
28
|
+
```js
|
|
30
29
|
npx maxserver [appname]
|
|
30
|
+
```
|
|
31
31
|
|
|
32
|
-
###
|
|
32
|
+
### Or install as packge
|
|
33
|
+
```js
|
|
33
34
|
npm install maxserver
|
|
35
|
+
```
|
|
36
|
+
<br>
|
|
34
37
|
|
|
35
38
|
## Setup
|
|
36
39
|
```js
|
|
37
40
|
import maxserver from "maxserver";
|
|
38
41
|
|
|
39
|
-
const server = await maxserver(
|
|
40
|
-
|
|
42
|
+
const server = await maxserver({
|
|
43
|
+
port: 3000,
|
|
44
|
+
secret: "your_secret"
|
|
45
|
+
});
|
|
41
46
|
|
|
42
|
-
|
|
47
|
+
await server.start();
|
|
43
48
|
export default server;
|
|
44
|
-
|
|
45
49
|
```
|
|
46
|
-
|
|
50
|
+
<br>
|
|
47
51
|
|
|
48
52
|
## ⚙️ Configure
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
Configs can be passed to the init call to **maxserver()** or set in your .env file.
|
|
54
|
+
If you define options in env, use all upper case letters.
|
|
55
|
+
Any fastify options can be passed to maxserver() too.
|
|
56
|
+
|
|
51
57
|
|
|
52
58
|
| Variable | Default | Description |
|
|
53
59
|
| :--- | :--- | :--- |
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
## 🗂️ Project Structure
|
|
60
|
+
| `port` | `3000` | Server port |
|
|
61
|
+
| `secret` | *-* | Secret used for jwt and cookies |
|
|
62
|
+
| `cors` | `*` | Allowed CORS origins, default all allowed |
|
|
63
|
+
| `docs` | `true` | Set `false` to disable auto generated docs` |
|
|
64
|
+
| `mongodb` | *-* | MongoDB URI, if set auto-connects db |
|
|
65
|
+
| `public` | `false` | Set `true` to expose the server publicly (binds to `0.0.0.0`) |
|
|
66
|
+
| `static` | *-* | If set, serves this directory statically |
|
|
67
|
+
<br><br>
|
|
64
68
|
|
|
65
|
-
|
|
69
|
+
## 🗂️ Project Structure
|
|
70
|
+
Our golden rule: **1 route = 1 handler file + 1 schema file**
|
|
66
71
|
|
|
67
72
|
Example:
|
|
68
73
|
|
|
@@ -75,12 +80,11 @@ src/
|
|
|
75
80
|
hello.schema.js
|
|
76
81
|
...
|
|
77
82
|
```
|
|
78
|
-
|
|
79
|
-
---
|
|
83
|
+
<br>
|
|
80
84
|
|
|
81
85
|
## 🛣️ Handlers
|
|
82
86
|
|
|
83
|
-
|
|
87
|
+
#### 1) Define method + path
|
|
84
88
|
Start each route file with a comment to define the path.
|
|
85
89
|
That comment is what the route loader uses to auto-register the route.
|
|
86
90
|
|
|
@@ -88,13 +92,9 @@ That comment is what the route loader uses to auto-register the route.
|
|
|
88
92
|
// GET /teams/:id
|
|
89
93
|
```
|
|
90
94
|
|
|
95
|
+
#### 2) Export default handler
|
|
91
96
|
|
|
92
97
|
|
|
93
|
-
### 2) Default export handler
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
### Example
|
|
97
|
-
|
|
98
98
|
```js
|
|
99
99
|
// GET /teams/:id
|
|
100
100
|
|
|
@@ -106,89 +106,90 @@ export default async function (req, res) {
|
|
|
106
106
|
}
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
If you don't want to autoregister some routes, then simply don't add that magic comment 😃
|
|
110
110
|
|
|
111
|
-
## 🧾 Define Schemas
|
|
112
|
-
Create a sibling file ending in **`.schema.js`**.
|
|
113
|
-
The schema is offcourse a jsonschema.
|
|
114
|
-
This file will be auto registered.
|
|
115
111
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
112
|
+
<br>
|
|
113
|
+
<br>
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
## 🧾 Schemas
|
|
117
|
+
Create a sibling file ending with **`.schema.js`**, so it will be auto registered.
|
|
118
|
+
For example: **hello.js** and **hello.schema.js**
|
|
119
|
+
|
|
120
|
+
Besides the basic validation fields we can set fields like summary and description,
|
|
121
|
+
which will appear in the docs. Mostly you don't need to write schemas yourself, chat gpt and gemini do it excelently.
|
|
120
122
|
|
|
121
123
|
|
|
122
|
-
**`Important - use default export`**
|
|
123
124
|
```js
|
|
124
125
|
export default {
|
|
125
|
-
tags: ["Teams"],
|
|
126
|
-
summary: "Get team",
|
|
127
|
-
description: "Returns a single team by identifier.",
|
|
128
126
|
|
|
129
|
-
|
|
127
|
+
tags: ["Test"],
|
|
128
|
+
summary: "Post hello",
|
|
129
|
+
description: "Accepts a name and returns a greeting.",
|
|
130
|
+
|
|
131
|
+
body: {
|
|
130
132
|
type: "object",
|
|
131
|
-
required: ["
|
|
133
|
+
required: ["name"],
|
|
132
134
|
properties: {
|
|
133
|
-
|
|
135
|
+
name: {
|
|
134
136
|
type: "string",
|
|
135
|
-
minLength: 24,
|
|
136
|
-
example: "",
|
|
137
137
|
},
|
|
138
138
|
},
|
|
139
139
|
},
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
200: {
|
|
143
|
-
type: "object",
|
|
144
|
-
properties: {
|
|
145
|
-
id: {
|
|
146
|
-
type: "string",
|
|
147
|
-
example: "507f1f77bcf86cd799439011",
|
|
148
|
-
},
|
|
149
|
-
name: { type: "string", example: "Team A" },
|
|
150
|
-
},
|
|
151
|
-
required: ["id", "name"],
|
|
152
|
-
},
|
|
153
|
-
},
|
|
141
|
+
...
|
|
154
142
|
};
|
|
155
143
|
```
|
|
156
144
|
|
|
157
|
-
|
|
145
|
+
**`‼️ Important use export default`**
|
|
158
146
|
|
|
147
|
+
<br>
|
|
159
148
|
|
|
160
|
-
##
|
|
149
|
+
## 🛠️ Route Options
|
|
150
|
+
Though we don't mostly register routes manually, we don't set route options on the register call.
|
|
151
|
+
If needed, you can wether register that route manually or just set them on the schema.
|
|
161
152
|
|
|
162
|
-
|
|
163
|
-
|
|
153
|
+
```js
|
|
154
|
+
// Inside schema
|
|
164
155
|
|
|
165
|
-
|
|
156
|
+
export default {
|
|
157
|
+
routeOptions: {
|
|
158
|
+
config: {
|
|
159
|
+
preHandler: ...
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
...
|
|
163
|
+
```
|
|
166
164
|
|
|
167
165
|
|
|
166
|
+
<br>
|
|
168
167
|
|
|
168
|
+
## 📚 API Docs
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
Open in your browser **`localhost:3000/docs`**
|
|
171
|
+
OpenAPI JSON: **`/openapi.json`**
|
|
171
172
|
|
|
172
|
-
|
|
173
|
+
<br>
|
|
173
174
|
|
|
174
|
-
- `JWT_SECRET`
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
176
|
+
## 🔐 Authentication
|
|
177
|
+
JWT header and cookie based auth is preconfigured.
|
|
178
|
+
To enable auth for a route set in it's schema **auth = true**
|
|
179
|
+
The authenticated user is available as **`req.userId`**
|
|
180
180
|
|
|
181
181
|
```js
|
|
182
|
+
// Inside schema
|
|
183
|
+
|
|
182
184
|
export default {
|
|
183
|
-
|
|
184
|
-
// ...
|
|
185
|
+
auth: true
|
|
185
186
|
};
|
|
186
187
|
```
|
|
187
188
|
|
|
188
189
|
<br>
|
|
189
190
|
|
|
190
191
|
## 🍃 MongoDB
|
|
191
|
-
|
|
192
|
+
Set option **`MONGODB`** your mongodbURI and it will auto-connect at server start and you get:
|
|
192
193
|
|
|
193
194
|
- global **`db`** (connected database handle)
|
|
194
195
|
- global **`oid(string)`** (string → MongoDB `ObjectId`)
|
|
@@ -198,8 +199,19 @@ Define in the env **`MONGODB_URI`** and it will auto-connect at server start and
|
|
|
198
199
|
| `db` | MongoDB database handle | Use it directly in handlers |
|
|
199
200
|
| `oid(id)` | string → `ObjectId` | Avoid importing `ObjectId` everywhere |
|
|
200
201
|
|
|
202
|
+
### Example
|
|
201
203
|
|
|
202
|
-
|
|
204
|
+
```js
|
|
205
|
+
// Inside route handlers
|
|
206
|
+
|
|
207
|
+
export default async function (req, res) {
|
|
208
|
+
|
|
209
|
+
await db.feedback.insert(...)
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
<br>
|
|
203
215
|
|
|
204
216
|
## 🧰 Error Handling
|
|
205
217
|
|
|
@@ -211,9 +223,13 @@ if (!user) throw createError(404, "User not found");
|
|
|
211
223
|
|
|
212
224
|
Rule of thumb: make the message something you would want to see at 03:00 in logs.
|
|
213
225
|
|
|
214
|
-
|
|
226
|
+
<br>
|
|
215
227
|
|
|
228
|
+
## About
|
|
229
|
+
- Dependencies: original fastify packages + scalar/fastify-api-reference
|
|
230
|
+
- The source is simple. Everyone can read, understand and modify if needed.
|
|
216
231
|
|
|
232
|
+
<br>
|
|
217
233
|
|
|
218
234
|
## 🛠️ Tips & Tools
|
|
219
235
|
|
|
@@ -230,5 +246,6 @@ In `.vscode/tasks.json`, enable the task with:
|
|
|
230
246
|
"runOptions": { "runOn": "folderOpen" }
|
|
231
247
|
```
|
|
232
248
|
|
|
233
|
-
### 🤖 AI Assistants
|
|
234
|
-
Copy **`RULES.md`** into your AI tool as system context,
|
|
249
|
+
### 🤖 AI Assistants
|
|
250
|
+
Copy **`RULES.md`** into your AI tool as system context,
|
|
251
|
+
then ask it to generate routes + schemas.
|
package/bin/init.js
CHANGED
|
@@ -15,17 +15,17 @@ function main() {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
try {
|
|
18
|
-
console.log(`🚀
|
|
18
|
+
console.log(`🚀 Setting up "${projectName}"...`);
|
|
19
19
|
fs.cpSync(templateDir, targetDir, { recursive: true });
|
|
20
20
|
|
|
21
21
|
fixDotfiles(targetDir);
|
|
22
22
|
patchPackageJson(targetDir, projectName);
|
|
23
23
|
|
|
24
24
|
process.chdir(targetDir);
|
|
25
|
-
console.log("📦 Installing maxserver
|
|
25
|
+
console.log("📦 Installing maxserver");
|
|
26
26
|
execSync("npm install maxserver@latest", { stdio: "inherit" });
|
|
27
27
|
|
|
28
|
-
console.log(
|
|
28
|
+
console.log(`\n✅ Install complete\n\ncd ${projectName}\nnpm run dev\n`);
|
|
29
29
|
} catch (err) {
|
|
30
30
|
console.error("❌ Init failed:", err.message);
|
|
31
31
|
process.exit(1);
|
package/package.json
CHANGED
package/src/getAddress.js
CHANGED
|
@@ -1,29 +1,5 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
|
|
3
|
-
export function setupGetAddress(app) {
|
|
4
|
-
app.decorate("getAddress", () => {
|
|
5
|
-
const addr = app.server?.address();
|
|
6
|
-
const protocol = app.initialConfig?.https ? "https" : "http";
|
|
7
|
-
|
|
8
|
-
if (!addr) return null;
|
|
9
|
-
if (typeof addr === "string") return addr;
|
|
10
|
-
|
|
11
|
-
const isPublicBind = addr.address === "0.0.0.0" || addr.address === "::";
|
|
12
|
-
const isLoopback = addr.address === "127.0.0.1" || addr.address === "::1";
|
|
13
|
-
|
|
14
|
-
const envIp = String(process.env.PUBLIC_IP || "").trim() || null;
|
|
15
|
-
const detectedIp = envIp || getLanIp();
|
|
16
|
-
|
|
17
|
-
const ip = (isPublicBind && !isLoopback)
|
|
18
|
-
? (detectedIp || addr.address)
|
|
19
|
-
: "localhost";
|
|
20
|
-
|
|
21
|
-
const host = ip.includes(":") ? `[${ip}]` : ip;
|
|
22
|
-
|
|
23
|
-
return `${protocol}://${host}:${addr.port}`;
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
3
|
function getLanIp() {
|
|
28
4
|
const nets = os.networkInterfaces();
|
|
29
5
|
|
|
@@ -35,3 +11,28 @@ function getLanIp() {
|
|
|
35
11
|
|
|
36
12
|
return null;
|
|
37
13
|
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export function getAddress(app) {
|
|
17
|
+
|
|
18
|
+
const addr = app.server?.address();
|
|
19
|
+
const protocol = app.initialConfig?.https ? "https" : "http";
|
|
20
|
+
|
|
21
|
+
if (!addr) return null;
|
|
22
|
+
if (typeof addr === "string") return addr;
|
|
23
|
+
|
|
24
|
+
const isPublicBind = addr.address === "0.0.0.0" || addr.address === "::";
|
|
25
|
+
const isLoopback = addr.address === "127.0.0.1" || addr.address === "::1";
|
|
26
|
+
|
|
27
|
+
const envIp = String(process.env.PUBLIC_IP || "").trim() || null;
|
|
28
|
+
const detectedIp = envIp || getLanIp();
|
|
29
|
+
|
|
30
|
+
const ip = (isPublicBind && !isLoopback)
|
|
31
|
+
? (detectedIp || addr.address)
|
|
32
|
+
: "localhost";
|
|
33
|
+
|
|
34
|
+
const host = ip.includes(":") ? `[${ip}]` : ip;
|
|
35
|
+
|
|
36
|
+
return `${protocol}://${host}:${addr.port}`;
|
|
37
|
+
}
|
|
38
|
+
|
package/src/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
getHttpsOptions,
|
|
13
13
|
} from "./setup.js";
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import { getAddress } from "./getAddress.js";
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
export default async function maxserver(config = {}) {
|
|
@@ -20,47 +20,53 @@ export default async function maxserver(config = {}) {
|
|
|
20
20
|
const {
|
|
21
21
|
|
|
22
22
|
// maxserver options
|
|
23
|
+
port = Number(process.env.PORT || 3000),
|
|
23
24
|
secret = process.env.SECRET,
|
|
24
|
-
|
|
25
|
-
mongodbUri = process.env.MONGODB_URI,
|
|
25
|
+
mongodb = process.env.MONGODB,
|
|
26
26
|
docs = process.env.DOCS !== "false",
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
cors = process.env.CORS || "*",
|
|
28
|
+
env = process.env.NODE_ENV || "development",
|
|
29
|
+
openapiInfo,
|
|
30
|
+
static: isStatic = process.env.STATIC,
|
|
31
|
+
public: isPublic = process.env.PUBLIC === "true",
|
|
29
32
|
|
|
30
33
|
// everything else goes straight to Fastify
|
|
31
34
|
...fastifyOpts
|
|
32
35
|
|
|
33
36
|
} = config;
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
let maxserverConfig = {
|
|
41
|
-
secret, mongodbUri, docs, staticDir, corsOrigin
|
|
38
|
+
const maxserverConfig = {
|
|
39
|
+
port, secret, mongodb, docs, cors,
|
|
40
|
+
static: isStatic,
|
|
41
|
+
public: isPublic
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
process.env.NODE_ENV;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
44
|
|
|
49
|
-
|
|
45
|
+
let app;
|
|
46
|
+
try {
|
|
47
|
+
app = Fastify({
|
|
48
|
+
https: getHttpsOptions() || undefined,
|
|
49
|
+
trustProxy: true,
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// Required to allow adding doc fields on schema
|
|
52
|
+
ajv: { customOptions: { strictSchema: false } },
|
|
53
|
+
...fastifyOpts
|
|
54
|
+
});
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error("❌ Fastify initialization failed:", err);
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
53
59
|
|
|
54
|
-
// To allow writing example value fields to schemas for doucumentation
|
|
55
|
-
ajv: { customOptions: { strictSchema: false } },
|
|
56
|
-
...fastifyOpts,
|
|
57
|
-
});
|
|
58
60
|
|
|
59
61
|
app.decorate("maxserver", maxserverConfig);
|
|
60
62
|
|
|
63
|
+
app.decorate("start", async function () {
|
|
64
|
+
const port = this.maxserver.port ?? 3000;
|
|
65
|
+
const host = this.maxserver.public ? '0.0.0.0' : '127.0.0.1';
|
|
66
|
+
await this.listen({ port, host });
|
|
67
|
+
console.log('Server running at ', getAddress(this));
|
|
68
|
+
});
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
setupGetAddress(app);
|
|
64
70
|
await setupCookie(app);
|
|
65
71
|
await setupHelmet(app);
|
|
66
72
|
await setupCors(app);
|
|
@@ -70,13 +76,11 @@ export default async function maxserver(config = {}) {
|
|
|
70
76
|
await setupDocs(app);
|
|
71
77
|
await setupRoutes(app);
|
|
72
78
|
|
|
73
|
-
|
|
74
79
|
global.createError = function (code, message) {
|
|
75
80
|
const err = new Error(message);
|
|
76
81
|
err.statusCode = code;
|
|
77
82
|
return err;
|
|
78
83
|
};
|
|
79
84
|
|
|
80
|
-
|
|
81
85
|
return app;
|
|
82
86
|
}
|
package/src/routeLoader.js
CHANGED
|
@@ -1,89 +1,91 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 🚀 AUTO-LOADER
|
|
3
|
+
* Scans src/ for files with "// METHOD /url" comments.
|
|
4
|
+
* Automatically registers them as Fastify routes.
|
|
5
|
+
*/
|
|
4
6
|
|
|
5
7
|
import fs from "fs";
|
|
6
8
|
import path from "path";
|
|
9
|
+
import { pathToFileURL } from "url";
|
|
7
10
|
|
|
8
|
-
const ROUTE_OPTION_KEYS = new Set([
|
|
9
|
-
"config",
|
|
10
|
-
"preHandler",
|
|
11
|
-
"onRequest",
|
|
12
|
-
"preValidation",
|
|
13
|
-
"preSerialization",
|
|
14
|
-
"errorHandler",
|
|
15
|
-
"logLevel",
|
|
16
|
-
"bodyLimit",
|
|
17
|
-
"attachValidation",
|
|
18
|
-
"exposeHeadRoute",
|
|
19
|
-
"constraints",
|
|
20
|
-
"timeout",
|
|
21
|
-
"websocket",
|
|
22
|
-
"prefixTrailingSlash",
|
|
23
|
-
]);
|
|
24
11
|
|
|
12
|
+
// Matches lines like: // GET /api/v1/users
|
|
13
|
+
const ROUTE_REGEX = /^\/\/\s*(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/gm;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Recursively finds all .js files in a directory.
|
|
18
|
+
* Skips node_modules and dotfiles.
|
|
19
|
+
*/
|
|
25
20
|
function walk(dir, out = []) {
|
|
26
21
|
if (!fs.existsSync(dir)) return out;
|
|
27
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
28
22
|
|
|
29
|
-
for (const
|
|
30
|
-
if (
|
|
31
|
-
if (
|
|
32
|
-
const full = path.join(dir, entry.name);
|
|
23
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
24
|
+
if (e.name === "node_modules") continue;
|
|
25
|
+
if (e.name.startsWith(".")) continue;
|
|
33
26
|
|
|
34
|
-
|
|
27
|
+
const full = path.join(dir, e.name);
|
|
28
|
+
if (e.isDirectory()) {
|
|
35
29
|
walk(full, out);
|
|
36
|
-
|
|
37
|
-
out.push(full);
|
|
30
|
+
continue;
|
|
38
31
|
}
|
|
32
|
+
|
|
33
|
+
if (!e.name.endsWith(".js")) continue;
|
|
34
|
+
out.push(full);
|
|
39
35
|
}
|
|
36
|
+
|
|
40
37
|
return out;
|
|
41
38
|
}
|
|
42
39
|
|
|
43
|
-
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extracts method and URL from the file's "magic comment".
|
|
43
|
+
* Enforces strict "One Route Per File" policy.
|
|
44
|
+
*/
|
|
45
|
+
function getRoute(file) {
|
|
44
46
|
const text = fs.readFileSync(file, "utf8");
|
|
45
|
-
const
|
|
46
|
-
const firstContent = lines.find((line) => line.trim().length > 0);
|
|
47
|
+
const matches = [...text.matchAll(ROUTE_REGEX)];
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
}
|
|
49
|
+
if (matches.length === 0) return null;
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// Warn if user accidentally defines multiple routes in one file
|
|
52
|
+
if (matches.length > 1) {
|
|
53
|
+
console.warn(
|
|
54
|
+
`⚠️ Ignored "${file}": Found ${matches.length} route comments. ` +
|
|
55
|
+
`Only 1 allowed per file.`
|
|
56
|
+
);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
52
59
|
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
const m = matches[0];
|
|
61
|
+
const method = m[1].toLowerCase();
|
|
62
|
+
// Normalize URL: Remove all leading slashes, then add exactly one
|
|
63
|
+
const url = "/" + m[2].trim().replace(/^\/+/, "");
|
|
57
64
|
|
|
58
|
-
return { method
|
|
65
|
+
return { method, url };
|
|
59
66
|
}
|
|
60
67
|
|
|
61
|
-
function splitSchemaExport(raw) {
|
|
62
|
-
const routeOptions = {};
|
|
63
|
-
const schema = {};
|
|
64
|
-
|
|
65
|
-
for (const [key, value] of Object.entries(raw || {})) {
|
|
66
|
-
if (ROUTE_OPTION_KEYS.has(key)) {
|
|
67
|
-
routeOptions[key] = value;
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
schema[key] = value;
|
|
71
|
-
}
|
|
72
68
|
|
|
73
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Safe dynamic import that handles Windows paths correctly.
|
|
71
|
+
*/
|
|
72
|
+
async function importDefault(file) {
|
|
73
|
+
return (await import(pathToFileURL(file).href)).default;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
|
|
76
77
|
export async function loadRoutes(fastify) {
|
|
77
|
-
const ROOT = path.join(process.cwd(), "src");
|
|
78
|
-
const files = walk(ROOT);
|
|
79
78
|
const seen = new Map();
|
|
79
|
+
const root = path.join(process.cwd(), "src");
|
|
80
80
|
|
|
81
|
-
for (const file of
|
|
81
|
+
for (const file of walk(root)) {
|
|
82
|
+
// Skip schema files; they are loaded alongside their route file
|
|
82
83
|
if (file.endsWith(".schema.js")) continue;
|
|
83
84
|
|
|
84
|
-
const info =
|
|
85
|
+
const info = getRoute(file);
|
|
85
86
|
if (!info) continue;
|
|
86
87
|
|
|
88
|
+
// 🛡️ Collision Detection: Ensure no two files claim the same route
|
|
87
89
|
const key = info.method + " " + info.url;
|
|
88
90
|
if (seen.has(key)) {
|
|
89
91
|
throw new Error(
|
|
@@ -94,29 +96,35 @@ export async function loadRoutes(fastify) {
|
|
|
94
96
|
}
|
|
95
97
|
seen.set(key, file);
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
const handler =
|
|
99
|
+
// Import the route handler
|
|
100
|
+
const handler = await importDefault(file);
|
|
101
|
+
if (typeof handler !== "function") {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Route "${key}" in "${file}" must export a default function.`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
99
106
|
|
|
107
|
+
// 🤝 Schema Loading: Look for sibling .schema.js file
|
|
100
108
|
const schemaFile = file.replace(/\.js$/, ".schema.js");
|
|
101
|
-
let raw =
|
|
109
|
+
let raw = {};
|
|
102
110
|
|
|
103
111
|
if (fs.existsSync(schemaFile)) {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
fastify.log.warn(
|
|
108
|
-
`Route schema missing: ` +
|
|
109
|
-
`${info.method.toUpperCase()} ${info.url}`
|
|
110
|
-
);
|
|
111
|
-
raw = {};
|
|
112
|
+
const loaded = await importDefault(schemaFile);
|
|
113
|
+
// 🛡️ Guard: Ensure export is a valid object before using it
|
|
114
|
+
if (loaded && typeof loaded === "object") raw = loaded;
|
|
112
115
|
}
|
|
113
116
|
|
|
114
|
-
|
|
117
|
+
// ✨ Magic: Extract 'auth' and 'routeOptions' specifically
|
|
118
|
+
let { auth, routeOptions = {}, ...schema } = raw;
|
|
115
119
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
// Inject 'auth' into config if present (Syntactic Sugar)
|
|
121
|
+
if (auth !== undefined) {
|
|
122
|
+
routeOptions = {
|
|
123
|
+
...routeOptions,
|
|
124
|
+
config: { ...(routeOptions.config || {}), auth: !!auth },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fastify[info.method](info.url, { ...routeOptions, schema }, handler);
|
|
121
129
|
}
|
|
122
|
-
}
|
|
130
|
+
}
|
package/src/setup.js
CHANGED
|
@@ -27,18 +27,21 @@ export async function setupHelmet(app) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
|
|
31
|
+
|
|
30
32
|
export async function setupCors(app) {
|
|
31
33
|
const isProd = process.env.NODE_ENV === "production";
|
|
32
|
-
const origin = app.maxserver.
|
|
34
|
+
const origin = app.maxserver.cors ?? "*";
|
|
33
35
|
|
|
34
|
-
if (isProd && origin ===
|
|
35
|
-
app.log.warn("CORS
|
|
36
|
+
if (isProd && origin === "*") {
|
|
37
|
+
app.log.warn("CORS: allowing all origins (*) in production");
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
await app.register(cors, { origin });
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
|
|
44
|
+
|
|
42
45
|
export function getHttpsOptions() {
|
|
43
46
|
const { TLS_KEY, TLS_CERT } = process.env;
|
|
44
47
|
if (!TLS_KEY || !TLS_CERT) return null;
|
|
@@ -91,7 +94,7 @@ export async function setupJwt(app) {
|
|
|
91
94
|
|
|
92
95
|
|
|
93
96
|
export async function setupMongo(app) {
|
|
94
|
-
const url = app.maxserver.
|
|
97
|
+
const url = app.maxserver.mongodb;
|
|
95
98
|
if (!url) return;
|
|
96
99
|
await app.register(mongodb, { url });
|
|
97
100
|
|
|
@@ -139,7 +142,7 @@ export async function setupDocs(app) {
|
|
|
139
142
|
|
|
140
143
|
|
|
141
144
|
export async function setupStatic(app) {
|
|
142
|
-
const dir = app.maxserver.
|
|
145
|
+
const dir = app.maxserver.static;
|
|
143
146
|
if (!dir) return;
|
|
144
147
|
|
|
145
148
|
await app.register(fastifyStatic, {
|
package/templates/env
CHANGED
|
@@ -1,24 +1,8 @@
|
|
|
1
|
-
# Environment
|
|
2
|
-
NODE_ENV = development
|
|
3
|
-
|
|
4
|
-
# Server
|
|
5
|
-
PORT = 3000
|
|
6
|
-
|
|
7
|
-
# CORS(prod only, ignored in dev)
|
|
8
|
-
# CORS_ORIGIN = https://app.example.com
|
|
9
|
-
|
|
10
|
-
# JWT;
|
|
11
|
-
# JWT_SECRET = supersecretkey
|
|
12
1
|
|
|
13
|
-
|
|
14
|
-
# MONGODB_URI = mongodb://127.0.0.1:27017/testdb
|
|
15
|
-
|
|
16
|
-
# Static files
|
|
17
|
-
# STATIC_DIR =./ public
|
|
18
|
-
|
|
19
|
-
# API Docs in production
|
|
20
|
-
DOCS = 1
|
|
2
|
+
NODE_ENV = development
|
|
21
3
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
4
|
+
# PORT
|
|
5
|
+
# CORS
|
|
6
|
+
# SECRET
|
|
7
|
+
# MONGODB
|
|
8
|
+
# STATIC
|
package/templates/server.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import maxserver from "maxserver";
|
|
2
2
|
|
|
3
|
-
const server = await maxserver(
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
port: Number(process.env.PORT || 3000),
|
|
3
|
+
const server = await maxserver({
|
|
4
|
+
port: 3000,
|
|
5
|
+
secret: "your_secret"
|
|
7
6
|
});
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
await server.start();
|
|
10
9
|
export default server;
|