s3db.js 12.3.0 → 13.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.
@@ -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 "./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";
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 Client extends EventEmitter {
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 S3Client(options);
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 default Client;
593
+ // Default export for backward compatibility
594
+ export default S3Client;
@@ -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 Client from "./client.class.js";
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";
@@ -14,7 +14,12 @@ export class Database extends EventEmitter {
14
14
  constructor(options) {
15
15
  super();
16
16
 
17
- this.id = idGenerator(7)
17
+ // Generate database ID with fallback for reliability
18
+ this.id = (() => {
19
+ const [ok, err, id] = tryFn(() => idGenerator(7));
20
+ return ok && id ? id : `db-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
21
+ })();
22
+
18
23
  this.version = "1";
19
24
  // Version is injected during build, fallback to "latest" for development
20
25
  this.s3dbVersion = (() => {
@@ -64,6 +69,7 @@ export class Database extends EventEmitter {
64
69
  this.versioningEnabled = options.versioningEnabled || false;
65
70
  this.persistHooks = options.persistHooks || false; // New configuration for hook persistence
66
71
  this.strictValidation = options.strictValidation !== false; // Enable strict validation by default
72
+ this.strictHooks = options.strictHooks || false; // Throw on first hook error instead of continuing
67
73
 
68
74
  // Initialize hooks system
69
75
  this._initHooks();
@@ -98,7 +104,7 @@ export class Database extends EventEmitter {
98
104
  }
99
105
  }
100
106
 
101
- this.client = options.client || new Client({
107
+ this.client = options.client || new S3Client({
102
108
  verbose: this.verbose,
103
109
  parallelism: this.parallelism,
104
110
  connectionString: connectionString,
@@ -110,21 +116,32 @@ export class Database extends EventEmitter {
110
116
  this.bucket = this.client.bucket;
111
117
  this.keyPrefix = this.client.keyPrefix;
112
118
 
113
- // Add process exit listener for cleanup
114
- if (!this._exitListenerRegistered) {
119
+ // Register exit listener for cleanup
120
+ this._registerExitListener();
121
+ }
122
+
123
+ /**
124
+ * Register process exit listener for automatic cleanup
125
+ * @private
126
+ */
127
+ _registerExitListener() {
128
+ if (!this._exitListenerRegistered && typeof process !== 'undefined') {
115
129
  this._exitListenerRegistered = true;
116
- if (typeof process !== 'undefined') {
117
- process.on('exit', async () => {
118
- if (this.isConnected()) {
119
- // Silently ignore errors on exit
120
- await tryFn(() => this.disconnect());
121
- }
122
- });
123
- }
130
+ // Store listener reference for cleanup
131
+ this._exitListener = async () => {
132
+ if (this.isConnected()) {
133
+ // Silently ignore errors on exit
134
+ await tryFn(() => this.disconnect());
135
+ }
136
+ };
137
+ process.on('exit', this._exitListener);
124
138
  }
125
139
  }
126
-
140
+
127
141
  async connect() {
142
+ // Re-register exit listener if it was cleaned up
143
+ this._registerExitListener();
144
+
128
145
  await this.startPlugins();
129
146
 
130
147
  let metadata = null;
@@ -1271,15 +1288,24 @@ export class Database extends EventEmitter {
1271
1288
  this.client.removeAllListeners();
1272
1289
  }
1273
1290
 
1274
- // 4. Remove all listeners from the database itself
1291
+ // 4. Emit disconnected event BEFORE removing database listeners (race condition fix)
1292
+ // This ensures listeners can actually receive the event
1293
+ await this.emit('disconnected', new Date());
1294
+
1295
+ // 5. Remove all listeners from the database itself
1275
1296
  this.removeAllListeners();
1276
1297
 
1277
- // 5. Clear saved metadata and plugin lists
1298
+ // 6. Cleanup process exit listener (memory leak fix)
1299
+ if (this._exitListener && typeof process !== 'undefined') {
1300
+ process.off('exit', this._exitListener);
1301
+ this._exitListener = null;
1302
+ this._exitListenerRegistered = false;
1303
+ }
1304
+
1305
+ // 7. Clear saved metadata and plugin lists
1278
1306
  this.savedMetadata = null;
1279
1307
  this.plugins = {};
1280
1308
  this.pluginList = [];
1281
-
1282
- this.emit('disconnected', new Date());
1283
1309
  });
1284
1310
  }
1285
1311
 
@@ -1400,8 +1426,17 @@ export class Database extends EventEmitter {
1400
1426
  for (const hook of hooks) {
1401
1427
  const [ok, error] = await tryFn(() => hook({ database: this, ...context }));
1402
1428
  if (!ok) {
1403
- // Emit error but don't stop hook execution
1429
+ // Emit error event
1404
1430
  this.emit('hookError', { event, error, context });
1431
+
1432
+ // In strict mode, throw on first error instead of continuing
1433
+ if (this.strictHooks) {
1434
+ throw new DatabaseError(`Hook execution failed for event '${event}': ${error.message}`, {
1435
+ event,
1436
+ originalError: error,
1437
+ context
1438
+ });
1439
+ }
1405
1440
  }
1406
1441
  }
1407
1442
  }
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'