s3db.js 11.2.2 → 11.2.4

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.
Files changed (46) hide show
  1. package/dist/s3db.cjs.js +1650 -136
  2. package/dist/s3db.cjs.js.map +1 -1
  3. package/dist/s3db.es.js +1644 -137
  4. package/dist/s3db.es.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/behaviors/enforce-limits.js +28 -4
  7. package/src/behaviors/index.js +6 -1
  8. package/src/client.class.js +11 -1
  9. package/src/concerns/partition-queue.js +7 -1
  10. package/src/concerns/plugin-storage.js +75 -13
  11. package/src/database.class.js +22 -4
  12. package/src/errors.js +414 -24
  13. package/src/partition-drivers/base-partition-driver.js +12 -2
  14. package/src/partition-drivers/index.js +7 -1
  15. package/src/partition-drivers/memory-partition-driver.js +20 -5
  16. package/src/partition-drivers/sqs-partition-driver.js +6 -1
  17. package/src/plugins/audit.errors.js +46 -0
  18. package/src/plugins/backup/base-backup-driver.class.js +36 -6
  19. package/src/plugins/backup/filesystem-backup-driver.class.js +55 -7
  20. package/src/plugins/backup/index.js +40 -9
  21. package/src/plugins/backup/multi-backup-driver.class.js +69 -9
  22. package/src/plugins/backup/s3-backup-driver.class.js +48 -6
  23. package/src/plugins/backup.errors.js +45 -0
  24. package/src/plugins/cache/cache.class.js +8 -1
  25. package/src/plugins/cache/memory-cache.class.js +216 -33
  26. package/src/plugins/cache.errors.js +47 -0
  27. package/src/plugins/cache.plugin.js +94 -3
  28. package/src/plugins/eventual-consistency/analytics.js +145 -0
  29. package/src/plugins/eventual-consistency/index.js +203 -1
  30. package/src/plugins/fulltext.errors.js +46 -0
  31. package/src/plugins/fulltext.plugin.js +15 -3
  32. package/src/plugins/metrics.errors.js +46 -0
  33. package/src/plugins/queue-consumer.plugin.js +31 -4
  34. package/src/plugins/queue.errors.js +46 -0
  35. package/src/plugins/replicator.errors.js +46 -0
  36. package/src/plugins/replicator.plugin.js +40 -5
  37. package/src/plugins/replicators/base-replicator.class.js +19 -3
  38. package/src/plugins/replicators/index.js +9 -3
  39. package/src/plugins/replicators/s3db-replicator.class.js +38 -8
  40. package/src/plugins/scheduler.errors.js +46 -0
  41. package/src/plugins/scheduler.plugin.js +79 -19
  42. package/src/plugins/state-machine.errors.js +47 -0
  43. package/src/plugins/state-machine.plugin.js +86 -17
  44. package/src/resource.class.js +8 -1
  45. package/src/stream/index.js +6 -1
  46. package/src/stream/resource-reader.class.js +6 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s3db.js",
3
- "version": "11.2.2",
3
+ "version": "11.2.4",
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",
@@ -1,5 +1,6 @@
1
1
  import { calculateTotalSize } from '../concerns/calculator.js';
2
2
  import { calculateEffectiveLimit } from '../concerns/calculator.js';
3
+ import { MetadataLimitError } from '../errors.js';
3
4
 
4
5
  export const S3_METADATA_LIMIT_BYTES = 2047;
5
6
 
@@ -146,9 +147,16 @@ export async function handleInsert({ resource, data, mappedData, originalData })
146
147
  });
147
148
 
148
149
  if (totalSize > effectiveLimit) {
149
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, effective limit: ${effectiveLimit} bytes, absolute limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
150
+ throw new MetadataLimitError('Metadata size exceeds 2KB limit on insert', {
151
+ totalSize,
152
+ effectiveLimit,
153
+ absoluteLimit: S3_METADATA_LIMIT_BYTES,
154
+ excess: totalSize - effectiveLimit,
155
+ resourceName: resource.name,
156
+ operation: 'insert'
157
+ });
150
158
  }
