s3db.js 12.0.0 → 12.1.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/dist/s3db.cjs.js +454 -3833
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +9 -1
- package/dist/s3db.es.js +451 -3830
- package/dist/s3db.es.js.map +1 -1
- package/mcp/entrypoint.js +91 -57
- package/package.json +15 -15
- package/src/client.class.js +40 -5
- package/src/concerns/plugin-storage.js +67 -37
- package/src/plugins/api/index.js +5 -0
- package/src/plugins/api/server.js +4 -0
- package/src/plugins/plugin.class.js +5 -0
- package/src/plugins/relation.plugin.js +183 -47
- package/src/plugins/replicator.plugin.js +2 -1
- package/src/plugins/ttl.plugin.js +478 -303
- package/src/resource.class.js +309 -34
- package/src/s3db.d.ts +9 -1
- package/dist/s3db-cli.js +0 -55543
package/mcp/entrypoint.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
-
import {
|
|
5
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
6
6
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
7
7
|
import { S3db, CachePlugin, CostsPlugin } from '../dist/s3db.es.js';
|
|
8
8
|
import { FilesystemCache } from '../src/plugins/cache/filesystem-cache.class.js';
|
|
@@ -10,6 +10,7 @@ import { config } from 'dotenv';
|
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import { dirname, join } from 'path';
|
|
12
12
|
import { readFileSync } from 'fs';
|
|
13
|
+
import express from 'express';
|
|
13
14
|
|
|
14
15
|
// Load environment variables
|
|
15
16
|
config();
|
|
@@ -1109,70 +1110,103 @@ class S3dbMCPServer {
|
|
|
1109
1110
|
}
|
|
1110
1111
|
|
|
1111
1112
|
setupTransport() {
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
const port = process.env.MCP_SERVER_PORT || '17500';
|
|
1122
|
-
|
|
1123
|
-
console.log(`S3DB MCP Server running on http://${host}:${port}/sse`);
|
|
1124
|
-
|
|
1125
|
-
// Add health check endpoint for SSE transport
|
|
1126
|
-
this.setupHealthCheck(host, port);
|
|
1113
|
+
const useHttp = process.argv.includes('--transport=http') || process.env.MCP_TRANSPORT === 'http';
|
|
1114
|
+
|
|
1115
|
+
if (useHttp) {
|
|
1116
|
+
// Setup Express server for Streamable HTTP transport
|
|
1117
|
+
this.setupHttpTransport();
|
|
1118
|
+
} else {
|
|
1119
|
+
// Use stdio transport (default)
|
|
1120
|
+
const transport = new StdioServerTransport();
|
|
1121
|
+
this.server.connect(transport);
|
|
1127
1122
|
}
|
|
1128
1123
|
}
|
|
1129
1124
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1125
|
+
setupHttpTransport() {
|
|
1126
|
+
const host = process.env.MCP_SERVER_HOST || '0.0.0.0';
|
|
1127
|
+
const port = parseInt(process.env.MCP_SERVER_PORT || '17500');
|
|
1128
|
+
|
|
1129
|
+
const app = express();
|
|
1130
|
+
app.use(express.json());
|
|
1131
|
+
|
|
1132
|
+
// Enable CORS for browser-based clients
|
|
1133
|
+
app.use((req, res, next) => {
|
|
1134
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
1135
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
1136
|
+
res.header('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');
|
|
1137
|
+
res.header('Access-Control-Expose-Headers', 'Mcp-Session-Id');
|
|
1138
|
+
|
|
1139
|
+
if (req.method === 'OPTIONS') {
|
|
1140
|
+
return res.sendStatus(200);
|
|
1141
|
+
}
|
|
1142
|
+
next();
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
// Streamable HTTP endpoint (stateless mode - recommended)
|
|
1146
|
+
app.post('/mcp', async (req, res) => {
|
|
1147
|
+
try {
|
|
1148
|
+
// Create a new transport for each request to prevent request ID collisions
|
|
1149
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1150
|
+
sessionIdGenerator: undefined,
|
|
1151
|
+
enableJsonResponse: true
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
res.on('close', () => {
|
|
1155
|
+
transport.close();
|
|
1156
|
+
});
|
|
1152
1157
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
+
await this.server.connect(transport);
|
|
1159
|
+
await transport.handleRequest(req, res, req.body);
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
console.error('Error handling MCP request:', error);
|
|
1162
|
+
if (!res.headersSent) {
|
|
1163
|
+
res.status(500).json({
|
|
1164
|
+
jsonrpc: '2.0',
|
|
1165
|
+
error: {
|
|
1166
|
+
code: -32603,
|
|
1167
|
+
message: 'Internal server error'
|
|
1168
|
+
},
|
|
1169
|
+
id: null
|
|
1158
1170
|
});
|
|
1159
|
-
res.end(JSON.stringify(healthStatus, null, 2));
|
|
1160
|
-
} else {
|
|
1161
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1162
|
-
res.end('Not Found');
|
|
1163
1171
|
}
|
|
1164
|
-
}
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1165
1174
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1175
|
+
// Health check endpoint
|
|
1176
|
+
app.get('/health', (req, res) => {
|
|
1177
|
+
const healthStatus = {
|
|
1178
|
+
status: 'healthy',
|
|
1179
|
+
timestamp: new Date().toISOString(),
|
|
1180
|
+
uptime: process.uptime(),
|
|
1181
|
+
version: SERVER_VERSION,
|
|
1182
|
+
database: {
|
|
1183
|
+
connected: database ? database.isConnected() : false,
|
|
1184
|
+
bucket: database?.bucket || null,
|
|
1185
|
+
keyPrefix: database?.keyPrefix || null,
|
|
1186
|
+
resourceCount: database ? Object.keys(database.resources || {}).length : 0
|
|
1187
|
+
},
|
|
1188
|
+
memory: process.memoryUsage(),
|
|
1189
|
+
environment: {
|
|
1190
|
+
nodeVersion: process.version,
|
|
1191
|
+
platform: process.platform,
|
|
1192
|
+
transport: 'streamable-http'
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
|
|
1196
|
+
res.json(healthStatus);
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// Start Express server
|
|
1200
|
+
app.listen(port, host, () => {
|
|
1201
|
+
console.log(`S3DB MCP Server running on http://${host}:${port}/mcp`);
|
|
1202
|
+
console.log(`Health check endpoint: http://${host}:${port}/health`);
|
|
1203
|
+
}).on('error', error => {
|
|
1204
|
+
console.error('Server error:', error);
|
|
1205
|
+
process.exit(1);
|
|
1173
1206
|
});
|
|
1174
1207
|
}
|
|
1175
1208
|
|
|
1209
|
+
|
|
1176
1210
|
// 📖 DOCUMENTATION TOOLS HANDLERS
|
|
1177
1211
|
|
|
1178
1212
|
async handleS3dbQueryDocs(args) {
|
|
@@ -2718,8 +2752,8 @@ async function main() {
|
|
|
2718
2752
|
|
|
2719
2753
|
console.log(`S3DB MCP Server v${SERVER_VERSION} started`);
|
|
2720
2754
|
console.log(`Transport: ${args.transport}`);
|
|
2721
|
-
if (args.transport === '
|
|
2722
|
-
console.log(`URL: http://${args.host}:${args.port}/
|
|
2755
|
+
if (args.transport === 'http') {
|
|
2756
|
+
console.log(`URL: http://${args.host}:${args.port}/mcp`);
|
|
2723
2757
|
}
|
|
2724
2758
|
}
|
|
2725
2759
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "s3db.js",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.1.0",
|
|
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",
|
|
@@ -65,11 +65,12 @@
|
|
|
65
65
|
"UNLICENSE"
|
|
66
66
|
],
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@aws-sdk/client-s3": "^3.
|
|
69
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
70
|
-
"@smithy/node-http-handler": "^4.
|
|
68
|
+
"@aws-sdk/client-s3": "^3.914.0",
|
|
69
|
+
"@modelcontextprotocol/sdk": "^1.20.1",
|
|
70
|
+
"@smithy/node-http-handler": "^4.4.2",
|
|
71
71
|
"@supercharge/promise-pool": "^3.2.0",
|
|
72
72
|
"dotenv": "^17.2.3",
|
|
73
|
+
"express": "^5.1.0",
|
|
73
74
|
"fastest-validator": "^1.19.1",
|
|
74
75
|
"flat": "^6.0.1",
|
|
75
76
|
"glob": "^11.0.3",
|
|
@@ -114,36 +115,35 @@
|
|
|
114
115
|
}
|
|
115
116
|
},
|
|
116
117
|
"devDependencies": {
|
|
117
|
-
"@aws-sdk/client-sqs": "^3.
|
|
118
|
+
"@aws-sdk/client-sqs": "^3.914.0",
|
|
118
119
|
"@babel/core": "^7.28.4",
|
|
119
120
|
"@babel/preset-env": "^7.28.3",
|
|
120
|
-
"@google-cloud/bigquery": "^7.
|
|
121
|
-
"@rollup/plugin-commonjs": "^28.0.
|
|
121
|
+
"@google-cloud/bigquery": "^7.9.4",
|
|
122
|
+
"@rollup/plugin-commonjs": "^28.0.8",
|
|
122
123
|
"@rollup/plugin-json": "^6.1.0",
|
|
123
|
-
"@rollup/plugin-node-resolve": "^16.0.
|
|
124
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
124
125
|
"@rollup/plugin-replace": "^6.0.2",
|
|
125
126
|
"@rollup/plugin-terser": "^0.4.4",
|
|
126
127
|
"@types/node": "24.7.0",
|
|
127
128
|
"@xenova/transformers": "^2.17.2",
|
|
128
|
-
"amqplib": "^0.10.
|
|
129
|
+
"amqplib": "^0.10.9",
|
|
129
130
|
"babel-loader": "^10.0.0",
|
|
130
131
|
"chalk": "^5.6.2",
|
|
131
132
|
"cli-table3": "^0.6.5",
|
|
132
133
|
"commander": "^14.0.1",
|
|
133
|
-
"esbuild": "^0.25.
|
|
134
|
-
"inquirer": "^12.
|
|
134
|
+
"esbuild": "^0.25.11",
|
|
135
|
+
"inquirer": "^12.10.0",
|
|
135
136
|
"jest": "^30.2.0",
|
|
136
|
-
"node-cron": "^4.
|
|
137
|
+
"node-cron": "^4.2.1",
|
|
137
138
|
"node-loader": "^2.1.0",
|
|
138
139
|
"ora": "^9.0.0",
|
|
139
|
-
"pg": "^8.
|
|
140
|
+
"pg": "^8.16.3",
|
|
140
141
|
"pkg": "^5.8.1",
|
|
141
|
-
"rollup": "^4.52.
|
|
142
|
+
"rollup": "^4.52.5",
|
|
142
143
|
"rollup-plugin-copy": "^3.5.0",
|
|
143
144
|
"rollup-plugin-esbuild": "^6.2.1",
|
|
144
145
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
|
145
146
|
"rollup-plugin-shebang-bin": "^0.1.0",
|
|
146
|
-
"rollup-plugin-terser": "^7.0.2",
|
|
147
147
|
"tsx": "^4.20.6",
|
|
148
148
|
"typescript": "5.9.3",
|
|
149
149
|
"uuid": "^13.0.0",
|
package/src/client.class.js
CHANGED
|
@@ -203,7 +203,21 @@ export class Client extends EventEmitter {
|
|
|
203
203
|
Key: keyPrefix ? path.join(keyPrefix, key) : key,
|
|
204
204
|
};
|
|
205
205
|
|
|
206
|
-
const [ok, err, response] = await tryFn(() =>
|
|
206
|
+
const [ok, err, response] = await tryFn(async () => {
|
|
207
|
+
const res = await this.sendCommand(new HeadObjectCommand(options));
|
|
208
|
+
|
|
209
|
+
// Smart decode metadata values (same as getObject)
|
|
210
|
+
if (res.Metadata) {
|
|
211
|
+
const decodedMetadata = {};
|
|
212
|
+
for (const [key, value] of Object.entries(res.Metadata)) {
|
|
213
|
+
decodedMetadata[key] = metadataDecode(value);
|
|
214
|
+
}
|
|
215
|
+
res.Metadata = decodedMetadata;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return res;
|
|
219
|
+
});
|
|
220
|
+
|
|
207
221
|
this.emit('headObject', err || response, { key });
|
|
208
222
|
|
|
209
223
|
if (!ok) {
|
|
@@ -218,15 +232,36 @@ export class Client extends EventEmitter {
|
|
|
218
232
|
return response;
|
|
219
233
|
}
|
|
220
234
|
|
|
221
|
-
async copyObject({ from, to }) {
|
|
235
|
+
async copyObject({ from, to, metadata, metadataDirective, contentType }) {
|
|
236
|
+
const keyPrefix = typeof this.config.keyPrefix === 'string' ? this.config.keyPrefix : '';
|
|
222
237
|
const options = {
|
|
223
238
|
Bucket: this.config.bucket,
|
|
224
|
-
Key:
|
|
225
|
-
CopySource: path.join(this.config.bucket,
|
|
239
|
+
Key: keyPrefix ? path.join(keyPrefix, to) : to,
|
|
240
|
+
CopySource: path.join(this.config.bucket, keyPrefix ? path.join(keyPrefix, from) : from),
|
|
226
241
|
};
|
|
227
242
|
|
|
243
|
+
// Add metadata directive if specified
|
|
244
|
+
if (metadataDirective) {
|
|
245
|
+
options.MetadataDirective = metadataDirective; // 'COPY' or 'REPLACE'
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Add metadata if specified (and encode values)
|
|
249
|
+
if (metadata && typeof metadata === 'object') {
|
|
250
|
+
const encodedMetadata = {};
|
|
251
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
252
|
+
const { encoded } = metadataEncode(value);
|
|
253
|
+
encodedMetadata[key] = encoded;
|
|
254
|
+
}
|
|
255
|
+
options.Metadata = encodedMetadata;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Add content type if specified
|
|
259
|
+
if (contentType) {
|
|
260
|
+
options.ContentType = contentType;
|
|
261
|
+
}
|
|
262
|
+
|
|
228
263
|
const [ok, err, response] = await tryFn(() => this.sendCommand(new CopyObjectCommand(options)));
|
|
229
|
-
this.emit('copyObject', err || response, { from, to });
|
|
264
|
+
this.emit('copyObject', err || response, { from, to, metadataDirective });
|
|
230
265
|
|
|
231
266
|
if (!ok) {
|
|
232
267
|
throw mapAwsError(err, {
|
|
@@ -456,7 +456,9 @@ export class PluginStorage {
|
|
|
456
456
|
* @returns {Promise<boolean>} True if extended, false if not found or no TTL
|
|
457
457
|
*/
|
|
458
458
|
async touch(key, additionalSeconds) {
|
|
459
|
-
|
|
459
|
+
// Optimization: Use HEAD + COPY instead of GET + PUT for metadata-only updates
|
|
460
|
+
// This avoids transferring the body when only updating the TTL
|
|
461
|
+
const [ok, err, response] = await tryFn(() => this.client.headObject(key));
|
|
460
462
|
|
|
461
463
|
if (!ok) {
|
|
462
464
|
return false;
|
|
@@ -465,50 +467,34 @@ export class PluginStorage {
|
|
|
465
467
|
const metadata = response.Metadata || {};
|
|
466
468
|
const parsedMetadata = this._parseMetadataValues(metadata);
|
|
467
469
|
|
|
468
|
-
let data = parsedMetadata;
|
|
469
|
-
|
|
470
|
-
if (response.Body) {
|
|
471
|
-
const [ok, err, result] = await tryFn(async () => {
|
|
472
|
-
const bodyContent = await response.Body.transformToString();
|
|
473
|
-
if (bodyContent && bodyContent.trim()) {
|
|
474
|
-
const body = JSON.parse(bodyContent);
|
|
475
|
-
return { ...parsedMetadata, ...body };
|
|
476
|
-
}
|
|
477
|
-
return parsedMetadata;
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
if (!ok) {
|
|
481
|
-
return false; // Parse error
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
data = result;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
470
|
// S3 lowercases metadata keys
|
|
488
|
-
const expiresAt =
|
|
471
|
+
const expiresAt = parsedMetadata._expiresat || parsedMetadata._expiresAt;
|
|
489
472
|
if (!expiresAt) {
|
|
490
473
|
return false; // No TTL to extend
|
|
491
474
|
}
|
|
492
475
|
|
|
493
476
|
// Extend TTL - use the standard field name (will be lowercased by S3)
|
|
494
|
-
|
|
495
|
-
delete
|
|
496
|
-
|
|
497
|
-
//
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
metadata: newMetadata,
|
|
503
|
-
contentType: 'application/json'
|
|
504
|
-
};
|
|
505
|
-
|
|
506
|
-
if (newBody !== null) {
|
|
507
|
-
putParams.body = JSON.stringify(newBody);
|
|
477
|
+
parsedMetadata._expiresAt = expiresAt + (additionalSeconds * 1000);
|
|
478
|
+
delete parsedMetadata._expiresat; // Remove lowercased version
|
|
479
|
+
|
|
480
|
+
// Encode metadata for S3
|
|
481
|
+
const encodedMetadata = {};
|
|
482
|
+
for (const [metaKey, metaValue] of Object.entries(parsedMetadata)) {
|
|
483
|
+
const { encoded } = metadataEncode(metaValue);
|
|
484
|
+
encodedMetadata[metaKey] = encoded;
|
|
508
485
|
}
|
|
509
486
|
|
|
510
|
-
|
|
511
|
-
|
|
487
|
+
// Use COPY with MetadataDirective: REPLACE to update metadata atomically
|
|
488
|
+
// This preserves the body without re-transferring it
|
|
489
|
+
const [copyOk] = await tryFn(() => this.client.copyObject({
|
|
490
|
+
from: key,
|
|
491
|
+
to: key,
|
|
492
|
+
metadata: encodedMetadata,
|
|
493
|
+
metadataDirective: 'REPLACE',
|
|
494
|
+
contentType: response.ContentType || 'application/json'
|
|
495
|
+
}));
|
|
496
|
+
|
|
497
|
+
return copyOk;
|
|
512
498
|
}
|
|
513
499
|
|
|
514
500
|
/**
|
|
@@ -675,12 +661,56 @@ export class PluginStorage {
|
|
|
675
661
|
/**
|
|
676
662
|
* Increment a counter value
|
|
677
663
|
*
|
|
664
|
+
* Optimization: Uses HEAD + COPY for existing counters to avoid body transfer.
|
|
665
|
+
* Falls back to GET + PUT for non-existent counters or those with additional data.
|
|
666
|
+
*
|
|
678
667
|
* @param {string} key - S3 key
|
|
679
668
|
* @param {number} amount - Amount to increment (default: 1)
|
|
680
669
|
* @param {Object} options - Options (e.g., ttl)
|
|
681
670
|
* @returns {Promise<number>} New value
|
|
682
671
|
*/
|
|
683
672
|
async increment(key, amount = 1, options = {}) {
|
|
673
|
+
// Try optimized path first: HEAD + COPY for existing counters
|
|
674
|
+
const [headOk, headErr, headResponse] = await tryFn(() => this.client.headObject(key));
|
|
675
|
+
|
|
676
|
+
if (headOk && headResponse.Metadata) {
|
|
677
|
+
// Counter exists, use optimized HEAD + COPY
|
|
678
|
+
const metadata = headResponse.Metadata || {};
|
|
679
|
+
const parsedMetadata = this._parseMetadataValues(metadata);
|
|
680
|
+
|
|
681
|
+
const currentValue = parsedMetadata.value || 0;
|
|
682
|
+
const newValue = currentValue + amount;
|
|
683
|
+
|
|
684
|
+
// Update only the value field
|
|
685
|
+
parsedMetadata.value = newValue;
|
|
686
|
+
|
|
687
|
+
// Handle TTL if specified
|
|
688
|
+
if (options.ttl) {
|
|
689
|
+
parsedMetadata._expiresAt = Date.now() + (options.ttl * 1000);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Encode metadata
|
|
693
|
+
const encodedMetadata = {};
|
|
694
|
+
for (const [metaKey, metaValue] of Object.entries(parsedMetadata)) {
|
|
695
|
+
const { encoded } = metadataEncode(metaValue);
|
|
696
|
+
encodedMetadata[metaKey] = encoded;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Atomic update via COPY
|
|
700
|
+
const [copyOk] = await tryFn(() => this.client.copyObject({
|
|
701
|
+
from: key,
|
|
702
|
+
to: key,
|
|
703
|
+
metadata: encodedMetadata,
|
|
704
|
+
metadataDirective: 'REPLACE',
|
|
705
|
+
contentType: headResponse.ContentType || 'application/json'
|
|
706
|
+
}));
|
|
707
|
+
|
|
708
|
+
if (copyOk) {
|
|
709
|
+
return newValue;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Fallback: counter doesn't exist or has body data, use traditional path
|
|
684
714
|
const data = await this.get(key);
|
|
685
715
|
const value = (data?.value || 0) + amount;
|
|
686
716
|
await this.set(key, { value }, options);
|
package/src/plugins/api/index.js
CHANGED
|
@@ -37,6 +37,11 @@ import { ApiServer } from './server.js';
|
|
|
37
37
|
import { requirePluginDependency } from '../concerns/plugin-dependencies.js';
|
|
38
38
|
import tryFn from '../../concerns/try-fn.js';
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* API Plugin class
|
|
42
|
+
* @class
|
|
43
|
+
* @extends Plugin
|
|
44
|
+
*/
|
|
40
45
|
export class ApiPlugin extends Plugin {
|
|
41
46
|
/**
|
|
42
47
|
* Create API Plugin instance
|
|
@@ -12,6 +12,10 @@ import { errorHandler } from './utils/error-handler.js';
|
|
|
12
12
|
import * as formatter from './utils/response-formatter.js';
|
|
13
13
|
import { generateOpenAPISpec } from './utils/openapi-generator.js';
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* API Server class
|
|
17
|
+
* @class
|
|
18
|
+
*/
|
|
15
19
|
export class ApiServer {
|
|
16
20
|
/**
|
|
17
21
|
* Create API server
|
|
@@ -176,6 +176,11 @@ export class Plugin extends EventEmitter {
|
|
|
176
176
|
* - Pode modificar argumentos/resultados.
|
|
177
177
|
*/
|
|
178
178
|
addMiddleware(resource, methodName, middleware) {
|
|
179
|
+
// Safety check: verify method exists
|
|
180
|
+
if (typeof resource[methodName] !== 'function') {
|
|
181
|
+
throw new Error(`Cannot add middleware to "${methodName}": method does not exist on resource "${resource.name || 'unknown'}"`);
|
|
182
|
+
}
|
|
183
|
+
|
|
179
184
|
if (!resource._pluginMiddlewares) {
|
|
180
185
|
resource._pluginMiddlewares = {};
|
|
181
186
|
}
|