s3db.js 13.6.0 → 14.0.2
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 +139 -43
- package/dist/s3db.cjs +72425 -38970
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +72177 -38764
- package/dist/s3db.es.js.map +1 -1
- package/mcp/lib/base-handler.js +157 -0
- package/mcp/lib/handlers/connection-handler.js +280 -0
- package/mcp/lib/handlers/query-handler.js +533 -0
- package/mcp/lib/handlers/resource-handler.js +428 -0
- package/mcp/lib/tool-registry.js +336 -0
- package/mcp/lib/tools/connection-tools.js +161 -0
- package/mcp/lib/tools/query-tools.js +267 -0
- package/mcp/lib/tools/resource-tools.js +404 -0
- package/package.json +94 -49
- package/src/clients/memory-client.class.js +346 -191
- package/src/clients/memory-storage.class.js +300 -84
- package/src/clients/s3-client.class.js +7 -6
- package/src/concerns/geo-encoding.js +19 -2
- package/src/concerns/ip.js +59 -9
- package/src/concerns/money.js +8 -1
- package/src/concerns/password-hashing.js +49 -8
- package/src/concerns/plugin-storage.js +186 -18
- package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
- package/src/database.class.js +139 -29
- package/src/errors.js +332 -42
- package/src/plugins/api/auth/oidc-auth.js +66 -17
- package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
- package/src/plugins/api/auth/strategies/factory.class.js +63 -0
- package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
- package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
- package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
- package/src/plugins/api/concerns/failban-manager.js +106 -57
- package/src/plugins/api/concerns/opengraph-helper.js +116 -0
- package/src/plugins/api/concerns/route-context.js +601 -0
- package/src/plugins/api/concerns/state-machine.js +288 -0
- package/src/plugins/api/index.js +180 -41
- package/src/plugins/api/routes/auth-routes.js +198 -30
- package/src/plugins/api/routes/resource-routes.js +19 -4
- package/src/plugins/api/server/health-manager.class.js +163 -0
- package/src/plugins/api/server/middleware-chain.class.js +310 -0
- package/src/plugins/api/server/router.class.js +472 -0
- package/src/plugins/api/server.js +280 -1303
- package/src/plugins/api/utils/custom-routes.js +17 -5
- package/src/plugins/api/utils/guards.js +76 -17
- package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
- package/src/plugins/api/utils/openapi-generator.js +7 -6
- package/src/plugins/api/utils/template-engine.js +77 -3
- package/src/plugins/audit.plugin.js +30 -8
- package/src/plugins/backup.plugin.js +110 -14
- package/src/plugins/cache/cache.class.js +22 -5
- package/src/plugins/cache/filesystem-cache.class.js +116 -19
- package/src/plugins/cache/memory-cache.class.js +211 -57
- package/src/plugins/cache/multi-tier-cache.class.js +371 -0
- package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
- package/src/plugins/cache/redis-cache.class.js +552 -0
- package/src/plugins/cache/s3-cache.class.js +17 -8
- package/src/plugins/cache.plugin.js +176 -61
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
- package/src/plugins/cloud-inventory/index.js +29 -8
- package/src/plugins/cloud-inventory/registry.js +64 -42
- package/src/plugins/cloud-inventory.plugin.js +240 -138
- package/src/plugins/concerns/plugin-dependencies.js +54 -0
- package/src/plugins/concerns/resource-names.js +100 -0
- package/src/plugins/consumers/index.js +10 -2
- package/src/plugins/consumers/sqs-consumer.js +12 -2
- package/src/plugins/cookie-farm-suite.plugin.js +278 -0
- package/src/plugins/cookie-farm.errors.js +73 -0
- package/src/plugins/cookie-farm.plugin.js +869 -0
- package/src/plugins/costs.plugin.js +7 -1
- package/src/plugins/eventual-consistency/analytics.js +94 -19
- package/src/plugins/eventual-consistency/config.js +15 -7
- package/src/plugins/eventual-consistency/consolidation.js +29 -11
- package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
- package/src/plugins/eventual-consistency/helpers.js +39 -14
- package/src/plugins/eventual-consistency/install.js +21 -2
- package/src/plugins/eventual-consistency/utils.js +32 -10
- package/src/plugins/fulltext.plugin.js +38 -11
- package/src/plugins/geo.plugin.js +61 -9
- package/src/plugins/identity/concerns/config.js +61 -0
- package/src/plugins/identity/concerns/mfa-manager.js +15 -2
- package/src/plugins/identity/concerns/rate-limit.js +124 -0
- package/src/plugins/identity/concerns/resource-schemas.js +9 -1
- package/src/plugins/identity/concerns/token-generator.js +29 -4
- package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
- package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
- package/src/plugins/identity/drivers/index.js +18 -0
- package/src/plugins/identity/drivers/password-driver.js +122 -0
- package/src/plugins/identity/email-service.js +17 -2
- package/src/plugins/identity/index.js +413 -69
- package/src/plugins/identity/oauth2-server.js +413 -30
- package/src/plugins/identity/oidc-discovery.js +16 -8
- package/src/plugins/identity/rsa-keys.js +115 -35
- package/src/plugins/identity/server.js +166 -45
- package/src/plugins/identity/session-manager.js +53 -7
- package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
- package/src/plugins/identity/ui/routes.js +363 -255
- package/src/plugins/importer/index.js +153 -20
- package/src/plugins/index.js +9 -2
- package/src/plugins/kubernetes-inventory/index.js +6 -0
- package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
- package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
- package/src/plugins/kubernetes-inventory.plugin.js +980 -0
- package/src/plugins/metrics.plugin.js +64 -16
- package/src/plugins/ml/base-model.class.js +25 -15
- package/src/plugins/ml/regression-model.class.js +1 -1
- package/src/plugins/ml.errors.js +57 -25
- package/src/plugins/ml.plugin.js +28 -4
- package/src/plugins/namespace.js +210 -0
- package/src/plugins/plugin.class.js +180 -8
- package/src/plugins/puppeteer/console-monitor.js +729 -0
- package/src/plugins/puppeteer/cookie-manager.js +492 -0
- package/src/plugins/puppeteer/network-monitor.js +816 -0
- package/src/plugins/puppeteer/performance-manager.js +746 -0
- package/src/plugins/puppeteer/proxy-manager.js +478 -0
- package/src/plugins/puppeteer/stealth-manager.js +556 -0
- package/src/plugins/puppeteer.errors.js +81 -0
- package/src/plugins/puppeteer.plugin.js +1327 -0
- package/src/plugins/queue-consumer.plugin.js +69 -14
- package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
- package/src/plugins/recon/concerns/command-runner.js +148 -0
- package/src/plugins/recon/concerns/diff-detector.js +372 -0
- package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
- package/src/plugins/recon/concerns/process-manager.js +338 -0
- package/src/plugins/recon/concerns/report-generator.js +478 -0
- package/src/plugins/recon/concerns/security-analyzer.js +571 -0
- package/src/plugins/recon/concerns/target-normalizer.js +68 -0
- package/src/plugins/recon/config/defaults.js +321 -0
- package/src/plugins/recon/config/resources.js +370 -0
- package/src/plugins/recon/index.js +778 -0
- package/src/plugins/recon/managers/dependency-manager.js +174 -0
- package/src/plugins/recon/managers/scheduler-manager.js +179 -0
- package/src/plugins/recon/managers/storage-manager.js +745 -0
- package/src/plugins/recon/managers/target-manager.js +274 -0
- package/src/plugins/recon/stages/asn-stage.js +314 -0
- package/src/plugins/recon/stages/certificate-stage.js +84 -0
- package/src/plugins/recon/stages/dns-stage.js +107 -0
- package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
- package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
- package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
- package/src/plugins/recon/stages/http-stage.js +89 -0
- package/src/plugins/recon/stages/latency-stage.js +148 -0
- package/src/plugins/recon/stages/massdns-stage.js +302 -0
- package/src/plugins/recon/stages/osint-stage.js +1373 -0
- package/src/plugins/recon/stages/ports-stage.js +169 -0
- package/src/plugins/recon/stages/screenshot-stage.js +94 -0
- package/src/plugins/recon/stages/secrets-stage.js +514 -0
- package/src/plugins/recon/stages/subdomains-stage.js +295 -0
- package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
- package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
- package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
- package/src/plugins/recon/stages/whois-stage.js +349 -0
- package/src/plugins/recon.plugin.js +75 -0
- package/src/plugins/recon.plugin.js.backup +2635 -0
- package/src/plugins/relation.errors.js +87 -14
- package/src/plugins/replicator.plugin.js +514 -137
- package/src/plugins/replicators/base-replicator.class.js +89 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
- package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mysql-replicator.class.js +52 -17
- package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
- package/src/plugins/replicators/postgres-replicator.class.js +62 -27
- package/src/plugins/replicators/s3db-replicator.class.js +25 -18
- package/src/plugins/replicators/schema-sync.helper.js +3 -3
- package/src/plugins/replicators/sqs-replicator.class.js +8 -2
- package/src/plugins/replicators/turso-replicator.class.js +23 -3
- package/src/plugins/replicators/webhook-replicator.class.js +42 -4
- package/src/plugins/s3-queue.plugin.js +464 -65
- package/src/plugins/scheduler.plugin.js +20 -6
- package/src/plugins/state-machine.plugin.js +40 -9
- package/src/plugins/tfstate/README.md +126 -126
- package/src/plugins/tfstate/base-driver.js +28 -4
- package/src/plugins/tfstate/errors.js +65 -10
- package/src/plugins/tfstate/filesystem-driver.js +52 -8
- package/src/plugins/tfstate/index.js +163 -90
- package/src/plugins/tfstate/s3-driver.js +64 -6
- package/src/plugins/ttl.plugin.js +72 -17
- package/src/plugins/vector/distances.js +18 -12
- package/src/plugins/vector/kmeans.js +26 -4
- package/src/resource.class.js +115 -19
- package/src/testing/factory.class.js +20 -3
- package/src/testing/seeder.class.js +7 -1
- package/src/clients/memory-client.md +0 -917
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
|
@@ -3,13 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Drop-in replacement for the standard S3 Client that stores everything in memory.
|
|
5
5
|
* Implements the complete Client interface including all AWS SDK commands.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* import { Database } from 's3db.js';
|
|
9
|
-
* import { MemoryClient } from 's3db.js/plugins/emulator';
|
|
10
|
-
*
|
|
11
|
-
* const db = new Database({ client: new MemoryClient() });
|
|
12
|
-
* await db.connect();
|
|
13
6
|
*/
|
|
14
7
|
|
|
15
8
|
import path from 'path';
|
|
@@ -20,36 +13,45 @@ import { PromisePool } from '@supercharge/promise-pool';
|
|
|
20
13
|
import tryFn from '../concerns/try-fn.js';
|
|
21
14
|
import { idGenerator } from '../concerns/id.js';
|
|
22
15
|
import { metadataEncode, metadataDecode } from '../concerns/metadata-encoding.js';
|
|
23
|
-
import { mapAwsError } from '../errors.js';
|
|
16
|
+
import { mapAwsError, DatabaseError, BaseError } from '../errors.js';
|
|
24
17
|
import { MemoryStorage } from './memory-storage.class.js';
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
const pathPosix = path.posix;
|
|
20
|
+
|
|
21
|
+
// Global storage registry - share storage between MemoryClient instances with same bucket
|
|
22
|
+
// This allows reconnection to work properly (simulates S3 persistence)
|
|
23
|
+
const globalStorageRegistry = new Map();
|
|
24
|
+
|
|
29
25
|
export class MemoryClient extends EventEmitter {
|
|
30
26
|
constructor(config = {}) {
|
|
31
27
|
super();
|
|
32
28
|
|
|
33
29
|
// Client configuration
|
|
34
30
|
this.id = config.id || idGenerator(77);
|
|
35
|
-
this.verbose = config.verbose
|
|
31
|
+
this.verbose = Boolean(config.verbose);
|
|
36
32
|
this.parallelism = config.parallelism || 10;
|
|
37
33
|
|
|
38
34
|
// Storage configuration
|
|
39
35
|
this.bucket = config.bucket || 's3db';
|
|
40
36
|
this.keyPrefix = config.keyPrefix || '';
|
|
41
37
|
this.region = config.region || 'us-east-1';
|
|
38
|
+
this._keyPrefixForStrip = this.keyPrefix ? pathPosix.join(this.keyPrefix, '') : '';
|
|
42
39
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
// Get or create shared storage for this bucket
|
|
41
|
+
// This allows multiple MemoryClient instances to share the same data (simulating S3 persistence)
|
|
42
|
+
if (!globalStorageRegistry.has(this.bucket)) {
|
|
43
|
+
globalStorageRegistry.set(this.bucket, new MemoryStorage({
|
|
44
|
+
bucket: this.bucket,
|
|
45
|
+
enforceLimits: config.enforceLimits || false,
|
|
46
|
+
metadataLimit: config.metadataLimit || 2048,
|
|
47
|
+
maxObjectSize: config.maxObjectSize || 5 * 1024 * 1024 * 1024,
|
|
48
|
+
persistPath: config.persistPath,
|
|
49
|
+
autoPersist: config.autoPersist || false,
|
|
50
|
+
verbose: this.verbose
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.storage = globalStorageRegistry.get(this.bucket);
|
|
53
55
|
|
|
54
56
|
// Mock config object (for compatibility with Client interface)
|
|
55
57
|
this.config = {
|
|
@@ -103,7 +105,12 @@ export class MemoryClient extends EventEmitter {
|
|
|
103
105
|
response = await this._handleListObjects(input);
|
|
104
106
|
break;
|
|
105
107
|
default:
|
|
106
|
-
throw new
|
|
108
|
+
throw new DatabaseError(`Unsupported command: ${commandName}`, {
|
|
109
|
+
operation: 'sendCommand',
|
|
110
|
+
statusCode: 400,
|
|
111
|
+
retriable: false,
|
|
112
|
+
suggestion: 'Use one of the supported commands: PutObject, GetObject, HeadObject, CopyObject, DeleteObject, DeleteObjects, or ListObjectsV2.'
|
|
113
|
+
});
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
this.emit('cl:response', commandName, response, input);
|
|
@@ -111,6 +118,9 @@ export class MemoryClient extends EventEmitter {
|
|
|
111
118
|
return response;
|
|
112
119
|
|
|
113
120
|
} catch (error) {
|
|
121
|
+
if (error instanceof BaseError) {
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
114
124
|
// Map errors to AWS SDK format
|
|
115
125
|
const mappedError = mapAwsError(error, {
|
|
116
126
|
bucket: this.bucket,
|
|
@@ -126,13 +136,14 @@ export class MemoryClient extends EventEmitter {
|
|
|
126
136
|
* PutObjectCommand handler
|
|
127
137
|
*/
|
|
128
138
|
async _handlePutObject(input) {
|
|
129
|
-
const key = input.Key;
|
|
130
|
-
const metadata = input.Metadata || {};
|
|
139
|
+
const key = this._applyKeyPrefix(input.Key);
|
|
140
|
+
const metadata = this._encodeMetadata(input.Metadata || {});
|
|
131
141
|
const contentType = input.ContentType;
|
|
132
142
|
const body = input.Body;
|
|
133
143
|
const contentEncoding = input.ContentEncoding;
|
|
134
144
|
const contentLength = input.ContentLength;
|
|
135
145
|
const ifMatch = input.IfMatch;
|
|
146
|
+
const ifNoneMatch = input.IfNoneMatch;
|
|
136
147
|
|
|
137
148
|
return await this.storage.put(key, {
|
|
138
149
|
body,
|
|
@@ -140,7 +151,8 @@ export class MemoryClient extends EventEmitter {
|
|
|
140
151
|
contentType,
|
|
141
152
|
contentEncoding,
|
|
142
153
|
contentLength,
|
|
143
|
-
ifMatch
|
|
154
|
+
ifMatch,
|
|
155
|
+
ifNoneMatch
|
|
144
156
|
});
|
|
145
157
|
}
|
|
146
158
|
|
|
@@ -148,36 +160,41 @@ export class MemoryClient extends EventEmitter {
|
|
|
148
160
|
* GetObjectCommand handler
|
|
149
161
|
*/
|
|
150
162
|
async _handleGetObject(input) {
|
|
151
|
-
const key = input.Key;
|
|
152
|
-
|
|
163
|
+
const key = this._applyKeyPrefix(input.Key);
|
|
164
|
+
const response = await this.storage.get(key);
|
|
165
|
+
return this._decodeMetadataResponse(response);
|
|
153
166
|
}
|
|
154
167
|
|
|
155
168
|
/**
|
|
156
169
|
* HeadObjectCommand handler
|
|
157
170
|
*/
|
|
158
171
|
async _handleHeadObject(input) {
|
|
159
|
-
const key = input.Key;
|
|
160
|
-
|
|
172
|
+
const key = this._applyKeyPrefix(input.Key);
|
|
173
|
+
const response = await this.storage.head(key);
|
|
174
|
+
return this._decodeMetadataResponse(response);
|
|
161
175
|
}
|
|
162
176
|
|
|
163
177
|
/**
|
|
164
178
|
* CopyObjectCommand handler
|
|
165
179
|
*/
|
|
166
180
|
async _handleCopyObject(input) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
const { sourceBucket, sourceKey } = this._parseCopySource(input.CopySource);
|
|
182
|
+
|
|
183
|
+
if (sourceBucket !== this.bucket) {
|
|
184
|
+
throw new DatabaseError(`Cross-bucket copy is not supported in MemoryClient (requested ${sourceBucket} → ${this.bucket})`, {
|
|
185
|
+
operation: 'CopyObject',
|
|
186
|
+
retriable: false,
|
|
187
|
+
suggestion: 'Instantiate a MemoryClient with the desired bucket or copy within the same bucket.'
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const destinationKey = this._applyKeyPrefix(input.Key);
|
|
192
|
+
const encodedMetadata = this._encodeMetadata(input.Metadata);
|
|
176
193
|
|
|
177
194
|
return await this.storage.copy(sourceKey, destinationKey, {
|
|
178
|
-
metadata,
|
|
179
|
-
metadataDirective,
|
|
180
|
-
contentType
|
|
195
|
+
metadata: encodedMetadata,
|
|
196
|
+
metadataDirective: input.MetadataDirective,
|
|
197
|
+
contentType: input.ContentType
|
|
181
198
|
});
|
|
182
199
|
}
|
|
183
200
|
|
|
@@ -185,7 +202,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
185
202
|
* DeleteObjectCommand handler
|
|
186
203
|
*/
|
|
187
204
|
async _handleDeleteObject(input) {
|
|
188
|
-
const key = input.Key;
|
|
205
|
+
const key = this._applyKeyPrefix(input.Key);
|
|
189
206
|
return await this.storage.delete(key);
|
|
190
207
|
}
|
|
191
208
|
|
|
@@ -194,7 +211,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
194
211
|
*/
|
|
195
212
|
async _handleDeleteObjects(input) {
|
|
196
213
|
const objects = input.Delete?.Objects || [];
|
|
197
|
-
const keys = objects.map(obj => obj.Key);
|
|
214
|
+
const keys = objects.map(obj => this._applyKeyPrefix(obj.Key));
|
|
198
215
|
return await this.storage.deleteMultiple(keys);
|
|
199
216
|
}
|
|
200
217
|
|
|
@@ -202,33 +219,30 @@ export class MemoryClient extends EventEmitter {
|
|
|
202
219
|
* ListObjectsV2Command handler
|
|
203
220
|
*/
|
|
204
221
|
async _handleListObjects(input) {
|
|
205
|
-
const fullPrefix = this.
|
|
206
|
-
|
|
207
|
-
: (this.keyPrefix || input.Prefix || '');
|
|
208
|
-
|
|
209
|
-
return await this.storage.list({
|
|
222
|
+
const fullPrefix = this._applyKeyPrefix(input.Prefix || '');
|
|
223
|
+
const params = {
|
|
210
224
|
prefix: fullPrefix,
|
|
211
225
|
delimiter: input.Delimiter,
|
|
212
226
|
maxKeys: input.MaxKeys,
|
|
213
227
|
continuationToken: input.ContinuationToken
|
|
214
|
-
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (input.StartAfter) {
|
|
231
|
+
params.startAfter = this._applyKeyPrefix(input.StartAfter);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const response = await this.storage.list(params);
|
|
235
|
+
return this._normalizeListResponse(response);
|
|
215
236
|
}
|
|
216
237
|
|
|
217
238
|
/**
|
|
218
239
|
* Put an object (Client interface method)
|
|
219
240
|
*/
|
|
220
|
-
async putObject({ key, metadata, contentType, body, contentEncoding, contentLength, ifMatch }) {
|
|
221
|
-
const fullKey = this.
|
|
241
|
+
async putObject({ key, metadata, contentType, body, contentEncoding, contentLength, ifMatch, ifNoneMatch }) {
|
|
242
|
+
const fullKey = this._applyKeyPrefix(key);
|
|
243
|
+
const stringMetadata = this._encodeMetadata(metadata) || {};
|
|
222
244
|
|
|
223
|
-
|
|
224
|
-
const stringMetadata = {};
|
|
225
|
-
if (metadata) {
|
|
226
|
-
for (const [k, v] of Object.entries(metadata)) {
|
|
227
|
-
const validKey = String(k).replace(/[^a-zA-Z0-9\-_]/g, '_');
|
|
228
|
-
const { encoded } = metadataEncode(v);
|
|
229
|
-
stringMetadata[validKey] = encoded;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
245
|
+
const input = { Key: key, Metadata: metadata, ContentType: contentType, Body: body, ContentEncoding: contentEncoding, ContentLength: contentLength, IfMatch: ifMatch, IfNoneMatch: ifNoneMatch };
|
|
232
246
|
|
|
233
247
|
const response = await this.storage.put(fullKey, {
|
|
234
248
|
body,
|
|
@@ -236,10 +250,12 @@ export class MemoryClient extends EventEmitter {
|
|
|
236
250
|
contentType,
|
|
237
251
|
contentEncoding,
|
|
238
252
|
contentLength,
|
|
239
|
-
ifMatch
|
|
253
|
+
ifMatch,
|
|
254
|
+
ifNoneMatch
|
|
240
255
|
});
|
|
241
256
|
|
|
242
|
-
|
|
257
|
+
// Emit cl:response event for CostsPlugin compatibility
|
|
258
|
+
this.emit('cl:response', 'PutObjectCommand', response, input);
|
|
243
259
|
|
|
244
260
|
return response;
|
|
245
261
|
}
|
|
@@ -248,64 +264,41 @@ export class MemoryClient extends EventEmitter {
|
|
|
248
264
|
* Get an object (Client interface method)
|
|
249
265
|
*/
|
|
250
266
|
async getObject(key) {
|
|
251
|
-
const fullKey = this.
|
|
267
|
+
const fullKey = this._applyKeyPrefix(key);
|
|
268
|
+
const input = { Key: key };
|
|
252
269
|
const response = await this.storage.get(fullKey);
|
|
270
|
+
const decodedResponse = this._decodeMetadataResponse(response);
|
|
253
271
|
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
if (response.Metadata) {
|
|
257
|
-
for (const [k, v] of Object.entries(response.Metadata)) {
|
|
258
|
-
decodedMetadata[k] = metadataDecode(v);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
this.emit('cl:GetObject', null, { key });
|
|
272
|
+
// Emit cl:response event for CostsPlugin compatibility
|
|
273
|
+
this.emit('cl:response', 'GetObjectCommand', decodedResponse, input);
|
|
263
274
|
|
|
264
|
-
return
|
|
265
|
-
...response,
|
|
266
|
-
Metadata: decodedMetadata
|
|
267
|
-
};
|
|
275
|
+
return decodedResponse;
|
|
268
276
|
}
|
|
269
277
|
|
|
270
278
|
/**
|
|
271
279
|
* Head object (get metadata only)
|
|
272
280
|
*/
|
|
273
281
|
async headObject(key) {
|
|
274
|
-
const fullKey = this.
|
|
282
|
+
const fullKey = this._applyKeyPrefix(key);
|
|
283
|
+
const input = { Key: key };
|
|
275
284
|
const response = await this.storage.head(fullKey);
|
|
285
|
+
const decodedResponse = this._decodeMetadataResponse(response);
|
|
276
286
|
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
if (response.Metadata) {
|
|
280
|
-
for (const [k, v] of Object.entries(response.Metadata)) {
|
|
281
|
-
decodedMetadata[k] = metadataDecode(v);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
this.emit('cl:HeadObject', null, { key });
|
|
287
|
+
// Emit cl:response event for CostsPlugin compatibility
|
|
288
|
+
this.emit('cl:response', 'HeadObjectCommand', decodedResponse, input);
|
|
286
289
|
|
|
287
|
-
return
|
|
288
|
-
...response,
|
|
289
|
-
Metadata: decodedMetadata
|
|
290
|
-
};
|
|
290
|
+
return decodedResponse;
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
/**
|
|
294
294
|
* Copy an object
|
|
295
295
|
*/
|
|
296
296
|
async copyObject({ from, to, metadata, metadataDirective, contentType }) {
|
|
297
|
-
const fullFrom = this.
|
|
298
|
-
const fullTo = this.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
if (metadata) {
|
|
303
|
-
for (const [k, v] of Object.entries(metadata)) {
|
|
304
|
-
const validKey = String(k).replace(/[^a-zA-Z0-9\-_]/g, '_');
|
|
305
|
-
const { encoded } = metadataEncode(v);
|
|
306
|
-
encodedMetadata[validKey] = encoded;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
297
|
+
const fullFrom = this._applyKeyPrefix(from);
|
|
298
|
+
const fullTo = this._applyKeyPrefix(to);
|
|
299
|
+
const encodedMetadata = this._encodeMetadata(metadata);
|
|
300
|
+
|
|
301
|
+
const input = { CopySource: from, Key: to, Metadata: metadata, MetadataDirective: metadataDirective, ContentType: contentType };
|
|
309
302
|
|
|
310
303
|
const response = await this.storage.copy(fullFrom, fullTo, {
|
|
311
304
|
metadata: encodedMetadata,
|
|
@@ -313,7 +306,8 @@ export class MemoryClient extends EventEmitter {
|
|
|
313
306
|
contentType
|
|
314
307
|
});
|
|
315
308
|
|
|
316
|
-
|
|
309
|
+
// Emit cl:response event for CostsPlugin compatibility
|
|
310
|
+
this.emit('cl:response', 'CopyObjectCommand', response, input);
|
|
317
311
|
|
|
318
312
|
return response;
|
|
319
313
|
}
|
|
@@ -322,7 +316,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
322
316
|
* Check if object exists
|
|
323
317
|
*/
|
|
324
318
|
async exists(key) {
|
|
325
|
-
const fullKey = this.
|
|
319
|
+
const fullKey = this._applyKeyPrefix(key);
|
|
326
320
|
return this.storage.exists(fullKey);
|
|
327
321
|
}
|
|
328
322
|
|
|
@@ -330,10 +324,12 @@ export class MemoryClient extends EventEmitter {
|
|
|
330
324
|
* Delete an object
|
|
331
325
|
*/
|
|
332
326
|
async deleteObject(key) {
|
|
333
|
-
const fullKey = this.
|
|
327
|
+
const fullKey = this._applyKeyPrefix(key);
|
|
328
|
+
const input = { Key: key };
|
|
334
329
|
const response = await this.storage.delete(fullKey);
|
|
335
330
|
|
|
336
|
-
|
|
331
|
+
// Emit cl:response event for CostsPlugin compatibility
|
|
332
|
+
this.emit('cl:response', 'DeleteObjectCommand', response, input);
|
|
337
333
|
|
|
338
334
|
return response;
|
|
339
335
|
}
|
|
@@ -343,9 +339,9 @@ export class MemoryClient extends EventEmitter {
|
|
|
343
339
|
*/
|
|
344
340
|
async deleteObjects(keys) {
|
|
345
341
|
// Add keyPrefix to all keys
|
|
346
|
-
const fullKeys = keys.map(key =>
|
|
347
|
-
|
|
348
|
-
);
|
|
342
|
+
const fullKeys = keys.map(key => this._applyKeyPrefix(key));
|
|
343
|
+
|
|
344
|
+
const input = { Delete: { Objects: keys.map(key => ({ Key: key })) } };
|
|
349
345
|
|
|
350
346
|
// Split into batches for parallel processing
|
|
351
347
|
const batches = chunk(fullKeys, this.parallelism);
|
|
@@ -360,11 +356,12 @@ export class MemoryClient extends EventEmitter {
|
|
|
360
356
|
|
|
361
357
|
// Merge results
|
|
362
358
|
for (const result of results) {
|
|
363
|
-
allResults.Deleted.push(...result.Deleted);
|
|
359
|
+
allResults.Deleted.push(...result.Deleted.map(item => ({ Key: this._stripKeyPrefix(item.Key) })));
|
|
364
360
|
allResults.Errors.push(...result.Errors);
|
|
365
361
|
}
|
|
366
362
|
|
|
367
|
-
|
|
363
|
+
// Emit cl:response event for CostsPlugin compatibility
|
|
364
|
+
this.emit('cl:response', 'DeleteObjectsCommand', allResults, input);
|
|
368
365
|
|
|
369
366
|
return allResults;
|
|
370
367
|
}
|
|
@@ -372,19 +369,28 @@ export class MemoryClient extends EventEmitter {
|
|
|
372
369
|
/**
|
|
373
370
|
* List objects with pagination support
|
|
374
371
|
*/
|
|
375
|
-
async listObjects({ prefix = '', delimiter = null, maxKeys = 1000, continuationToken = null }) {
|
|
376
|
-
const fullPrefix = this.
|
|
377
|
-
|
|
378
|
-
const response = await this.storage.list({
|
|
372
|
+
async listObjects({ prefix = '', delimiter = null, maxKeys = 1000, continuationToken = null, startAfter = null } = {}) {
|
|
373
|
+
const fullPrefix = this._applyKeyPrefix(prefix || '');
|
|
374
|
+
const listParams = {
|
|
379
375
|
prefix: fullPrefix,
|
|
380
376
|
delimiter,
|
|
381
377
|
maxKeys,
|
|
382
378
|
continuationToken
|
|
383
|
-
}
|
|
379
|
+
};
|
|
384
380
|
|
|
385
|
-
|
|
381
|
+
if (startAfter) {
|
|
382
|
+
listParams.startAfter = this._applyKeyPrefix(startAfter);
|
|
383
|
+
}
|
|
386
384
|
|
|
387
|
-
|
|
385
|
+
const input = { Prefix: prefix, Delimiter: delimiter, MaxKeys: maxKeys, ContinuationToken: continuationToken, StartAfter: startAfter };
|
|
386
|
+
|
|
387
|
+
const response = await this.storage.list(listParams);
|
|
388
|
+
const normalized = this._normalizeListResponse(response);
|
|
389
|
+
|
|
390
|
+
// Emit cl:response event for CostsPlugin compatibility
|
|
391
|
+
this.emit('cl:response', 'ListObjectsV2Command', normalized, input);
|
|
392
|
+
|
|
393
|
+
return normalized;
|
|
388
394
|
}
|
|
389
395
|
|
|
390
396
|
/**
|
|
@@ -398,22 +404,29 @@ export class MemoryClient extends EventEmitter {
|
|
|
398
404
|
|
|
399
405
|
// If offset > 0, need to skip ahead
|
|
400
406
|
if (offset > 0) {
|
|
401
|
-
|
|
402
|
-
const fullPrefix = this.keyPrefix ? path.join(this.keyPrefix, prefix) : prefix;
|
|
407
|
+
const fullPrefix = this._applyKeyPrefix(prefix || '');
|
|
403
408
|
const response = await this.storage.list({
|
|
404
409
|
prefix: fullPrefix,
|
|
405
410
|
maxKeys: offset + amount
|
|
406
411
|
});
|
|
407
|
-
keys = response.Contents
|
|
412
|
+
keys = (response.Contents || [])
|
|
413
|
+
.map(x => this._stripKeyPrefix(x.Key))
|
|
414
|
+
.slice(offset, offset + amount);
|
|
415
|
+
truncated = Boolean(response.NextContinuationToken);
|
|
416
|
+
continuationToken = response.NextContinuationToken;
|
|
408
417
|
} else {
|
|
409
418
|
// Regular fetch with amount as maxKeys
|
|
410
419
|
while (truncated) {
|
|
411
|
-
const
|
|
420
|
+
const remaining = amount - keys.length;
|
|
421
|
+
if (remaining <= 0) {
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const res = await this.listObjects({
|
|
412
426
|
prefix,
|
|
413
427
|
continuationToken,
|
|
414
|
-
maxKeys:
|
|
415
|
-
};
|
|
416
|
-
const res = await this.listObjects(options);
|
|
428
|
+
maxKeys: remaining
|
|
429
|
+
});
|
|
417
430
|
if (res.Contents) {
|
|
418
431
|
keys = keys.concat(res.Contents.map(x => x.Key));
|
|
419
432
|
}
|
|
@@ -426,13 +439,6 @@ export class MemoryClient extends EventEmitter {
|
|
|
426
439
|
}
|
|
427
440
|
}
|
|
428
441
|
|
|
429
|
-
// Strip keyPrefix from results
|
|
430
|
-
if (this.keyPrefix) {
|
|
431
|
-
keys = keys
|
|
432
|
-
.map(x => x.replace(this.keyPrefix, ''))
|
|
433
|
-
.map(x => (x.startsWith('/') ? x.replace('/', '') : x));
|
|
434
|
-
}
|
|
435
|
-
|
|
436
442
|
this.emit('cl:GetKeysPage', keys, params);
|
|
437
443
|
return keys;
|
|
438
444
|
}
|
|
@@ -441,20 +447,13 @@ export class MemoryClient extends EventEmitter {
|
|
|
441
447
|
* Get all keys with a given prefix
|
|
442
448
|
*/
|
|
443
449
|
async getAllKeys({ prefix = '' }) {
|
|
444
|
-
const fullPrefix = this.
|
|
450
|
+
const fullPrefix = this._applyKeyPrefix(prefix || '');
|
|
445
451
|
const response = await this.storage.list({
|
|
446
452
|
prefix: fullPrefix,
|
|
447
|
-
maxKeys:
|
|
453
|
+
maxKeys: Number.MAX_SAFE_INTEGER
|
|
448
454
|
});
|
|
449
455
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
// Strip keyPrefix from results
|
|
453
|
-
if (this.keyPrefix) {
|
|
454
|
-
keys = keys
|
|
455
|
-
.map(x => x.replace(this.keyPrefix, ''))
|
|
456
|
-
.map(x => (x.startsWith('/') ? x.replace('/', '') : x));
|
|
457
|
-
}
|
|
456
|
+
const keys = (response.Contents || []).map(x => this._stripKeyPrefix(x.Key));
|
|
458
457
|
|
|
459
458
|
this.emit('cl:GetAllKeys', keys, { prefix });
|
|
460
459
|
return keys;
|
|
@@ -511,40 +510,47 @@ export class MemoryClient extends EventEmitter {
|
|
|
511
510
|
}
|
|
512
511
|
|
|
513
512
|
// Return the key at offset position as continuation token
|
|
514
|
-
const
|
|
513
|
+
const keyForToken = keys[offset];
|
|
514
|
+
const fullKey = this._applyKeyPrefix(keyForToken || '');
|
|
515
|
+
const token = this._encodeContinuationTokenKey(fullKey);
|
|
515
516
|
this.emit('cl:GetContinuationTokenAfterOffset', token, { prefix, offset });
|
|
516
517
|
return token;
|
|
517
518
|
}
|
|
518
519
|
|
|
519
520
|
/**
|
|
520
|
-
* Move
|
|
521
|
+
* Move a single object (copy + delete)
|
|
521
522
|
*/
|
|
522
523
|
async moveObject({ from, to }) {
|
|
523
|
-
|
|
524
|
-
|
|
524
|
+
const [ok, err] = await tryFn(async () => {
|
|
525
|
+
await this.copyObject({ from, to, metadataDirective: 'COPY' });
|
|
526
|
+
await this.deleteObject(from);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
if (!ok) {
|
|
530
|
+
throw new DatabaseError('Unknown error in moveObject', {
|
|
531
|
+
bucket: this.bucket,
|
|
532
|
+
from,
|
|
533
|
+
to,
|
|
534
|
+
original: err
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return true;
|
|
525
539
|
}
|
|
526
540
|
|
|
527
541
|
/**
|
|
528
|
-
* Move all objects
|
|
542
|
+
* Move all objects under a prefix
|
|
529
543
|
*/
|
|
530
544
|
async moveAllObjects({ prefixFrom, prefixTo }) {
|
|
531
545
|
const keys = await this.getAllKeys({ prefix: prefixFrom });
|
|
532
|
-
const results =
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
try {
|
|
546
|
+
const { results, errors } = await PromisePool
|
|
547
|
+
.withConcurrency(this.parallelism)
|
|
548
|
+
.for(keys)
|
|
549
|
+
.process(async (key) => {
|
|
537
550
|
const to = key.replace(prefixFrom, prefixTo);
|
|
538
551
|
await this.moveObject({ from: key, to });
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
errors.push({
|
|
542
|
-
message: error.message,
|
|
543
|
-
raw: error,
|
|
544
|
-
key
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
}
|
|
552
|
+
return { from: key, to };
|
|
553
|
+
});
|
|
548
554
|
|
|
549
555
|
this.emit('moveAllObjects', { results, errors });
|
|
550
556
|
|
|
@@ -595,15 +601,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
595
601
|
}
|
|
596
602
|
|
|
597
603
|
/**
|
|
598
|
-
* Export to BackupPlugin-compatible format
|
|
599
|
-
* Compatible with BackupPlugin for easy migration
|
|
600
|
-
*
|
|
601
|
-
* @param {string} outputDir - Output directory path
|
|
602
|
-
* @param {Object} options - Export options
|
|
603
|
-
* @param {Array<string>} options.resources - Resource names to export (default: all)
|
|
604
|
-
* @param {boolean} options.compress - Use gzip compression (default: true)
|
|
605
|
-
* @param {Object} options.database - Database instance for schema metadata
|
|
606
|
-
* @returns {Promise<Object>} Export manifest with file paths and stats
|
|
604
|
+
* Export to BackupPlugin-compatible format
|
|
607
605
|
*/
|
|
608
606
|
async exportBackup(outputDir, options = {}) {
|
|
609
607
|
const { mkdir, writeFile } = await import('fs/promises');
|
|
@@ -648,7 +646,10 @@ export class MemoryClient extends EventEmitter {
|
|
|
648
646
|
for (const key of keys) {
|
|
649
647
|
// Extract id from key (e.g., resource=products/id=pr1 -> pr1)
|
|
650
648
|
const idMatch = key.match(/\/id=([^/]+)/);
|
|
651
|
-
|
|
649
|
+
let recordId = null;
|
|
650
|
+
if (idMatch && idMatch[1]) {
|
|
651
|
+
recordId = idMatch[1];
|
|
652
|
+
}
|
|
652
653
|
|
|
653
654
|
let record;
|
|
654
655
|
|
|
@@ -656,8 +657,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
656
657
|
if (resource && recordId) {
|
|
657
658
|
try {
|
|
658
659
|
record = await resource.get(recordId);
|
|
659
|
-
} catch
|
|
660
|
-
// Fallback to manual reconstruction if get() fails
|
|
660
|
+
} catch {
|
|
661
661
|
console.warn(`Failed to get record ${recordId} from resource ${resourceName}, using fallback`);
|
|
662
662
|
record = null;
|
|
663
663
|
}
|
|
@@ -776,14 +776,6 @@ export class MemoryClient extends EventEmitter {
|
|
|
776
776
|
|
|
777
777
|
/**
|
|
778
778
|
* Import from BackupPlugin-compatible format
|
|
779
|
-
* Loads data from s3db.json + JSONL files created by BackupPlugin or exportBackup()
|
|
780
|
-
*
|
|
781
|
-
* @param {string} backupDir - Backup directory path containing s3db.json
|
|
782
|
-
* @param {Object} options - Import options
|
|
783
|
-
* @param {Array<string>} options.resources - Resource names to import (default: all)
|
|
784
|
-
* @param {boolean} options.clear - Clear existing data first (default: false)
|
|
785
|
-
* @param {Object} options.database - Database instance to recreate schemas
|
|
786
|
-
* @returns {Promise<Object>} Import stats
|
|
787
779
|
*/
|
|
788
780
|
async importBackup(backupDir, options = {}) {
|
|
789
781
|
const { readFile, readdir } = await import('fs/promises');
|
|
@@ -812,7 +804,8 @@ export class MemoryClient extends EventEmitter {
|
|
|
812
804
|
// Recreate resources if database instance provided
|
|
813
805
|
if (database && metadata.resources) {
|
|
814
806
|
for (const [resourceName, resourceMeta] of Object.entries(metadata.resources)) {
|
|
815
|
-
|
|
807
|
+
/* c8 ignore next -- helper coverage exercised separately */
|
|
808
|
+
if (!this._shouldProcessResource(resourceFilter, resourceName)) continue;
|
|
816
809
|
|
|
817
810
|
if (resourceMeta.schema) {
|
|
818
811
|
try {
|
|
@@ -822,6 +815,9 @@ export class MemoryClient extends EventEmitter {
|
|
|
822
815
|
});
|
|
823
816
|
} catch (error) {
|
|
824
817
|
// Resource might already exist, that's ok
|
|
818
|
+
if (this.verbose) {
|
|
819
|
+
console.warn(`Failed to create resource ${resourceName} during import: ${error.message}`);
|
|
820
|
+
}
|
|
825
821
|
}
|
|
826
822
|
}
|
|
827
823
|
}
|
|
@@ -835,7 +831,8 @@ export class MemoryClient extends EventEmitter {
|
|
|
835
831
|
if (!file.endsWith('.jsonl') && !file.endsWith('.jsonl.gz')) continue;
|
|
836
832
|
|
|
837
833
|
const resourceName = file.replace(/\.jsonl(\.gz)?$/, '');
|
|
838
|
-
|
|
834
|
+
/* c8 ignore next -- helper coverage exercised separately */
|
|
835
|
+
if (!this._shouldProcessResource(resourceFilter, resourceName)) continue;
|
|
839
836
|
|
|
840
837
|
const filePath = `${backupDir}/${file}`;
|
|
841
838
|
let content = await readFile(filePath);
|
|
@@ -854,18 +851,26 @@ export class MemoryClient extends EventEmitter {
|
|
|
854
851
|
const record = JSON.parse(line);
|
|
855
852
|
|
|
856
853
|
// Extract id or use generated one
|
|
857
|
-
|
|
854
|
+
let id;
|
|
855
|
+
if (record.id) {
|
|
856
|
+
id = record.id;
|
|
857
|
+
} else if (record._id) {
|
|
858
|
+
id = record._id;
|
|
859
|
+
} else {
|
|
860
|
+
id = `imported_${Date.now()}_${Math.random()}`;
|
|
861
|
+
}
|
|
858
862
|
|
|
859
863
|
// Separate _body from other fields
|
|
860
|
-
const { _body, id: _, _id: __, ...
|
|
864
|
+
const { _body, id: _, _id: __, ...metadataRecord } = record;
|
|
865
|
+
let bodyBuffer;
|
|
866
|
+
if (typeof _body === 'string') {
|
|
867
|
+
bodyBuffer = Buffer.from(_body);
|
|
868
|
+
}
|
|
861
869
|
|
|
862
|
-
// Store in MemoryClient
|
|
863
|
-
// If _body exists, it's non-JSON body data
|
|
864
|
-
// Otherwise, metadata contains all the data
|
|
865
870
|
await this.putObject({
|
|
866
871
|
key: `resource=${resourceName}/id=${id}`,
|
|
867
|
-
metadata,
|
|
868
|
-
body:
|
|
872
|
+
metadata: metadataRecord,
|
|
873
|
+
body: bodyBuffer
|
|
869
874
|
});
|
|
870
875
|
|
|
871
876
|
importStats.recordsImported++;
|
|
@@ -897,6 +902,156 @@ export class MemoryClient extends EventEmitter {
|
|
|
897
902
|
clear() {
|
|
898
903
|
this.storage.clear();
|
|
899
904
|
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Encode metadata values using s3db metadata encoding
|
|
908
|
+
* Note: S3 metadata keys are case-insensitive and stored as lowercase
|
|
909
|
+
*/
|
|
910
|
+
_encodeMetadata(metadata) {
|
|
911
|
+
if (!metadata) return undefined;
|
|
912
|
+
|
|
913
|
+
const encoded = {};
|
|
914
|
+
for (const [rawKey, value] of Object.entries(metadata)) {
|
|
915
|
+
const validKey = String(rawKey).replace(/[^a-zA-Z0-9\-_]/g, '_').toLowerCase();
|
|
916
|
+
const { encoded: encodedValue } = metadataEncode(value);
|
|
917
|
+
encoded[validKey] = encodedValue;
|
|
918
|
+
}
|
|
919
|
+
return encoded;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
_shouldProcessResource(resourceFilter, resourceName) {
|
|
923
|
+
if (!Array.isArray(resourceFilter) || resourceFilter.length === 0) {
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
return resourceFilter.includes(resourceName);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Decode metadata in S3 responses
|
|
932
|
+
*/
|
|
933
|
+
_decodeMetadataResponse(response) {
|
|
934
|
+
const decodedMetadata = {};
|
|
935
|
+
if (response.Metadata) {
|
|
936
|
+
for (const [k, v] of Object.entries(response.Metadata)) {
|
|
937
|
+
decodedMetadata[k] = metadataDecode(v);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
return {
|
|
942
|
+
...response,
|
|
943
|
+
Metadata: decodedMetadata
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Apply configured keyPrefix to a storage key
|
|
949
|
+
*/
|
|
950
|
+
/* c8 ignore start */
|
|
951
|
+
_applyKeyPrefix(key = '') {
|
|
952
|
+
if (!this.keyPrefix) {
|
|
953
|
+
if (key === undefined || key === null) {
|
|
954
|
+
return '';
|
|
955
|
+
}
|
|
956
|
+
return key;
|
|
957
|
+
}
|
|
958
|
+
if (key === undefined || key === null || key === '') {
|
|
959
|
+
return pathPosix.join(this.keyPrefix, '');
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return pathPosix.join(this.keyPrefix, key);
|
|
963
|
+
}
|
|
964
|
+
/* c8 ignore end */
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Strip configured keyPrefix from a storage key
|
|
968
|
+
*/
|
|
969
|
+
_stripKeyPrefix(key = '') {
|
|
970
|
+
if (!this.keyPrefix) {
|
|
971
|
+
return key;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const normalizedPrefix = this._keyPrefixForStrip;
|
|
975
|
+
if (normalizedPrefix && key.startsWith(normalizedPrefix)) {
|
|
976
|
+
return key.slice(normalizedPrefix.length).replace(/^\/+/, '');
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return key;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Encode continuation token (base64) to mimic AWS S3
|
|
984
|
+
*/
|
|
985
|
+
_encodeContinuationTokenKey(key) {
|
|
986
|
+
return Buffer.from(String(key), 'utf8').toString('base64');
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Parse CopySource header and return bucket/key
|
|
991
|
+
*/
|
|
992
|
+
_parseCopySource(copySource = '') {
|
|
993
|
+
const trimmedSource = String(copySource).replace(/^\//, '');
|
|
994
|
+
const [sourcePath] = trimmedSource.split('?');
|
|
995
|
+
const decodedSource = decodeURIComponent(sourcePath);
|
|
996
|
+
const [sourceBucket, ...sourceKeyParts] = decodedSource.split('/');
|
|
997
|
+
|
|
998
|
+
if (!sourceBucket || sourceKeyParts.length === 0) {
|
|
999
|
+
throw new DatabaseError(`Invalid CopySource value: ${copySource}`, {
|
|
1000
|
+
operation: 'CopyObject',
|
|
1001
|
+
retriable: false,
|
|
1002
|
+
suggestion: 'Provide CopySource in the format "<bucket>/<key>" as expected by AWS S3.'
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return {
|
|
1007
|
+
sourceBucket,
|
|
1008
|
+
sourceKey: sourceKeyParts.join('/')
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Normalize storage list response into client-level structure
|
|
1014
|
+
*/
|
|
1015
|
+
_normalizeListResponse(response) {
|
|
1016
|
+
const rawContents = Array.isArray(response.Contents) ? response.Contents : [];
|
|
1017
|
+
const contents = rawContents.map(item => ({
|
|
1018
|
+
...item,
|
|
1019
|
+
Key: this._stripKeyPrefix(item.Key)
|
|
1020
|
+
}));
|
|
1021
|
+
|
|
1022
|
+
const rawPrefixes = Array.isArray(response.CommonPrefixes) ? response.CommonPrefixes : [];
|
|
1023
|
+
const commonPrefixes = rawPrefixes.map(({ Prefix }) => ({
|
|
1024
|
+
Prefix: this._stripKeyPrefix(Prefix)
|
|
1025
|
+
}));
|
|
1026
|
+
|
|
1027
|
+
return {
|
|
1028
|
+
Contents: contents,
|
|
1029
|
+
CommonPrefixes: commonPrefixes,
|
|
1030
|
+
IsTruncated: response.IsTruncated,
|
|
1031
|
+
ContinuationToken: response.ContinuationToken,
|
|
1032
|
+
NextContinuationToken: response.NextContinuationToken,
|
|
1033
|
+
KeyCount: contents.length,
|
|
1034
|
+
MaxKeys: response.MaxKeys,
|
|
1035
|
+
Prefix: this.keyPrefix ? undefined : response.Prefix,
|
|
1036
|
+
Delimiter: response.Delimiter,
|
|
1037
|
+
StartAfter: response.StartAfter
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Clear all shared storage for a specific bucket (useful for testing)
|
|
1043
|
+
* @param {string} bucket - Bucket name to clear
|
|
1044
|
+
*/
|
|
1045
|
+
static clearBucketStorage(bucket) {
|
|
1046
|
+
globalStorageRegistry.delete(bucket);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Clear ALL shared storage (useful for test cleanup)
|
|
1051
|
+
*/
|
|
1052
|
+
static clearAllStorage() {
|
|
1053
|
+
globalStorageRegistry.clear();
|
|
1054
|
+
}
|
|
900
1055
|
}
|
|
901
1056
|
|
|
902
1057
|
export default MemoryClient;
|