151
-
159
+
152
160
  // If data fits in metadata, store only in metadata
153
161
  return { mappedData, body: "" };
154
162
  }
@@ -167,7 +175,15 @@ export async function handleUpdate({ resource, id, data, mappedData, originalDat
167
175
  });
168
176
 
169
177
  if (totalSize > effectiveLimit) {
170
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, effective limit: ${effectiveLimit} bytes, absolute limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
178
+ throw new MetadataLimitError('Metadata size exceeds 2KB limit on update', {
179
+ totalSize,
180
+ effectiveLimit,
181
+ absoluteLimit: S3_METADATA_LIMIT_BYTES,
182
+ excess: totalSize - effectiveLimit,
183
+ resourceName: resource.name,
184
+ operation: 'update',
185
+ id
186
+ });
171
187
  }
172
188
  return { mappedData, body: JSON.stringify(mappedData) };
173
189
  }
@@ -186,7 +202,15 @@ export async function handleUpsert({ resource, id, data, mappedData }) {
186
202
  });
187
203
 
188
204
  if (totalSize > effectiveLimit) {
189
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, effective limit: ${effectiveLimit} bytes, absolute limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
205
+ throw new MetadataLimitError('Metadata size exceeds 2KB limit on upsert', {
206
+ totalSize,
207
+ effectiveLimit,
208
+ absoluteLimit: S3_METADATA_LIMIT_BYTES,
209
+ excess: totalSize - effectiveLimit,
210
+ resourceName: resource.name,
211
+ operation: 'upsert',
212
+ id
213
+ });
190
214
  }
191
215
  return { mappedData, body: "" };
192
216
  }
@@ -3,6 +3,7 @@ import * as enforceLimits from './enforce-limits.js';
3
3
  import * as dataTruncate from './truncate-data.js';
4
4
  import * as bodyOverflow from './body-overflow.js';
5
5
  import * as bodyOnly from './body-only.js';
6
+ import { BehaviorError } from '../errors.js';
6
7
 
7
8
  /**
8
9
  * Available behaviors for Resource metadata handling
@@ -23,7 +24,11 @@ export const behaviors = {
23
24
  export function getBehavior(behaviorName) {
24
25
  const behavior = behaviors[behaviorName];
25
26
  if (!behavior) {
26
- throw new Error(`Unknown behavior: ${behaviorName}. Available behaviors: ${Object.keys(behaviors).join(', ')}`);
27
+ throw new BehaviorError(`Unknown behavior: ${behaviorName}`, {
28
+ behavior: behaviorName,
29
+ availableBehaviors: Object.keys(behaviors),
30
+ operation: 'getBehavior'
31
+ });
27
32
  }
28
33
  return behavior;
29
34
  }
@@ -544,7 +544,17 @@ export class Client extends EventEmitter {
544
544
  });
545
545
  this.emit("moveAllObjects", { results, errors }, { prefixFrom, prefixTo });
546
546
  if (errors.length > 0) {
547
- throw new Error("Some objects could not be moved");
547
+ throw new UnknownError("Some objects could not be moved", {
548
+ bucket: this.config.bucket,
549
+ operation: 'moveAllObjects',
550
+ prefixFrom,
551
+ prefixTo,
552
+ totalKeys: keys.length,
553
+ failedCount: errors.length,
554
+ successCount: results.length,
555
+ errors: errors.map(e => ({ message: e.message, raw: e.raw })),
556
+ suggestion: 'Check S3 permissions and retry failed objects individually'
557
+ });
548
558
  }
549
559
  return results;
550
560
  }
@@ -1,4 +1,5 @@
1
1
  import { EventEmitter } from 'events';
2
+ import { PartitionDriverError } from '../errors.js';
2
3
 
3
4
  /**
4
5
  * Robust partition operation queue with retry and persistence
@@ -107,7 +108,12 @@ export class PartitionQueue extends EventEmitter {
107
108
  case 'delete':
108
109
  return await resource.deletePartitionReferences(data);
109
110
  default:
110
- throw new Error(`Unknown operation type: ${type}`);
111
+ throw new PartitionDriverError(`Unknown partition operation type: ${type}`, {
112
+ driver: 'PartitionQueue',
113
+ operation: type,
114
+ availableOperations: ['create', 'update', 'delete'],
115
+ suggestion: 'Use one of the supported partition operations: create, update, or delete'
116
+ });
111
117
  }
112
118
  }
113
119
 
@@ -31,6 +31,7 @@
31
31
  import { metadataEncode, metadataDecode } from './metadata-encoding.js';
32
32
  import { calculateEffectiveLimit, calculateUTF8Bytes } from './calculator.js';
33
33
  import { tryFn } from './try-fn.js';
34
+ import { PluginStorageError, MetadataLimitError, BehaviorError } from '../errors.js';
34
35
 
35
36
  const S3_METADATA_LIMIT = 2047; // AWS S3 metadata limit in bytes
36
37
 
@@ -41,10 +42,17 @@ export class PluginStorage {
41
42
  */
