s3db.js 11.2.3 → 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 (42) hide show
  1. package/dist/s3db.cjs.js +1177 -128
  2. package/dist/s3db.cjs.js.map +1 -1
  3. package/dist/s3db.es.js +1172 -129
  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 +19 -4
  12. package/src/errors.js +306 -27
  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.errors.js +47 -0
  26. package/src/plugins/cache.plugin.js +8 -1
  27. package/src/plugins/fulltext.errors.js +46 -0
  28. package/src/plugins/fulltext.plugin.js +15 -3
  29. package/src/plugins/metrics.errors.js +46 -0
  30. package/src/plugins/queue-consumer.plugin.js +31 -4
  31. package/src/plugins/queue.errors.js +46 -0
  32. package/src/plugins/replicator.errors.js +46 -0
  33. package/src/plugins/replicator.plugin.js +40 -5
  34. package/src/plugins/replicators/base-replicator.class.js +19 -3
  35. package/src/plugins/replicators/index.js +9 -3
  36. package/src/plugins/replicators/s3db-replicator.class.js +38 -8
  37. package/src/plugins/scheduler.errors.js +46 -0
  38. package/src/plugins/scheduler.plugin.js +79 -19
  39. package/src/plugins/state-machine.errors.js +47 -0
  40. package/src/plugins/state-machine.plugin.js +86 -17
  41. package/src/stream/index.js +6 -1
  42. package/src/stream/resource-reader.class.js +6 -1
@@ -5,6 +5,7 @@ import { pipeline } from 'stream/promises';
5
5
  import path from 'path';
6
6
  import crypto from 'crypto';
7
7
  import tryFn from '../../concerns/try-fn.js';
8
+ import { BackupError } from '../backup.errors.js';
8
9
 
