s3db.js 9.2.1 → 9.3.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 +115 -32
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +115 -32
- package/dist/s3db.es.js.map +1 -1
- package/mcp/server.js +12 -8
- package/package.json +4 -4
- package/src/concerns/async-event-emitter.js +2 -2
- package/src/resource.class.js +148 -36
package/mcp/server.js
CHANGED
|
@@ -628,7 +628,7 @@ class S3dbMCPServer {
|
|
|
628
628
|
|
|
629
629
|
setupTransport() {
|
|
630
630
|
const transport = process.argv.includes('--transport=sse') || process.env.MCP_TRANSPORT === 'sse'
|
|
631
|
-
? new SSEServerTransport('/sse', process.env.MCP_SERVER_HOST || '0.0.0.0', parseInt(process.env.MCP_SERVER_PORT || '
|
|
631
|
+
? new SSEServerTransport('/sse', process.env.MCP_SERVER_HOST || '0.0.0.0', parseInt(process.env.MCP_SERVER_PORT || '17500'))
|
|
632
632
|
: new StdioServerTransport();
|
|
633
633
|
|
|
634
634
|
this.server.connect(transport);
|
|
@@ -636,7 +636,7 @@ class S3dbMCPServer {
|
|
|
636
636
|
// SSE specific setup
|
|
637
637
|
if (transport instanceof SSEServerTransport) {
|
|
638
638
|
const host = process.env.MCP_SERVER_HOST || '0.0.0.0';
|
|
639
|
-
const port = process.env.MCP_SERVER_PORT || '
|
|
639
|
+
const port = process.env.MCP_SERVER_PORT || '17500';
|
|
640
640
|
|
|
641
641
|
console.log(`S3DB MCP Server running on http://${host}:${port}/sse`);
|
|
642
642
|
|
|
@@ -723,12 +723,16 @@ class S3dbMCPServer {
|
|
|
723
723
|
|
|
724
724
|
// Add CachePlugin (enabled by default, configurable)
|
|
725
725
|
const cacheEnabled = enableCache !== false && process.env.S3DB_CACHE_ENABLED !== 'false';
|
|
726
|
+
|
|
727
|
+
// Declare cache variables in outer scope to avoid reference errors
|
|
728
|
+
let cacheMaxSizeEnv, cacheTtlEnv, cacheDriverEnv, cacheDirectoryEnv, cachePrefixEnv;
|
|
729
|
+
|
|
726
730
|
if (cacheEnabled) {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
731
|
+
cacheMaxSizeEnv = process.env.S3DB_CACHE_MAX_SIZE ? parseInt(process.env.S3DB_CACHE_MAX_SIZE) : cacheMaxSize;
|
|
732
|
+
cacheTtlEnv = process.env.S3DB_CACHE_TTL ? parseInt(process.env.S3DB_CACHE_TTL) : cacheTtl;
|
|
733
|
+
cacheDriverEnv = process.env.S3DB_CACHE_DRIVER || cacheDriver;
|
|
734
|
+
cacheDirectoryEnv = process.env.S3DB_CACHE_DIRECTORY || cacheDirectory;
|
|
735
|
+
cachePrefixEnv = process.env.S3DB_CACHE_PREFIX || cachePrefix;
|
|
732
736
|
|
|
733
737
|
let cacheConfig = {
|
|
734
738
|
includePartitions: true
|
|
@@ -1358,7 +1362,7 @@ function parseArgs() {
|
|
|
1358
1362
|
const args = {
|
|
1359
1363
|
transport: 'stdio',
|
|
1360
1364
|
host: '0.0.0.0',
|
|
1361
|
-
port:
|
|
1365
|
+
port: 17500
|
|
1362
1366
|
};
|
|
1363
1367
|
|
|
1364
1368
|
process.argv.forEach((arg, index) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "s3db.js",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.3.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",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"UNLICENSE"
|
|
59
59
|
],
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@aws-sdk/client-s3": "^3.
|
|
62
|
-
"@modelcontextprotocol/sdk": "^1.17.
|
|
61
|
+
"@aws-sdk/client-s3": "^3.873.0",
|
|
62
|
+
"@modelcontextprotocol/sdk": "^1.17.4",
|
|
63
63
|
"@smithy/node-http-handler": "^4.1.1",
|
|
64
64
|
"@supercharge/promise-pool": "^3.2.0",
|
|
65
65
|
"dotenv": "^17.2.1",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"node-loader": "^2.1.0",
|
|
113
113
|
"ora": "^8.2.0",
|
|
114
114
|
"pkg": "^5.8.1",
|
|
115
|
-
"rollup": "^4.
|
|
115
|
+
"rollup": "^4.48.0",
|
|
116
116
|
"rollup-plugin-copy": "^3.5.0",
|
|
117
117
|
"rollup-plugin-esbuild": "^6.2.1",
|
|
118
118
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
|
@@ -17,10 +17,10 @@ class AsyncEventEmitter extends EventEmitter {
|
|
|
17
17
|
return false;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
setImmediate(() => {
|
|
20
|
+
setImmediate(async () => {
|
|
21
21
|
for (const listener of listeners) {
|
|
22
22
|
try {
|
|
23
|
-
listener(...args);
|
|
23
|
+
await listener(...args);
|
|
24
24
|
} catch (error) {
|
|
25
25
|
if (event !== 'error') {
|
|
26
26
|
this.emit('error', error);
|
package/src/resource.class.js
CHANGED
|
@@ -135,7 +135,8 @@ export class Resource extends AsyncEventEmitter {
|
|
|
135
135
|
idSize = 22,
|
|
136
136
|
versioningEnabled = false,
|
|
137
137
|
events = {},
|
|
138
|
-
asyncEvents = true
|
|
138
|
+
asyncEvents = true,
|
|
139
|
+
asyncPartitions = true
|
|
139
140
|
} = config;
|
|
140
141
|
|
|
141
142
|
// Set instance properties
|
|
@@ -177,6 +178,7 @@ export class Resource extends AsyncEventEmitter {
|
|
|
177
178
|
autoDecrypt,
|
|
178
179
|
allNestedObjectsOptional,
|
|
179
180
|
asyncEvents,
|
|
181
|
+
asyncPartitions,
|
|
180
182
|
};
|
|
181
183
|
|
|
182
184
|
// Initialize hooks system
|
|
@@ -817,14 +819,42 @@ export class Resource extends AsyncEventEmitter {
|
|
|
817
819
|
// Get the inserted object
|
|
818
820
|
const insertedObject = await this.get(finalId);
|
|
819
821
|
|
|
820
|
-
//
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
822
|
+
// Handle partition indexing based on asyncPartitions config
|
|
823
|
+
if (this.config.asyncPartitions && this.config.partitions && Object.keys(this.config.partitions).length > 0) {
|
|
824
|
+
// Async mode: create partition indexes in background
|
|
825
|
+
setImmediate(() => {
|
|
826
|
+
this.createPartitionReferences(insertedObject).catch(err => {
|
|
827
|
+
this.emit('partitionIndexError', {
|
|
828
|
+
operation: 'insert',
|
|
829
|
+
id: finalId,
|
|
830
|
+
error: err,
|
|
831
|
+
message: err.message
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// Execute other afterInsert hooks synchronously (excluding partition hook)
|
|
837
|
+
const nonPartitionHooks = this.hooks.afterInsert.filter(hook =>
|
|
838
|
+
!hook.toString().includes('createPartitionReferences')
|
|
839
|
+
);
|
|
840
|
+
let finalResult = insertedObject;
|
|
841
|
+
for (const hook of nonPartitionHooks) {
|
|
842
|
+
finalResult = await hook(finalResult);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Emit insert event
|
|
846
|
+
this.emit('insert', finalResult);
|
|
847
|
+
return finalResult;
|
|
848
|
+
} else {
|
|
849
|
+
// Sync mode: execute all hooks including partition creation
|
|
850
|
+
const finalResult = await this.executeHooks('afterInsert', insertedObject);
|
|
851
|
+
|
|
852
|
+
// Emit insert event
|
|
853
|
+
this.emit('insert', finalResult);
|
|
854
|
+
|
|
855
|
+
// Return the final object
|
|
856
|
+
return finalResult;
|
|
857
|
+
}
|
|
828
858
|
}
|
|
829
859
|
|
|
830
860
|
/**
|
|
@@ -1087,13 +1117,46 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1087
1117
|
body: finalBody,
|
|
1088
1118
|
behavior: this.behavior
|
|
1089
1119
|
});
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1120
|
+
|
|
1121
|
+
// Handle partition updates based on asyncPartitions config
|
|
1122
|
+
if (this.config.asyncPartitions && this.config.partitions && Object.keys(this.config.partitions).length > 0) {
|
|
1123
|
+
// Async mode: update partition indexes in background
|
|
1124
|
+
setImmediate(() => {
|
|
1125
|
+
this.handlePartitionReferenceUpdates(originalData, updatedData).catch(err => {
|
|
1126
|
+
this.emit('partitionIndexError', {
|
|
1127
|
+
operation: 'update',
|
|
1128
|
+
id,
|
|
1129
|
+
error: err,
|
|
1130
|
+
message: err.message
|
|
1131
|
+
});
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
// Execute other afterUpdate hooks synchronously (excluding partition hook)
|
|
1136
|
+
const nonPartitionHooks = this.hooks.afterUpdate.filter(hook =>
|
|
1137
|
+
!hook.toString().includes('handlePartitionReferenceUpdates')
|
|
1138
|
+
);
|
|
1139
|
+
let finalResult = updatedData;
|
|
1140
|
+
for (const hook of nonPartitionHooks) {
|
|
1141
|
+
finalResult = await hook(finalResult);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
this.emit('update', {
|
|
1145
|
+
...updatedData,
|
|
1146
|
+
$before: { ...originalData },
|
|
1147
|
+
$after: { ...finalResult }
|
|
1148
|
+
});
|
|
1149
|
+
return finalResult;
|
|
1150
|
+
} else {
|
|
1151
|
+
// Sync mode: execute all hooks including partition updates
|
|
1152
|
+
const finalResult = await this.executeHooks('afterUpdate', updatedData);
|
|
1153
|
+
this.emit('update', {
|
|
1154
|
+
...updatedData,
|
|
1155
|
+
$before: { ...originalData },
|
|
1156
|
+
$after: { ...finalResult }
|
|
1157
|
+
});
|
|
1158
|
+
return finalResult;
|
|
1159
|
+
}
|
|
1097
1160
|
}
|
|
1098
1161
|
|
|
1099
1162
|
/**
|
|
@@ -1149,8 +1212,34 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1149
1212
|
id
|
|
1150
1213
|
});
|
|
1151
1214
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1215
|
+
// Handle partition cleanup based on asyncPartitions config
|
|
1216
|
+
if (this.config.asyncPartitions && this.config.partitions && Object.keys(this.config.partitions).length > 0) {
|
|
1217
|
+
// Async mode: delete partition indexes in background
|
|
1218
|
+
setImmediate(() => {
|
|
1219
|
+
this.deletePartitionReferences(objectData).catch(err => {
|
|
1220
|
+
this.emit('partitionIndexError', {
|
|
1221
|
+
operation: 'delete',
|
|
1222
|
+
id,
|
|
1223
|
+
error: err,
|
|
1224
|
+
message: err.message
|
|
1225
|
+
});
|
|
1226
|
+
});
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
// Execute other afterDelete hooks synchronously (excluding partition hook)
|
|
1230
|
+
const nonPartitionHooks = this.hooks.afterDelete.filter(hook =>
|
|
1231
|
+
!hook.toString().includes('deletePartitionReferences')
|
|
1232
|
+
);
|
|
1233
|
+
let afterDeleteData = objectData;
|
|
1234
|
+
for (const hook of nonPartitionHooks) {
|
|
1235
|
+
afterDeleteData = await hook(afterDeleteData);
|
|
1236
|
+
}
|
|
1237
|
+
return response;
|
|
1238
|
+
} else {
|
|
1239
|
+
// Sync mode: execute all hooks including partition deletion
|
|
1240
|
+
const afterDeleteData = await this.executeHooks('afterDelete', objectData);
|
|
1241
|
+
return response;
|
|
1242
|
+
}
|
|
1154
1243
|
}
|
|
1155
1244
|
|
|
1156
1245
|
/**
|
|
@@ -1943,21 +2032,36 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1943
2032
|
return;
|
|
1944
2033
|
}
|
|
1945
2034
|
|
|
1946
|
-
// Create
|
|
1947
|
-
|
|
2035
|
+
// Create all partition references in parallel
|
|
2036
|
+
const promises = Object.entries(partitions).map(async ([partitionName, partition]) => {
|
|
1948
2037
|
const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
|
|
1949
2038
|
if (partitionKey) {
|
|
1950
2039
|
// Save only version as metadata, never object attributes
|
|
1951
2040
|
const partitionMetadata = {
|
|
1952
2041
|
_v: String(this.version)
|
|
1953
2042
|
};
|
|
1954
|
-
|
|
2043
|
+
return this.client.putObject({
|
|
1955
2044
|
key: partitionKey,
|
|
1956
2045
|
metadata: partitionMetadata,
|
|
1957
2046
|
body: '',
|
|
1958
2047
|
contentType: undefined,
|
|
1959
2048
|
});
|
|
1960
2049
|
}
|
|
2050
|
+
return null;
|
|
2051
|
+
});
|
|
2052
|
+
|
|
2053
|
+
// Wait for all partition references to be created
|
|
2054
|
+
const results = await Promise.allSettled(promises);
|
|
2055
|
+
|
|
2056
|
+
// Check for any failures
|
|
2057
|
+
const failures = results.filter(r => r.status === 'rejected');
|
|
2058
|
+
if (failures.length > 0) {
|
|
2059
|
+
// Emit warning but don't throw - partitions are secondary indexes
|
|
2060
|
+
this.emit('partitionIndexWarning', {
|
|
2061
|
+
operation: 'create',
|
|
2062
|
+
id: data.id,
|
|
2063
|
+
failures: failures.map(f => f.reason)
|
|
2064
|
+
});
|
|
1961
2065
|
}
|
|
1962
2066
|
}
|
|
1963
2067
|
|
|
@@ -2077,33 +2181,41 @@ export class Resource extends AsyncEventEmitter {
|
|
|
2077
2181
|
if (!partitions || Object.keys(partitions).length === 0) {
|
|
2078
2182
|
return;
|
|
2079
2183
|
}
|
|
2080
|
-
|
|
2184
|
+
|
|
2185
|
+
// Update all partitions in parallel
|
|
2186
|
+
const updatePromises = Object.entries(partitions).map(async ([partitionName, partition]) => {
|
|
2081
2187
|
const [ok, err] = await tryFn(() => this.handlePartitionReferenceUpdate(partitionName, partition, oldData, newData));
|
|
2082
2188
|
if (!ok) {
|
|
2083
2189
|
// console.warn(`Failed to update partition references for ${partitionName}:`, err.message);
|
|
2190
|
+
return { partitionName, error: err };
|
|
2084
2191
|
}
|
|
2085
|
-
|
|
2192
|
+
return { partitionName, success: true };
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
await Promise.allSettled(updatePromises);
|
|
2196
|
+
|
|
2197
|
+
// Aggressive cleanup: remove stale partition keys in parallel
|
|
2086
2198
|
const id = newData.id || oldData.id;
|
|
2087
|
-
|
|
2199
|
+
const cleanupPromises = Object.entries(partitions).map(async ([partitionName, partition]) => {
|
|
2088
2200
|
const prefix = `resource=${this.name}/partition=${partitionName}`;
|
|
2089
|
-
let allKeys = [];
|
|
2090
2201
|
const [okKeys, errKeys, keys] = await tryFn(() => this.client.getAllKeys({ prefix }));
|
|
2091
|
-
if (okKeys) {
|
|
2092
|
-
allKeys = keys;
|
|
2093
|
-
} else {
|
|
2202
|
+
if (!okKeys) {
|
|
2094
2203
|
// console.warn(`Aggressive cleanup: could not list keys for partition ${partitionName}:`, errKeys.message);
|
|
2095
|
-
|
|
2204
|
+
return;
|
|
2096
2205
|
}
|
|
2206
|
+
|
|
2097
2207
|
const validKey = this.getPartitionKey({ partitionName, id, data: newData });
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2208
|
+
const staleKeys = keys.filter(key => key.endsWith(`/id=${id}`) && key !== validKey);
|
|
2209
|
+
|
|
2210
|
+
if (staleKeys.length > 0) {
|
|
2211
|
+
const [okDel, errDel] = await tryFn(() => this.client.deleteObjects(staleKeys));
|
|
2212
|
+
if (!okDel) {
|
|
2213
|
+
// console.warn(`Aggressive cleanup: could not delete stale partition keys:`, errDel.message);
|
|
2104
2214
|
}
|
|
2105
2215
|
}
|
|
2106
|
-
}
|
|
2216
|
+
});
|
|
2217
|
+
|
|
2218
|
+
await Promise.allSettled(cleanupPromises);
|
|
2107
2219
|
}
|
|
2108
2220
|
|
|
2109
2221
|
/**
|