42
43
  constructor(client, pluginSlug) {
43
44
  if (!client) {
44
- throw new Error('PluginStorage requires a client instance');
45
+ throw new PluginStorageError('PluginStorage requires a client instance', {
46
+ operation: 'constructor',
47
+ pluginSlug,
48
+ suggestion: 'Pass a valid S3db Client instance when creating PluginStorage'
49
+ });
45
50
  }
46
51
  if (!pluginSlug) {
47
- throw new Error('PluginStorage requires a pluginSlug');
52
+ throw new PluginStorageError('PluginStorage requires a pluginSlug', {
53
+ operation: 'constructor',
54
+ suggestion: 'Provide a plugin slug (e.g., "eventual-consistency", "cache", "audit")'
55
+ });
48
56
  }
49
57
 
50
58
  this.client = client;
@@ -113,7 +121,15 @@ export class PluginStorage {
113
121
  const [ok, err] = await tryFn(() => this.client.putObject(putParams));
114
122
 
115
123
  if (!ok) {
116
- throw new Error(`PluginStorage.set failed for key ${key}: ${err.message}`);
124
+ throw new PluginStorageError(`Failed to save plugin data`, {
125
+ pluginSlug: this.pluginSlug,
126
+ key,
127
+ operation: 'set',
128
+ behavior,
129
+ ttl,
130
+ original: err,
131
+ suggestion: 'Check S3 permissions and key format'
132
+ });
117
133
  }
118
134
  }
119
135
 
@@ -139,7 +155,13 @@ export class PluginStorage {
139
155
  if (err.name === 'NoSuchKey' || err.Code === 'NoSuchKey') {
140
156
  return null;
141
157
  }
142
- throw new Error(`PluginStorage.get failed for key ${key}: ${err.message}`);
158
+ throw new PluginStorageError(`Failed to retrieve plugin data`, {
159
+ pluginSlug: this.pluginSlug,
160
+ key,
161
+ operation: 'get',
162
+ original: err,
163
+ suggestion: 'Check if the key exists and S3 permissions are correct'
164
+ });
143
165
  }
144
166
 
145
167
  // Metadata is already decoded by Client, but values are strings
@@ -162,7 +184,13 @@ export class PluginStorage {
162
184
  data = { ...parsedMetadata, ...body };
163
185
  }
164
186
  } catch (parseErr) {
165
- throw new Error(`PluginStorage.get failed to parse body for key ${key}: ${parseErr.message}`);
187
+ throw new PluginStorageError(`Failed to parse JSON body`, {
188
+ pluginSlug: this.pluginSlug,
189
+ key,
190
+ operation: 'get',
191
+ original: parseErr,
192
+ suggestion: 'Body content may be corrupted. Check S3 object integrity'
193
+ });
166
194
  }