9
10
  /**
10
11
  * FilesystemBackupDriver - Stores backups on local/network filesystem
@@ -31,7 +32,11 @@ export default class FilesystemBackupDriver extends BaseBackupDriver {
31
32
  async onSetup() {
32
33
  // Validate path configuration
33
34
  if (!this.config.path) {
34
- throw new Error('FilesystemBackupDriver: path configuration is required');
35
+ throw new BackupError('FilesystemBackupDriver: path configuration is required', {
36
+ operation: 'onSetup',
37
+ driver: 'filesystem',
38
+ suggestion: 'Provide a path in config: new FilesystemBackupDriver({ path: "/path/to/backups" })'
39
+ });
35
40
  }
36
41
 
37
42
  this.log(`Initialized with path: ${this.config.path}`);
@@ -69,13 +74,28 @@ export default class FilesystemBackupDriver extends BaseBackupDriver {
69
74
  );
70
75
 
71
76
  if (!createDirOk) {
72
- throw new Error(`Failed to create backup directory: ${createDirErr.message}`);
77
+ throw new BackupError('Failed to create backup directory', {
78
+ operation: 'upload',
79
+ driver: 'filesystem',
80
+ backupId,
81
+ targetDir,
82
+ original: createDirErr,
83
+ suggestion: 'Check directory permissions and disk space'
84
+ });
73
85
  }
74
86
 
75
87
  // Copy backup file
76
88
  const [copyOk, copyErr] = await tryFn(() => copyFile(filePath, targetPath));
77
89
  if (!copyOk) {
78
- throw new Error(`Failed to copy backup file: ${copyErr.message}`);
90
+ throw new BackupError('Failed to copy backup file', {
91
+ operation: 'upload',
92
+ driver: 'filesystem',
93
+ backupId,
94
+ filePath,
95
+ targetPath,
96
+ original: copyErr,
97
+ suggestion: 'Check file permissions and disk space'
98
+ });
79
99
  }
80
100
 
81
101
  // Write manifest
@@ -90,7 +110,14 @@ export default class FilesystemBackupDriver extends BaseBackupDriver {
90
110
  if (!manifestOk) {
91
111
  // Clean up backup file if manifest fails
92
112
  await tryFn(() => unlink(targetPath));
93
- throw new Error(`Failed to write manifest: ${manifestErr.message}`);
113
+ throw new BackupError('Failed to write manifest file', {
114
+ operation: 'upload',
115
+ driver: 'filesystem',
116
+ backupId,
117
+ manifestPath,
118
+ original: manifestErr,
119
+ suggestion: 'Check directory permissions and disk space'
120
+ });
94
121
  }
95
122
 
96
123
  // Get file stats
@@ -116,7 +143,13 @@ export default class FilesystemBackupDriver extends BaseBackupDriver {
116
143
  // Check if source exists
117
144
  const [existsOk] = await tryFn(() => access(sourcePath));
118
145
  if (!existsOk) {
119
- throw new Error(`Backup file not found: ${sourcePath}`);
146
+ throw new BackupError('Backup file not found', {
147
+ operation: 'download',
148
+ driver: 'filesystem',
149
+ backupId,
150
+ sourcePath,
151
+ suggestion: 'Check if backup exists using list() method'
152
+ });
120
153
  }
121
154
 
122
155
  // Create target directory if needed
@@ -126,7 +159,15 @@ export default class FilesystemBackupDriver extends BaseBackupDriver {
126
159
  // Copy file
127
160
  const [copyOk, copyErr] = await tryFn(() => copyFile(sourcePath, targetPath));
128
161
  if (!copyOk) {
129
- throw new Error(`Failed to download backup: ${copyErr.message}`);
162
+ throw new BackupError('Failed to download backup', {
163
+ operation: 'download',
164
+ driver: 'filesystem',
165
+ backupId,
166
+ sourcePath,
167
+ targetPath,
168
+ original: copyErr,
169
+ suggestion: 'Check file permissions and disk space'
170
+ });
130
171
  }
131
172
 
132
173
  this.log(`Downloaded backup ${backupId} from ${sourcePath} to ${targetPath}`);
@@ -150,7 +191,14 @@ export default class FilesystemBackupDriver extends BaseBackupDriver {
150
191
  const [deleteManifestOk] = await tryFn(() => unlink(manifestPath));
151
192
 
152
193
  if (!deleteBackupOk && !deleteManifestOk) {
153
- throw new Error(`Failed to delete backup files for ${backupId}`);
194
+ throw new BackupError('Failed to delete backup files', {
195
+ operation: 'delete',
196
+ driver: 'filesystem',
197
+ backupId,
198
+ backupPath,
199
+ manifestPath,
200
+ suggestion: 'Check file permissions'
201
+ });
154
202
  }
155
203
 
156
204
  this.log(`Deleted backup ${backupId}`);
@@ -2,6 +2,7 @@ import BaseBackupDriver from './base-backup-driver.class.js';
2
2
  import FilesystemBackupDriver from './filesystem-backup-driver.class.js';
3
3
  import S3BackupDriver from './s3-backup-driver.class.js';
4
4
  import MultiBackupDriver from './multi-backup-driver.class.js';
5
+ import { BackupError } from '../backup.errors.js';
5
6
 
6
7
  export {
7
8
  BaseBackupDriver,
@@ -27,11 +28,16 @@ export const BACKUP_DRIVERS = {
27
28
  */
28
29
  export function createBackupDriver(driver, config = {}) {
29
30
  const DriverClass = BACKUP_DRIVERS[driver];
30
-
31
+
31
32
  if (!DriverClass) {
32
- throw new Error(`Unknown backup driver: ${driver}. Available drivers: ${Object.keys(BACKUP_DRIVERS).join(', ')}`);
33
+ throw new BackupError(`Unknown backup driver: ${driver}`, {
34
+ operation: 'createBackupDriver',
35
+ driver,
36
+ availableDrivers: Object.keys(BACKUP_DRIVERS),
37
+ suggestion: `Use one of the available drivers: ${Object.keys(BACKUP_DRIVERS).join(', ')}`
38
+ });
33
39
  }
34
-
40
+
35
41
  return new DriverClass(config);
36
42
  }
37
43
 
