s3db.js 7.4.2 → 8.0.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
@@ -747,7 +747,7 @@ class S3dbMCPServer {
747
747
  });
748
748
  } else {
749
749
  // Memory cache configuration (default)
750
- cacheConfig.driverType = 'memory';
750
+ cacheConfig.driver = 'memory';
751
751
  cacheConfig.memoryOptions = {
752
752
  maxSize: cacheMaxSizeEnv,
753
753
  ttl: cacheTtlEnv,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s3db.js",
3
- "version": "7.4.2",
3
+ "version": "8.0.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",
@@ -49,8 +49,31 @@
49
49
  "PLUGINS.md",
50
50
  "UNLICENSE"
51
51
  ],
52
+ "scripts": {
53
+ "build": "rollup -c",
54
+ "dev": "rollup -c -w",
55
+ "test": "npm run test:js && npm run test:ts",
56
+ "test:js": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testPathIgnorePatterns=\"plugin-audit.test.js|tests/typescript/\" --testTimeout=10000",
57
+ "test:ts": "tsc --noEmit --project tests/typescript/tsconfig.json",
58
+ "test:js-converage": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --detectOpenHandles --coverage --runInBand",
59
+ "test:js-ai": "node --max-old-space-size=4096 --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testPathIgnorePatterns=\"plugin-audit.test.js|tests/typescript/\"",
60
+ "test:full": "npm run test:js-ai && npm run test:ts",
61
+ "test:audit": "node --max-old-space-size=8192 --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/plugin-audit.test.js --runInBand --testTimeout=60000",
62
+ "test:cache": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/plugin-cache*.test.js --runInBand",
63
+ "test:quick": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testTimeout=10000",
64
+ "test:batch": "./test-batch.sh",
65
+ "test:plugins": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/ --runInBand --testTimeout=60000",
66
+ "test:plugins:fast": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/ --runInBand --testTimeout=15000 --testPathIgnorePatterns='plugin-audit.test.js|plugin-replicator-s3db.test.js|plugin-fulltext.test.js'",
67
+ "test:slow": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/plugin-audit.test.js tests/plugins/plugin-replicator-s3db.test.js tests/plugins/plugin-fulltext.test.js --runInBand --testTimeout=120000",
68
+ "test:types": "tsc --noEmit --project tests/typescript/tsconfig.json",
69
+ "test:types:basic": "tsc --noEmit tests/typescript/basic-usage.test.ts",
70
+ "test:types:direct": "tsc --noEmit tests/typescript/direct-type-test.ts",
71
+ "test:types:watch": "tsc --noEmit --watch --project tests/typescript/tsconfig.json",
72
+ "validate:types": "npm run test:types && echo 'TypeScript definitions are valid!'"
73
+ },
52
74
  "dependencies": {
53
- "@aws-sdk/client-s3": "^3.848.0",
75
+ "@aws-sdk/client-s3": "^3.850.0",
76
+ "@smithy/node-http-handler": "^4.1.0",
54
77
  "@supercharge/promise-pool": "^3.2.0",
55
78
  "fastest-validator": "^1.19.1",
56
79
  "flat": "^6.0.1",
@@ -88,10 +111,10 @@
88
111
  "@rollup/plugin-node-resolve": "^16.0.1",
89
112
  "@rollup/plugin-replace": "^6.0.2",
90
113
  "@rollup/plugin-terser": "^0.4.4",
91
- "@types/node": "24.0.15",
92
- "dotenv": "^17.2.0",
93
- "jest": "^30.0.4",
94
- "rollup": "^4.45.1",
114
+ "@types/node": "24.1.0",
115
+ "dotenv": "^17.2.1",
116
+ "jest": "^30.0.5",
117
+ "rollup": "^4.46.1",
95
118
  "rollup-plugin-copy": "^3.5.0",
96
119
  "rollup-plugin-esbuild": "^6.2.1",
97
120
  "rollup-plugin-polyfill-node": "^0.13.0",
@@ -100,26 +123,5 @@
100
123
  },
101
124
  "funding": [
102
125
  "https://github.com/sponsors/forattini-dev"
103
- ],
104
- "scripts": {
105
- "build": "rollup -c",
106
- "dev": "rollup -c -w",
107
- "test": "npm run test:js && npm run test:ts",
108
- "test:js": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand",
109
- "test:ts": "tsc --noEmit --project tests/typescript/tsconfig.json",
110
- "test:js-converage": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --detectOpenHandles --coverage --runInBand",
111
- "test:js-ai": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --detectOpenHandles --runInBand",
112
- "test:full": "npm run test:js && npm run test:ts",
113
- "test:cache": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/plugin-cache*.test.js --runInBand",
114
- "test:quick": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testTimeout=10000",
115
- "test:batch": "./test-batch.sh",
116
- "test:plugins": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/ --runInBand --testTimeout=60000",
117
- "test:plugins:fast": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/ --runInBand --testTimeout=15000 --testPathIgnorePatterns='plugin-audit.test.js|plugin-replicator-s3db.test.js|plugin-fulltext.test.js'",
118
- "test:slow": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/plugin-audit.test.js tests/plugins/plugin-replicator-s3db.test.js tests/plugins/plugin-fulltext.test.js --runInBand --testTimeout=120000",
119
- "test:types": "tsc --noEmit --project tests/typescript/tsconfig.json",
120
- "test:types:basic": "tsc --noEmit tests/typescript/basic-usage.test.ts",
121
- "test:types:direct": "tsc --noEmit tests/typescript/direct-type-test.ts",
122
- "test:types:watch": "tsc --noEmit --watch --project tests/typescript/tsconfig.json",
123
- "validate:types": "npm run test:types && echo 'TypeScript definitions are valid!'"
124
- }
125
- }
126
+ ]
127
+ }
@@ -1,10 +1,10 @@
1
1
  import path from "path";