167
195
  }
168
196
 
@@ -248,7 +276,15 @@ export class PluginStorage {
248
276
  );
249
277
 
250
278
  if (!ok) {
251
- throw new Error(`PluginStorage.list failed: ${err.message}`);
279
+ throw new PluginStorageError(`Failed to list plugin data`, {
280
+ pluginSlug: this.pluginSlug,
281
+ operation: 'list',
282
+ prefix,
283
+ fullPrefix,
284
+ limit,
285
+ original: err,
286
+ suggestion: 'Check S3 permissions and bucket configuration'
287
+ });
252
288
  }
253
289
 
254
290
  // Remove keyPrefix from keys
@@ -277,7 +313,16 @@ export class PluginStorage {
277
313
  );
278
314
 
279
315
  if (!ok) {
280
- throw new Error(`PluginStorage.listForResource failed: ${err.message}`);
316
+ throw new PluginStorageError(`Failed to list resource data`, {
317
+ pluginSlug: this.pluginSlug,
318
+ operation: 'listForResource',
319
+ resourceName,
320
+ subPrefix,
321
+ fullPrefix,
322
+ limit,
323
+ original: err,
324
+ suggestion: 'Check resource name and S3 permissions'
325
+ });
281
326
  }
282
327
 
283
328
  // Remove keyPrefix from keys
@@ -456,7 +501,13 @@ export class PluginStorage {
456
501
  const [ok, err] = await tryFn(() => this.client.deleteObject(key));
457
502
 
458
503
  if (!ok) {
459
- throw new Error(`PluginStorage.delete failed for key ${key}: ${err.message}`);
504
+ throw new PluginStorageError(`Failed to delete plugin data`, {
505
+ pluginSlug: this.pluginSlug,
506
+ key,
507
+ operation: 'delete',
508
+ original: err,
509
+ suggestion: 'Check S3 delete permissions'
510
+ });
460
511
  }
461
512
  }
462
513
 
@@ -686,10 +737,15 @@ export class PluginStorage {
686
737
  currentSize += keySize + valueSize;
687
738
 
688
739
  if (currentSize > effectiveLimit) {
689
- throw new Error(
690
- `Data exceeds metadata limit (${currentSize} > ${effectiveLimit} bytes). ` +
691
- `Use 'body-overflow' or 'body-only' behavior.`
692
- );
740
+ throw new MetadataLimitError(`Data exceeds metadata limit with enforce-limits behavior`, {
741
+ totalSize: currentSize,
742
+ effectiveLimit,
743
+ absoluteLimit: S3_METADATA_LIMIT,
744
+ excess: currentSize - effectiveLimit,
745
+ operation: 'PluginStorage.set',
746
+ pluginSlug: this.pluginSlug,
747
+ suggestion: "Use 'body-overflow' or 'body-only' behavior to handle large data"
748
+ });
693
749
  }
694
750
 
695
751
  metadata[key] = jsonValue;
@@ -698,7 +754,13 @@ export class PluginStorage {
698
754
  }
699
755
 
700
756
  default:
701
- throw new Error(`Unknown behavior: ${behavior}. Use 'body-overflow', 'body-only', or 'enforce-limits'.`);
757
+ throw new BehaviorError(`Unknown behavior: ${behavior}`, {
758
+ behavior,
759
+ availableBehaviors: ['body-overflow', 'body-only', 'enforce-limits'],
760
+ operation: 'PluginStorage._applyBehavior',
761
+ pluginSlug: this.pluginSlug,
762
+ suggestion: "Use 'body-overflow', 'body-only', or 'enforce-limits'"
763
+ });
702
764
  }
703
765
 
704
766
  return { metadata, body };
