s3db.js 13.4.0 → 13.6.0
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 +25 -10
- package/dist/{s3db.cjs.js → s3db.cjs} +38801 -32446
- package/dist/s3db.cjs.map +1 -0
- package/dist/s3db.es.js +38653 -32291
- package/dist/s3db.es.js.map +1 -1
- package/package.json +218 -22
- package/src/concerns/id.js +90 -6
- package/src/concerns/index.js +2 -1
- package/src/concerns/password-hashing.js +150 -0
- package/src/database.class.js +6 -2
- package/src/plugins/api/auth/basic-auth.js +40 -10
- package/src/plugins/api/auth/index.js +49 -3
- package/src/plugins/api/auth/oauth2-auth.js +171 -0
- package/src/plugins/api/auth/oidc-auth.js +789 -0
- package/src/plugins/api/auth/oidc-client.js +462 -0
- package/src/plugins/api/auth/path-auth-matcher.js +284 -0
- package/src/plugins/api/concerns/event-emitter.js +134 -0
- package/src/plugins/api/concerns/failban-manager.js +651 -0
- package/src/plugins/api/concerns/guards-helpers.js +402 -0
- package/src/plugins/api/concerns/metrics-collector.js +346 -0
- package/src/plugins/api/index.js +510 -57
- package/src/plugins/api/middlewares/failban.js +305 -0
- package/src/plugins/api/middlewares/rate-limit.js +301 -0
- package/src/plugins/api/middlewares/request-id.js +74 -0
- package/src/plugins/api/middlewares/security-headers.js +120 -0
- package/src/plugins/api/middlewares/session-tracking.js +194 -0
- package/src/plugins/api/routes/auth-routes.js +119 -78
- package/src/plugins/api/routes/resource-routes.js +73 -30
- package/src/plugins/api/server.js +1139 -45
- package/src/plugins/api/utils/custom-routes.js +102 -0
- package/src/plugins/api/utils/guards.js +213 -0
- package/src/plugins/api/utils/mime-types.js +154 -0
- package/src/plugins/api/utils/openapi-generator.js +91 -12
- package/src/plugins/api/utils/path-matcher.js +173 -0
- package/src/plugins/api/utils/static-filesystem.js +262 -0
- package/src/plugins/api/utils/static-s3.js +231 -0
- package/src/plugins/api/utils/template-engine.js +188 -0
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
- package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
- package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
- package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
- package/src/plugins/cloud-inventory/index.js +20 -0
- package/src/plugins/cloud-inventory/registry.js +146 -0
- package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
- package/src/plugins/cloud-inventory.plugin.js +1333 -0
- package/src/plugins/concerns/plugin-dependencies.js +62 -2
- package/src/plugins/eventual-consistency/analytics.js +1 -0
- package/src/plugins/eventual-consistency/consolidation.js +2 -2
- package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
- package/src/plugins/eventual-consistency/install.js +2 -2
- package/src/plugins/identity/README.md +335 -0
- package/src/plugins/identity/concerns/mfa-manager.js +204 -0
- package/src/plugins/identity/concerns/password.js +138 -0
- package/src/plugins/identity/concerns/resource-schemas.js +273 -0
- package/src/plugins/identity/concerns/token-generator.js +172 -0
- package/src/plugins/identity/email-service.js +422 -0
- package/src/plugins/identity/index.js +1052 -0
- package/src/plugins/identity/oauth2-server.js +1033 -0
- package/src/plugins/identity/oidc-discovery.js +285 -0
- package/src/plugins/identity/rsa-keys.js +323 -0
- package/src/plugins/identity/server.js +500 -0
- package/src/plugins/identity/session-manager.js +453 -0
- package/src/plugins/identity/ui/layouts/base.js +251 -0
- package/src/plugins/identity/ui/middleware.js +135 -0
- package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
- package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
- package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
- package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
- package/src/plugins/identity/ui/pages/admin/users.js +263 -0
- package/src/plugins/identity/ui/pages/consent.js +262 -0
- package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
- package/src/plugins/identity/ui/pages/login.js +144 -0
- package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
- package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
- package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
- package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
- package/src/plugins/identity/ui/pages/profile.js +361 -0
- package/src/plugins/identity/ui/pages/register.js +226 -0
- package/src/plugins/identity/ui/pages/reset-password.js +128 -0
- package/src/plugins/identity/ui/pages/verify-email.js +172 -0
- package/src/plugins/identity/ui/routes.js +2541 -0
- package/src/plugins/identity/ui/styles/main.css +465 -0
- package/src/plugins/index.js +4 -1
- package/src/plugins/ml/base-model.class.js +65 -16
- package/src/plugins/ml/classification-model.class.js +1 -1
- package/src/plugins/ml/timeseries-model.class.js +3 -1
- package/src/plugins/ml.plugin.js +584 -31
- package/src/plugins/shared/error-handler.js +147 -0
- package/src/plugins/shared/index.js +9 -0
- package/src/plugins/shared/middlewares/compression.js +117 -0
- package/src/plugins/shared/middlewares/cors.js +49 -0
- package/src/plugins/shared/middlewares/index.js +11 -0
- package/src/plugins/shared/middlewares/logging.js +54 -0
- package/src/plugins/shared/middlewares/rate-limit.js +73 -0
- package/src/plugins/shared/middlewares/security.js +158 -0
- package/src/plugins/shared/response-formatter.js +264 -0
- package/src/plugins/state-machine.plugin.js +57 -2
- package/src/resource.class.js +140 -12
- package/src/schema.class.js +30 -1
- package/src/validator.class.js +57 -6
- package/dist/s3db.cjs.js.map +0 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Routes Utilities
|
|
3
|
+
*
|
|
4
|
+
* Parse and mount custom routes defined in resources or plugins
|
|
5
|
+
* Inspired by moleculer-js route syntax
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { asyncHandler } from './error-handler.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse route definition from key
|
|
12
|
+
* @param {string} key - Route key (e.g., 'GET /users', 'POST /custom/:id/action')
|
|
13
|
+
* @returns {Object} { method, path }
|
|
14
|
+
*/
|
|
15
|
+
export function parseRouteKey(key) {
|
|
16
|
+
const match = key.match(/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(.+)$/i);
|
|
17
|
+
|
|
18
|
+
if (!match) {
|
|
19
|
+
throw new Error(`Invalid route key format: "${key}". Expected format: "METHOD /path"`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
method: match[1].toUpperCase(),
|
|
24
|
+
path: match[2]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Mount custom routes on Hono app
|
|
30
|
+
* @param {Object} app - Hono app instance
|
|
31
|
+
* @param {Object} routes - Routes object { 'METHOD /path': handler }
|
|
32
|
+
* @param {Object} context - Context to pass to handlers (resource, database, etc.)
|
|
33
|
+
* @param {boolean} verbose - Enable verbose logging
|
|
34
|
+
*/
|
|
35
|
+
export function mountCustomRoutes(app, routes, context = {}, verbose = false) {
|
|
36
|
+
if (!routes || typeof routes !== 'object') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const [key, handler] of Object.entries(routes)) {
|
|
41
|
+
try {
|
|
42
|
+
const { method, path } = parseRouteKey(key);
|
|
43
|
+
|
|
44
|
+
// Wrap handler with async error handler and context
|
|
45
|
+
const wrappedHandler = asyncHandler(async (c) => {
|
|
46
|
+
// Inject context into Hono context
|
|
47
|
+
c.set('customRouteContext', context);
|
|
48
|
+
|
|
49
|
+
// Call user handler with Hono context
|
|
50
|
+
return await handler(c);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Mount route
|
|
54
|
+
app.on(method, path, wrappedHandler);
|
|
55
|
+
|
|
56
|
+
if (verbose) {
|
|
57
|
+
console.log(`[Custom Routes] Mounted ${method} ${path}`);
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error(`[Custom Routes] Error mounting route "${key}":`, err.message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validate custom routes object
|
|
67
|
+
* @param {Object} routes - Routes to validate
|
|
68
|
+
* @returns {Array} Array of validation errors
|
|
69
|
+
*/
|
|
70
|
+
export function validateCustomRoutes(routes) {
|
|
71
|
+
const errors = [];
|
|
72
|
+
|
|
73
|
+
if (!routes || typeof routes !== 'object') {
|
|
74
|
+
return errors;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const [key, handler] of Object.entries(routes)) {
|
|
78
|
+
// Validate key format
|
|
79
|
+
try {
|
|
80
|
+
parseRouteKey(key);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
errors.push({ key, error: err.message });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validate handler is a function
|
|
87
|
+
if (typeof handler !== 'function') {
|
|
88
|
+
errors.push({
|
|
89
|
+
key,
|
|
90
|
+
error: `Handler must be a function, got ${typeof handler}`
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return errors;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default {
|
|
99
|
+
parseRouteKey,
|
|
100
|
+
mountCustomRoutes,
|
|
101
|
+
validateCustomRoutes
|
|
102
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guards - Authorization checks for resources
|
|
3
|
+
*
|
|
4
|
+
* Guards determine if a user can perform an operation on a resource.
|
|
5
|
+
* Supports: functions, scopes, roles, and combined logic.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if user passes guard
|
|
10
|
+
* @param {Object} user - Authenticated user object
|
|
11
|
+
* @param {Function|string|Array|Object|null} guard - Guard configuration
|
|
12
|
+
* @param {Object} context - Additional context (data, resourceName, operation)
|
|
13
|
+
* @returns {boolean} True if authorized
|
|
14
|
+
*/
|
|
15
|
+
export function checkGuard(user, guard, context = {}) {
|
|
16
|
+
// No guard = public access
|
|
17
|
+
if (!guard) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// No user = unauthorized (unless guard explicitly allows)
|
|
22
|
+
if (!user && guard !== true) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Guard is boolean
|
|
27
|
+
if (typeof guard === 'boolean') {
|
|
28
|
+
return guard;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Guard is function: (user, context) => boolean
|
|
32
|
+
if (typeof guard === 'function') {
|
|
33
|
+
try {
|
|
34
|
+
return guard(user, context);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error('[Guards] Error executing guard function:', err);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Guard is string: scope name (e.g., 'read:users')
|
|
42
|
+
if (typeof guard === 'string') {
|
|
43
|
+
return hasScope(user, guard);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Guard is array: any scope matches (OR logic)
|
|
47
|
+
if (Array.isArray(guard)) {
|
|
48
|
+
return guard.some(scope => hasScope(user, scope));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Guard is object: check properties
|
|
52
|
+
if (typeof guard === 'object') {
|
|
53
|
+
// Check role
|
|
54
|
+
if (guard.role) {
|
|
55
|
+
if (Array.isArray(guard.role)) {
|
|
56
|
+
if (!guard.role.includes(user.role)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
} else if (user.role !== guard.role) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check scopes (all must match - AND logic)
|
|
65
|
+
if (guard.scopes) {
|
|
66
|
+
const requiredScopes = Array.isArray(guard.scopes) ? guard.scopes : [guard.scopes];
|
|
67
|
+
if (!requiredScopes.every(scope => hasScope(user, scope))) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check custom function
|
|
73
|
+
if (guard.check && typeof guard.check === 'function') {
|
|
74
|
+
try {
|
|
75
|
+
return guard.check(user, context);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error('[Guards] Error executing guard.check function:', err);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Unknown guard type = deny
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if user has specific scope
|
|
91
|
+
* @param {Object} user - User object
|
|
92
|
+
* @param {string} scope - Scope name (e.g., 'read:users')
|
|
93
|
+
* @returns {boolean} True if user has scope
|
|
94
|
+
*/
|
|
95
|
+
export function hasScope(user, scope) {
|
|
96
|
+
if (!user || !user.scopes) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!Array.isArray(user.scopes)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Direct match
|
|
105
|
+
if (user.scopes.includes(scope)) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Wildcard match (e.g., 'admin:*' matches 'admin:users')
|
|
110
|
+
const wildcards = user.scopes.filter(s => s.endsWith(':*'));
|
|
111
|
+
for (const wildcard of wildcards) {
|
|
112
|
+
const prefix = wildcard.slice(0, -2); // Remove ':*'
|
|
113
|
+
if (scope.startsWith(prefix + ':')) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Super admin wildcard ('*' matches everything)
|
|
119
|
+
if (user.scopes.includes('*')) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get operation-specific guard from guards config
|
|
128
|
+
* @param {Object} guards - Guards configuration
|
|
129
|
+
* @param {string} operation - Operation name ('list', 'get', 'create', 'update', 'delete')
|
|
130
|
+
* @returns {Function|string|Array|Object|null} Guard for operation
|
|
131
|
+
*/
|
|
132
|
+
export function getOperationGuard(guards, operation) {
|
|
133
|
+
if (!guards) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// If guards is a function/string/array, apply to all operations
|
|
138
|
+
if (typeof guards === 'function' || typeof guards === 'string' || Array.isArray(guards)) {
|
|
139
|
+
return guards;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// If guards is object, get operation-specific guard
|
|
143
|
+
if (typeof guards === 'object') {
|
|
144
|
+
// Check for specific operation
|
|
145
|
+
if (guards[operation] !== undefined) {
|
|
146
|
+
return guards[operation];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Fallback to 'all' or default
|
|
150
|
+
if (guards.all !== undefined) {
|
|
151
|
+
return guards.all;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Map operation aliases
|
|
155
|
+
const aliases = {
|
|
156
|
+
list: 'read',
|
|
157
|
+
get: 'read',
|
|
158
|
+
create: 'write',
|
|
159
|
+
update: 'write',
|
|
160
|
+
delete: 'write'
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (aliases[operation] && guards[aliases[operation]] !== undefined) {
|
|
164
|
+
return guards[aliases[operation]];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Create guard middleware for Hono
|
|
173
|
+
* @param {Object} guards - Guards configuration
|
|
174
|
+
* @param {string} operation - Operation name
|
|
175
|
+
* @returns {Function} Hono middleware
|
|
176
|
+
*/
|
|
177
|
+
export function guardMiddleware(guards, operation) {
|
|
178
|
+
return async (c, next) => {
|
|
179
|
+
const user = c.get('user');
|
|
180
|
+
const guard = getOperationGuard(guards, operation);
|
|
181
|
+
|
|
182
|
+
// Check guard
|
|
183
|
+
const authorized = checkGuard(user, guard, {
|
|
184
|
+
operation,
|
|
185
|
+
resourceName: c.req.param('resource'),
|
|
186
|
+
data: c.req.method !== 'GET' ? await c.req.json().catch(() => ({})) : {}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (!authorized) {
|
|
190
|
+
return c.json({
|
|
191
|
+
success: false,
|
|
192
|
+
error: {
|
|
193
|
+
message: 'Forbidden: Insufficient permissions',
|
|
194
|
+
code: 'FORBIDDEN',
|
|
195
|
+
details: {
|
|
196
|
+
operation,
|
|
197
|
+
user: user ? { id: user.id, role: user.role } : null
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
_status: 403
|
|
201
|
+
}, 403);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await next();
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export default {
|
|
209
|
+
checkGuard,
|
|
210
|
+
hasScope,
|
|
211
|
+
getOperationGuard,
|
|
212
|
+
guardMiddleware
|
|
213
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MIME Type Detection
|
|
3
|
+
*
|
|
4
|
+
* Lightweight MIME type detection based on file extensions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Common MIME types mapped by extension
|
|
9
|
+
*/
|
|
10
|
+
const MIME_TYPES = {
|
|
11
|
+
// Text
|
|
12
|
+
'txt': 'text/plain',
|
|
13
|
+
'html': 'text/html',
|
|
14
|
+
'htm': 'text/html',
|
|
15
|
+
'css': 'text/css',
|
|
16
|
+
'js': 'text/javascript',
|
|
17
|
+
'mjs': 'text/javascript',
|
|
18
|
+
'json': 'application/json',
|
|
19
|
+
'xml': 'application/xml',
|
|
20
|
+
'csv': 'text/csv',
|
|
21
|
+
'md': 'text/markdown',
|
|
22
|
+
|
|
23
|
+
// Images
|
|
24
|
+
'jpg': 'image/jpeg',
|
|
25
|
+
'jpeg': 'image/jpeg',
|
|
26
|
+
'png': 'image/png',
|
|
27
|
+
'gif': 'image/gif',
|
|
28
|
+
'webp': 'image/webp',
|
|
29
|
+
'svg': 'image/svg+xml',
|
|
30
|
+
'ico': 'image/x-icon',
|
|
31
|
+
'bmp': 'image/bmp',
|
|
32
|
+
'tiff': 'image/tiff',
|
|
33
|
+
'tif': 'image/tiff',
|
|
34
|
+
|
|
35
|
+
// Audio
|
|
36
|
+
'mp3': 'audio/mpeg',
|
|
37
|
+
'wav': 'audio/wav',
|
|
38
|
+
'ogg': 'audio/ogg',
|
|
39
|
+
'flac': 'audio/flac',
|
|
40
|
+
'm4a': 'audio/mp4',
|
|
41
|
+
|
|
42
|
+
// Video
|
|
43
|
+
'mp4': 'video/mp4',
|
|
44
|
+
'webm': 'video/webm',
|
|
45
|
+
'ogv': 'video/ogg',
|
|
46
|
+
'avi': 'video/x-msvideo',
|
|
47
|
+
'mov': 'video/quicktime',
|
|
48
|
+
'mkv': 'video/x-matroska',
|
|
49
|
+
|
|
50
|
+
// Documents
|
|
51
|
+
'pdf': 'application/pdf',
|
|
52
|
+
'doc': 'application/msword',
|
|
53
|
+
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
54
|
+
'xls': 'application/vnd.ms-excel',
|
|
55
|
+
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
56
|
+
'ppt': 'application/vnd.ms-powerpoint',
|
|
57
|
+
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
58
|
+
|
|
59
|
+
// Archives
|
|
60
|
+
'zip': 'application/zip',
|
|
61
|
+
'tar': 'application/x-tar',
|
|
62
|
+
'gz': 'application/gzip',
|
|
63
|
+
'bz2': 'application/x-bzip2',
|
|
64
|
+
'7z': 'application/x-7z-compressed',
|
|
65
|
+
'rar': 'application/vnd.rar',
|
|
66
|
+
|
|
67
|
+
// Fonts
|
|
68
|
+
'ttf': 'font/ttf',
|
|
69
|
+
'otf': 'font/otf',
|
|
70
|
+
'woff': 'font/woff',
|
|
71
|
+
'woff2': 'font/woff2',
|
|
72
|
+
'eot': 'application/vnd.ms-fontobject',
|
|
73
|
+
|
|
74
|
+
// Application
|
|
75
|
+
'wasm': 'application/wasm',
|
|
76
|
+
'bin': 'application/octet-stream'
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get MIME type from filename
|
|
81
|
+
* @param {string} filename - Filename with extension
|
|
82
|
+
* @returns {string} MIME type (defaults to 'application/octet-stream')
|
|
83
|
+
* @example
|
|
84
|
+
* getMimeType('image.png') // 'image/png'
|
|
85
|
+
* getMimeType('document.pdf') // 'application/pdf'
|
|
86
|
+
* getMimeType('unknown.xyz') // 'application/octet-stream'
|
|
87
|
+
*/
|
|
88
|
+
export function getMimeType(filename) {
|
|
89
|
+
if (!filename || typeof filename !== 'string') {
|
|
90
|
+
return 'application/octet-stream';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Extract extension (lowercase)
|
|
94
|
+
const ext = filename.split('.').pop().toLowerCase();
|
|
95
|
+
|
|
96
|
+
return MIME_TYPES[ext] || 'application/octet-stream';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if MIME type is compressible
|
|
101
|
+
* @param {string} mimeType - MIME type
|
|
102
|
+
* @returns {boolean} True if compressible
|
|
103
|
+
*/
|
|
104
|
+
export function isCompressible(mimeType) {
|
|
105
|
+
if (!mimeType) return false;
|
|
106
|
+
|
|
107
|
+
// Text-based content is compressible
|
|
108
|
+
if (mimeType.startsWith('text/')) return true;
|
|
109
|
+
if (mimeType.includes('javascript')) return true;
|
|
110
|
+
if (mimeType.includes('json')) return true;
|
|
111
|
+
if (mimeType.includes('xml')) return true;
|
|
112
|
+
if (mimeType.includes('svg')) return true;
|
|
113
|
+
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get charset for MIME type
|
|
119
|
+
* @param {string} mimeType - MIME type
|
|
120
|
+
* @returns {string|null} Charset or null
|
|
121
|
+
*/
|
|
122
|
+
export function getCharset(mimeType) {
|
|
123
|
+
if (!mimeType) return null;
|
|
124
|
+
|
|
125
|
+
// Text types should have UTF-8 charset
|
|
126
|
+
if (mimeType.startsWith('text/')) return 'utf-8';
|
|
127
|
+
if (mimeType.includes('javascript')) return 'utf-8';
|
|
128
|
+
if (mimeType.includes('json')) return 'utf-8';
|
|
129
|
+
if (mimeType.includes('xml')) return 'utf-8';
|
|
130
|
+
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Build complete Content-Type header
|
|
136
|
+
* @param {string} filename - Filename
|
|
137
|
+
* @returns {string} Complete Content-Type header
|
|
138
|
+
* @example
|
|
139
|
+
* getContentType('file.html') // 'text/html; charset=utf-8'
|
|
140
|
+
* getContentType('image.png') // 'image/png'
|
|
141
|
+
*/
|
|
142
|
+
export function getContentType(filename) {
|
|
143
|
+
const mimeType = getMimeType(filename);
|
|
144
|
+
const charset = getCharset(mimeType);
|
|
145
|
+
|
|
146
|
+
return charset ? `${mimeType}; charset=${charset}` : mimeType;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default {
|
|
150
|
+
getMimeType,
|
|
151
|
+
isCompressible,
|
|
152
|
+
getCharset,
|
|
153
|
+
getContentType
|
|
154
|
+
};
|
|
@@ -185,7 +185,20 @@ function generateResourceSchema(resource) {
|
|
|
185
185
|
*/
|
|
186
186
|
function generateResourcePaths(resource, version, config = {}) {
|
|
187
187
|
const resourceName = resource.name;
|
|
188
|
-
|
|
188
|
+
|
|
189
|
+
// Determine version prefix (same logic as server.js)
|
|
190
|
+
let versionPrefixConfig = config.versionPrefix !== undefined ? config.versionPrefix : false;
|
|
191
|
+
|
|
192
|
+
let prefix = '';
|
|
193
|
+
if (versionPrefixConfig === true) {
|
|
194
|
+
prefix = version;
|
|
195
|
+
} else if (versionPrefixConfig === false) {
|
|
196
|
+
prefix = '';
|
|
197
|
+
} else if (typeof versionPrefixConfig === 'string') {
|
|
198
|
+
prefix = versionPrefixConfig;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const basePath = prefix ? `/${prefix}/${resourceName}` : `/${resourceName}`;
|
|
189
202
|
const schema = generateResourceSchema(resource);
|
|
190
203
|
const methods = config.methods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
191
204
|
const authMethods = config.auth || [];
|
|
@@ -820,11 +833,14 @@ The response includes pagination metadata in the \`pagination\` object with tota
|
|
|
820
833
|
* @param {Object} relationConfig - Relation configuration
|
|
821
834
|
* @param {string} version - Resource version
|
|
822
835
|
* @param {Object} relatedSchema - OpenAPI schema for related resource
|
|
836
|
+
* @param {string} versionPrefix - Version prefix to use (empty string for no prefix)
|
|
823
837
|
* @returns {Object} OpenAPI paths for relation
|
|
824
838
|
*/
|
|
825
|
-
function generateRelationalPaths(resource, relationName, relationConfig, version, relatedSchema) {
|
|
839
|
+
function generateRelationalPaths(resource, relationName, relationConfig, version, relatedSchema, versionPrefix = '') {
|
|
826
840
|
const resourceName = resource.name;
|
|
827
|
-
const basePath =
|
|
841
|
+
const basePath = versionPrefix
|
|
842
|
+
? `/${versionPrefix}/${resourceName}/{id}/${relationName}`
|
|
843
|
+
: `/${resourceName}/{id}/${relationName}`;
|
|
828
844
|
const relatedResourceName = relationConfig.resource;
|
|
829
845
|
const isToMany = relationConfig.type === 'hasMany' || relationConfig.type === 'belongsToMany';
|
|
830
846
|
|
|
@@ -933,14 +949,22 @@ export function generateOpenAPISpec(database, config = {}) {
|
|
|
933
949
|
description = 'Auto-generated REST API documentation for s3db.js resources',
|
|
934
950
|
serverUrl = 'http://localhost:3000',
|
|
935
951
|
auth = {},
|
|
936
|
-
resources: resourceConfigs = {}
|
|
952
|
+
resources: resourceConfigs = {},
|
|
953
|
+
versionPrefix: globalVersionPrefix
|
|
937
954
|
} = config;
|
|
938
955
|
|
|
939
956
|
// Build resources table for description
|
|
940
957
|
const resourcesTableRows = [];
|
|
941
958
|
for (const [name, resource] of Object.entries(database.resources)) {
|
|
959
|
+
const rawConfig = resourceConfigs[name];
|
|
960
|
+
|
|
961
|
+
// Skip resources explicitly disabled
|
|
962
|
+
if (rawConfig?.enabled === false) {
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
|
|
942
966
|
// Skip plugin resources unless explicitly configured
|
|
943
|
-
if (name.startsWith('plg_') && !
|
|
967
|
+
if (name.startsWith('plg_') && !rawConfig) {
|
|
944
968
|
continue;
|
|
945
969
|
}
|
|
946
970
|
|
|
@@ -950,7 +974,31 @@ export function generateOpenAPISpec(database, config = {}) {
|
|
|
950
974
|
? resourceDescription.resource
|
|
951
975
|
: resourceDescription || 'No description';
|
|
952
976
|
|
|
953
|
-
|
|
977
|
+
// Check version prefix for this resource (same logic as server.js)
|
|
978
|
+
const resourceConfig = rawConfig && typeof rawConfig === 'object' ? rawConfig : {};
|
|
979
|
+
let versionPrefixConfig;
|
|
980
|
+
if (resourceConfig.versionPrefix !== undefined) {
|
|
981
|
+
versionPrefixConfig = resourceConfig.versionPrefix;
|
|
982
|
+
} else if (resource.config && resource.config.versionPrefix !== undefined) {
|
|
983
|
+
versionPrefixConfig = resource.config.versionPrefix;
|
|
984
|
+
} else if (globalVersionPrefix !== undefined) {
|
|
985
|
+
versionPrefixConfig = globalVersionPrefix;
|
|
986
|
+
} else {
|
|
987
|
+
versionPrefixConfig = false; // Default to no prefix
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
let prefix = '';
|
|
991
|
+
if (versionPrefixConfig === true) {
|
|
992
|
+
prefix = version;
|
|
993
|
+
} else if (versionPrefixConfig === false) {
|
|
994
|
+
prefix = '';
|
|
995
|
+
} else if (typeof versionPrefixConfig === 'string') {
|
|
996
|
+
prefix = versionPrefixConfig;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const basePath = prefix ? `/${prefix}/${name}` : `/${name}`;
|
|
1000
|
+
|
|
1001
|
+
resourcesTableRows.push(`| ${name} | ${descText} | \`${basePath}\` |`);
|
|
954
1002
|
}
|
|
955
1003
|
|
|
956
1004
|
// Build enhanced description with resources table
|
|
@@ -1070,13 +1118,19 @@ For detailed information about each endpoint, see the sections below.`;
|
|
|
1070
1118
|
const relationsPlugin = database.plugins?.relation || database.plugins?.RelationPlugin || null;
|
|
1071
1119
|
|
|
1072
1120
|
for (const [name, resource] of Object.entries(resources)) {
|
|
1121
|
+
const rawConfig = resourceConfigs[name];
|
|
1122
|
+
|
|
1123
|
+
if (rawConfig?.enabled === false) {
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1073
1127
|
// Skip plugin resources unless explicitly configured
|
|
1074
|
-
if (name.startsWith('plg_') && !
|
|
1128
|
+
if (name.startsWith('plg_') && !rawConfig) {
|
|
1075
1129
|
continue;
|
|
1076
1130
|
}
|
|
1077
1131
|
|
|
1078
1132
|
// Get resource configuration
|
|
1079
|
-
const
|
|
1133
|
+
const resourceConfig = rawConfig && typeof rawConfig === 'object' ? { ...rawConfig } : {
|
|
1080
1134
|
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
1081
1135
|
auth: false
|
|
1082
1136
|
};
|
|
@@ -1084,8 +1138,32 @@ For detailed information about each endpoint, see the sections below.`;
|
|
|
1084
1138
|
// Determine version
|
|
1085
1139
|
const version = resource.config?.currentVersion || resource.version || 'v1';
|
|
1086
1140
|
|
|
1141
|
+
// Determine version prefix (same logic as server.js)
|
|
1142
|
+
let versionPrefixConfig;
|
|
1143
|
+
if (resourceConfig.versionPrefix !== undefined) {
|
|
1144
|
+
versionPrefixConfig = resourceConfig.versionPrefix;
|
|
1145
|
+
} else if (resource.config && resource.config.versionPrefix !== undefined) {
|
|
1146
|
+
versionPrefixConfig = resource.config.versionPrefix;
|
|
1147
|
+
} else if (globalVersionPrefix !== undefined) {
|
|
1148
|
+
versionPrefixConfig = globalVersionPrefix;
|
|
1149
|
+
} else {
|
|
1150
|
+
versionPrefixConfig = false;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
let prefix = '';
|
|
1154
|
+
if (versionPrefixConfig === true) {
|
|
1155
|
+
prefix = version;
|
|
1156
|
+
} else if (versionPrefixConfig === false) {
|
|
1157
|
+
prefix = '';
|
|
1158
|
+
} else if (typeof versionPrefixConfig === 'string') {
|
|
1159
|
+
prefix = versionPrefixConfig;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1087
1162
|
// Generate paths
|
|
1088
|
-
const paths = generateResourcePaths(resource, version,
|
|
1163
|
+
const paths = generateResourcePaths(resource, version, {
|
|
1164
|
+
...resourceConfig,
|
|
1165
|
+
versionPrefix: versionPrefixConfig
|
|
1166
|
+
});
|
|
1089
1167
|
|
|
1090
1168
|
// Merge paths
|
|
1091
1169
|
Object.assign(spec.paths, paths);
|
|
@@ -1115,7 +1193,7 @@ For detailed information about each endpoint, see the sections below.`;
|
|
|
1115
1193
|
}
|
|
1116
1194
|
|
|
1117
1195
|
// Check if relation should be exposed (default: yes)
|
|
1118
|
-
const exposeRelation =
|
|
1196
|
+
const exposeRelation = resourceConfig?.relations?.[relationName]?.expose !== false;
|
|
1119
1197
|
if (!exposeRelation) {
|
|
1120
1198
|
continue;
|
|
1121
1199
|
}
|
|
@@ -1128,13 +1206,14 @@ For detailed information about each endpoint, see the sections below.`;
|
|
|
1128
1206
|
|
|
1129
1207
|
const relatedSchema = generateResourceSchema(relatedResource);
|
|
1130
1208
|
|
|
1131
|
-
// Generate relational paths
|
|
1209
|
+
// Generate relational paths (using the same prefix calculated above)
|
|
1132
1210
|
const relationalPaths = generateRelationalPaths(
|
|
1133
1211
|
resource,
|
|
1134
1212
|
relationName,
|
|
1135
1213
|
relationConfig,
|
|
1136
1214
|
version,
|
|
1137
|
-
relatedSchema
|
|
1215
|
+
relatedSchema,
|
|
1216
|
+
prefix
|
|
1138
1217
|
);
|
|
1139
1218
|
|
|
1140
1219
|
// Merge relational paths
|