@@ -43,18 +49,32 @@ export function createBackupDriver(driver, config = {}) {
43
49
  */
44
50
  export function validateBackupConfig(driver, config = {}) {
45
51
  if (!driver || typeof driver !== 'string') {
46
- throw new Error('Driver type must be a non-empty string');
52
+ throw new BackupError('Driver type must be a non-empty string', {
53
+ operation: 'validateBackupConfig',
54
+ driver,
55
+ suggestion: 'Provide a valid driver type string (filesystem, s3, or multi)'
56
+ });
47
57
  }
48
58
 
49
59
  if (!BACKUP_DRIVERS[driver]) {
50
- throw new Error(`Unknown backup driver: ${driver}. Available drivers: ${Object.keys(BACKUP_DRIVERS).join(', ')}`);
60
+ throw new BackupError(`Unknown backup driver: ${driver}`, {
61
+ operation: 'validateBackupConfig',
62
+ driver,
63
+ availableDrivers: Object.keys(BACKUP_DRIVERS),
64
+ suggestion: `Use one of the available drivers: ${Object.keys(BACKUP_DRIVERS).join(', ')}`
65
+ });
51
66
  }
52
67
 
53
68
  // Driver-specific validation
54
69
  switch (driver) {
55
70
  case 'filesystem':
56
71
  if (!config.path) {
57
- throw new Error('FilesystemBackupDriver requires "path" configuration');
72
+ throw new BackupError('FilesystemBackupDriver requires "path" configuration', {
73
+ operation: 'validateBackupConfig',
74
+ driver: 'filesystem',
75
+ config,
76
+ suggestion: 'Provide a "path" property in config: { path: "/path/to/backups" }'
77
+ });
58
78
  }
59
79
  break;
60
80
 
@@ -64,13 +84,24 @@ export function validateBackupConfig(driver, config = {}) {
64
84
 
65
85
  case 'multi':
66
86
  if (!Array.isArray(config.destinations) || config.destinations.length === 0) {
67
- throw new Error('MultiBackupDriver requires non-empty "destinations" array');
87
+ throw new BackupError('MultiBackupDriver requires non-empty "destinations" array', {
88
+ operation: 'validateBackupConfig',
89
+ driver: 'multi',
90
+ config,
91
+ suggestion: 'Provide destinations array: { destinations: [{ driver: "s3", config: {...} }] }'
92
+ });
68
93
  }
69
-
94
+
70
95
  // Validate each destination
71
96
  config.destinations.forEach((dest, index) => {
72
97
  if (!dest.driver) {
73
- throw new Error(`Destination ${index} must have a "driver" property`);
98
+ throw new BackupError(`Destination ${index} must have a "driver" property`, {
99
+ operation: 'validateBackupConfig',
100
+ driver: 'multi',
101
+ destinationIndex: index,
102
+ destination: dest,
103
+ suggestion: 'Each destination must have a driver property: { driver: "s3", config: {...} }'
104
+ });
74
105
  }
75
106
 
76
107
  // Recursive validation for nested drivers
@@ -1,6 +1,7 @@
1
1
  import BaseBackupDriver from './base-backup-driver.class.js';
2
2
  import { createBackupDriver } from './index.js';
3
3
  import tryFn from '../../concerns/try-fn.js';
4
+ import { BackupError } from '../backup.errors.js';
4
5
 
5
6
  /**
6
7
  * MultiBackupDriver - Manages multiple backup destinations
@@ -34,13 +35,24 @@ export default class MultiBackupDriver extends BaseBackupDriver {
34
35
 
35
36
  async onSetup() {
36
37
  if (!Array.isArray(this.config.destinations) || this.config.destinations.length === 0) {
37
- throw new Error('MultiBackupDriver: destinations array is required and must not be empty');
38
+ throw new BackupError('MultiBackupDriver requires non-empty destinations array', {
39
+ operation: 'onSetup',
40
+ driver: 'multi',
41
+ destinationsProvided: this.config.destinations,
42
+ suggestion: 'Provide destinations array: { destinations: [{ driver: "s3", config: {...} }, { driver: "filesystem", config: {...} }] }'
43
+ });
38
44
  }
39
45
 
40
46
  // Create and setup all driver instances
41
47
  for (const [index, destConfig] of this.config.destinations.entries()) {
42
48
  if (!destConfig.driver) {
43
- throw new Error(`MultiBackupDriver: destination[${index}] must have a driver type`);
49
+ throw new BackupError(`Destination ${index} missing driver type`, {
50
+ operation: 'onSetup',
51
+ driver: 'multi',
52
+ destinationIndex: index,
53
+ destination: destConfig,
54
+ suggestion: 'Each destination must have a driver property: { driver: "s3", config: {...} } or { driver: "filesystem", config: {...} }'
55
+ });
44
56
  }
45
57
 
46
58
  try {
@@ -51,10 +63,18 @@ export default class MultiBackupDriver extends BaseBackupDriver {
51
63
  config: destConfig,
52
64
  index
53
65
  });
54
-
66
+
55
67
  this.log(`Setup destination ${index}: ${destConfig.driver}`);
56
68
  } catch (error) {
57
- throw new Error(`Failed to setup destination ${index} (${destConfig.driver}): ${error.message}`);
69
+ throw new BackupError(`Failed to setup destination ${index}`, {
70
+ operation: 'onSetup',
71
+ driver: 'multi',
72
+ destinationIndex: index,
73
+ destinationDriver: destConfig.driver,
74
+ destinationConfig: destConfig.config,
75
+ original: error,
76
+ suggestion: 'Check destination driver configuration and ensure dependencies are available'
77
+ });
58
78
  }
59
79
  }
60
80
 
@@ -92,7 +112,15 @@ export default class MultiBackupDriver extends BaseBackupDriver {
92
112
  }
93
113
  }
94
114
 
95
- throw new Error(`All priority destinations failed: ${errors.map(e => `${e.destination}: ${e.error}`).join('; ')}`);
115
+ throw new BackupError('All priority destinations failed', {
116
+ operation: 'upload',
117
+ driver: 'multi',
118
+ strategy: 'priority',
119
+ backupId,
120
+ totalDestinations: this.drivers.length,
121
+ failures: errors,
122
+ suggestion: 'Check destination configurations and ensure at least one destination is accessible'
123
+ });
96
124
  }
97
125
 
98
126
  // For 'all' and 'any' strategies, upload to all destinations
@@ -128,11 +156,29 @@ export default class MultiBackupDriver extends BaseBackupDriver {
128
156
  const failedResults = allResults.filter(r => r.status === 'failed');
129
157
 
130
158
  if (strategy === 'all' && failedResults.length > 0) {
131
- throw new Error(`Some destinations failed: ${failedResults.map(r => `${r.destination}: ${r.error}`).join('; ')}`);
159
+ throw new BackupError('Some destinations failed with strategy "all"', {
160
+ operation: 'upload',
161
+ driver: 'multi',
162
+ strategy: 'all',
163
+ backupId,
164
+ totalDestinations: this.drivers.length,
165
+ successCount: successResults.length,
166
+ failedCount: failedResults.length,
167
+ failures: failedResults,
168
+ suggestion: 'All destinations must succeed with "all" strategy. Use "any" strategy to tolerate failures, or fix failing destinations.'
169
+ });
132
170
  }
133
171
 
134
172
  if (strategy === 'any' && successResults.length === 0) {
135
- throw new Error(`All destinations failed: ${failedResults.map(r => `${r.destination}: ${r.error}`).join('; ')}`);
173
+ throw new BackupError('All destinations failed with strategy "any"', {
174
+ operation: 'upload',
175
+ driver: 'multi',
176
+ strategy: 'any',
177
+ backupId,
178
+ totalDestinations: this.drivers.length,
179
+ failures: failedResults,
180
+ suggestion: 'At least one destination must succeed with "any" strategy. Check all destination configurations.'
181
+ });
136
182
  }
137
183
 
138
184
  return allResults;
@@ -160,7 +206,14 @@ export default class MultiBackupDriver extends BaseBackupDriver {
160
206
  }
161
207
  }
162
208
 
163
- throw new Error(`Failed to download backup from any destination`);
209
+ throw new BackupError('Failed to download backup from any destination', {
210
+ operation: 'download',
211
+ driver: 'multi',
212
+ backupId,
213
+ targetPath,
214
+ attemptedDestinations: destinations.length,
215
+ suggestion: 'Check if backup exists in at least one destination and destinations are accessible'
216
+ });
164
217
  }
165
218
 
166
219
  async delete(backupId, metadata) {
@@ -188,7 +241,14 @@ export default class MultiBackupDriver extends BaseBackupDriver {
188
241
  }
189
242
 
190
243
  if (successCount === 0 && errors.length > 0) {
191
- throw new Error(`Failed to delete from any destination: ${errors.join('; ')}`);
244
+ throw new BackupError('Failed to delete from any destination', {
245
+ operation: 'delete',
246
+ driver: 'multi',
247
+ backupId,
248
+ attemptedDestinations: destinations.length,
249
+ failures: errors,
250
+ suggestion: 'Check if backup exists in destinations and destinations are accessible with delete permissions'
251
+ });
192
252
  }
193
253
 
194
254
  if (errors.length > 0) {
@@ -4,6 +4,7 @@ import { stat } from 'fs/promises';
4
4
  import path from 'path';
5
5
  import crypto from 'crypto';
6
6
  import tryFn from '../../concerns/try-fn.js';
7
+ import { BackupError } from '../backup.errors.js';
7
8
 
8
9
  /**
9
10
  * S3BackupDriver - Stores backups in S3-compatible storage
@@ -43,11 +44,19 @@ export default class S3BackupDriver extends BaseBackupDriver {
43
44
  }
44
45
 
45
46
  if (!this.config.client) {
46
- throw new Error('S3BackupDriver: client is required (either via config or database)');
47
+ throw new BackupError('S3BackupDriver: client is required', {
48
+ operation: 'onSetup',
49
+ driver: 's3',
50
+ suggestion: 'Provide a client in config or ensure database has a client configured'
51
+ });
47
52
  }
48
53
 
49
54
  if (!this.config.bucket) {
50
- throw new Error('S3BackupDriver: bucket is required (either via config or database)');
55
+ throw new BackupError('S3BackupDriver: bucket is required', {
56
+ operation: 'onSetup',
57
+ driver: 's3',
58
+ suggestion: 'Provide a bucket in config or ensure database has a bucket configured'
59
+ });
51
60
  }
52
61
 
53
62
  this.log(`Initialized with bucket: ${this.config.bucket}, path: ${this.config.path}`);
@@ -108,7 +117,15 @@ export default class S3BackupDriver extends BaseBackupDriver {
108
117
  });
109
118
 
110
119
  if (!uploadOk) {
111
- throw new Error(`Failed to upload backup file: ${uploadErr.message}`);
120
+ throw new BackupError('Failed to upload backup file to S3', {
121
+ operation: 'upload',
122
+ driver: 's3',
123
+ backupId,
124
+ bucket: this.config.bucket,
125
+ key: backupKey,
126
+ original: uploadErr,
127
+ suggestion: 'Check S3 permissions and bucket configuration'
128
+ });
112
129
  }
113
130
 
114
131
  // Upload manifest
@@ -133,7 +150,15 @@ export default class S3BackupDriver extends BaseBackupDriver {
133
150
  bucket: this.config.bucket,
134
151
  key: backupKey
135
152
  }));
136
- throw new Error(`Failed to upload manifest: ${manifestErr.message}`);
153
+ throw new BackupError('Failed to upload manifest to S3', {
154
+ operation: 'upload',
155
+ driver: 's3',
156
+ backupId,
157
+ bucket: this.config.bucket,
158
+ manifestKey,
159
+ original: manifestErr,
160
+ suggestion: 'Check S3 permissions and bucket configuration'
161
+ });
137
162
  }
138
163
 
139
164
  this.log(`Uploaded backup ${backupId} to s3://${this.config.bucket}/${backupKey} (${fileSize} bytes)`);
@@ -161,7 +186,16 @@ export default class S3BackupDriver extends BaseBackupDriver {
161
186
  );
162
187
 
163
188
  if (!downloadOk) {
164
- throw new Error(`Failed to download backup: ${downloadErr.message}`);
189
+ throw new BackupError('Failed to download backup from S3', {
190
+ operation: 'download',
191
+ driver: 's3',
192
+ backupId,
193
+ bucket: this.config.bucket,
194
+ key: backupKey,
195
+ targetPath,
196
+ original: downloadErr,
197
+ suggestion: 'Check if backup exists and S3 permissions are correct'
198
+ });
165
199
  }
166
200
 
167
201
  this.log(`Downloaded backup ${backupId} from s3://${this.config.bucket}/${backupKey} to ${targetPath}`);
@@ -189,7 +223,15 @@ export default class S3BackupDriver extends BaseBackupDriver {
189
223
  );
190
224
 
191
225
  if (!deleteBackupOk && !deleteManifestOk) {
192
- throw new Error(`Failed to delete backup objects for ${backupId}`);
226
+ throw new BackupError('Failed to delete backup from S3', {
227
+ operation: 'delete',
228
+ driver: 's3',
229
+ backupId,
230
+ bucket: this.config.bucket,
231
+ backupKey,
232
+ manifestKey,
233
+ suggestion: 'Check S3 delete permissions'
234
+ });
193
235
  }
194
236
 
195
237
  this.log(`Deleted backup ${backupId} from S3`);
@@ -0,0 +1,45 @@
1
+ import { S3dbError } from '../errors.js';
2
+
3
+ /**
4
+ * BackupError - Errors related to backup operations
5
+ *
6
+ * Used for backup driver operations including:
7
+ * - Driver initialization and setup
8
+ * - Backup upload/download/delete operations
9
+ * - Driver configuration validation
10
+ * - Multi-destination backup strategies
11
+ *
12
+ * @extends S3dbError
13
+ */
14
+ export class BackupError extends S3dbError {
15
+ constructor(message, details = {}) {
16
+ const { driver = 'unknown', operation = 'unknown', backupId, ...rest } = details;
17
+
18
+ let description = details.description;
19
+ if (!description) {
20
+ description = `
21
+ Backup Operation Error
22
+
23
+ Driver: ${driver}
24
+ Operation: ${operation}
25
+ ${backupId ? `Backup ID: ${backupId}` : ''}
26
+
27
+ Common causes:
28
+ 1. Invalid backup driver configuration
29
+ 2. Destination storage not accessible
30
+ 3. Insufficient permissions
31
+ 4. Network connectivity issues
32
+ 5. Invalid backup file format
33
+
34
+ Solution:
35
+ Check driver configuration and ensure destination storage is accessible.
36
+
37
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/backup.md
38
+ `.trim();
39
+ }
40
+
41
+ super(message, { ...rest, driver, operation, backupId, description });
42
+ }
43
+ }
44
+
45
+ export default BackupError;
@@ -1,4 +1,5 @@
1
1
  import EventEmitter from "events";
2
+ import { CacheError } from "../cache.errors.js";
2
3
 
3
4
  export class Cache extends EventEmitter {
4
5
  constructor(config = {}) {
@@ -13,7 +14,13 @@ export class Cache extends EventEmitter {
13
14
 
14
15
  validateKey(key) {
15
16
  if (key === null || key === undefined || typeof key !== 'string' || !key) {
16
- throw new Error('Invalid key');
17
+ throw new CacheError('Invalid cache key', {
18
+ operation: 'validateKey',
19
+ driver: this.constructor.name,
20
+ key,
21
+ keyType: typeof key,
22
+ suggestion: 'Cache key must be a non-empty string'
23
+ });
17
24
  }
18
25
  }
19
26
 
@@ -0,0 +1,47 @@
1
+ import { S3dbError } from '../errors.js';
2
+
3
+ /**
4
+ * CacheError - Errors related to cache operations
5
+ *
6
+ * Used for cache operations including:
7
+ * - Cache driver initialization and setup
8
+ * - Cache get/set/delete operations
9
+ * - Cache invalidation and warming
10
+ * - Driver-specific operations (memory, filesystem, S3)
11
+ * - Resource-level caching
12
+ *
13
+ * @extends S3dbError
14
+ */
15
+ export class CacheError extends S3dbError {
16
+ constructor(message, details = {}) {
17
+ const { driver = 'unknown', operation = 'unknown', resourceName, key, ...rest } = details;
18
+
19
+ let description = details.description;
20
+ if (!description) {
21
+ description = `
22
+ Cache Operation Error
23
+
24
+ Driver: ${driver}
25
+ Operation: ${operation}
26
+ ${resourceName ? `Resource: ${resourceName}` : ''}
27
+ ${key ? `Key: ${key}` : ''}
28
+
29
+ Common causes:
30
+ 1. Invalid cache key format
31
+ 2. Cache driver not properly initialized
32
+ 3. Resource not found or not cached
33
+ 4. Memory limits exceeded
34
+ 5. Filesystem permissions issues
35
+
36
+ Solution:
37
+ Check cache configuration and ensure the cache driver is properly initialized.
38
+
39
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/cache.md
40
+ `.trim();
41
+ }
42
+
43
+ super(message, { ...rest, driver, operation, resourceName, key, description });
44
+ }
45
+ }
46
+
47
+ export default CacheError;
@@ -8,6 +8,7 @@ import MemoryCache from "./cache/memory-cache.class.js";
8
8
  import { FilesystemCache } from "./cache/filesystem-cache.class.js";
9
9
  import { PartitionAwareFilesystemCache } from "./cache/partition-aware-filesystem-cache.class.js";
10
10
  import tryFn from "../concerns/try-fn.js";
11
+ import { CacheError } from "./cache.errors.js";
11
12
 
12
13
  /**
13
14
  * Cache Plugin Configuration
@@ -555,7 +556,13 @@ export class CachePlugin extends Plugin {
555
556
  async warmCache(resourceName, options = {}) {
556
557
  const resource = this.database.resources[resourceName];
557
558
  if (!resource) {
558
- throw new Error(`Resource '${resourceName}' not found`);
559
+ throw new CacheError('Resource not found for cache warming', {
560
+ operation: 'warmCache',
561
+ driver: this.driver?.constructor.name,
562
+ resourceName,
563
+ availableResources: Object.keys(this.database.resources),
564
+ suggestion: 'Check resource name spelling or ensure resource has been created'
565
+ });
559
566
  }
560
567
 
561
568
  const { includePartitions = true, sampleSize = 100 } = options;
@@ -0,0 +1,46 @@
1
+ import { S3dbError } from '../errors.js';
2
+
3
+ /**
4
+ * FulltextError - Errors related to fulltext search operations
5
+ *
6
+ * Used for fulltext search operations including:
7
+ * - Index creation and updates
8
+ * - Search query execution
9
+ * - Index configuration
10
+ * - Text analysis and tokenization
11
+ * - Search result ranking
12
+ *
13
+ * @extends S3dbError
14
+ */
15
+ export class FulltextError extends S3dbError {
16
+ constructor(message, details = {}) {
17
+ const { resourceName, query, operation = 'unknown', ...rest } = details;
18
+
19
+ let description = details.description;
20
+ if (!description) {
21
+ description = `
22
+ Fulltext Search Operation Error
23
+
24
+ Operation: ${operation}
25
+ ${resourceName ? `Resource: ${resourceName}` : ''}
26
+ ${query ? `Query: ${query}` : ''}
27
+
28
+ Common causes:
29
+ 1. Resource not indexed for fulltext search
30
+ 2. Invalid query syntax
31
+ 3. Index not built yet
32
+ 4. Search configuration missing
33
+ 5. Field not indexed
34
+
35
+ Solution:
36
+ Ensure resource is configured for fulltext search and index is built.
37
+
38
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/fulltext.md
39
+ `.trim();
40
+ }
41
+
42
+ super(message, { ...rest, resourceName, query, operation, description });
43
+ }
44
+ }
45
+
46
+ export default FulltextError;
@@ -1,5 +1,6 @@
1
1
  import Plugin from "./plugin.class.js";
2
2
  import tryFn from "../concerns/try-fn.js";
3
+ import { FulltextError } from "./fulltext.errors.js";
3
4
 
4
5
  export class FullTextPlugin extends Plugin {
5
6
  constructor(options = {}) {
@@ -399,14 +400,20 @@ export class FullTextPlugin extends Plugin {
399
400
  // Search and return full records
400
401
  async searchRecords(resourceName, query, options = {}) {
401
402
  const searchResults = await this.search(resourceName, query, options);
402
-
403
+
403
404
  if (searchResults.length === 0) {
404
405
  return [];
405
406
  }
406
407
 
407
408
  const resource = this.database.resources[resourceName];
408
409
  if (!resource) {
409
- throw new Error(`Resource '${resourceName}' not found`);
410
+ throw new FulltextError(`Resource '${resourceName}' not found`, {
411
+ operation: 'searchRecords',
412
+ resourceName,
413
+ query,
414
+ availableResources: Object.keys(this.database.resources),
415
+ suggestion: 'Check resource name or ensure resource is created before searching'
416
+ });
410
417
  }
411
418
 
412
419
  const recordIds = searchResults.map(result => result.recordId);
@@ -430,7 +437,12 @@ export class FullTextPlugin extends Plugin {
430
437
  async rebuildIndex(resourceName) {
431
438
  const resource = this.database.resources[resourceName];
432
439
  if (!resource) {
433
- throw new Error(`Resource '${resourceName}' not found`);
440
+ throw new FulltextError(`Resource '${resourceName}' not found`, {
441
+ operation: 'rebuildIndex',
442
+ resourceName,
443
+ availableResources: Object.keys(this.database.resources),
444
+ suggestion: 'Check resource name or ensure resource is created before rebuilding index'
445
+ });
434
446
  }
435
447
 
436
448
  // Clear existing indexes for this resource