maxserver 0.0.16 โ 0.0.18
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 +38 -22
- package/package.json +1 -1
- package/src/index.js +1 -0
- package/src/routeLoader.js +80 -72
package/README.md
CHANGED
|
@@ -64,7 +64,9 @@ Any fastify options can be passed to maxserver() too.
|
|
|
64
64
|
| `mongodb` | *-* | MongoDB URI, if set auto-connects db |
|
|
65
65
|
| `public` | `false` | Set `true` to expose the server publicly (binds to `0.0.0.0`) |
|
|
66
66
|
| `static` | *-* | If set, serves this directory statically |
|
|
67
|
-
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
<br>
|
|
68
70
|
|
|
69
71
|
## ๐๏ธ Project Structure
|
|
70
72
|
Our golden rule: **1 route = 1 handler file + 1 schema file**
|
|
@@ -82,21 +84,16 @@ src/
|
|
|
82
84
|
```
|
|
83
85
|
<br>
|
|
84
86
|
|
|
85
|
-
##
|
|
87
|
+
## ๐ค Auto Routing
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
To auto-register routes, simple add a comment of the form:
|
|
90
|
+
**// METHOD /path**
|
|
91
|
+
**// GET /user**
|
|
92
|
+
**// POST /feedback/something**
|
|
93
|
+
...
|
|
90
94
|
|
|
91
95
|
```js
|
|
92
|
-
// GET /
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
#### 2) Export default handler
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
```js
|
|
99
|
-
// GET /teams/:id
|
|
96
|
+
// GET /example/:id
|
|
100
97
|
|
|
101
98
|
export default async function (req, res) {
|
|
102
99
|
|
|
@@ -105,17 +102,23 @@ export default async function (req, res) {
|
|
|
105
102
|
return team;
|
|
106
103
|
}
|
|
107
104
|
```
|
|
105
|
+
<br>
|
|
106
|
+
|
|
107
|
+
And remember to use default export for your handler.
|
|
108
|
+
If you don't want to autoregister some routes, then simply don't add that magic comment ๐
|
|
109
|
+
That's it.
|
|
110
|
+
|
|
108
111
|
|
|
109
112
|
<br>
|
|
110
113
|
<br>
|
|
111
114
|
|
|
112
115
|
|
|
113
116
|
## ๐งพ Schemas
|
|
114
|
-
Create a sibling file ending with **`.schema.js`**, so it will be auto registered.
|
|
115
|
-
|
|
117
|
+
Create a sibling file ending with **`.schema.js`**, so it will be auto registered. For example: **hello.js** and **hello.schema.js**
|
|
118
|
+
|
|
119
|
+
Besides the basic validation fields we can set fields like summary and description, which will appear in the docs. Mostly you don't need to write schemas yourself, chat gpt and gemini do it excelently.
|
|
120
|
+
|
|
116
121
|
|
|
117
|
-
Besides the basic validation fields we can set fields like summary and description,
|
|
118
|
-
which will appear in the docs. Mostly you don't need to write schemas yourself, chat gpt and gemini do it excelently.
|
|
119
122
|
|
|
120
123
|
|
|
121
124
|
```js
|
|
@@ -139,14 +142,27 @@ export default {
|
|
|
139
142
|
};
|
|
140
143
|
```
|
|
141
144
|
|
|
142
|
-
|
|
145
|
+
**โผ๏ธ Important use export default
|
|
146
|
+
**
|
|
143
147
|
|
|
144
148
|
<br>
|
|
145
149
|
|
|
146
|
-
## Route Options
|
|
147
|
-
Though we don't register routes manually, we don't set route options on the register call.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
## ๐ ๏ธ Route Options
|
|
151
|
+
Though we don't mostly register routes manually, we don't set route options on the register call.
|
|
152
|
+
If needed, you can wether register that route manually or just set them on the schema.
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
// Inside schema
|
|
156
|
+
|
|
157
|
+
export default {
|
|
158
|
+
routeOptions: {
|
|
159
|
+
config: {
|
|
160
|
+
preHandler: ...
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
...
|
|
164
|
+
```
|
|
165
|
+
|
|
150
166
|
|
|
151
167
|
<br>
|
|
152
168
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -25,6 +25,7 @@ export default async function maxserver(config = {}) {
|
|
|
25
25
|
mongodb = process.env.MONGODB,
|
|
26
26
|
docs = process.env.DOCS !== "false",
|
|
27
27
|
cors = process.env.CORS || "*",
|
|
28
|
+
env = process.env.NODE_ENV || "development",
|
|
28
29
|
openapiInfo,
|
|
29
30
|
static: isStatic = process.env.STATIC,
|
|
30
31
|
public: isPublic = process.env.PUBLIC === "true",
|
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
|
+
}
|