s3db.js 12.2.4 → 12.4.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/README.md +117 -0
- package/dist/s3db.cjs.js +1596 -167
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1499 -73
- package/dist/s3db.es.js.map +1 -1
- package/package.json +2 -2
- package/src/behaviors/body-only.js +15 -5
- package/src/behaviors/body-overflow.js +9 -0
- package/src/behaviors/user-managed.js +8 -1
- package/src/clients/index.js +14 -0
- package/src/clients/memory-client.class.js +883 -0
- package/src/clients/memory-client.md +917 -0
- package/src/clients/memory-storage.class.js +504 -0
- package/src/{client.class.js → clients/s3-client.class.js} +11 -10
- package/src/concerns/typescript-generator.js +12 -2
- package/src/database.class.js +2 -2
- package/src/index.js +2 -1
- package/src/plugins/api/utils/openapi-generator.js +21 -2
- package/src/plugins/replicators/bigquery-replicator.class.js +109 -21
- package/src/plugins/replicators/mysql-replicator.class.js +9 -1
- package/src/plugins/replicators/planetscale-replicator.class.js +9 -1
- package/src/plugins/replicators/postgres-replicator.class.js +9 -1
- package/src/plugins/replicators/schema-sync.helper.js +53 -2
- package/src/plugins/replicators/turso-replicator.class.js +9 -1
- package/src/plugins/tfstate/s3-driver.js +3 -3
- package/src/plugins/vector.plugin.js +3 -3
- package/src/resource.class.js +203 -4
- package/src/schema.class.js +223 -33
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemoryStorage - Internal Storage Engine for Memory Client
|
|
3
|
+
*
|
|
4
|
+
* Simulates S3 object storage in memory using Map data structure.
|
|
5
|
+
* Supports snapshot/restore, persistence, and configurable limits.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
import { writeFile, readFile } from 'fs/promises';
|
|
10
|
+
import { Readable } from 'stream';
|
|
11
|
+
import tryFn from '../concerns/try-fn.js';
|
|
12
|
+
|
|
13
|
+
export class MemoryStorage {
|
|
14
|
+
constructor(config = {}) {
|
|
15
|
+
/**
|
|
16
|
+
* Main storage: Map<key, ObjectData>
|
|
17
|
+
* ObjectData: { body, metadata, contentType, etag, lastModified, size, contentEncoding, contentLength }
|
|
18
|
+
*/
|
|
19
|
+
this.objects = new Map();
|
|
20
|
+
|
|
21
|
+
// Configuration
|
|
22
|
+
this.bucket = config.bucket || 's3db';
|
|
23
|
+
this.enforceLimits = config.enforceLimits || false;
|
|
24
|
+
this.metadataLimit = config.metadataLimit || 2048; // 2KB like S3
|
|
25
|
+
this.maxObjectSize = config.maxObjectSize || 5 * 1024 * 1024 * 1024; // 5GB
|
|
26
|
+
this.persistPath = config.persistPath;
|
|
27
|
+
this.autoPersist = config.autoPersist || false;
|
|
28
|
+
this.verbose = config.verbose || false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate ETag (MD5 hash) for object body
|
|
33
|
+
*/
|
|
34
|
+
_generateETag(body) {
|
|
35
|
+
const buffer = Buffer.isBuffer(body) ? body : Buffer.from(body || '');
|
|
36
|
+
return createHash('md5').update(buffer).digest('hex');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Calculate metadata size in bytes
|
|
41
|
+
*/
|
|
42
|
+
_calculateMetadataSize(metadata) {
|
|
43
|
+
if (!metadata) return 0;
|
|
44
|
+
|
|
45
|
+
let size = 0;
|
|
46
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
47
|
+
// S3 counts key + value in UTF-8 bytes
|
|
48
|
+
size += Buffer.byteLength(key, 'utf8');
|
|
49
|
+
size += Buffer.byteLength(String(value), 'utf8');
|
|
50
|
+
}
|
|
51
|
+
return size;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate limits if enforceLimits is enabled
|
|
56
|
+
*/
|
|
57
|
+
_validateLimits(body, metadata) {
|
|
58
|
+
if (!this.enforceLimits) return;
|
|
59
|
+
|
|
60
|
+
// Check metadata size
|
|
61
|
+
const metadataSize = this._calculateMetadataSize(metadata);
|
|
62
|
+
if (metadataSize > this.metadataLimit) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Metadata size (${metadataSize} bytes) exceeds limit of ${this.metadataLimit} bytes`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check object size
|
|
69
|
+
const bodySize = Buffer.isBuffer(body) ? body.length : Buffer.byteLength(body || '', 'utf8');
|
|
70
|
+
if (bodySize > this.maxObjectSize) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Object size (${bodySize} bytes) exceeds limit of ${this.maxObjectSize} bytes`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Store an object
|
|
79
|
+
*/
|
|
80
|
+
async put(key, { body, metadata, contentType, contentEncoding, contentLength, ifMatch }) {
|
|
81
|
+
// Validate limits
|
|
82
|
+
this._validateLimits(body, metadata);
|
|
83
|
+
|
|
84
|
+
// Check ifMatch (conditional put)
|
|
85
|
+
if (ifMatch !== undefined) {
|
|
86
|
+
const existing = this.objects.get(key);
|
|
87
|
+
if (existing && existing.etag !== ifMatch) {
|
|
88
|
+
throw new Error(`Precondition failed: ETag mismatch for key "${key}"`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const buffer = Buffer.isBuffer(body) ? body : Buffer.from(body || '');
|
|
93
|
+
const etag = this._generateETag(buffer);
|
|
94
|
+
const lastModified = new Date().toISOString();
|
|
95
|
+
const size = buffer.length;
|
|
96
|
+
|
|
97
|
+
const objectData = {
|
|
98
|
+
body: buffer,
|
|
99
|
+
metadata: metadata || {},
|
|
100
|
+
contentType: contentType || 'application/octet-stream',
|
|
101
|
+
etag,
|
|
102
|
+
lastModified,
|
|
103
|
+
size,
|
|
104
|
+
contentEncoding,
|
|
105
|
+
contentLength: contentLength || size
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
this.objects.set(key, objectData);
|
|
109
|
+
|
|
110
|
+
if (this.verbose) {
|
|
111
|
+
console.log(`[MemoryStorage] PUT ${key} (${size} bytes, etag: ${etag})`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Auto-persist if enabled
|
|
115
|
+
if (this.autoPersist && this.persistPath) {
|
|
116
|
+
await this.saveToDisk();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
ETag: etag,
|
|
121
|
+
VersionId: null, // Memory storage doesn't support versioning
|
|
122
|
+
ServerSideEncryption: null,
|
|
123
|
+
Location: `/${this.bucket}/${key}`
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Retrieve an object
|
|
129
|
+
*/
|
|
130
|
+
async get(key) {
|
|
131
|
+
const obj = this.objects.get(key);
|
|
132
|
+
|
|
133
|
+
if (!obj) {
|
|
134
|
+
const error = new Error(`Object not found: ${key}`);
|
|
135
|
+
error.name = 'NoSuchKey';
|
|
136
|
+
error.$metadata = {
|
|
137
|
+
httpStatusCode: 404,
|
|
138
|
+
requestId: 'memory-' + Date.now(),
|
|
139
|
+
attempts: 1,
|
|
140
|
+
totalRetryDelay: 0
|
|
141
|
+
};
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (this.verbose) {
|
|
146
|
+
console.log(`[MemoryStorage] GET ${key} (${obj.size} bytes)`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Convert Buffer to Readable stream (same as real S3 Client)
|
|
150
|
+
const bodyStream = Readable.from(obj.body);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
Body: bodyStream,
|
|
154
|
+
Metadata: { ...obj.metadata },
|
|
155
|
+
ContentType: obj.contentType,
|
|
156
|
+
ContentLength: obj.size,
|
|
157
|
+
ETag: obj.etag,
|
|
158
|
+
LastModified: new Date(obj.lastModified),
|
|
159
|
+
ContentEncoding: obj.contentEncoding
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get object metadata only (like S3 HeadObject)
|
|
165
|
+
*/
|
|
166
|
+
async head(key) {
|
|
167
|
+
const obj = this.objects.get(key);
|
|
168
|
+
|
|
169
|
+
if (!obj) {
|
|
170
|
+
const error = new Error(`Object not found: ${key}`);
|
|
171
|
+
error.name = 'NoSuchKey';
|
|
172
|
+
error.$metadata = {
|
|
173
|
+
httpStatusCode: 404,
|
|
174
|
+
requestId: 'memory-' + Date.now(),
|
|
175
|
+
attempts: 1,
|
|
176
|
+
totalRetryDelay: 0
|
|
177
|
+
};
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (this.verbose) {
|
|
182
|
+
console.log(`[MemoryStorage] HEAD ${key}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
Metadata: { ...obj.metadata },
|
|
187
|
+
ContentType: obj.contentType,
|
|
188
|
+
ContentLength: obj.size,
|
|
189
|
+
ETag: obj.etag,
|
|
190
|
+
LastModified: new Date(obj.lastModified),
|
|
191
|
+
ContentEncoding: obj.contentEncoding
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Copy an object
|
|
197
|
+
*/
|
|
198
|
+
async copy(from, to, { metadata, metadataDirective, contentType }) {
|
|
199
|
+
const source = this.objects.get(from);
|
|
200
|
+
|
|
201
|
+
if (!source) {
|
|
202
|
+
const error = new Error(`Source object not found: ${from}`);
|
|
203
|
+
error.name = 'NoSuchKey';
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Determine final metadata based on directive
|
|
208
|
+
let finalMetadata = { ...source.metadata };
|
|
209
|
+
if (metadataDirective === 'REPLACE' && metadata) {
|
|
210
|
+
finalMetadata = metadata;
|
|
211
|
+
} else if (metadata) {
|
|
212
|
+
finalMetadata = { ...finalMetadata, ...metadata };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Copy the object
|
|
216
|
+
const result = await this.put(to, {
|
|
217
|
+
body: source.body,
|
|
218
|
+
metadata: finalMetadata,
|
|
219
|
+
contentType: contentType || source.contentType,
|
|
220
|
+
contentEncoding: source.contentEncoding
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (this.verbose) {
|
|
224
|
+
console.log(`[MemoryStorage] COPY ${from} → ${to}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Check if object exists
|
|
232
|
+
*/
|
|
233
|
+
exists(key) {
|
|
234
|
+
return this.objects.has(key);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Delete an object
|
|
239
|
+
*/
|
|
240
|
+
async delete(key) {
|
|
241
|
+
const existed = this.objects.has(key);
|
|
242
|
+
this.objects.delete(key);
|
|
243
|
+
|
|
244
|
+
if (this.verbose) {
|
|
245
|
+
console.log(`[MemoryStorage] DELETE ${key} (existed: ${existed})`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Auto-persist if enabled
|
|
249
|
+
if (this.autoPersist && this.persistPath) {
|
|
250
|
+
await this.saveToDisk();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
DeleteMarker: false,
|
|
255
|
+
VersionId: null
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Delete multiple objects (batch)
|
|
261
|
+
*/
|
|
262
|
+
async deleteMultiple(keys) {
|
|
263
|
+
const deleted = [];
|
|
264
|
+
const errors = [];
|
|
265
|
+
|
|
266
|
+
for (const key of keys) {
|
|
267
|
+
try {
|
|
268
|
+
await this.delete(key);
|
|
269
|
+
deleted.push({ Key: key });
|
|
270
|
+
} catch (error) {
|
|
271
|
+
errors.push({
|
|
272
|
+
Key: key,
|
|
273
|
+
Code: error.name || 'InternalError',
|
|
274
|
+
Message: error.message
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (this.verbose) {
|
|
280
|
+
console.log(`[MemoryStorage] DELETE BATCH (${deleted.length} deleted, ${errors.length} errors)`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return { Deleted: deleted, Errors: errors };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* List objects with prefix/delimiter support
|
|
288
|
+
*/
|
|
289
|
+
async list({ prefix = '', delimiter = null, maxKeys = 1000, continuationToken = null }) {
|
|
290
|
+
const allKeys = Array.from(this.objects.keys());
|
|
291
|
+
|
|
292
|
+
// Filter by prefix
|
|
293
|
+
let filteredKeys = prefix
|
|
294
|
+
? allKeys.filter(key => key.startsWith(prefix))
|
|
295
|
+
: allKeys;
|
|
296
|
+
|
|
297
|
+
// Sort keys
|
|
298
|
+
filteredKeys.sort();
|
|
299
|
+
|
|
300
|
+
// Handle continuation token (simple offset-based pagination)
|
|
301
|
+
let startIndex = 0;
|
|
302
|
+
if (continuationToken) {
|
|
303
|
+
startIndex = parseInt(continuationToken) || 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Apply pagination
|
|
307
|
+
const paginatedKeys = filteredKeys.slice(startIndex, startIndex + maxKeys);
|
|
308
|
+
const isTruncated = startIndex + maxKeys < filteredKeys.length;
|
|
309
|
+
const nextContinuationToken = isTruncated ? String(startIndex + maxKeys) : null;
|
|
310
|
+
|
|
311
|
+
// Group by common prefixes if delimiter is set
|
|
312
|
+
const commonPrefixes = new Set();
|
|
313
|
+
const contents = [];
|
|
314
|
+
|
|
315
|
+
for (const key of paginatedKeys) {
|
|
316
|
+
if (delimiter && prefix) {
|
|
317
|
+
// Find the next delimiter after prefix
|
|
318
|
+
const suffix = key.substring(prefix.length);
|
|
319
|
+
const delimiterIndex = suffix.indexOf(delimiter);
|
|
320
|
+
|
|
321
|
+
if (delimiterIndex !== -1) {
|
|
322
|
+
// This key has a delimiter - add to common prefixes
|
|
323
|
+
const commonPrefix = prefix + suffix.substring(0, delimiterIndex + 1);
|
|
324
|
+
commonPrefixes.add(commonPrefix);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Add to contents
|
|
330
|
+
const obj = this.objects.get(key);
|
|
331
|
+
contents.push({
|
|
332
|
+
Key: key,
|
|
333
|
+
Size: obj.size,
|
|
334
|
+
LastModified: new Date(obj.lastModified),
|
|
335
|
+
ETag: obj.etag,
|
|
336
|
+
StorageClass: 'STANDARD'
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (this.verbose) {
|
|
341
|
+
console.log(`[MemoryStorage] LIST prefix="${prefix}" (${contents.length} objects, ${commonPrefixes.size} prefixes)`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
Contents: contents,
|
|
346
|
+
CommonPrefixes: Array.from(commonPrefixes).map(prefix => ({ Prefix: prefix })),
|
|
347
|
+
IsTruncated: isTruncated,
|
|
348
|
+
NextContinuationToken: nextContinuationToken,
|
|
349
|
+
KeyCount: contents.length + commonPrefixes.size,
|
|
350
|
+
MaxKeys: maxKeys,
|
|
351
|
+
Prefix: prefix,
|
|
352
|
+
Delimiter: delimiter
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Create a snapshot of current state
|
|
358
|
+
*/
|
|
359
|
+
snapshot() {
|
|
360
|
+
const snapshot = {
|
|
361
|
+
timestamp: new Date().toISOString(),
|
|
362
|
+
bucket: this.bucket,
|
|
363
|
+
objectCount: this.objects.size,
|
|
364
|
+
objects: {}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
for (const [key, obj] of this.objects.entries()) {
|
|
368
|
+
snapshot.objects[key] = {
|
|
369
|
+
body: obj.body.toString('base64'),
|
|
370
|
+
metadata: obj.metadata,
|
|
371
|
+
contentType: obj.contentType,
|
|
372
|
+
etag: obj.etag,
|
|
373
|
+
lastModified: obj.lastModified,
|
|
374
|
+
size: obj.size,
|
|
375
|
+
contentEncoding: obj.contentEncoding,
|
|
376
|
+
contentLength: obj.contentLength
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return snapshot;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Restore from a snapshot
|
|
385
|
+
*/
|
|
386
|
+
restore(snapshot) {
|
|
387
|
+
if (!snapshot || !snapshot.objects) {
|
|
388
|
+
throw new Error('Invalid snapshot format');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
this.objects.clear();
|
|
392
|
+
|
|
393
|
+
for (const [key, obj] of Object.entries(snapshot.objects)) {
|
|
394
|
+
this.objects.set(key, {
|
|
395
|
+
body: Buffer.from(obj.body, 'base64'),
|
|
396
|
+
metadata: obj.metadata,
|
|
397
|
+
contentType: obj.contentType,
|
|
398
|
+
etag: obj.etag,
|
|
399
|
+
lastModified: obj.lastModified,
|
|
400
|
+
size: obj.size,
|
|
401
|
+
contentEncoding: obj.contentEncoding,
|
|
402
|
+
contentLength: obj.contentLength
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (this.verbose) {
|
|
407
|
+
console.log(`[MemoryStorage] Restored snapshot with ${this.objects.size} objects`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Save current state to disk
|
|
413
|
+
*/
|
|
414
|
+
async saveToDisk(customPath) {
|
|
415
|
+
const path = customPath || this.persistPath;
|
|
416
|
+
if (!path) {
|
|
417
|
+
throw new Error('No persist path configured');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const snapshot = this.snapshot();
|
|
421
|
+
const json = JSON.stringify(snapshot, null, 2);
|
|
422
|
+
|
|
423
|
+
const [ok, err] = await tryFn(() => writeFile(path, json, 'utf-8'));
|
|
424
|
+
|
|
425
|
+
if (!ok) {
|
|
426
|
+
throw new Error(`Failed to save to disk: ${err.message}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (this.verbose) {
|
|
430
|
+
console.log(`[MemoryStorage] Saved ${this.objects.size} objects to ${path}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return path;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Load state from disk
|
|
438
|
+
*/
|
|
439
|
+
async loadFromDisk(customPath) {
|
|
440
|
+
const path = customPath || this.persistPath;
|
|
441
|
+
if (!path) {
|
|
442
|
+
throw new Error('No persist path configured');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const [ok, err, json] = await tryFn(() => readFile(path, 'utf-8'));
|
|
446
|
+
|
|
447
|
+
if (!ok) {
|
|
448
|
+
throw new Error(`Failed to load from disk: ${err.message}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const snapshot = JSON.parse(json);
|
|
452
|
+
this.restore(snapshot);
|
|
453
|
+
|
|
454
|
+
if (this.verbose) {
|
|
455
|
+
console.log(`[MemoryStorage] Loaded ${this.objects.size} objects from ${path}`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return snapshot;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Get storage statistics
|
|
463
|
+
*/
|
|
464
|
+
getStats() {
|
|
465
|
+
let totalSize = 0;
|
|
466
|
+
const keys = [];
|
|
467
|
+
|
|
468
|
+
for (const [key, obj] of this.objects.entries()) {
|
|
469
|
+
totalSize += obj.size;
|
|
470
|
+
keys.push(key);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
objectCount: this.objects.size,
|
|
475
|
+
totalSize,
|
|
476
|
+
totalSizeFormatted: this._formatBytes(totalSize),
|
|
477
|
+
keys: keys.sort(),
|
|
478
|
+
bucket: this.bucket
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Format bytes for human reading
|
|
484
|
+
*/
|
|
485
|
+
_formatBytes(bytes) {
|
|
486
|
+
if (bytes === 0) return '0 Bytes';
|
|
487
|
+
const k = 1024;
|
|
488
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
489
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
490
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Clear all objects
|
|
495
|
+
*/
|
|
496
|
+
clear() {
|
|
497
|
+
this.objects.clear();
|
|
498
|
+
if (this.verbose) {
|
|
499
|
+
console.log(`[MemoryStorage] Cleared all objects`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export default MemoryStorage;
|
|
@@ -7,7 +7,7 @@ import { PromisePool } from "@supercharge/promise-pool";
|
|
|
7
7
|
import { NodeHttpHandler } from '@smithy/node-http-handler';
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
S3Client,
|
|
10
|
+
S3Client as AwsS3Client,
|
|
11
11
|
PutObjectCommand,
|
|
12
12
|
GetObjectCommand,
|
|
13
13
|
CopyObjectCommand,
|
|
@@ -17,14 +17,14 @@ import {
|
|
|
17
17
|
ListObjectsV2Command,
|
|
18
18
|
} from '@aws-sdk/client-s3';
|
|
19
19
|
|
|
20
|
-
import tryFn from "
|
|
21
|
-
import { md5 } from "
|
|
22
|
-
import { idGenerator } from "
|
|
23
|
-
import { metadataEncode, metadataDecode } from "
|
|
24
|
-
import { ConnectionString } from "
|
|
25
|
-
import { mapAwsError, UnknownError, NoSuchKey, NotFound } from "
|
|
20
|
+
import tryFn from "../concerns/try-fn.js";
|
|
21
|
+
import { md5 } from "../concerns/crypto.js";
|
|
22
|
+
import { idGenerator } from "../concerns/id.js";
|
|
23
|
+
import { metadataEncode, metadataDecode } from "../concerns/metadata-encoding.js";
|
|
24
|
+
import { ConnectionString } from "../connection-string.class.js";
|
|
25
|
+
import { mapAwsError, UnknownError, NoSuchKey, NotFound } from "../errors.js";
|
|
26
26
|
|
|
27
|
-
export class
|
|
27
|
+
export class S3Client extends EventEmitter {
|
|
28
28
|
constructor({
|
|
29
29
|
verbose = false,
|
|
30
30
|
id = null,
|
|
@@ -75,7 +75,7 @@ export class Client extends EventEmitter {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
const client = new
|
|
78
|
+
const client = new AwsS3Client(options);
|
|
79
79
|
|
|
80
80
|
// Adiciona middleware para Content-MD5 em DeleteObjectsCommand
|
|
81
81
|
client.middlewareStack.add(
|
|
@@ -590,4 +590,5 @@ export class Client extends EventEmitter {
|
|
|
590
590
|
}
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
-
export
|
|
593
|
+
// Default export for backward compatibility
|
|
594
|
+
export default S3Client;
|
|
@@ -190,10 +190,20 @@ export async function generateTypes(database, options = {}) {
|
|
|
190
190
|
const resourceInterfaces = [];
|
|
191
191
|
|
|
192
192
|
for (const [name, resource] of Object.entries(database.resources)) {
|
|
193
|
-
const
|
|
193
|
+
const allAttributes = resource.config?.attributes || resource.attributes || {};
|
|
194
194
|
const timestamps = resource.config?.timestamps || false;
|
|
195
195
|
|
|
196
|
-
|
|
196
|
+
// Filter out plugin attributes - they are internal implementation details
|
|
197
|
+
// and should not be exposed in public TypeScript interfaces
|
|
198
|
+
const pluginAttrNames = resource.schema?._pluginAttributes
|
|
199
|
+
? Object.values(resource.schema._pluginAttributes).flat()
|
|
200
|
+
: [];
|
|
201
|
+
|
|
202
|
+
const userAttributes = Object.fromEntries(
|
|
203
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const interfaceDef = generateResourceInterface(name, userAttributes, timestamps);
|
|
197
207
|
lines.push(interfaceDef);
|
|
198
208
|
|
|
199
209
|
resourceInterfaces.push({
|
package/src/database.class.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createHash } from "crypto";
|
|
|
3
3
|
import { isEmpty, isFunction } from "lodash-es";
|
|
4
4
|
import jsonStableStringify from "json-stable-stringify";
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import { S3Client } from "./clients/s3-client.class.js";
|
|
7
7
|
import tryFn from "./concerns/try-fn.js";
|
|
8
8
|
import Resource from "./resource.class.js";
|
|
9
9
|
import { ResourceNotFound, DatabaseError } from "./errors.js";
|
|
@@ -98,7 +98,7 @@ export class Database extends EventEmitter {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
this.client = options.client || new
|
|
101
|
+
this.client = options.client || new S3Client({
|
|
102
102
|
verbose: this.verbose,
|
|
103
103
|
parallelism: this.parallelism,
|
|
104
104
|
connectionString: connectionString,
|
package/src/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// directories (keep wildcard exports for these)
|
|
2
2
|
export * from './concerns/index.js'
|
|
3
3
|
export * from './plugins/index.js'
|
|
4
|
+
export * from './clients/index.js'
|
|
4
5
|
export * from './errors.js'
|
|
5
6
|
|
|
6
7
|
// main classes (explicit named exports for better tree-shaking)
|
|
7
8
|
export { Database as S3db } from './database.class.js'
|
|
8
9
|
export { Database } from './database.class.js'
|
|
9
|
-
export { Client } from './client.class.js'
|
|
10
|
+
export { S3Client as Client } from './clients/s3-client.class.js' // backward compatibility
|
|
10
11
|
export { Resource } from './resource.class.js'
|
|
11
12
|
export { Schema } from './schema.class.js'
|
|
12
13
|
export { Validator } from './validator.class.js'
|
|
@@ -98,7 +98,17 @@ function generateResourceSchema(resource) {
|
|
|
98
98
|
const properties = {};
|
|
99
99
|
const required = [];
|
|
100
100
|
|
|
101
|
-
const
|
|
101
|
+
const allAttributes = resource.config?.attributes || resource.attributes || {};
|
|
102
|
+
|
|
103
|
+
// Filter out plugin attributes - they are internal implementation details
|
|
104
|
+
// and should not be exposed in public API documentation
|
|
105
|
+
const pluginAttrNames = resource.schema?._pluginAttributes
|
|
106
|
+
? Object.values(resource.schema._pluginAttributes).flat()
|
|
107
|
+
: [];
|
|
108
|
+
|
|
109
|
+
const attributes = Object.fromEntries(
|
|
110
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
111
|
+
);
|
|
102
112
|
|
|
103
113
|
// Extract resource description (supports both string and object format)
|
|
104
114
|
const resourceDescription = resource.config?.description;
|
|
@@ -254,7 +264,16 @@ function generateResourcePaths(resource, version, config = {}) {
|
|
|
254
264
|
}
|
|
255
265
|
|
|
256
266
|
// Create query parameters only for partition fields
|
|
257
|
-
const
|
|
267
|
+
const allAttributes = resource.config?.attributes || resource.attributes || {};
|
|
268
|
+
|
|
269
|
+
// Filter out plugin attributes
|
|
270
|
+
const pluginAttrNames = resource.schema?._pluginAttributes
|
|
271
|
+
? Object.values(resource.schema._pluginAttributes).flat()
|
|
272
|
+
: [];
|
|
273
|
+
|
|
274
|
+
const attributes = Object.fromEntries(
|
|
275
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
276
|
+
);
|
|
258
277
|
|
|
259
278
|
for (const fieldName of partitionFieldsSet) {
|
|
260
279
|
const fieldDef = attributes[fieldName];
|