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/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 || '8000'))
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 || '8000';
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
- const cacheMaxSizeEnv = process.env.S3DB_CACHE_MAX_SIZE ? parseInt(process.env.S3DB_CACHE_MAX_SIZE) : cacheMaxSize;
728
- const cacheTtlEnv = process.env.S3DB_CACHE_TTL ? parseInt(process.env.S3DB_CACHE_TTL) : cacheTtl;
729
- const cacheDriverEnv = process.env.S3DB_CACHE_DRIVER || cacheDriver;
730
- const cacheDirectoryEnv = process.env.S3DB_CACHE_DIRECTORY || cacheDirectory;
731
- const cachePrefixEnv = process.env.S3DB_CACHE_PREFIX || cachePrefix;
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: 8000
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.2.1",
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.864.0",
62
- "@modelcontextprotocol/sdk": "^1.17.3",
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.46.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);
@@ -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
- // Execute afterInsert hooks
821
- const finalResult = await this.executeHooks('afterInsert', insertedObject);
822
-
823
- // Emit insert event
824
- this.emit('insert', finalResult);
825
-
826
- // Return the final object
827
- return finalResult;
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
- const finalResult = await this.executeHooks('afterUpdate', updatedData);
1091
- this.emit('update', {
1092
- ...updatedData,
1093
- $before: { ...originalData },
1094
- $after: { ...finalResult }
1095
- });
1096
- return finalResult;
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
- const afterDeleteData = await this.executeHooks('afterDelete', objectData);
1153
- return response;
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 reference in each partition
1947
- for (const [partitionName, partition] of Object.entries(partitions)) {
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
- await this.client.putObject({
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
- for (const [partitionName, partition] of Object.entries(partitions)) {
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
- for (const [partitionName, partition] of Object.entries(partitions)) {
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
- continue;
2204
+ return;
2096
2205
  }
2206
+
2097
2207
  const validKey = this.getPartitionKey({ partitionName, id, data: newData });
2098
- for (const key of allKeys) {
2099
- if (key.endsWith(`/id=${id}`) && key !== validKey) {
2100
- const [okDel, errDel] = await tryFn(() => this.client.deleteObject(key));
2101
- if (!okDel) {
2102
- // console.warn(`Aggressive cleanup: could not delete stale partition key ${key}:`, errDel.message);
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
  /**