s3db.js 13.3.1 → 13.5.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 +34 -10
- package/dist/s3db.cjs.js +12208 -11139
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +12209 -11140
- package/dist/s3db.es.js.map +1 -1
- package/package.json +16 -3
- package/src/database.class.js +2 -2
- package/src/plugins/api/auth/basic-auth.js +17 -9
- package/src/plugins/api/index.js +23 -19
- package/src/plugins/api/routes/auth-routes.js +100 -79
- package/src/plugins/api/routes/resource-routes.js +3 -2
- package/src/plugins/api/server.js +176 -5
- package/src/plugins/api/utils/custom-routes.js +102 -0
- package/src/plugins/api/utils/openapi-generator.js +52 -6
- package/src/plugins/audit.plugin.js +427 -0
- package/src/plugins/concerns/plugin-dependencies.js +1 -1
- package/src/plugins/costs.plugin.js +524 -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/fulltext.plugin.js +484 -0
- package/src/plugins/metrics.plugin.js +575 -0
- package/src/plugins/ml/base-model.class.js +33 -9
- package/src/plugins/ml.plugin.js +474 -13
- package/src/plugins/queue-consumer.plugin.js +607 -19
- package/src/plugins/state-machine.plugin.js +187 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "s3db.js",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.5.1",
|
|
4
4
|
"description": "Use AWS S3, the world's most reliable document storage, as a database with this ORM.",
|
|
5
5
|
"main": "dist/s3db.cjs.js",
|
|
6
6
|
"module": "dist/s3db.es.js",
|
|
@@ -87,7 +87,9 @@
|
|
|
87
87
|
"@aws-sdk/client-sqs": "^3.0.0",
|
|
88
88
|
"@google-cloud/bigquery": "^7.0.0",
|
|
89
89
|
"@hono/node-server": "^1.0.0",
|
|
90
|
-
"@hono/swagger-ui": "^0.5.
|
|
90
|
+
"@hono/swagger-ui": "^0.5.2",
|
|
91
|
+
"@libsql/client": "^0.14.0",
|
|
92
|
+
"@planetscale/database": "^1.0.0",
|
|
91
93
|
"@tensorflow/tfjs-node": "^4.0.0",
|
|
92
94
|
"amqplib": "^0.10.8",
|
|
93
95
|
"hono": "^4.0.0",
|
|
@@ -107,6 +109,12 @@
|
|
|
107
109
|
"@hono/swagger-ui": {
|
|
108
110
|
"optional": true
|
|
109
111
|
},
|
|
112
|
+
"@libsql/client": {
|
|
113
|
+
"optional": true
|
|
114
|
+
},
|
|
115
|
+
"@planetscale/database": {
|
|
116
|
+
"optional": true
|
|
117
|
+
},
|
|
110
118
|
"@tensorflow/tfjs-node": {
|
|
111
119
|
"optional": true
|
|
112
120
|
},
|
|
@@ -128,6 +136,10 @@
|
|
|
128
136
|
"@babel/core": "^7.28.4",
|
|
129
137
|
"@babel/preset-env": "^7.28.3",
|
|
130
138
|
"@google-cloud/bigquery": "^7.9.4",
|
|
139
|
+
"@hono/node-server": "^1.13.7",
|
|
140
|
+
"@hono/swagger-ui": "^0.5.2",
|
|
141
|
+
"@libsql/client": "^0.14.0",
|
|
142
|
+
"@planetscale/database": "^1.19.0",
|
|
131
143
|
"@rollup/plugin-commonjs": "^28.0.8",
|
|
132
144
|
"@rollup/plugin-json": "^6.1.0",
|
|
133
145
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
@@ -143,6 +155,7 @@
|
|
|
143
155
|
"cli-table3": "^0.6.5",
|
|
144
156
|
"commander": "^14.0.1",
|
|
145
157
|
"esbuild": "^0.25.11",
|
|
158
|
+
"hono": "^4.7.11",
|
|
146
159
|
"inquirer": "^12.10.0",
|
|
147
160
|
"jest": "^30.2.0",
|
|
148
161
|
"node-cron": "^4.2.1",
|
|
@@ -185,7 +198,7 @@
|
|
|
185
198
|
"validate:types": "pnpm run test:ts && echo 'TypeScript definitions are valid!'",
|
|
186
199
|
"test:ts:runtime": "tsx tests/typescript/types-runtime-simple.ts",
|
|
187
200
|
"test:mcp": "node mcp/entrypoint.js --help",
|
|
188
|
-
"install:peers": "pnpm add -D @aws-sdk/client-sqs @google-cloud/bigquery amqplib node-cron pg",
|
|
201
|
+
"install:peers": "pnpm add -D @aws-sdk/client-sqs @google-cloud/bigquery @hono/node-server @hono/swagger-ui @libsql/client @planetscale/database @tensorflow/tfjs-node amqplib hono node-cron pg",
|
|
189
202
|
"install:peers:script": "./scripts/install-peer-deps.sh"
|
|
190
203
|
}
|
|
191
204
|
}
|
package/src/database.class.js
CHANGED
|
@@ -1091,7 +1091,7 @@ export class Database extends EventEmitter {
|
|
|
1091
1091
|
if (!existingVersionData || existingVersionData.hash !== newHash) {
|
|
1092
1092
|
await this.uploadMetadataFile();
|
|
1093
1093
|
}
|
|
1094
|
-
this.emit("
|
|
1094
|
+
this.emit("db:resource-updated", name);
|
|
1095
1095
|
return existingResource;
|
|
1096
1096
|
}
|
|
1097
1097
|
const existingMetadata = this.savedMetadata?.resources?.[name];
|
|
@@ -1131,7 +1131,7 @@ export class Database extends EventEmitter {
|
|
|
1131
1131
|
}
|
|
1132
1132
|
|
|
1133
1133
|
await this.uploadMetadataFile();
|
|
1134
|
-
this.emit("
|
|
1134
|
+
this.emit("db:resource-created", name);
|
|
1135
1135
|
return resource;
|
|
1136
1136
|
}
|
|
1137
1137
|
|
|
@@ -67,7 +67,9 @@ async function verifyPassword(inputPassword, storedPassword, passphrase) {
|
|
|
67
67
|
* Create Basic Auth middleware
|
|
68
68
|
* @param {Object} options - Basic Auth options
|
|
69
69
|
* @param {string} options.realm - Authentication realm (default: 'API Access')
|
|
70
|
-
* @param {Object} options.
|
|
70
|
+
* @param {Object} options.authResource - Resource for credential validation
|
|
71
|
+
* @param {string} options.usernameField - Field name for username (default: 'email')
|
|
72
|
+
* @param {string} options.passwordField - Field name for password (default: 'password')
|
|
71
73
|
* @param {string} options.passphrase - Passphrase for password decryption
|
|
72
74
|
* @param {boolean} options.optional - If true, allows requests without auth
|
|
73
75
|
* @returns {Function} Hono middleware
|
|
@@ -75,13 +77,15 @@ async function verifyPassword(inputPassword, storedPassword, passphrase) {
|
|
|
75
77
|
export function basicAuth(options = {}) {
|
|
76
78
|
const {
|
|
77
79
|
realm = 'API Access',
|
|
78
|
-
|
|
80
|
+
authResource,
|
|
81
|
+
usernameField = 'email',
|
|
82
|
+
passwordField = 'password',
|
|
79
83
|
passphrase = 'secret',
|
|
80
84
|
optional = false
|
|
81
85
|
} = options;
|
|
82
86
|
|
|
83
|
-
if (!
|
|
84
|
-
throw new Error('
|
|
87
|
+
if (!authResource) {
|
|
88
|
+
throw new Error('authResource is required for Basic authentication');
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
return async (c, next) => {
|
|
@@ -107,9 +111,10 @@ export function basicAuth(options = {}) {
|
|
|
107
111
|
|
|
108
112
|
const { username, password } = credentials;
|
|
109
113
|
|
|
110
|
-
// Query user by username
|
|
114
|
+
// Query user by configured username field
|
|
111
115
|
try {
|
|
112
|
-
const
|
|
116
|
+
const queryFilter = { [usernameField]: username };
|
|
117
|
+
const users = await authResource.query(queryFilter);
|
|
113
118
|
|
|
114
119
|
if (!users || users.length === 0) {
|
|
115
120
|
c.header('WWW-Authenticate', `Basic realm="${realm}"`);
|
|
@@ -119,14 +124,17 @@ export function basicAuth(options = {}) {
|
|
|
119
124
|
|
|
120
125
|
const user = users[0];
|
|
121
126
|
|
|
122
|
-
if
|
|
127
|
+
// Check if user is active (if field exists)
|
|
128
|
+
if (user.active !== undefined && !user.active) {
|
|
123
129
|
c.header('WWW-Authenticate', `Basic realm="${realm}"`);
|
|
124
130
|
const response = unauthorized('User account is inactive');
|
|
125
131
|
return c.json(response, response._status);
|
|
126
132
|
}
|
|
127
133
|
|
|
128
|
-
// Verify password
|
|
129
|
-
|
|
134
|
+
// Verify password using configured password field
|
|
135
|
+
// Schema handles encryption/decryption for 'secret' field types
|
|
136
|
+
const storedPassword = user[passwordField];
|
|
137
|
+
const isValid = storedPassword === password;
|
|
130
138
|
|
|
131
139
|
if (!isValid) {
|
|
132
140
|
c.header('WWW-Authenticate', `Basic realm="${realm}"`);
|
package/src/plugins/api/index.js
CHANGED
|
@@ -56,6 +56,10 @@ export class ApiPlugin extends Plugin {
|
|
|
56
56
|
host: options.host || '0.0.0.0',
|
|
57
57
|
verbose: options.verbose || false,
|
|
58
58
|
|
|
59
|
+
// Version prefix configuration (global default)
|
|
60
|
+
// Can be: true (use resource version), false (no prefix - DEFAULT), or string (custom prefix like 'api/v1')
|
|
61
|
+
versionPrefix: options.versionPrefix !== undefined ? options.versionPrefix : false,
|
|
62
|
+
|
|
59
63
|
docs: {
|
|
60
64
|
enabled: options.docs?.enabled !== false && options.docsEnabled !== false, // Enable by default
|
|
61
65
|
ui: options.docs?.ui || 'redoc', // 'swagger' or 'redoc' (redoc is prettier!)
|
|
@@ -64,26 +68,27 @@ export class ApiPlugin extends Plugin {
|
|
|
64
68
|
description: options.docs?.description || options.apiDescription || 'Auto-generated REST API for s3db.js resources'
|
|
65
69
|
},
|
|
66
70
|
|
|
67
|
-
// Authentication configuration
|
|
68
|
-
auth: {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
realm: options.auth?.basic?.realm || 'API Access'
|
|
81
|
-
}
|
|
71
|
+
// Authentication configuration (driver-based)
|
|
72
|
+
auth: options.auth ? {
|
|
73
|
+
driver: options.auth.driver || null, // 'jwt' or 'basic'
|
|
74
|
+
resource: options.auth.resource || 'users', // Resource that manages auth
|
|
75
|
+
usernameField: options.auth.usernameField || 'email', // Default: email
|
|
76
|
+
passwordField: options.auth.passwordField || 'password', // Default: password
|
|
77
|
+
config: options.auth.config || {} // Driver-specific config
|
|
78
|
+
} : {
|
|
79
|
+
driver: null,
|
|
80
|
+
resource: 'users',
|
|
81
|
+
usernameField: 'email',
|
|
82
|
+
passwordField: 'password',
|
|
83
|
+
config: {}
|
|
82
84
|
},
|
|
83
85
|
|
|
84
86
|
// Resource configuration
|
|
85
87
|
resources: options.resources || {},
|
|
86
88
|
|
|
89
|
+
// Custom routes (plugin-level)
|
|
90
|
+
routes: options.routes || {},
|
|
91
|
+
|
|
87
92
|
// CORS configuration
|
|
88
93
|
cors: {
|
|
89
94
|
enabled: options.cors?.enabled || false,
|
|
@@ -176,10 +181,8 @@ export class ApiPlugin extends Plugin {
|
|
|
176
181
|
throw err;
|
|
177
182
|
}
|
|
178
183
|
|
|
179
|
-
// Create users resource if authentication is
|
|
180
|
-
const authEnabled = this.config.auth.
|
|
181
|
-
this.config.auth.apiKey.enabled ||
|
|
182
|
-
this.config.auth.basic.enabled;
|
|
184
|
+
// Create users resource if authentication driver is configured
|
|
185
|
+
const authEnabled = this.config.auth.driver !== null;
|
|
183
186
|
|
|
184
187
|
if (authEnabled) {
|
|
185
188
|
await this._createUsersResource();
|
|
@@ -467,6 +470,7 @@ export class ApiPlugin extends Plugin {
|
|
|
467
470
|
host: this.config.host,
|
|
468
471
|
database: this.database,
|
|
469
472
|
resources: this.config.resources,
|
|
473
|
+
routes: this.config.routes,
|
|
470
474
|
middlewares: this.compiledMiddlewares,
|
|
471
475
|
verbose: this.config.verbose,
|
|
472
476
|
auth: this.config.auth,
|
|
@@ -14,13 +14,16 @@ import tryFn from '../../../concerns/try-fn.js';
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Create authentication routes
|
|
17
|
-
* @param {Object}
|
|
17
|
+
* @param {Object} authResource - s3db.js resource that manages authentication
|
|
18
18
|
* @param {Object} config - Auth configuration
|
|
19
19
|
* @returns {Hono} Hono app with auth routes
|
|
20
20
|
*/
|
|
21
|
-
export function createAuthRoutes(
|
|
21
|
+
export function createAuthRoutes(authResource, config = {}) {
|
|
22
22
|
const app = new Hono();
|
|
23
23
|
const {
|
|
24
|
+
driver, // 'jwt' or 'basic'
|
|
25
|
+
usernameField = 'email', // Field name for username (default: 'email')
|
|
26
|
+
passwordField = 'password', // Field name for password (default: 'password')
|
|
24
27
|
jwtSecret,
|
|
25
28
|
jwtExpiresIn = '7d',
|
|
26
29
|
passphrase = 'secret',
|
|
@@ -31,134 +34,152 @@ export function createAuthRoutes(usersResource, config = {}) {
|
|
|
31
34
|
if (allowRegistration) {
|
|
32
35
|
app.post('/register', asyncHandler(async (c) => {
|
|
33
36
|
const data = await c.req.json();
|
|
34
|
-
const
|
|
37
|
+
const username = data[usernameField];
|
|
38
|
+
const password = data[passwordField];
|
|
39
|
+
const role = data.role || 'user';
|
|
35
40
|
|
|
36
41
|
// Validate input
|
|
37
42
|
if (!username || !password) {
|
|
38
43
|
const response = formatter.validationError([
|
|
39
|
-
{ field:
|
|
40
|
-
{ field:
|
|
44
|
+
{ field: usernameField, message: `${usernameField} is required` },
|
|
45
|
+
{ field: passwordField, message: `${passwordField} is required` }
|
|
41
46
|
]);
|
|
42
47
|
return c.json(response, response._status);
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
if (password.length < 8) {
|
|
46
51
|
const response = formatter.validationError([
|
|
47
|
-
{ field:
|
|
52
|
+
{ field: passwordField, message: 'Password must be at least 8 characters' }
|
|
48
53
|
]);
|
|
49
54
|
return c.json(response, response._status);
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
// Check if username already exists
|
|
53
|
-
const
|
|
58
|
+
const queryFilter = { [usernameField]: username };
|
|
59
|
+
const existing = await authResource.query(queryFilter);
|
|
54
60
|
if (existing && existing.length > 0) {
|
|
55
|
-
const response = formatter.error(
|
|
61
|
+
const response = formatter.error(`${usernameField} already exists`, {
|
|
56
62
|
status: 409,
|
|
57
63
|
code: 'CONFLICT'
|
|
58
64
|
});
|
|
59
65
|
return c.json(response, response._status);
|
|
60
66
|
}
|
|
61
67
|
|
|
62
|
-
// Create user
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
// Create user with dynamic fields
|
|
69
|
+
// Only include fields from request + required auth fields
|
|
70
|
+
const userData = {
|
|
71
|
+
...data, // Include all fields from request first
|
|
72
|
+
[usernameField]: username, // Override to ensure correct value
|
|
73
|
+
[passwordField]: password // Will be auto-encrypted by schema (secret field)
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Add optional fields only if not provided
|
|
77
|
+
if (!userData.role) {
|
|
78
|
+
userData.role = role;
|
|
79
|
+
}
|
|
80
|
+
if (userData.active === undefined) {
|
|
81
|
+
userData.active = true;
|
|
82
|
+
}
|
|
72
83
|
|
|
73
|
-
|
|
84
|
+
const user = await authResource.insert(userData);
|
|
85
|
+
|
|
86
|
+
// Generate JWT token (only for JWT driver)
|
|
74
87
|
let token = null;
|
|
75
|
-
if (jwtSecret) {
|
|
88
|
+
if (driver === 'jwt' && jwtSecret) {
|
|
76
89
|
token = createToken(
|
|
77
|
-
{
|
|
90
|
+
{
|
|
91
|
+
userId: user.id,
|
|
92
|
+
[usernameField]: user[usernameField],
|
|
93
|
+
role: user.role
|
|
94
|
+
},
|
|
78
95
|
jwtSecret,
|
|
79
96
|
jwtExpiresIn
|
|
80
97
|
);
|
|
81
98
|
}
|
|
82
99
|
|
|
83
100
|
// Remove sensitive data from response
|
|
84
|
-
const {
|
|
101
|
+
const { [passwordField]: _, ...userWithoutPassword } = user;
|
|
85
102
|
|
|
86
103
|
const response = formatter.created({
|
|
87
104
|
user: userWithoutPassword,
|
|
88
|
-
token
|
|
105
|
+
...(token && { token }) // Only include token if JWT driver
|
|
89
106
|
}, `/auth/users/${user.id}`);
|
|
90
107
|
|
|
91
108
|
return c.json(response, response._status);
|
|
92
109
|
}));
|
|
93
110
|
}
|
|
94
111
|
|
|
95
|
-
// POST /auth/login - Login with username/password
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (!username || !password) {
|
|
102
|
-
const response = formatter.unauthorized('Username and password are required');
|
|
103
|
-
return c.json(response, response._status);
|
|
104
|
-
}
|
|
112
|
+
// POST /auth/login - Login with username/password (JWT driver only)
|
|
113
|
+
if (driver === 'jwt') {
|
|
114
|
+
app.post('/login', asyncHandler(async (c) => {
|
|
115
|
+
const data = await c.req.json();
|
|
116
|
+
const username = data[usernameField];
|
|
117
|
+
const password = data[passwordField];
|
|
105
118
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
119
|
+
// Validate input
|
|
120
|
+
if (!username || !password) {
|
|
121
|
+
const response = formatter.unauthorized(`${usernameField} and ${passwordField} are required`);
|
|
122
|
+
return c.json(response, response._status);
|
|
123
|
+
}
|
|
112
124
|
|
|
113
|
-
|
|
125
|
+
// Find user by username field
|
|
126
|
+
const queryFilter = { [usernameField]: username };
|
|
127
|
+
const users = await authResource.query(queryFilter);
|
|
128
|
+
if (!users || users.length === 0) {
|
|
129
|
+
const response = formatter.unauthorized('Invalid credentials');
|
|
130
|
+
return c.json(response, response._status);
|
|
131
|
+
}
|
|
114
132
|
|
|
115
|
-
|
|
116
|
-
const response = formatter.unauthorized('User account is inactive');
|
|
117
|
-
return c.json(response, response._status);
|
|
118
|
-
}
|
|
133
|
+
const user = users[0];
|
|
119
134
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
135
|
+
// Check if user is active
|
|
136
|
+
if (user.active !== undefined && !user.active) {
|
|
137
|
+
const response = formatter.unauthorized('User account is inactive');
|
|
138
|
+
return c.json(response, response._status);
|
|
139
|
+
}
|
|
125
140
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const isValid = user.password === password;
|
|
141
|
+
// Verify password (compare with password field)
|
|
142
|
+
// Schema handles encryption/decryption for 'secret' field types
|
|
143
|
+
const isValid = user[passwordField] === password;
|
|
130
144
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
if (!isValid) {
|
|
146
|
+
const response = formatter.unauthorized('Invalid credentials');
|
|
147
|
+
return c.json(response, response._status);
|
|
148
|
+
}
|
|
135
149
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
150
|
+
// Update last login if field exists
|
|
151
|
+
if (user.lastLoginAt !== undefined) {
|
|
152
|
+
await authResource.update(user.id, {
|
|
153
|
+
lastLoginAt: new Date().toISOString()
|
|
154
|
+
});
|
|
155
|
+
}
|
|
140
156
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
157
|
+
// Generate JWT token
|
|
158
|
+
let token = null;
|
|
159
|
+
if (jwtSecret) {
|
|
160
|
+
token = createToken(
|
|
161
|
+
{
|
|
162
|
+
userId: user.id,
|
|
163
|
+
[usernameField]: user[usernameField],
|
|
164
|
+
role: user.role
|
|
165
|
+
},
|
|
166
|
+
jwtSecret,
|
|
167
|
+
jwtExpiresIn
|
|
168
|
+
);
|
|
169
|
+
}
|
|
150
170
|
|
|
151
|
-
|
|
152
|
-
|
|
171
|
+
// Remove sensitive data from response
|
|
172
|
+
const { [passwordField]: _, ...userWithoutPassword } = user;
|
|
153
173
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
174
|
+
const response = formatter.success({
|
|
175
|
+
user: userWithoutPassword,
|
|
176
|
+
token,
|
|
177
|
+
expiresIn: jwtExpiresIn
|
|
178
|
+
});
|
|
159
179
|
|
|
160
|
-
|
|
161
|
-
|
|
180
|
+
return c.json(response, response._status);
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
162
183
|
|
|
163
184
|
// POST /auth/token/refresh - Refresh JWT token
|
|
164
185
|
if (jwtSecret) {
|
|
@@ -58,11 +58,12 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
|
58
58
|
const {
|
|
59
59
|
methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
|
|
60
60
|
customMiddleware = [],
|
|
61
|
-
enableValidation = true
|
|
61
|
+
enableValidation = true,
|
|
62
|
+
versionPrefix = '' // Empty string by default (calculated in server.js)
|
|
62
63
|
} = config;
|
|
63
64
|
|
|
64
65
|
const resourceName = resource.name;
|
|
65
|
-
const basePath = `/${
|
|
66
|
+
const basePath = versionPrefix ? `/${versionPrefix}/${resourceName}` : `/${resourceName}`;
|
|
66
67
|
|
|
67
68
|
// Apply custom middleware
|
|
68
69
|
customMiddleware.forEach(middleware => {
|