@@ -6,7 +6,7 @@ import jsonStableStringify from "json-stable-stringify";
6
6
  import Client from "./client.class.js";
7
7
  import tryFn from "./concerns/try-fn.js";
8
8
  import Resource from "./resource.class.js";
9
- import { ResourceNotFound } from "./errors.js";
9
+ import { ResourceNotFound, DatabaseError } from "./errors.js";
10
10
  import { idGenerator } from "./concerns/id.js";
11
11
  import { streamToString } from "./stream/index.js";
12
12
 
@@ -35,6 +35,7 @@ export class Database extends EventEmitter {
35
35
  this.passphrase = options.passphrase || "secret";
36
36
  this.versioningEnabled = options.versioningEnabled || false;
37
37
  this.persistHooks = options.persistHooks || false; // New configuration for hook persistence
38
+ this.strictValidation = options.strictValidation !== false; // Enable strict validation by default
38
39
 
39
40
  // Initialize hooks system
40
41
  this._initHooks();
@@ -199,6 +200,7 @@ export class Database extends EventEmitter {
199
200
  asyncEvents: versionData.asyncEvents !== undefined ? versionData.asyncEvents : true,
200
201
  hooks: this.persistHooks ? this._deserializeHooks(versionData.hooks || {}) : (versionData.hooks || {}),
201
202
  versioningEnabled: this.versioningEnabled,
203
+ strictValidation: this.strictValidation,
202
204
  map: versionData.map,
203
205
  idGenerator: restoredIdGenerator,
204
206
  idSize: restoredIdSize
@@ -450,7 +452,12 @@ export class Database extends EventEmitter {
450
452
  const plugin = this.plugins[pluginName] || this.pluginRegistry[pluginName];
451
453
 
452
454
  if (!plugin) {
453
- throw new Error(`Plugin '${name}' not found`);
455
+ throw new DatabaseError(`Plugin '${name}' not found`, {
456
+ operation: 'uninstallPlugin',
457
+ pluginName: name,
458
+ availablePlugins: Object.keys(this.pluginRegistry),
459
+ suggestion: 'Check plugin name or list available plugins using Object.keys(db.pluginRegistry)'
460
+ });
454
461
  }
455
462
 
456
463
  // Stop the plugin first
@@ -999,6 +1006,7 @@ export class Database extends EventEmitter {
999
1006
  autoDecrypt: config.autoDecrypt !== undefined ? config.autoDecrypt : true,
1000
1007
  hooks: hooks || {},
1001
1008
  versioningEnabled: this.versioningEnabled,
1009
+ strictValidation: this.strictValidation,
1002
1010
  map: config.map,
1003
1011
  idGenerator: config.idGenerator,
1004
1012
  idSize: config.idSize,
@@ -1215,10 +1223,20 @@ export class Database extends EventEmitter {
1215
1223
  addHook(event, fn) {
1216
1224
  if (!this._hooks) this._initHooks();
1217
1225
  if (!this._hooks.has(event)) {
1218
- throw new Error(`Unknown hook event: ${event}. Available events: ${this._hookEvents.join(', ')}`);
1226
+ throw new DatabaseError(`Unknown hook event: ${event}`, {
1227
+ operation: 'addHook',
1228
+ invalidEvent: event,
1229
+ availableEvents: this._hookEvents,
1230
+ suggestion: `Use one of the available hook events: ${this._hookEvents.join(', ')}`
1231
+ });
1219
1232
  }
1220
1233
  if (typeof fn !== 'function') {
1221
- throw new Error('Hook function must be a function');
1234
+ throw new DatabaseError('Hook function must be a function', {
1235
+ operation: 'addHook',
1236
+ event,
1237
+ receivedType: typeof fn,
1238
+ suggestion: 'Provide a function that will be called when the hook event occurs'
1239
+ });
1222
1240
  }
1223
1241
  this._hooks.get(event).push(fn);
1224
1242
  }