2
2
  import EventEmitter from "events";
3
3
  import { chunk } from "lodash-es";
4
+ import { Agent as HttpAgent } from 'http';
5
+ import { Agent as HttpsAgent } from 'https';
4
6
  import { PromisePool } from "@supercharge/promise-pool";
5
-
6
- import { idGenerator } from "./concerns/id.js";
7
- import tryFn from "./concerns/try-fn.js";
7
+ import { NodeHttpHandler } from '@smithy/node-http-handler';
8
8
 
9
9
  import {
10
10
  S3Client,
@@ -16,10 +16,12 @@ import {
16
16
  DeleteObjectsCommand,
17
17
  ListObjectsV2Command,
18
18
  } from '@aws-sdk/client-s3';
19
- import { md5 } from "./concerns/crypto.js";
20
19
 
21
- import { mapAwsError, UnknownError, NoSuchKey, NotFound } from "./errors.js";
20
+ import tryFn from "./concerns/try-fn.js";
21
+ import { md5 } from "./concerns/crypto.js";
22
+ import { idGenerator } from "./concerns/id.js";
22
23
  import { ConnectionString } from "./connection-string.class.js";
24
+ import { mapAwsError, UnknownError, NoSuchKey, NotFound } from "./errors.js";
23
25
 
24
26
  export class Client extends EventEmitter {
25
27
  constructor({
@@ -28,19 +30,38 @@ export class Client extends EventEmitter {
28
30
  AwsS3Client,
29
31
  connectionString,
30
32
  parallelism = 10,
33
+ httpClientOptions = {},
31
34
  }) {
32
35
  super();
33
36
  this.verbose = verbose;
34
37
  this.id = id ?? idGenerator();
35
38
  this.parallelism = parallelism;
36
39
  this.config = new ConnectionString(connectionString);
40
+ this.httpClientOptions = {
41
+ keepAlive: false, // Disabled for maximum creation speed
42
+ maxSockets: 10, // Minimal sockets
43
+ maxFreeSockets: 2, // Minimal pool
44
+ timeout: 15000, // Short timeout
45
+ ...httpClientOptions,
46
+ };
37
47
  this.client = AwsS3Client || this.createClient()
38
48
  }
39
49
 
40
50
  createClient() {
51
+ // Create HTTP agents with keep-alive configuration
52
+ const httpAgent = new HttpAgent(this.httpClientOptions);
53
+ const httpsAgent = new HttpsAgent(this.httpClientOptions);
54
+
55
+ // Create HTTP handler with agents
56
+ const httpHandler = new NodeHttpHandler({
57
+ httpAgent,
58
+ httpsAgent,
59
+ });
60
+
41
61
  let options = {
42
62
  region: this.config.region,
43
63
  endpoint: this.config.endpoint,
64
+ requestHandler: httpHandler,
44
65
  }
45
66
 
46
67
  if (this.config.forcePathStyle) options.forcePathStyle = true
@@ -32,6 +32,9 @@ export class Database extends EventEmitter {
32
32
  this.passphrase = options.passphrase || "secret";
33
33
  this.versioningEnabled = options.versioningEnabled || false;
34
34
 
35
+ // Initialize hooks system
36
+ this._initHooks();
37
+
35
38
  // Handle both connection string and individual parameters
36
39
  let connectionString = options.connectionString;
37
40
  if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
@@ -617,6 +620,154 @@ export class Database extends EventEmitter {
617
620
  // Silently ignore errors on exit
618
621
  }
619
622
  }
623
+
624
+ /**
625
+ * Initialize hooks system for database operations
626
+ * @private
627
+ */
628
+ _initHooks() {
629
+ // Map of hook name -> array of hook functions
630
+ this._hooks = new Map();
631
+
632
+ // Define available hooks
633
+ this._hookEvents = [
634
+ 'beforeConnect', 'afterConnect',
635
+ 'beforeCreateResource', 'afterCreateResource',
636
+ 'beforeUploadMetadata', 'afterUploadMetadata',
637
+ 'beforeDisconnect', 'afterDisconnect',
638
+ 'resourceCreated', 'resourceUpdated'
639
+ ];
640
+
641
+ // Initialize hook arrays
642
+ for (const event of this._hookEvents) {
643
+ this._hooks.set(event, []);
644
+ }
645
+
646
+ // Wrap hookable methods
647
+ this._wrapHookableMethods();
648
+ }
649
+
650
+ /**
651
+ * Wrap methods that can have hooks
652
+ * @private
653
+ */
654
+ _wrapHookableMethods() {
655
+ if (this._hooksInstalled) return; // Already wrapped
656
+
657
+ // Store original methods
658
+ this._originalConnect = this.connect.bind(this);
659
+ this._originalCreateResource = this.createResource.bind(this);
660
+ this._originalUploadMetadataFile = this.uploadMetadataFile.bind(this);
661
+ this._originalDisconnect = this.disconnect.bind(this);
662
+
663
+ // Wrap connect
664
+ this.connect = async (...args) => {
665
+ await this._executeHooks('beforeConnect', { args });
666
+ const result = await this._originalConnect(...args);
667
+ await this._executeHooks('afterConnect', { result, args });
668
+ return result;
669
+ };
670
+
671
+ // Wrap createResource
672
+ this.createResource = async (config) => {
673
+ await this._executeHooks('beforeCreateResource', { config });
674
+ const resource = await this._originalCreateResource(config);
675
+ await this._executeHooks('afterCreateResource', { resource, config });
676
+ return resource;
677
+ };
678
+
679
+ // Wrap uploadMetadataFile
680
+ this.uploadMetadataFile = async (...args) => {
681
+ await this._executeHooks('beforeUploadMetadata', { args });
682
+ const result = await this._originalUploadMetadataFile(...args);
683
+ await this._executeHooks('afterUploadMetadata', { result, args });
684
+ return result;
685
+ };
686
+
687
+ // Wrap disconnect
688
+ this.disconnect = async (...args) => {
689
+ await this._executeHooks('beforeDisconnect', { args });
690
+ const result = await this._originalDisconnect(...args);
691
+ await this._executeHooks('afterDisconnect', { result, args });
692
+ return result;
693
+ };
694
+
695
+ this._hooksInstalled = true;
696
+ }
697
+
698
+ /**
699
+ * Add a hook for a specific database event
700
+ * @param {string} event - Hook event name
701
+ * @param {Function} fn - Hook function
702
+ * @example
703
+ * database.addHook('afterCreateResource', async ({ resource }) => {
704
+ * console.log('Resource created:', resource.name);
705
+ * });
706
+ */
707
+ addHook(event, fn) {
708
+ if (!this._hooks) this._initHooks();
709
+ if (!this._hooks.has(event)) {
710
+ throw new Error(`Unknown hook event: ${event}. Available events: ${this._hookEvents.join(', ')}`);
711
+ }
712
+ if (typeof fn !== 'function') {
713
+ throw new Error('Hook function must be a function');
714
+ }
715
+ this._hooks.get(event).push(fn);
716
+ }
717
+
718
+ /**
719
+ * Execute hooks for a specific event
720
+ * @param {string} event - Hook event name
721
+ * @param {Object} context - Context data to pass to hooks
722
+ * @private
723
+ */
724
+ async _executeHooks(event, context = {}) {
725
+ if (!this._hooks || !this._hooks.has(event)) return;
726
+
727
+ const hooks = this._hooks.get(event);
728
+ for (const hook of hooks) {
729
+ try {
730
+ await hook({ database: this, ...context });
731
+ } catch (error) {
732
+ // Emit error but don't stop hook execution
733
+ this.emit('hookError', { event, error, context });
734
+ }
735
+ }
736
+ }
737
+
738
+ /**
739
+ * Remove a hook for a specific event
740
+ * @param {string} event - Hook event name
741
+ * @param {Function} fn - Hook function to remove
742
+ */
743
+ removeHook(event, fn) {
744
+ if (!this._hooks || !this._hooks.has(event)) return;
745
+
746
+ const hooks = this._hooks.get(event);
747
+ const index = hooks.indexOf(fn);
748
+ if (index > -1) {
749
+ hooks.splice(index, 1);
750
+ }
751
+ }
752
+
753
+ /**
754
+ * Get all hooks for a specific event
755
+ * @param {string} event - Hook event name
756
+ * @returns {Function[]} Array of hook functions
757
+ */
758
+ getHooks(event) {
759
+ if (!this._hooks || !this._hooks.has(event)) return [];
760
+ return [...this._hooks.get(event)];
761
+ }
762
+
763
+ /**
764
+ * Clear all hooks for a specific event
765
+ * @param {string} event - Hook event name
766
+ */
767
+ clearHooks(event) {
768
+ if (!this._hooks || !this._hooks.has(event)) return;
769
+ this._hooks.get(event).length = 0;
770
+ }
620
771
  }
621
772
 
622
773
  export class S3db extends Database {}