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
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// core/errorHandler.js
|
|
2
|
+
// Generic error handling middleware factory for Kaelum.
|
|
3
|
+
//
|
|
4
|
+
// Exports:
|
|
5
|
+
// - module.exports = errorHandlerFactory
|
|
6
|
+
// - module.exports.errorHandler = errorHandlerFactory
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// const errorHandler = require('./core/errorHandler');
|
|
10
|
+
// app.use(errorHandler({ exposeStack: false }));
|
|
11
|
+
//
|
|
12
|
+
// Options:
|
|
13
|
+
// - exposeStack: boolean (default false) -> include stack trace in JSON when true
|
|
14
|
+
// - logger: function(err, req, info?) optional -> custom logger
|
|
15
|
+
// - onError: function(err, req, res) optional -> hook called before sending response
|
|
16
|
+
//
|
|
17
|
+
// Response JSON shape:
|
|
18
|
+
// { error: { message, code, ...(stack) } }
|
|
19
|
+
// Status chosen from err.status || err.statusCode || 500
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {Object} options
|
|
23
|
+
* @param {boolean} [options.exposeStack=false] - include stack trace in responses
|
|
24
|
+
* @param {Function} [options.logger] - optional logger function: logger(err, req, info)
|
|
25
|
+
* @param {Function} [options.onError] - hook called before sending response: onError(err, req, res)
|
|
26
|
+
* @returns {Function} express error-handling middleware (err, req, res, next)
|
|
27
|
+
*/
|
|
28
|
+
function errorHandlerFactory(options = {}) {
|
|
29
|
+
const { exposeStack = false, logger = null, onError = null } = options || {};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Default logger used when no custom logger is provided.
|
|
33
|
+
* @param {Error|any} err
|
|
34
|
+
* @param {Object} req
|
|
35
|
+
*/
|
|
36
|
+
function defaultLog(err, req) {
|
|
37
|
+
const status =
|
|
38
|
+
err && (err.status || err.statusCode)
|
|
39
|
+
? err.status || err.statusCode
|
|
40
|
+
: 500;
|
|
41
|
+
if (status >= 500) {
|
|
42
|
+
if (err && err.stack) console.error(err.stack);
|
|
43
|
+
else console.error(err);
|
|
44
|
+
} else {
|
|
45
|
+
if (err && err.message) console.warn(err.message);
|
|
46
|
+
else console.warn(err);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return function errorHandler(err, req, res, next) {
|
|
51
|
+
// If headers already sent, fall back to default express handler
|
|
52
|
+
if (res.headersSent) {
|
|
53
|
+
return next(err);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// normalize non-error values (e.g., throw "string")
|
|
57
|
+
const normalizedErr =
|
|
58
|
+
err instanceof Error
|
|
59
|
+
? err
|
|
60
|
+
: new Error(typeof err === "string" ? err : "Unknown error");
|
|
61
|
+
|
|
62
|
+
// determine HTTP status
|
|
63
|
+
const status =
|
|
64
|
+
err && (err.status || err.statusCode)
|
|
65
|
+
? err.status || err.statusCode
|
|
66
|
+
: 500;
|
|
67
|
+
|
|
68
|
+
// prepare payload
|
|
69
|
+
const payload = {
|
|
70
|
+
error: {
|
|
71
|
+
message:
|
|
72
|
+
(err && (err.message || err.msg)) ||
|
|
73
|
+
normalizedErr.message ||
|
|
74
|
+
"Internal Server Error",
|
|
75
|
+
code: err && err.code ? err.code : "INTERNAL_ERROR",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (exposeStack && normalizedErr && normalizedErr.stack) {
|
|
80
|
+
payload.error.stack = normalizedErr.stack;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// logging: prefer custom logger if provided
|
|
84
|
+
try {
|
|
85
|
+
if (typeof logger === "function") {
|
|
86
|
+
// allow custom logger to receive (err, req, { status })
|
|
87
|
+
logger(normalizedErr, req, { status });
|
|
88
|
+
} else {
|
|
89
|
+
defaultLog(normalizedErr, req);
|
|
90
|
+
}
|
|
91
|
+
} catch (logErr) {
|
|
92
|
+
// don't crash if logger fails
|
|
93
|
+
console.error("Kaelum errorHandler: logger threw an error", logErr);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// onError hook (e.g., report to external service)
|
|
97
|
+
try {
|
|
98
|
+
if (typeof onError === "function") {
|
|
99
|
+
try {
|
|
100
|
+
onError(normalizedErr, req, res);
|
|
101
|
+
} catch (hookErr) {
|
|
102
|
+
// don't block response if hook fails
|
|
103
|
+
console.error("Kaelum errorHandler: onError hook threw", hookErr);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch (_) {
|
|
107
|
+
// ignore
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Respond according to Accept header: JSON preferred, fallback to text/html
|
|
111
|
+
if (req && req.accepts && req.accepts("html") && !req.accepts("json")) {
|
|
112
|
+
// simple HTML response for browsers preferring HTML
|
|
113
|
+
const title = `Error ${status}`;
|
|
114
|
+
const body = `
|
|
115
|
+
<!doctype html>
|
|
116
|
+
<html>
|
|
117
|
+
<head><meta charset="utf-8"/><title>${title}</title></head>
|
|
118
|
+
<body>
|
|
119
|
+
<h1>${title}</h1>
|
|
120
|
+
<p>${payload.error.message}</p>
|
|
121
|
+
${
|
|
122
|
+
exposeStack && payload.error.stack
|
|
123
|
+
? `<pre>${payload.error.stack}</pre>`
|
|
124
|
+
: ""
|
|
125
|
+
}
|
|
126
|
+
</body>
|
|
127
|
+
</html>`;
|
|
128
|
+
res.status(status).type("html").send(body);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// default: JSON response
|
|
133
|
+
res.status(status).json(payload);
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Export both default and named to keep compatibility with different import styles
|
|
138
|
+
module.exports = errorHandlerFactory;
|
|
139
|
+
module.exports.errorHandler = errorHandlerFactory;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// core/healthCheck.js
|
|
2
|
+
// Kaelum - health check helper
|
|
3
|
+
//
|
|
4
|
+
// Exports a factory function to register a health (liveness/readiness) endpoint.
|
|
5
|
+
//
|
|
6
|
+
// Usage examples:
|
|
7
|
+
// const registerHealth = require('./core/healthCheck');
|
|
8
|
+
// // simple
|
|
9
|
+
// registerHealth(app);
|
|
10
|
+
// // with options
|
|
11
|
+
// registerHealth(app, {
|
|
12
|
+
// path: '/health',
|
|
13
|
+
// readinessCheck: async () => {
|
|
14
|
+
// // custom checks (e.g. DB ping). Return { ok: true, details: { ... } } or { ok: false, details: {...} }
|
|
15
|
+
// const ok = await db.ping();
|
|
16
|
+
// return { ok, details: { db: ok } };
|
|
17
|
+
// },
|
|
18
|
+
// include: { uptime: true, pid: true, env: true, timestamp: true },
|
|
19
|
+
// replace: true
|
|
20
|
+
// });
|
|
21
|
+
|
|
22
|
+
const DEFAULT_PATH = "/health";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} HealthOptions
|
|
26
|
+
* @property {string} [path] - Endpoint path (default '/health')
|
|
27
|
+
* @property {string} [method] - HTTP method for health endpoint (default 'get')
|
|
28
|
+
* @property {boolean} [replace] - If true and a previous Kaelum-installed endpoint exists, replace it (default false)
|
|
29
|
+
* @property {Function} [readinessCheck] - Optional async function that returns { ok: boolean, details?: Object }
|
|
30
|
+
* @property {Object} [include] - Which fields to include in payload. Defaults to all true.
|
|
31
|
+
* @property {boolean} [include.uptime]
|
|
32
|
+
* @property {boolean} [include.pid]
|
|
33
|
+
* @property {boolean} [include.env]
|
|
34
|
+
* @property {boolean} [include.timestamp]
|
|
35
|
+
* @property {boolean} [include.metrics] - reserved for future metrics (default false)
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if the same route path+method already exists in the app router.
|
|
40
|
+
* Only inspects Kaelum-installed layers conservatively.
|
|
41
|
+
* @param {Object} app - express app
|
|
42
|
+
* @param {string} path
|
|
43
|
+
* @param {string} method
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function routeExists(app, path, method) {
|
|
47
|
+
try {
|
|
48
|
+
if (!app || !app._router || !Array.isArray(app._router.stack)) return false;
|
|
49
|
+
return app._router.stack.some((layer) => {
|
|
50
|
+
if (!layer || !layer.route) return false;
|
|
51
|
+
if (layer.route.path !== path) return false;
|
|
52
|
+
return (
|
|
53
|
+
!!layer.route.methods && !!layer.route.methods[method.toLowerCase()]
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
} catch (e) {
|
|
57
|
+
// if internal inspection fails, be conservative and return false
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Register a health check endpoint on the provided Express/Kaelum app.
|
|
64
|
+
* @param {Object} app - Express/Kaelum app instance
|
|
65
|
+
* @param {HealthOptions|string} [opts] - options or a string path
|
|
66
|
+
* @returns {Function} the handler function created (useful for tests)
|
|
67
|
+
*/
|
|
68
|
+
function registerHealth(app, opts = {}) {
|
|
69
|
+
if (!app || typeof app.get !== "function") {
|
|
70
|
+
throw new Error("Invalid app instance: cannot register health check");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// allow shorthand: passing a string path
|
|
74
|
+
const options =
|
|
75
|
+
typeof opts === "string"
|
|
76
|
+
? { path: opts }
|
|
77
|
+
: Object.assign(
|
|
78
|
+
{
|
|
79
|
+
path: DEFAULT_PATH,
|
|
80
|
+
method: "get",
|
|
81
|
+
replace: false,
|
|
82
|
+
readinessCheck: null,
|
|
83
|
+
include: {
|
|
84
|
+
uptime: true,
|
|
85
|
+
pid: true,
|
|
86
|
+
env: true,
|
|
87
|
+
timestamp: true,
|
|
88
|
+
metrics: false,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
opts || {}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// normalize path and method
|
|
95
|
+
let p =
|
|
96
|
+
options.path && typeof options.path === "string"
|
|
97
|
+
? options.path
|
|
98
|
+
: DEFAULT_PATH;
|
|
99
|
+
if (!p.startsWith("/")) p = "/" + p;
|
|
100
|
+
const method = (options.method || "get").toLowerCase();
|
|
101
|
+
|
|
102
|
+
// if route exists and replace is false -> skip registration
|
|
103
|
+
if (routeExists(app, p, method) && !options.replace) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// if replace requested, attempt to remove prior Kaelum-installed handler
|
|
108
|
+
if (options.replace) {
|
|
109
|
+
try {
|
|
110
|
+
// remove layers that match exactly the route path and method
|
|
111
|
+
if (app._router && Array.isArray(app._router.stack)) {
|
|
112
|
+
app._router.stack = app._router.stack.filter((layer) => {
|
|
113
|
+
if (!layer || !layer.route) return true; // keep non-route layers
|
|
114
|
+
if (layer.route.path !== p) return true; // keep other routes
|
|
115
|
+
// keep only layers that do not match the method we want to replace
|
|
116
|
+
return !layer.route.methods || !layer.route.methods[method];
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
} catch (e) {
|
|
120
|
+
// ignore removal errors - continue to register anyway
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
// if route exists and replace === false we already returned above
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The handler performs an optional readinessCheck. If readinessCheck returns
|
|
128
|
+
* { ok: false } then status 503 is sent, otherwise 200.
|
|
129
|
+
* readinessCheck may be synchronous or asynchronous.
|
|
130
|
+
*/
|
|
131
|
+
const handler = async (req, res) => {
|
|
132
|
+
// default payload base
|
|
133
|
+
const payload = {
|
|
134
|
+
status: "OK",
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// include optional fields
|
|
138
|
+
const inc = options.include || {};
|
|
139
|
+
if (inc.uptime) payload.uptime = process.uptime();
|
|
140
|
+
if (inc.pid) payload.pid = process.pid;
|
|
141
|
+
if (inc.env) payload.env = process.env.NODE_ENV || "development";
|
|
142
|
+
if (inc.timestamp) payload.timestamp = Date.now();
|
|
143
|
+
|
|
144
|
+
// readiness check (optional)
|
|
145
|
+
if (typeof options.readinessCheck === "function") {
|
|
146
|
+
try {
|
|
147
|
+
const result = await Promise.resolve(options.readinessCheck(req));
|
|
148
|
+
// result expected to be { ok: boolean, details?: object } or boolean
|
|
149
|
+
let ok = true;
|
|
150
|
+
let details = undefined;
|
|
151
|
+
if (typeof result === "boolean") {
|
|
152
|
+
ok = result;
|
|
153
|
+
} else if (result && typeof result === "object") {
|
|
154
|
+
ok = !!result.ok;
|
|
155
|
+
details = result.details;
|
|
156
|
+
} else {
|
|
157
|
+
// unrecognized result -> consider as ok
|
|
158
|
+
ok = true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!ok) {
|
|
162
|
+
payload.status = "FAIL";
|
|
163
|
+
if (details) payload.details = details;
|
|
164
|
+
return res.status(503).json(payload);
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
// readiness check threw -> respond 503 with error info (don't expose stack)
|
|
168
|
+
payload.status = "FAIL";
|
|
169
|
+
payload.details = {
|
|
170
|
+
message: err && err.message ? err.message : "readiness check error",
|
|
171
|
+
};
|
|
172
|
+
return res.status(503).json(payload);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// success
|
|
177
|
+
return res.status(200).json(payload);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// register route on app
|
|
181
|
+
try {
|
|
182
|
+
// attach a marker so we can detect Kaelum-installed static handlers if needed
|
|
183
|
+
app[method](p, handler);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Failed to register health route ${method.toUpperCase()} ${p}: ${
|
|
187
|
+
e.message
|
|
188
|
+
}`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// store reference in locals for future inspection/removal
|
|
193
|
+
try {
|
|
194
|
+
app.locals = app.locals || {};
|
|
195
|
+
app.locals._kaelum_health = app.locals._kaelum_health || [];
|
|
196
|
+
app.locals._kaelum_health.push({ path: p, method, handler });
|
|
197
|
+
} catch (_) {
|
|
198
|
+
// ignore locals storage errors
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return handler;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = registerHealth;
|
package/core/redirect.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// core/redirect.js
|
|
2
|
+
// Kaelum - redirect helper
|
|
3
|
+
//
|
|
4
|
+
// This module provides a flexible helper to register one or many redirect routes
|
|
5
|
+
// in a Kaelum/Express app. It stores references to Kaelum-installed redirect
|
|
6
|
+
// handlers so future calls can remove only the handlers Kaelum created (safe removal).
|
|
7
|
+
//
|
|
8
|
+
// Usage examples:
|
|
9
|
+
// const redirect = require('./core/redirect');
|
|
10
|
+
// // simple single redirect
|
|
11
|
+
// redirect(app, '/old', '/new', 301);
|
|
12
|
+
//
|
|
13
|
+
// // as map (object)
|
|
14
|
+
// redirect(app, { '/old': '/new', '/a': '/b' });
|
|
15
|
+
//
|
|
16
|
+
// // as array of objects
|
|
17
|
+
// redirect(app, [
|
|
18
|
+
// { from: '/x', to: '/y', status: 302 },
|
|
19
|
+
// { from: '/a', to: '/b' }
|
|
20
|
+
// ]);
|
|
21
|
+
//
|
|
22
|
+
// // returns array of registered entries or null if nothing registered.
|
|
23
|
+
|
|
24
|
+
function normalizePath(p) {
|
|
25
|
+
if (!p) return "/";
|
|
26
|
+
if (typeof p !== "string") throw new Error("path must be a string");
|
|
27
|
+
return p.startsWith("/") ? p : "/" + p;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeStatus(s) {
|
|
31
|
+
const n = Number(s);
|
|
32
|
+
if (!Number.isFinite(n)) return 302;
|
|
33
|
+
// limit to common redirect codes 300-399
|
|
34
|
+
if (n < 300 || n >= 400) return 302;
|
|
35
|
+
return Math.floor(n);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ensureLocals(app) {
|
|
39
|
+
app.locals = app.locals || {};
|
|
40
|
+
app.locals._kaelum_redirects = app.locals._kaelum_redirects || [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Safely remove a previously registered Kaelum redirect handler for a path.
|
|
45
|
+
* Only removes handlers that we registered (tracked in app.locals._kaelum_redirects).
|
|
46
|
+
* @param {Object} app
|
|
47
|
+
* @param {string} path
|
|
48
|
+
*/
|
|
49
|
+
function removeKaelumRedirectsForPath(app, path) {
|
|
50
|
+
if (!app || !app._router || !Array.isArray(app._router.stack)) return;
|
|
51
|
+
if (!app.locals || !Array.isArray(app.locals._kaelum_redirects)) return;
|
|
52
|
+
|
|
53
|
+
// find tracked entries for this path
|
|
54
|
+
const tracked = app.locals._kaelum_redirects.filter((r) => r.path === path);
|
|
55
|
+
if (tracked.length === 0) return;
|
|
56
|
+
|
|
57
|
+
// remove router layers whose handler matches tracked.handler
|
|
58
|
+
app._router.stack = app._router.stack.filter((layer) => {
|
|
59
|
+
// keep everything that is not a route
|
|
60
|
+
if (!layer || !layer.route) return true;
|
|
61
|
+
if (layer.route.path !== path) return true;
|
|
62
|
+
// check if route stack contains a tracked handler
|
|
63
|
+
const handlers = layer.route.stack || [];
|
|
64
|
+
for (const entry of tracked) {
|
|
65
|
+
for (const h of handlers) {
|
|
66
|
+
if (h && h.handle === entry.handler) {
|
|
67
|
+
// drop this layer (do not keep)
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// if none of the tracked handlers matched, keep the layer
|
|
73
|
+
return true;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// remove tracked entries for that path from locals
|
|
77
|
+
app.locals._kaelum_redirects = app.locals._kaelum_redirects.filter(
|
|
78
|
+
(r) => r.path !== path
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Register a single redirect handler and track it in app.locals.
|
|
84
|
+
* @param {Object} app
|
|
85
|
+
* @param {string} from
|
|
86
|
+
* @param {string} to
|
|
87
|
+
* @param {number} status
|
|
88
|
+
* @returns {{ path: string, to: string, status: number }}
|
|
89
|
+
*/
|
|
90
|
+
function registerSingleRedirect(app, from, to, status) {
|
|
91
|
+
const path = normalizePath(from);
|
|
92
|
+
const target = to;
|
|
93
|
+
const code = normalizeStatus(status);
|
|
94
|
+
|
|
95
|
+
// create handler and register GET route
|
|
96
|
+
const handler = function (req, res) {
|
|
97
|
+
res.redirect(code, target);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// add to express
|
|
101
|
+
app.get(path, handler);
|
|
102
|
+
|
|
103
|
+
// track it
|
|
104
|
+
ensureLocals(app);
|
|
105
|
+
app.locals._kaelum_redirects.push({
|
|
106
|
+
path,
|
|
107
|
+
handler,
|
|
108
|
+
to: target,
|
|
109
|
+
status: code,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return { path, to: target, status: code };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Main redirect export.
|
|
117
|
+
* Accepts:
|
|
118
|
+
* - (app, from, to, status?)
|
|
119
|
+
* - (app, mapObject) where mapObject is { from: to, ... }
|
|
120
|
+
* - (app, array) where array contains { from, to, status? } entries
|
|
121
|
+
*
|
|
122
|
+
* @param {Object} app - Express/Kaelum app
|
|
123
|
+
* @param {string|Object|Array} fromOrMap - path string or map/object or array of mappings
|
|
124
|
+
* @param {string|number} [toOrStatus] - when calling with (app, from, to, status) this is the "to" parameter
|
|
125
|
+
* @param {number} [maybeStatus] - optional status when calling the 4-arg form
|
|
126
|
+
* @returns {Array|null} list of registered redirect entries or null if none
|
|
127
|
+
*/
|
|
128
|
+
function redirect(app, fromOrMap, toOrStatus, maybeStatus) {
|
|
129
|
+
if (!app || typeof app.get !== "function") {
|
|
130
|
+
throw new Error("Invalid app instance: cannot register redirect");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
ensureLocals(app);
|
|
134
|
+
|
|
135
|
+
const registered = [];
|
|
136
|
+
|
|
137
|
+
// Helper: register one mapping (and remove previous Kaelum mapping for that path)
|
|
138
|
+
function applyMapping(from, to, status) {
|
|
139
|
+
const path = normalizePath(from);
|
|
140
|
+
// remove previously Kaelum-registered redirects for same path (safe removal)
|
|
141
|
+
removeKaelumRedirectsForPath(app, path);
|
|
142
|
+
// register and track
|
|
143
|
+
const res = registerSingleRedirect(app, path, to, status);
|
|
144
|
+
registered.push(res);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Determine input shape
|
|
148
|
+
if (typeof fromOrMap === "string") {
|
|
149
|
+
// form: redirect(app, '/old', '/new', 302)
|
|
150
|
+
const from = fromOrMap;
|
|
151
|
+
const to = typeof toOrStatus === "string" ? toOrStatus : "/";
|
|
152
|
+
const status =
|
|
153
|
+
typeof maybeStatus !== "undefined" ? maybeStatus : toOrStatus;
|
|
154
|
+
applyMapping(from, to, status);
|
|
155
|
+
} else if (Array.isArray(fromOrMap)) {
|
|
156
|
+
// array of objects: [{ from, to, status }]
|
|
157
|
+
for (const item of fromOrMap) {
|
|
158
|
+
if (!item) continue;
|
|
159
|
+
const from = item.from || item.path || null;
|
|
160
|
+
const to = item.to || item.target || null;
|
|
161
|
+
const status = item.status || item.code || 302;
|
|
162
|
+
if (!from || !to) continue;
|
|
163
|
+
applyMapping(from, to, status);
|
|
164
|
+
}
|
|
165
|
+
} else if (fromOrMap && typeof fromOrMap === "object") {
|
|
166
|
+
// object map: { '/old': '/new', 'a': 'b' }
|
|
167
|
+
for (const k of Object.keys(fromOrMap)) {
|
|
168
|
+
applyMapping(k, fromOrMap[k], 302);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
throw new Error("Invalid arguments for redirect");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return registered.length ? registered : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = redirect;
|