webspresso 0.0.74 → 0.0.75
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 +41 -3
- package/bin/commands/orm-map.js +139 -0
- package/bin/commands/skill.js +22 -8
- package/bin/utils/orm-map-html.js +689 -0
- package/bin/utils/orm-map-load.js +85 -0
- package/bin/utils/orm-map-snapshot.js +179 -0
- package/bin/utils/resolve-webspresso-orm.js +23 -0
- package/bin/webspresso.js +2 -0
- package/core/auth/manager.js +14 -1
- package/core/kernel/app.js +96 -0
- package/core/kernel/base-repository.js +143 -0
- package/core/kernel/events.js +101 -0
- package/core/kernel/flow.js +22 -0
- package/core/kernel/index.js +17 -0
- package/core/kernel/plugin.js +23 -0
- package/core/kernel/plugins/sample-seo.js +26 -0
- package/core/kernel/run-demo.js +58 -0
- package/core/kernel/view.js +167 -0
- package/core/openapi/build-from-api-routes.js +8 -2
- package/core/orm/model.js +3 -1
- package/core/url-path-normalize.js +30 -0
- package/index.d.ts +168 -1
- package/index.js +20 -2
- package/package.json +11 -1
- package/plugins/admin-panel/api.js +43 -15
- package/plugins/admin-panel/client/README.md +39 -0
- package/plugins/admin-panel/client/load-parts.js +74 -0
- package/plugins/admin-panel/client/manifest.parts.json +12 -0
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +150 -0
- package/plugins/admin-panel/client/parts/02-filter-components.js +554 -0
- package/plugins/admin-panel/client/parts/03-pagination-intro.js +70 -0
- package/plugins/admin-panel/client/parts/04-field-renderers.js +287 -0
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +335 -0
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +125 -0
- package/plugins/admin-panel/client/parts/07-model-list.js +596 -0
- package/plugins/admin-panel/client/parts/08-record-list.js +536 -0
- package/plugins/admin-panel/client/parts/09-record-form.js +170 -0
- package/plugins/admin-panel/client/parts/10-export-registry.js +11 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +32 -0
- package/plugins/admin-panel/client/vite.config.example.mjs +22 -0
- package/plugins/admin-panel/components.js +4 -2640
- package/plugins/admin-panel/core/api-extensions.js +100 -10
- package/plugins/admin-panel/index.js +3 -0
- package/plugins/admin-panel/lib/is-rich-text-empty.js +23 -0
- package/plugins/admin-panel/lib/sanitize-rich-html.js +106 -0
- package/plugins/admin-panel/modules/dashboard.js +3 -2
- package/plugins/admin-panel/modules/user-management.js +90 -20
- package/plugins/index.js +4 -0
- package/plugins/rate-limit/index.js +178 -0
- package/plugins/redirect/index.js +204 -0
- package/plugins/rest-resources/index.js +2 -1
- package/plugins/swagger.js +2 -1
- package/plugins/upload/local-file-provider.js +6 -2
- package/src/file-router.js +191 -50
- package/src/njk-frontmatter.js +156 -0
- package/src/plugin-manager.js +4 -2
- package/src/server.js +26 -9
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +276 -0
- package/templates/skills/webspresso-usage/REFERENCE-kernel.md +93 -0
- package/templates/skills/webspresso-usage/SKILL.md +29 -278
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limit plugin — registers named `rateLimit` middleware for file routes + optional global limiter
|
|
3
|
+
* @module plugins/rate-limit
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const PLUGIN_ONLY_KEYS = new Set(['global', 'globalOverrides', 'globalSkipPaths']);
|
|
7
|
+
|
|
8
|
+
const DEFAULT_BASE = {
|
|
9
|
+
windowMs: 60_000,
|
|
10
|
+
limit: 100,
|
|
11
|
+
legacyHeaders: false,
|
|
12
|
+
standardHeaders: 'draft-7',
|
|
13
|
+
message: { error: 'Too many requests, please try again later.' },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load express-rate-limit (peer). Throws if missing or too old for ipKeyGenerator.
|
|
18
|
+
* @returns {{ rateLimit: Function, ipKeyGenerator: Function }}
|
|
19
|
+
*/
|
|
20
|
+
function loadPeer() {
|
|
21
|
+
let mod;
|
|
22
|
+
try {
|
|
23
|
+
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
|
|
24
|
+
mod = require('express-rate-limit');
|
|
25
|
+
} catch (e) {
|
|
26
|
+
const err = new Error(
|
|
27
|
+
'rate-limit plugin: install peer dependency `express-rate-limit` (npm install express-rate-limit)'
|
|
28
|
+
);
|
|
29
|
+
err.cause = e;
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
const rateLimit = mod.rateLimit || mod.default;
|
|
33
|
+
const { ipKeyGenerator } = mod;
|
|
34
|
+
if (typeof rateLimit !== 'function') {
|
|
35
|
+
throw new Error('rate-limit plugin: express-rate-limit export rateLimit not found');
|
|
36
|
+
}
|
|
37
|
+
if (typeof ipKeyGenerator !== 'function') {
|
|
38
|
+
throw new Error(
|
|
39
|
+
'rate-limit plugin: express-rate-limit must be >= 8 (ipKeyGenerator export required)'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return { rateLimit, ipKeyGenerator };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Strip plugin-only options before passing to express-rate-limit.
|
|
47
|
+
* @param {Record<string, unknown>} opts
|
|
48
|
+
*/
|
|
49
|
+
function pickLimiterOptions(opts) {
|
|
50
|
+
const out = { ...opts };
|
|
51
|
+
for (const k of PLUGIN_ONLY_KEYS) {
|
|
52
|
+
delete out[k];
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Merge layers into express-rate-limit options. Default key uses ipKeyGenerator(req.ip, subnet).
|
|
59
|
+
* Custom keyGenerator and top-level ipv6Subnet are mutually exclusive in v8+ validation.
|
|
60
|
+
*
|
|
61
|
+
* @param {Function} ipKeyGenerator
|
|
62
|
+
* @param {...Record<string, unknown>} layers
|
|
63
|
+
*/
|
|
64
|
+
function resolveLimiterConfig(ipKeyGenerator, ...layers) {
|
|
65
|
+
/** @type {Record<string, unknown>} */
|
|
66
|
+
const merged = Object.assign({}, ...layers);
|
|
67
|
+
const keyGenerator = merged.keyGenerator;
|
|
68
|
+
const ipv6Subnet = merged.ipv6Subnet;
|
|
69
|
+
const rest = { ...merged };
|
|
70
|
+
delete rest.keyGenerator;
|
|
71
|
+
delete rest.ipv6Subnet;
|
|
72
|
+
|
|
73
|
+
if (typeof keyGenerator === 'function') {
|
|
74
|
+
return { ...rest, keyGenerator };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const subnet = ipv6Subnet !== undefined ? ipv6Subnet : 56;
|
|
78
|
+
return {
|
|
79
|
+
...rest,
|
|
80
|
+
keyGenerator: (req, res) => ipKeyGenerator(req.ip, subnet),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {import('express').RequestHandler|undefined} userSkip
|
|
86
|
+
* @param {import('express').RequestHandler} builtin
|
|
87
|
+
*/
|
|
88
|
+
function combineSkip(userSkip, builtin) {
|
|
89
|
+
if (!userSkip) return builtin;
|
|
90
|
+
return (req, res) => userSkip(req, res) || builtin(req, res);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {string[]} extraPathPrefixes
|
|
95
|
+
* @returns {import('express').RequestHandler}
|
|
96
|
+
*/
|
|
97
|
+
function createDefaultGlobalSkip(extraPathPrefixes = []) {
|
|
98
|
+
const extras = (Array.isArray(extraPathPrefixes) ? extraPathPrefixes : []).filter(Boolean);
|
|
99
|
+
/** @type {string[]} */
|
|
100
|
+
const prefixes = [
|
|
101
|
+
'/__webspresso/client-runtime',
|
|
102
|
+
'/_webspresso',
|
|
103
|
+
...extras,
|
|
104
|
+
];
|
|
105
|
+
return (req, res) => {
|
|
106
|
+
const p = req.path || '';
|
|
107
|
+
if (prefixes.some((x) => p.startsWith(x))) return true;
|
|
108
|
+
if (p === '/health' || p === '/robots.txt' || p === '/favicon.ico') return true;
|
|
109
|
+
return false;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {object} [options] — express-rate-limit options plus plugin keys
|
|
115
|
+
* @param {boolean} [options.global=false] — mount a global limiter on ctx.app
|
|
116
|
+
* @param {object} [options.globalOverrides] — shallow merge applied only for the global limiter (after factory defaults)
|
|
117
|
+
* @param {string[]} [options.globalSkipPaths] — extra path prefixes where global limiter skips counting
|
|
118
|
+
*/
|
|
119
|
+
function rateLimitPlugin(options = {}) {
|
|
120
|
+
const {
|
|
121
|
+
global: applyGlobal = false,
|
|
122
|
+
globalOverrides = null,
|
|
123
|
+
globalSkipPaths = [],
|
|
124
|
+
...rest
|
|
125
|
+
} = options;
|
|
126
|
+
|
|
127
|
+
const factoryDefaults = pickLimiterOptions(rest);
|
|
128
|
+
|
|
129
|
+
const plugin = {
|
|
130
|
+
name: 'rate-limit',
|
|
131
|
+
version: '1.0.0',
|
|
132
|
+
description: 'Named rateLimit middleware (express-rate-limit) for file routes; optional global limiter',
|
|
133
|
+
_options: options,
|
|
134
|
+
|
|
135
|
+
api: {
|
|
136
|
+
/**
|
|
137
|
+
* Build limiter options object (for setupRoutes / tests).
|
|
138
|
+
* @param {Record<string, unknown>} [routeOpts]
|
|
139
|
+
*/
|
|
140
|
+
createLimiterOptions(routeOpts = {}) {
|
|
141
|
+
const { ipKeyGenerator } = loadPeer();
|
|
142
|
+
const baseDefaults = { ...DEFAULT_BASE, ...factoryDefaults };
|
|
143
|
+
return resolveLimiterConfig(ipKeyGenerator, baseDefaults, routeOpts);
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
register(ctx) {
|
|
148
|
+
const { rateLimit, ipKeyGenerator } = loadPeer();
|
|
149
|
+
|
|
150
|
+
const baseDefaults = { ...DEFAULT_BASE, ...factoryDefaults };
|
|
151
|
+
|
|
152
|
+
ctx.middlewares.rateLimit = (routeOpts = {}) =>
|
|
153
|
+
rateLimit(resolveLimiterConfig(ipKeyGenerator, baseDefaults, routeOpts));
|
|
154
|
+
|
|
155
|
+
if (applyGlobal) {
|
|
156
|
+
const globalBuiltins = createDefaultGlobalSkip(globalSkipPaths);
|
|
157
|
+
const globalResolved = resolveLimiterConfig(
|
|
158
|
+
ipKeyGenerator,
|
|
159
|
+
baseDefaults,
|
|
160
|
+
globalOverrides && typeof globalOverrides === 'object' ? globalOverrides : {}
|
|
161
|
+
);
|
|
162
|
+
const UserSkip = globalResolved.skip;
|
|
163
|
+
const middleware = rateLimit({
|
|
164
|
+
...globalResolved,
|
|
165
|
+
skip: combineSkip(
|
|
166
|
+
typeof UserSkip === 'function' ? UserSkip : undefined,
|
|
167
|
+
globalBuiltins
|
|
168
|
+
),
|
|
169
|
+
});
|
|
170
|
+
ctx.app.use(middleware);
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return plugin;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = { rateLimitPlugin };
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP redirect plugin — runs in register() before file-based routes.
|
|
3
|
+
* @module plugins/redirect
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} to
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
function isExternalTarget(to) {
|
|
13
|
+
if (!to || typeof to !== 'string') return false;
|
|
14
|
+
const t = to.trim();
|
|
15
|
+
return /^https?:\/\//i.test(t) || t.startsWith('//');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {'strip'|'add'|false|undefined} mode
|
|
20
|
+
* @param {string} path
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
function normalizePathForTrailingSlash(mode, path) {
|
|
24
|
+
if (!path || mode === false || mode == null) return path;
|
|
25
|
+
if (path === '/') return path;
|
|
26
|
+
if (mode === 'strip') {
|
|
27
|
+
return path.endsWith('/') ? path.slice(0, -1) || '/' : path;
|
|
28
|
+
}
|
|
29
|
+
if (mode === 'add') {
|
|
30
|
+
return path.endsWith('/') ? path : `${path}/`;
|
|
31
|
+
}
|
|
32
|
+
return path;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {object} rule
|
|
37
|
+
* @param {object} pluginOpts
|
|
38
|
+
* @returns {object|null} compiled rule or null if invalid / disallowed external
|
|
39
|
+
*/
|
|
40
|
+
function compileRule(rule, pluginOpts) {
|
|
41
|
+
if (!rule || typeof rule.to !== 'string' || rule.to.trim() === '') {
|
|
42
|
+
console.warn('[redirect] Skipping rule: missing `to`');
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const to = rule.to.trim();
|
|
47
|
+
const external = isExternalTarget(to);
|
|
48
|
+
if (external && !pluginOpts.allowExternal) {
|
|
49
|
+
console.warn('[redirect] Skipping rule: external `to` requires allowExternal: true', rule.from);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let status = rule.status != null ? Number(rule.status) : pluginOpts.defaultStatus;
|
|
54
|
+
if (!REDIRECT_STATUSES.has(status)) {
|
|
55
|
+
status = pluginOpts.defaultStatus;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let matchAnyMethod = false;
|
|
59
|
+
/** @type {Set<string>|null} */
|
|
60
|
+
let methodSet = null;
|
|
61
|
+
if (rule.methods === '*') {
|
|
62
|
+
matchAnyMethod = true;
|
|
63
|
+
} else if (Array.isArray(rule.methods) && rule.methods.length > 0) {
|
|
64
|
+
methodSet = new Set(rule.methods.map((m) => String(m).toUpperCase()));
|
|
65
|
+
} else {
|
|
66
|
+
methodSet = new Set(pluginOpts.defaultMethods.map((m) => String(m).toUpperCase()));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const trailing = pluginOpts.trailingSlash;
|
|
70
|
+
|
|
71
|
+
if (typeof rule.from === 'string') {
|
|
72
|
+
const fromRaw = rule.from.trim();
|
|
73
|
+
const fromNorm = normalizePathForTrailingSlash(trailing, fromRaw);
|
|
74
|
+
return {
|
|
75
|
+
kind: 'string',
|
|
76
|
+
fromNorm,
|
|
77
|
+
fromRaw,
|
|
78
|
+
to,
|
|
79
|
+
status,
|
|
80
|
+
external,
|
|
81
|
+
matchAnyMethod,
|
|
82
|
+
methodSet,
|
|
83
|
+
trailing,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (rule.from instanceof RegExp) {
|
|
88
|
+
return {
|
|
89
|
+
kind: 'regex',
|
|
90
|
+
from: rule.from,
|
|
91
|
+
to,
|
|
92
|
+
status,
|
|
93
|
+
external,
|
|
94
|
+
matchAnyMethod,
|
|
95
|
+
methodSet,
|
|
96
|
+
trailing,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.warn('[redirect] Skipping rule: `from` must be string or RegExp');
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {object} compiled
|
|
106
|
+
* @param {string} pathForMatch
|
|
107
|
+
* @param {string} rawPath
|
|
108
|
+
* @returns {boolean}
|
|
109
|
+
*/
|
|
110
|
+
function pathMatches(compiled, pathForMatch, rawPath) {
|
|
111
|
+
if (compiled.kind === 'string') {
|
|
112
|
+
if (pathForMatch === compiled.fromNorm) return true;
|
|
113
|
+
if (!compiled.trailing) {
|
|
114
|
+
const a = rawPath.replace(/\/$/, '') || '/';
|
|
115
|
+
const b = compiled.fromRaw.replace(/\/$/, '') || '/';
|
|
116
|
+
if (a === b) return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
return compiled.from.test(pathForMatch);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {object} compiled
|
|
125
|
+
* @param {string} method
|
|
126
|
+
* @returns {boolean}
|
|
127
|
+
*/
|
|
128
|
+
function methodMatches(compiled, method) {
|
|
129
|
+
const m = method.toUpperCase();
|
|
130
|
+
if (compiled.matchAnyMethod) return true;
|
|
131
|
+
if (compiled.methodSet) return compiled.methodSet.has(m);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {object} options
|
|
137
|
+
* @param {Array<{from: string|RegExp, to: string, status?: number, methods?: string[]|'*'}>} [options.rules]
|
|
138
|
+
* @param {number} [options.defaultStatus=302]
|
|
139
|
+
* @param {boolean} [options.preserveQuery=true]
|
|
140
|
+
* @param {boolean} [options.allowExternal=false]
|
|
141
|
+
* @param {'strip'|'add'|false} [options.trailingSlash=false]
|
|
142
|
+
* @param {string[]} [options.defaultMethods=['GET','HEAD']]
|
|
143
|
+
* @returns {{ name: string, version: string, description: string, register: Function }}
|
|
144
|
+
*/
|
|
145
|
+
function redirectPlugin(options = {}) {
|
|
146
|
+
const {
|
|
147
|
+
rules = [],
|
|
148
|
+
defaultStatus = 302,
|
|
149
|
+
preserveQuery = true,
|
|
150
|
+
allowExternal = false,
|
|
151
|
+
trailingSlash = false,
|
|
152
|
+
defaultMethods = ['GET', 'HEAD'],
|
|
153
|
+
} = options;
|
|
154
|
+
|
|
155
|
+
const pluginOpts = {
|
|
156
|
+
defaultStatus: REDIRECT_STATUSES.has(Number(defaultStatus)) ? Number(defaultStatus) : 302,
|
|
157
|
+
preserveQuery,
|
|
158
|
+
allowExternal,
|
|
159
|
+
trailingSlash,
|
|
160
|
+
defaultMethods: Array.isArray(defaultMethods) && defaultMethods.length > 0 ? defaultMethods : ['GET', 'HEAD'],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const compiled = [];
|
|
164
|
+
for (const rule of rules) {
|
|
165
|
+
const c = compileRule(rule, pluginOpts);
|
|
166
|
+
if (c) compiled.push(c);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function redirectMiddleware(req, res, next) {
|
|
170
|
+
const rawPath = req.path || '/';
|
|
171
|
+
const pathForMatch = normalizePathForTrailingSlash(pluginOpts.trailingSlash, rawPath);
|
|
172
|
+
const method = req.method || 'GET';
|
|
173
|
+
|
|
174
|
+
for (const c of compiled) {
|
|
175
|
+
if (!methodMatches(c, method)) continue;
|
|
176
|
+
if (!pathMatches(c, pathForMatch, rawPath)) continue;
|
|
177
|
+
|
|
178
|
+
let location = c.to;
|
|
179
|
+
if (pluginOpts.preserveQuery && !location.includes('?')) {
|
|
180
|
+
const full = req.originalUrl || req.url || '';
|
|
181
|
+
const qi = full.indexOf('?');
|
|
182
|
+
if (qi !== -1) {
|
|
183
|
+
location += full.slice(qi);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
res.redirect(c.status, location);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
next();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
name: 'redirect',
|
|
194
|
+
version: '1.0.0',
|
|
195
|
+
description: 'Configurable HTTP redirects before file-based routes',
|
|
196
|
+
|
|
197
|
+
register(ctx) {
|
|
198
|
+
if (compiled.length === 0) return;
|
|
199
|
+
ctx.app.use(redirectMiddleware);
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = { redirectPlugin };
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const { attachDbMiddleware } = require('../../src/app-context');
|
|
7
7
|
const { getAllModels } = require('../../core/orm/model');
|
|
8
8
|
const { omit } = require('../../core/orm/utils');
|
|
9
|
+
const { trimUrlPathSlashes } = require('../../core/url-path-normalize');
|
|
9
10
|
|
|
10
11
|
const RESERVED_QUERY_KEYS = new Set(['page', 'perPage', 'sort', 'order', 'include', 'trashed']);
|
|
11
12
|
|
|
@@ -150,7 +151,7 @@ function resolveExposedModels(db, opts) {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
function normalizeBasePath(p) {
|
|
153
|
-
return `/${
|
|
154
|
+
return `/${trimUrlPathSlashes(p)}`;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
function applySoftDeleteScope(query, countQuery, model, trashed) {
|
package/plugins/swagger.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { trimUrlPathSlashes } = require('../core/url-path-normalize');
|
|
6
7
|
const { buildOpenApiDocument } = require('../core/openapi/build-from-api-routes');
|
|
7
8
|
|
|
8
9
|
const PKG = require(path.join(__dirname, '..', 'package.json'));
|
|
@@ -61,7 +62,7 @@ function swaggerPlugin(options = {}) {
|
|
|
61
62
|
serverUrl,
|
|
62
63
|
} = options;
|
|
63
64
|
|
|
64
|
-
const normalizedBase = `/${
|
|
65
|
+
const normalizedBase = `/${trimUrlPathSlashes(basePath)}`;
|
|
65
66
|
const jsonPath = `${normalizedBase}/openapi.json`;
|
|
66
67
|
const uiPath = normalizedBase;
|
|
67
68
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const fs = require('fs/promises');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const { randomBytes } = require('crypto');
|
|
9
|
+
const { trimUrlPathSlashes } = require('../../core/url-path-normalize');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Common MIME → canonical extension (lowercase, with leading dot).
|
|
@@ -38,8 +39,11 @@ const MIME_TO_EXT = {
|
|
|
38
39
|
* @returns {string}
|
|
39
40
|
*/
|
|
40
41
|
function normalizePublicBase(publicBasePath) {
|
|
41
|
-
let p =
|
|
42
|
-
|
|
42
|
+
let p =
|
|
43
|
+
publicBasePath == null || publicBasePath === ''
|
|
44
|
+
? '/uploads'
|
|
45
|
+
: String(publicBasePath).trim();
|
|
46
|
+
p = trimUrlPathSlashes(p);
|
|
43
47
|
if (!p.startsWith('/')) {
|
|
44
48
|
p = `/${p}`;
|
|
45
49
|
}
|