kaelum 1.2.0 → 1.3.1
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 +158 -61
- package/cli/create.js +71 -31
- package/cli/index.js +49 -7
- package/cli/templates/api/app.js +16 -2
- package/cli/templates/api/controllers/usersController.js +58 -0
- package/cli/templates/api/middlewares/authMock.js +13 -0
- package/cli/templates/api/package.json +14 -7
- package/cli/templates/api/routes.js +31 -8
- package/cli/templates/web/app.js +15 -4
- package/cli/templates/web/middlewares/logger.js +5 -5
- package/cli/templates/web/package.json +17 -7
- package/cli/templates/web/public/style.css +42 -35
- package/cli/templates/web/routes.js +22 -17
- package/cli/templates/web/views/index.html +39 -19
- package/cli/utils.js +50 -4
- package/core/addRoute.js +156 -5
- package/core/apiRoute.js +135 -0
- package/core/errorHandler.js +139 -0
- package/core/healthCheck.js +204 -0
- package/core/redirect.js +177 -0
- package/core/setConfig.js +245 -20
- package/core/setMiddleware.js +183 -6
- package/core/start.js +111 -4
- package/createApp.js +166 -13
- package/package.json +4 -3
- package/bin/.gitkeep +0 -0
- package/cli/templates/api/controllers/userController.js +0 -13
- package/cli/templates/api/middlewares/logger.js +0 -6
- package/utils/.gitkeep +0 -0
package/cli/templates/web/app.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
|
+
// app.js - example project generated by Kaelum (Web template)
|
|
1
2
|
const kaelum = require("kaelum");
|
|
3
|
+
|
|
2
4
|
const app = kaelum();
|
|
3
5
|
|
|
4
|
-
//
|
|
6
|
+
// Enable basic security + static serving via setConfig (uses Kaelum internals)
|
|
5
7
|
app.setConfig({
|
|
6
8
|
cors: true,
|
|
7
9
|
helmet: true,
|
|
10
|
+
static: "public", // will serve ./public
|
|
11
|
+
bodyParser: true, // default enabled — explicit for clarity
|
|
8
12
|
});
|
|
9
13
|
|
|
10
|
-
//
|
|
14
|
+
// Register routes (routes.js uses Kaelum helpers)
|
|
11
15
|
const routes = require("./routes");
|
|
12
16
|
routes(app);
|
|
13
17
|
|
|
14
|
-
//
|
|
15
|
-
app.
|
|
18
|
+
// optional: health check endpoint
|
|
19
|
+
app.healthCheck("/health");
|
|
20
|
+
|
|
21
|
+
// install Kaelum default error handler (returns JSON on errors)
|
|
22
|
+
app.useErrorHandler({ exposeStack: false });
|
|
23
|
+
|
|
24
|
+
// Start server (explicit port for template demo)
|
|
25
|
+
const PORT = process.env.PORT || 3000;
|
|
26
|
+
app.start(PORT);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// example middleware: simple request logger
|
|
2
|
+
module.exports = function (req, res, next) {
|
|
3
|
+
const now = new Date();
|
|
4
|
+
console.log(`[${now.toISOString()}] ${req.method} ${req.originalUrl}`);
|
|
3
5
|
next();
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
module.exports = logger;
|
|
6
|
+
};
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "app.js",
|
|
2
|
+
"name": "kaelum-web-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Exemplo de app gerado pela CLI Kaelum (web template)",
|
|
6
5
|
"scripts": {
|
|
7
|
-
"start": "node app.js"
|
|
6
|
+
"start": "node app.js",
|
|
7
|
+
"dev": "nodemon app.js"
|
|
8
8
|
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"kaelum",
|
|
11
|
+
"web"
|
|
12
|
+
],
|
|
13
|
+
"author": "",
|
|
14
|
+
"license": "MIT",
|
|
9
15
|
"dependencies": {
|
|
10
|
-
"kaelum": "^1.
|
|
16
|
+
"kaelum": "^1.3.0",
|
|
17
|
+
"cors": "^2.8.5",
|
|
18
|
+
"helmet": "^6.0.0"
|
|
11
19
|
},
|
|
12
|
-
"
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"nodemon": "^2.0.22"
|
|
22
|
+
}
|
|
13
23
|
}
|
|
@@ -1,54 +1,61 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #f6fbff;
|
|
3
|
+
--card: #ffffff;
|
|
4
|
+
--accent: #6fa8dc;
|
|
5
|
+
--text: #243746;
|
|
6
|
+
--muted: #6b7a86;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
* {
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
}
|
|
1
12
|
body {
|
|
2
13
|
margin: 0;
|
|
3
|
-
font-family: "Segoe UI",
|
|
4
|
-
|
|
5
|
-
|
|
14
|
+
font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto,
|
|
15
|
+
"Helvetica Neue", Arial;
|
|
16
|
+
background: var(--bg);
|
|
17
|
+
color: var(--text);
|
|
18
|
+
-webkit-font-smoothing: antialiased;
|
|
19
|
+
-moz-osx-font-smoothing: grayscale;
|
|
6
20
|
}
|
|
7
21
|
|
|
8
22
|
.container {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
text-align: center;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
h1 {
|
|
16
|
-
font-size: 2.5rem;
|
|
17
|
-
margin-bottom: 10px;
|
|
18
|
-
color: #4a90e2;
|
|
23
|
+
max-width: 900px;
|
|
24
|
+
margin: 48px auto;
|
|
25
|
+
padding: 24px;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
header h1 {
|
|
29
|
+
margin: 0;
|
|
30
|
+
color: var(--accent);
|
|
31
|
+
font-size: 2.1rem;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
|
-
.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
border-radius: 8px;
|
|
30
|
-
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
|
|
34
|
+
.subtitle {
|
|
35
|
+
margin: 6px 0 18px 0;
|
|
36
|
+
color: var(--muted);
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
padding: 0;
|
|
39
|
+
.content {
|
|
40
|
+
margin-top: 18px;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
.cards {
|
|
44
|
+
display: flex;
|
|
45
|
+
gap: 16px;
|
|
46
|
+
margin-top: 12px;
|
|
41
47
|
}
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
background
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
.card {
|
|
50
|
+
background: var(--card);
|
|
51
|
+
border-radius: 8px;
|
|
52
|
+
padding: 16px;
|
|
53
|
+
box-shadow: 0 6px 18px rgba(37, 57, 99, 0.06);
|
|
54
|
+
flex: 1;
|
|
48
55
|
}
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
margin-top:
|
|
57
|
+
.small {
|
|
58
|
+
margin-top: 20px;
|
|
59
|
+
color: var(--muted);
|
|
52
60
|
font-size: 0.9rem;
|
|
53
|
-
color: #999;
|
|
54
61
|
}
|
|
@@ -1,27 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
// routes.js - example route declarations using Kaelum API
|
|
2
|
+
const path = require("path");
|
|
2
3
|
|
|
3
|
-
function
|
|
4
|
+
module.exports = function (app) {
|
|
5
|
+
// Home: serve the index.html from /views
|
|
4
6
|
app.addRoute("/", {
|
|
5
7
|
get: (req, res) => {
|
|
6
|
-
|
|
8
|
+
// send the static HTML file from the views folder
|
|
9
|
+
res.sendFile(path.join(process.cwd(), "views", "index.html"));
|
|
7
10
|
},
|
|
8
|
-
post: (req, res) => res.send("POST: Dados recebidos na página inicial."),
|
|
9
11
|
});
|
|
10
12
|
|
|
13
|
+
// About page - simple text
|
|
11
14
|
app.addRoute("/about", {
|
|
12
|
-
get: (req, res) =>
|
|
15
|
+
get: (req, res) => {
|
|
16
|
+
res.send("About Kaelum — a minimal framework scaffolded by the CLI.");
|
|
17
|
+
},
|
|
13
18
|
});
|
|
14
19
|
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
(req, res) => {
|
|
20
|
-
res.send("GET: Área segura! Middleware foi executado.");
|
|
21
|
-
},
|
|
22
|
-
],
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
}
|
|
20
|
+
// Example route using per-path middleware (logger)
|
|
21
|
+
// The middleware is mounted on '/protected' and the route uses it.
|
|
22
|
+
const logger = require("./middlewares/logger");
|
|
23
|
+
app.setMiddleware("/protected", logger);
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
app.addRoute("/protected", {
|
|
26
|
+
get: (req, res) => {
|
|
27
|
+
res.send(
|
|
28
|
+
"This route is protected by a simple request logger middleware."
|
|
29
|
+
);
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
};
|
|
@@ -1,25 +1,45 @@
|
|
|
1
|
-
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="pt-BR">
|
|
2
3
|
<head>
|
|
3
|
-
<meta charset="
|
|
4
|
-
<meta name="viewport" content="width=device-width,
|
|
5
|
-
<title>Kaelum
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
+
<title>Kaelum — Hello</title>
|
|
6
7
|
<link rel="stylesheet" href="/style.css" />
|
|
7
8
|
</head>
|
|
8
9
|
<body>
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
10
|
+
<main class="container">
|
|
11
|
+
<header>
|
|
12
|
+
<h1>Kaelum.js</h1>
|
|
13
|
+
<p class="subtitle">
|
|
14
|
+
Framework simplificada para Web e APIs — Template de demonstração
|
|
15
|
+
</p>
|
|
16
|
+
</header>
|
|
17
|
+
|
|
18
|
+
<section class="content">
|
|
19
|
+
<h2>Bem-vindo(a) 👋</h2>
|
|
20
|
+
<p>
|
|
21
|
+
Este é um template gerado automaticamente pela CLI do
|
|
22
|
+
<strong>Kaelum</strong>.
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<div class="cards">
|
|
26
|
+
<div class="card">
|
|
27
|
+
<h3>Rotas</h3>
|
|
28
|
+
<p>
|
|
29
|
+
Veja as rotas: <code>/</code>, <code>/about</code>,
|
|
30
|
+
<code>/protected</code>, <code>/health</code>.
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="card">
|
|
34
|
+
<h3>Segurança</h3>
|
|
35
|
+
<p>CORS e Helmet foram ativados via <code>app.setConfig</code>.</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<p class="small">
|
|
40
|
+
Gerado pela Kaelum CLI • Versão do template para Kaelum ^1.3.0
|
|
41
|
+
</p>
|
|
42
|
+
</section>
|
|
43
|
+
</main>
|
|
24
44
|
</body>
|
|
25
45
|
</html>
|
package/cli/utils.js
CHANGED
|
@@ -1,12 +1,58 @@
|
|
|
1
1
|
const fs = require("fs-extra");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Copy a template directory to target and update package.json with projectName.
|
|
6
|
+
* @param {string} sourceDir
|
|
7
|
+
* @param {string} targetDir
|
|
8
|
+
* @param {string} projectName (optional) - will override package.json name if present
|
|
9
|
+
*/
|
|
10
|
+
async function copyTemplate(sourceDir, targetDir, projectName) {
|
|
5
11
|
try {
|
|
6
|
-
|
|
12
|
+
if (!sourceDir || !targetDir) {
|
|
13
|
+
throw new Error("Source and target directories are required.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ensure source exists
|
|
17
|
+
const exists = await fs.pathExists(sourceDir);
|
|
18
|
+
if (!exists) {
|
|
19
|
+
throw new Error(`Template not found: ${sourceDir}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// copy
|
|
23
|
+
await fs.copy(sourceDir, targetDir, {
|
|
24
|
+
overwrite: false,
|
|
25
|
+
errorOnExist: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// try update package.json in the copied template
|
|
29
|
+
const pkgPath = path.join(targetDir, "package.json");
|
|
30
|
+
const pkgExists = await fs.pathExists(pkgPath);
|
|
31
|
+
if (pkgExists && projectName) {
|
|
32
|
+
try {
|
|
33
|
+
const raw = await fs.readFile(pkgPath, "utf8");
|
|
34
|
+
const pkg = JSON.parse(raw);
|
|
35
|
+
// set sensible defaults for created project
|
|
36
|
+
pkg.name = projectName;
|
|
37
|
+
if (!pkg.version) pkg.version = "0.1.0";
|
|
38
|
+
if (!pkg.description)
|
|
39
|
+
pkg.description = `${projectName} - generated by Kaelum CLI`;
|
|
40
|
+
// ensure type commonjs by default for templates that expect require/module.exports
|
|
41
|
+
if (!pkg.type) pkg.type = "commonjs";
|
|
42
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2), "utf8");
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// non-fatal: warn but continue
|
|
45
|
+
console.warn(
|
|
46
|
+
"Warning: failed to update package.json in the template:",
|
|
47
|
+
e.message
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { ok: true };
|
|
7
53
|
} catch (err) {
|
|
8
|
-
|
|
54
|
+
return { ok: false, error: err.message || String(err) };
|
|
9
55
|
}
|
|
10
56
|
}
|
|
11
57
|
|
|
12
|
-
module.exports = { copyTemplate };
|
|
58
|
+
module.exports = { copyTemplate };
|
package/core/addRoute.js
CHANGED
|
@@ -1,10 +1,161 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// core/addRoute.js
|
|
2
|
+
// Adds routes to an Express app using a flexible handlers object.
|
|
3
|
+
// Supports:
|
|
4
|
+
// - handlers as a single function (assumed GET)
|
|
5
|
+
// - handlers as an object with HTTP methods (get, post, put, delete, patch, all)
|
|
6
|
+
// - nested subpaths as keys beginning with '/' (e.g. '/:id': { get: fn })
|
|
7
|
+
// - handlers as arrays of functions (middleware chains)
|
|
3
8
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
const supportedMethods = ["get", "post", "put", "delete", "patch", "all"];
|
|
10
|
+
|
|
11
|
+
function isPlainObject(v) {
|
|
12
|
+
return v && typeof v === "object" && !Array.isArray(v);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Normalize a handler entry into an array of functions.
|
|
17
|
+
* Acceptable inputs: function | [function, function, ...]
|
|
18
|
+
* @param {Function|Function[]} h
|
|
19
|
+
* @returns {Function[]}
|
|
20
|
+
*/
|
|
21
|
+
function normalizeHandlersToArray(h) {
|
|
22
|
+
if (typeof h === "function") return [h];
|
|
23
|
+
if (Array.isArray(h)) {
|
|
24
|
+
const invalid = h.find((fn) => typeof fn !== "function");
|
|
25
|
+
if (invalid) {
|
|
26
|
+
throw new Error("Each element in handler array must be a function");
|
|
27
|
+
}
|
|
28
|
+
return h;
|
|
29
|
+
}
|
|
30
|
+
throw new Error("Handler must be a function or an array of functions");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Wrap a handler function to catch thrown errors / rejected promises
|
|
35
|
+
* and forward them to next(err).
|
|
36
|
+
* @param {Function} fn
|
|
37
|
+
* @returns {Function} async wrapper (req, res, next)
|
|
38
|
+
*/
|
|
39
|
+
function wrapHandler(fn) {
|
|
40
|
+
return async function wrapped(req, res, next) {
|
|
41
|
+
try {
|
|
42
|
+
// support handlers that expect next as third argument
|
|
43
|
+
const maybePromise = fn(req, res, next);
|
|
44
|
+
// if handler returns a promise, await it to catch rejections
|
|
45
|
+
if (maybePromise && typeof maybePromise.then === "function") {
|
|
46
|
+
await maybePromise;
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
next(err);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Join basePath and subKey in a safe manner (avoid duplicate slashes).
|
|
56
|
+
* @param {string} basePath
|
|
57
|
+
* @param {string} key
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
function joinPaths(basePath, key) {
|
|
61
|
+
if (!basePath.endsWith("/")) basePath = basePath;
|
|
62
|
+
// remove trailing slash from basePath (except if basePath === '/')
|
|
63
|
+
if (basePath !== "/" && basePath.endsWith("/")) {
|
|
64
|
+
basePath = basePath.slice(0, -1);
|
|
65
|
+
}
|
|
66
|
+
// ensure key starts with '/'
|
|
67
|
+
const k = key.startsWith("/") ? key : "/" + key;
|
|
68
|
+
// special case: basePath === '/' -> result is key
|
|
69
|
+
return basePath === "/" ? k : basePath + k;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Registers routes on the provided Express app.
|
|
74
|
+
* @param {Object} app - Express app instance
|
|
75
|
+
* @param {string} basePath - base route path (e.g. '/users')
|
|
76
|
+
* @param {Function|Object} handlers - single handler function or object map of handlers
|
|
77
|
+
*/
|
|
78
|
+
function addRoute(app, basePath, handlers = {}) {
|
|
79
|
+
if (!app || typeof app.use !== "function") {
|
|
80
|
+
throw new Error("Invalid app instance: cannot register routes");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof basePath !== "string") {
|
|
84
|
+
throw new Error("Invalid path: basePath must be a string");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If handlers is a single function, register it as GET
|
|
88
|
+
if (typeof handlers === "function" || Array.isArray(handlers)) {
|
|
89
|
+
const fns = normalizeHandlersToArray(handlers);
|
|
90
|
+
const wrapped = fns.map(wrapHandler);
|
|
91
|
+
app.get(basePath, ...wrapped);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!isPlainObject(handlers)) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"Handlers must be a function, an array of functions, or a plain object"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Iterate keys of handlers
|
|
102
|
+
for (const key of Object.keys(handlers)) {
|
|
103
|
+
const value = handlers[key];
|
|
104
|
+
|
|
105
|
+
// Nested subpath (key starts with '/')
|
|
106
|
+
if (key.startsWith("/")) {
|
|
107
|
+
const subPath = joinPaths(basePath, key);
|
|
108
|
+
|
|
109
|
+
// If nested value is a function or array -> assume GET
|
|
110
|
+
if (typeof value === "function" || Array.isArray(value)) {
|
|
111
|
+
const fns = normalizeHandlersToArray(value);
|
|
112
|
+
const wrapped = fns.map(wrapHandler);
|
|
113
|
+
app.get(subPath, ...wrapped);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// If nested value is object -> iterate methods
|
|
118
|
+
if (isPlainObject(value)) {
|
|
119
|
+
for (const method of Object.keys(value)) {
|
|
120
|
+
const handlerFn = value[method];
|
|
121
|
+
const m = method.toLowerCase();
|
|
122
|
+
if (!supportedMethods.includes(m)) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Unsupported HTTP method "${method}" for route "${subPath}"`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
// allow single function or array for handlerFn
|
|
128
|
+
if (typeof handlerFn !== "function" && !Array.isArray(handlerFn)) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Handler for ${method} ${subPath} must be a function or array of functions`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const fns = normalizeHandlersToArray(handlerFn);
|
|
134
|
+
const wrapped = fns.map(wrapHandler);
|
|
135
|
+
app[m](subPath, ...wrapped);
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
throw new Error(`Invalid handler for nested path "${subPath}"`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Top-level method keys (like 'get', 'post', 'all', etc.)
|
|
144
|
+
const m = key.toLowerCase();
|
|
145
|
+
if (!supportedMethods.includes(m)) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Unsupported key "${key}" in handlers for path "${basePath}"`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const fn = handlers[key];
|
|
151
|
+
if (typeof fn !== "function" && !Array.isArray(fn)) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Handler for ${m.toUpperCase()} ${basePath} must be a function or array of functions`
|
|
154
|
+
);
|
|
7
155
|
}
|
|
156
|
+
const fns = normalizeHandlersToArray(fn);
|
|
157
|
+
const wrapped = fns.map(wrapHandler);
|
|
158
|
+
app[m](basePath, ...wrapped);
|
|
8
159
|
}
|
|
9
160
|
}
|
|
10
161
|
|
package/core/apiRoute.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// core/apiRoute.js
|
|
2
|
+
// Provide a simple helper to create RESTful resource routes.
|
|
3
|
+
// Internally uses addRoute(app, basePath, handlers).
|
|
4
|
+
// Supports:
|
|
5
|
+
// - handlers as a single function (assumed GET on collection)
|
|
6
|
+
// - handlers as an object mapping methods and/or nested subpaths
|
|
7
|
+
// - shorthand CRUD generation: pass { crud: true } or { crud: { list, create, show, update, remove } }
|
|
8
|
+
|
|
9
|
+
const addRoute = require("./addRoute");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Normalize resource into a base path string.
|
|
13
|
+
* @param {string} resource
|
|
14
|
+
* @returns {string} normalized path starting with '/'
|
|
15
|
+
*/
|
|
16
|
+
function normalizeResource(resource) {
|
|
17
|
+
if (!resource) return "/";
|
|
18
|
+
if (typeof resource !== "string") {
|
|
19
|
+
throw new Error("resource must be a string");
|
|
20
|
+
}
|
|
21
|
+
let r = resource.trim();
|
|
22
|
+
if (!r.startsWith("/")) r = "/" + r;
|
|
23
|
+
if (r.length > 1 && r.endsWith("/")) r = r.slice(0, -1);
|
|
24
|
+
return r;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a stub handler returning 501 Not Implemented.
|
|
29
|
+
* @param {string} actionName
|
|
30
|
+
* @returns {Function} express handler
|
|
31
|
+
*/
|
|
32
|
+
function notImplementedHandler(actionName) {
|
|
33
|
+
return (req, res) => {
|
|
34
|
+
res
|
|
35
|
+
.status(501)
|
|
36
|
+
.json({ error: "Not Implemented", action: actionName || "unknown" });
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build handlers object for CRUD shorthand.
|
|
42
|
+
* Accepts optional mapping of handler functions.
|
|
43
|
+
*
|
|
44
|
+
* Expected keys supported in `h` (any subset):
|
|
45
|
+
* - list / index -> GET /resource
|
|
46
|
+
* - create -> POST /resource
|
|
47
|
+
* - show / get / getById -> GET /resource/:id
|
|
48
|
+
* - update -> PUT /resource/:id
|
|
49
|
+
* - remove / delete -> DELETE /resource/:id
|
|
50
|
+
*
|
|
51
|
+
* @param {Object|boolean} h - handlers or true to auto-generate stubs
|
|
52
|
+
* @returns {Object} handlers object consumable by addRoute
|
|
53
|
+
*/
|
|
54
|
+
function buildCrudHandlers(h) {
|
|
55
|
+
const provided = h && typeof h === "object" ? h : {};
|
|
56
|
+
const pick = (keys) => {
|
|
57
|
+
for (const k of keys) {
|
|
58
|
+
if (typeof provided[k] === "function") return provided[k];
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const listFn = pick(["list", "index"]);
|
|
64
|
+
const createFn = pick(["create"]);
|
|
65
|
+
const showFn = pick(["show", "get", "getById"]);
|
|
66
|
+
const updateFn = pick(["update"]);
|
|
67
|
+
const removeFn = pick(["remove", "delete"]);
|
|
68
|
+
|
|
69
|
+
const handlers = {};
|
|
70
|
+
|
|
71
|
+
// collection endpoints
|
|
72
|
+
handlers.get = listFn || notImplementedHandler("list");
|
|
73
|
+
handlers.post = createFn || notImplementedHandler("create");
|
|
74
|
+
|
|
75
|
+
// member endpoints under '/:id'
|
|
76
|
+
handlers["/:id"] = {
|
|
77
|
+
get: showFn || notImplementedHandler("show"),
|
|
78
|
+
put: updateFn || notImplementedHandler("update"),
|
|
79
|
+
delete: removeFn || notImplementedHandler("remove"),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return handlers;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* apiRoute(app, resource, handlers)
|
|
87
|
+
* @param {Object} app - Express app (Kaelum app)
|
|
88
|
+
* @param {string} resource - resource name or path (e.g. 'users' or '/users')
|
|
89
|
+
* @param {Function|Object|boolean} handlers - function, object or shorthand (see README)
|
|
90
|
+
*/
|
|
91
|
+
function apiRoute(app, resource, handlers = {}) {
|
|
92
|
+
if (!app || typeof app.use !== "function") {
|
|
93
|
+
throw new Error("Invalid app instance: cannot register apiRoute");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const basePath = normalizeResource(resource);
|
|
97
|
+
|
|
98
|
+
// If handlers is a function, assume it's a GET on basePath
|
|
99
|
+
if (typeof handlers === "function") {
|
|
100
|
+
addRoute(app, basePath, handlers);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// If handlers is boolean true -> auto CRUD stubs
|
|
105
|
+
if (handlers === true) {
|
|
106
|
+
const crudHandlers = buildCrudHandlers(true);
|
|
107
|
+
addRoute(app, basePath, crudHandlers);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// If handlers is an object and has a 'crud' key, use it
|
|
112
|
+
if (
|
|
113
|
+
handlers &&
|
|
114
|
+
typeof handlers === "object" &&
|
|
115
|
+
handlers.hasOwnProperty("crud")
|
|
116
|
+
) {
|
|
117
|
+
const crudSpec = handlers.crud;
|
|
118
|
+
const crudHandlers = buildCrudHandlers(crudSpec);
|
|
119
|
+
addRoute(app, basePath, crudHandlers);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// If handlers is an object, assume it's the direct handlers map for addRoute
|
|
124
|
+
if (handlers && typeof handlers === "object") {
|
|
125
|
+
addRoute(app, basePath, handlers);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// anything else is invalid
|
|
130
|
+
throw new Error(
|
|
131
|
+
"Handlers must be a function, an object, or boolean true for CRUD shorthand"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = apiRoute;
|