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
@@ -0,0 +1,46 @@
1
+ import { S3dbError } from '../errors.js';
2
+
3
+ /**
4
+ * MetricsError - Errors related to metrics operations
5
+ *
6
+ * Used for metrics operations including:
7
+ * - Metric collection and recording
8
+ * - Metric aggregation
9
+ * - Metric querying
10
+ * - Performance tracking
11
+ * - Statistics computation
12
+ *
13
+ * @extends S3dbError
14
+ */
15
+ export class MetricsError extends S3dbError {
16
+ constructor(message, details = {}) {
17
+ const { metricName, operation = 'unknown', resourceName, ...rest } = details;
18
+
19
+ let description = details.description;
20
+ if (!description) {
21
+ description = `
22
+ Metrics Operation Error
23
+
24
+ Operation: ${operation}
25
+ ${metricName ? `Metric: ${metricName}` : ''}
26
+ ${resourceName ? `Resource: ${resourceName}` : ''}
27
+
28
+ Common causes:
29
+ 1. Metric not configured
30
+ 2. Invalid metric value or type
31
+ 3. Metrics storage not accessible
32
+ 4. Aggregation function error
33
+ 5. Query parameters invalid
34
+
35
+ Solution:
36
+ Check metrics configuration and ensure proper initialization.
37
+
38
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/metrics.md
39
+ `.trim();
40
+ }
41
+
42
+ super(message, { ...rest, metricName, operation, resourceName, description });
43
+ }
44
+ }
45
+
46
+ export default MetricsError;
@@ -1,6 +1,7 @@
1
1
  import { Plugin } from './plugin.class.js';
2
2
  import { createConsumer } from './consumers/index.js';
3
3
  import tryFn from "../concerns/try-fn.js";
4
+ import { QueueError } from "./queue.errors.js";
4
5
 
5
6
  // Example configuration for SQS:
6
7
  // const plugin = new QueueConsumerPlugin({
@@ -101,13 +102,32 @@ export class QueueConsumerPlugin extends Plugin {
101
102
 
102
103
 
103
104
  if (!resource) {
104
- throw new Error('QueueConsumerPlugin: resource not found in message');
105
+ throw new QueueError('Resource not found in message', {
106
+ operation: 'handleMessage',
107
+ queueName: configuredResource,
108
+ messageBody: body,
109
+ suggestion: 'Ensure message includes a "resource" field specifying the target resource name'
110
+ });
105
111
  }
106
112
  if (!action) {
107
- throw new Error('QueueConsumerPlugin: action not found in message');
113
+ throw new QueueError('Action not found in message', {
114
+ operation: 'handleMessage',
115
+ queueName: configuredResource,
116
+ resource,
117
+ messageBody: body,
118
+ suggestion: 'Ensure message includes an "action" field (insert, update, or delete)'
119
+ });
108
120
  }
109
121
  const resourceObj = this.database.resources[resource];
110
- if (!resourceObj) throw new Error(`QueueConsumerPlugin: resource '${resource}' not found`);
122
+ if (!resourceObj) {
123
+ throw new QueueError(`Resource '${resource}' not found`, {
124
+ operation: 'handleMessage',
125
+ queueName: configuredResource,
126
+ resource,
127
+ availableResources: Object.keys(this.database.resources),
128
+ suggestion: 'Check resource name or ensure resource is created before consuming messages'
129
+ });
130
+ }
111
131
 
112
132
  let result;
113
133
  const [ok, err, res] = await tryFn(async () => {
@@ -119,7 +139,14 @@ export class QueueConsumerPlugin extends Plugin {
119
139
  } else if (action === 'delete') {
120
140
  result = await resourceObj.delete(data.id);
121
141
  } else {
122
- throw new Error(`QueueConsumerPlugin: unsupported action '${action}'`);
142
+ throw new QueueError(`Unsupported action '${action}'`, {
143
+ operation: 'handleMessage',
144
+ queueName: configuredResource,
145
+ resource,
146
+ action,
147
+ supportedActions: ['insert', 'update', 'delete'],
148
+ suggestion: 'Use one of the supported actions: insert, update, or delete'
149
+ });
123
150
  }
124
151
  return result;
125
152
  });
@@ -0,0 +1,46 @@
1
+ import { S3dbError } from '../errors.js';
2
+
3
+ /**
4
+ * QueueError - Errors related to queue operations
5
+ *
6
+ * Used for queue operations including:
7
+ * - Message enqueueing and dequeueing
8
+ * - Queue consumer registration
9
+ * - Message processing
10
+ * - Dead letter queue handling
11
+ * - Queue configuration and management
12
+ *
13
+ * @extends S3dbError
14
+ */
15
+ export class QueueError extends S3dbError {
16
+ constructor(message, details = {}) {
17
+ const { queueName, operation = 'unknown', messageId, ...rest } = details;
18
+
19
+ let description = details.description;
20
+ if (!description) {
21
+ description = `
22
+ Queue Operation Error
23
+
24
+ Operation: ${operation}
25
+ ${queueName ? `Queue: ${queueName}` : ''}
26
+ ${messageId ? `Message ID: ${messageId}` : ''}
27
+
28
+ Common causes:
29
+ 1. Queue not properly configured
30
+ 2. Message handler not registered
31
+ 3. Queue resource not found
32
+ 4. SQS/RabbitMQ connection failed
33
+ 5. Message processing timeout
34
+
35
+ Solution:
36
+ Check queue configuration and message handler registration.
37
+
38
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue.md
39
+ `.trim();
40
+ }
41
+
42
+ super(message, { ...rest, queueName, operation, messageId, description });
43
+ }
44
+ }
45
+
46
+ export default QueueError;
@@ -0,0 +1,46 @@
1
+ import { S3dbError } from '../errors.js';
2
+
3
+ /**
4
+ * ReplicationError - Errors related to replication operations
5
+ *
6
+ * Used for replicator operations including:
7
+ * - Replicator initialization and setup
8
+ * - Data replication to target systems
9
+ * - Resource mapping and transformation
10
+ * - Connection management
11
+ * - Batch replication operations
12
+ *
13
+ * @extends S3dbError
14
+ */
15
+ export class ReplicationError extends S3dbError {
16
+ constructor(message, details = {}) {
17
+ const { replicatorClass = 'unknown', operation = 'unknown', resourceName, ...rest } = details;
18
+
19
+ let description = details.description;
20
+ if (!description) {
21
+ description = `
22
+ Replication Operation Error
23
+
24
+ Replicator: ${replicatorClass}
25
+ Operation: ${operation}
26
+ ${resourceName ? `Resource: ${resourceName}` : ''}
27
+
28
+ Common causes:
29
+ 1. Invalid replicator configuration
30
+ 2. Target system not accessible
31
+ 3. Resource not configured for replication
32
+ 4. Invalid operation type
33
+ 5. Transformation function errors
34
+
35
+ Solution:
36
+ Check replicator configuration and ensure target system is accessible.
37
+
38
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md
39
+ `.trim();
40
+ }
41
+
42
+ super(message, { ...rest, replicatorClass, operation, resourceName, description });
43
+ }
44
+ }
45
+
46
+ export default ReplicationError;
@@ -1,6 +1,7 @@
1
1
  import Plugin from "./plugin.class.js";
2
2
  import tryFn from "../concerns/try-fn.js";
3
3
  import { createReplicator, validateReplicatorConfig } from "./replicators/index.js";
4
+ import { ReplicationError } from "./replicator.errors.js";
4
5
 
5
6
  function normalizeResourceName(name) {
6
7
  return typeof name === 'string' ? name.trim().toLowerCase() : name;
@@ -120,12 +121,40 @@ export class ReplicatorPlugin extends Plugin {
120
121
  super();
121
122
  // Validation for config tests
122
123
  if (!options.replicators || !Array.isArray(options.replicators)) {
123
- throw new Error('ReplicatorPlugin: replicators array is required');
124
+ throw new ReplicationError('ReplicatorPlugin requires replicators array', {
125
+ operation: 'constructor',
126
+ pluginName: 'ReplicatorPlugin',
127
+ providedOptions: Object.keys(options),
128
+ suggestion: 'Provide replicators array: new ReplicatorPlugin({ replicators: [{ driver: "s3db", resources: [...] }] })'
129
+ });
124
130
  }
125
131
  for (const rep of options.replicators) {
126
- if (!rep.driver) throw new Error('ReplicatorPlugin: each replicator must have a driver');
127
- if (!rep.resources || typeof rep.resources !== 'object') throw new Error('ReplicatorPlugin: each replicator must have resources config');
128
- if (Object.keys(rep.resources).length === 0) throw new Error('ReplicatorPlugin: each replicator must have at least one resource configured');
132
+ if (!rep.driver) {
133
+ throw new ReplicationError('Each replicator must have a driver', {
134
+ operation: 'constructor',
135
+ pluginName: 'ReplicatorPlugin',
136
+ replicatorConfig: rep,
137
+ suggestion: 'Each replicator entry must specify a driver: { driver: "s3db", resources: {...} }'
138
+ });
139
+ }
140
+ if (!rep.resources || typeof rep.resources !== 'object') {
141
+ throw new ReplicationError('Each replicator must have resources config', {
142
+ operation: 'constructor',
143
+ pluginName: 'ReplicatorPlugin',
144
+ driver: rep.driver,
145
+ replicatorConfig: rep,
146
+ suggestion: 'Provide resources as object or array: { driver: "s3db", resources: ["users"] } or { resources: { users: "people" } }'
147
+ });
148
+ }
149
+ if (Object.keys(rep.resources).length === 0) {
150
+ throw new ReplicationError('Each replicator must have at least one resource configured', {
151
+ operation: 'constructor',
152
+ pluginName: 'ReplicatorPlugin',
153
+ driver: rep.driver,
154
+ replicatorConfig: rep,
155
+ suggestion: 'Add at least one resource to replicate: { driver: "s3db", resources: ["users"] }'
156
+ });
157
+ }
129
158
  }
130
159
 
131
160
  this.config = {
@@ -657,7 +686,13 @@ export class ReplicatorPlugin extends Plugin {
657
686
  async syncAllData(replicatorId) {
658
687
  const replicator = this.replicators.find(r => r.id === replicatorId);
659
688
  if (!replicator) {
660
- throw new Error(`Replicator not found: ${replicatorId}`);
689
+ throw new ReplicationError('Replicator not found', {
690
+ operation: 'syncAllData',
691
+ pluginName: 'ReplicatorPlugin',
692
+ replicatorId,
693
+ availableReplicators: this.replicators.map(r => r.id),
694
+ suggestion: 'Check replicator ID or use getReplicatorStats() to list available replicators'
695
+ });
661
696
  }
662
697
 
663
698
  this.stats.lastSync = new Date().toISOString();
@@ -1,4 +1,5 @@
1
1
  import EventEmitter from 'events';
2
+ import { ReplicationError } from '../replicator.errors.js';
2
3
 
3
4
  /**
4
5
  * Base class for all replicator drivers
@@ -31,7 +32,12 @@ export class BaseReplicator extends EventEmitter {
31
32
  * @returns {Promise<Object>} replicator result
32
33
  */
33
34
  async replicate(resourceName, operation, data, id) {
34
- throw new Error(`replicate() method must be implemented by ${this.name}`);
35
+ throw new ReplicationError('replicate() method must be implemented by subclass', {
36
+ operation: 'replicate',
37
+ replicatorClass: this.name,
38
+ resourceName,
39
+ suggestion: 'Extend BaseReplicator and implement the replicate() method'
40
+ });
35
41
  }
36
42
 
37
43
  /**
@@ -41,7 +47,13 @@ export class BaseReplicator extends EventEmitter {
41
47
  * @returns {Promise<Object>} Batch replicator result
42
48
  */
43
49
  async replicateBatch(resourceName, records) {
44
- throw new Error(`replicateBatch() method must be implemented by ${this.name}`);
50
+ throw new ReplicationError('replicateBatch() method must be implemented by subclass', {
51
+ operation: 'replicateBatch',
52
+ replicatorClass: this.name,
53
+ resourceName,
54
+ batchSize: records?.length,
55
+ suggestion: 'Extend BaseReplicator and implement the replicateBatch() method'
56
+ });
45
57
  }
46
58
 
47
59
  /**
@@ -49,7 +61,11 @@ export class BaseReplicator extends EventEmitter {
49
61
  * @returns {Promise<boolean>} True if connection is successful
50
62
  */
51
63
  async testConnection() {
52
- throw new Error(`testConnection() method must be implemented by ${this.name}`);
64
+ throw new ReplicationError('testConnection() method must be implemented by subclass', {
65
+ operation: 'testConnection',
66
+ replicatorClass: this.name,
67
+ suggestion: 'Extend BaseReplicator and implement the testConnection() method'
68
+ });
53
69
  }
54
70
 
55
71
  /**
@@ -3,6 +3,7 @@ import BigqueryReplicator from './bigquery-replicator.class.js';
3
3
  import PostgresReplicator from './postgres-replicator.class.js';
4
4
  import S3dbReplicator from './s3db-replicator.class.js';
5
5
  import SqsReplicator from './sqs-replicator.class.js';
6
+ import { ReplicationError } from '../replicator.errors.js';
6
7
 
7
8
  export { BaseReplicator, BigqueryReplicator, PostgresReplicator, S3dbReplicator, SqsReplicator };
8
9
 
@@ -24,11 +25,16 @@ export const REPLICATOR_DRIVERS = {
24
25
  */
25
26
  export function createReplicator(driver, config = {}, resources = [], client = null) {
26
27
  const ReplicatorClass = REPLICATOR_DRIVERS[driver];
27
-
28
+
28
29
  if (!ReplicatorClass) {
29
- throw new Error(`Unknown replicator driver: ${driver}. Available drivers: ${Object.keys(REPLICATOR_DRIVERS).join(', ')}`);
30
+ throw new ReplicationError(`Unknown replicator driver: ${driver}`, {
31
+ operation: 'createReplicator',
32
+ driver,
33
+ availableDrivers: Object.keys(REPLICATOR_DRIVERS),
34
+ suggestion: `Use one of the available drivers: ${Object.keys(REPLICATOR_DRIVERS).join(', ')}`
35
+ });
30
36
  }
31
-
37
+
32
38
  return new ReplicatorClass(config, resources, client);
33
39
  }
34
40
 
@@ -1,6 +1,7 @@
1
1
  import tryFn from "#src/concerns/try-fn.js";
2
2
  import { S3db } from '#src/database.class.js';
3
3
  import BaseReplicator from './base-replicator.class.js';
4
+ import { ReplicationError } from '../replicator.errors.js';
4
5
 
5
6
  function normalizeResourceName(name) {
6
7
  return typeof name === 'string' ? name.trim().toLowerCase() : name;
@@ -118,7 +119,11 @@ class S3dbReplicator extends BaseReplicator {
118
119
  this.targetDatabase = new S3db(targetConfig);
119
120
  await this.targetDatabase.connect();
120
121
  } else {
121
- throw new Error('S3dbReplicator: No client or connectionString provided');
122
+ throw new ReplicationError('S3dbReplicator requires client or connectionString', {
123
+ operation: 'initialize',
124
+ replicatorClass: 'S3dbReplicator',
125
+ suggestion: 'Provide either a client instance or connectionString in config: { client: db } or { connectionString: "s3://..." }'
126
+ });
122
127
  }
123
128
 
124
129
  this.emit('connected', {
@@ -155,9 +160,15 @@ class S3dbReplicator extends BaseReplicator {
155
160
 
156
161
  const normResource = normalizeResourceName(resource);
157
162
  const entry = this.resourcesMap[normResource];
158
-
163
+
159
164
  if (!entry) {
160
- throw new Error(`[S3dbReplicator] Resource not configured: ${resource}`);
165
+ throw new ReplicationError('Resource not configured for replication', {
166
+ operation: 'replicate',
167
+ replicatorClass: 'S3dbReplicator',
168
+ resourceName: resource,
169
+ configuredResources: Object.keys(this.resourcesMap),
170
+ suggestion: 'Add resource to replicator resources map: { resources: { [resourceName]: "destination" } }'
171
+ });
161
172
  }
162
173
 
163
174
  // Handle multi-destination arrays
@@ -242,7 +253,14 @@ class S3dbReplicator extends BaseReplicator {
242
253
  } else if (operation === 'delete') {
243
254
  result = await destResourceObj.delete(recordId);
244
255
  } else {
245
- throw new Error(`Invalid operation: ${operation}. Supported operations are: insert, update, delete`);
256
+ throw new ReplicationError(`Invalid replication operation: ${operation}`, {
257
+ operation: 'replicate',
258
+ replicatorClass: 'S3dbReplicator',
259
+ invalidOperation: operation,
260
+ supportedOperations: ['insert', 'update', 'delete'],
261
+ resourceName: sourceResource,
262
+ suggestion: 'Use one of the supported operations: insert, update, delete'
263
+ });
246
264
  }
247
265
 
248
266
  return result;
@@ -333,7 +351,13 @@ class S3dbReplicator extends BaseReplicator {
333
351
  const norm = normalizeResourceName(resource);
334
352
  const found = available.find(r => normalizeResourceName(r) === norm);
335
353
  if (!found) {
336
- throw new Error(`[S3dbReplicator] Destination resource not found: ${resource}. Available: ${available.join(', ')}`);
354
+ throw new ReplicationError('Destination resource not found in target database', {
355
+ operation: '_getDestResourceObj',
356
+ replicatorClass: 'S3dbReplicator',
357
+ destinationResource: resource,
358
+ availableResources: available,
359
+ suggestion: 'Create the resource in target database or check resource name spelling'
360
+ });
337
361
  }
338
362
  return db.resources[found];
339
363
  }
@@ -390,13 +414,19 @@ class S3dbReplicator extends BaseReplicator {
390
414
 
391
415
  async testConnection() {
392
416
  const [ok, err] = await tryFn(async () => {
393
- if (!this.targetDatabase) throw new Error('No target database configured');
394
-
417
+ if (!this.targetDatabase) {
418
+ throw new ReplicationError('No target database configured for connection test', {
419
+ operation: 'testConnection',
420
+ replicatorClass: 'S3dbReplicator',
421
+ suggestion: 'Initialize replicator with client or connectionString before testing connection'
422
+ });
423
+ }
424
+
395
425
  // Try to list resources to test connection
396
426
  if (typeof this.targetDatabase.connect === 'function') {
397
427
  await this.targetDatabase.connect();
398
428
  }
399
-
429
+
400
430
  return true;
401
431
  });
402
432
 
@@ -0,0 +1,46 @@
1
+ import { S3dbError } from '../errors.js';
2
+
3
+ /**
4
+ * SchedulerError - Errors related to scheduler operations
5
+ *
6
+ * Used for scheduled task operations including:
7
+ * - Task creation and scheduling
8
+ * - Cron expression validation
9
+ * - Task execution and retries
10
+ * - Job queue management
11
+ * - Scheduler lifecycle management
12
+ *
13
+ * @extends S3dbError
14
+ */
15
+ export class SchedulerError extends S3dbError {
16
+ constructor(message, details = {}) {
17
+ const { taskId, operation = 'unknown', cronExpression, ...rest } = details;
18
+
19
+ let description = details.description;
20
+ if (!description) {
21
+ description = `
22
+ Scheduler Operation Error
23
+
24
+ Operation: ${operation}
25
+ ${taskId ? `Task ID: ${taskId}` : ''}
26
+ ${cronExpression ? `Cron: ${cronExpression}` : ''}
27
+
28
+ Common causes:
29
+ 1. Invalid cron expression format
30
+ 2. Task not found or already exists
31
+ 3. Scheduler not properly initialized
32
+ 4. Job execution failure
33
+ 5. Resource conflicts
34
+
35
+ Solution:
36
+ Check task configuration and ensure scheduler is properly initialized.
37
+
38
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/scheduler.md
39
+ `.trim();
40
+ }
41
+
42
+ super(message, { ...rest, taskId, operation, cronExpression, description });
43
+ }
44
+ }
45
+
46
+ export default SchedulerError;
@@ -1,6 +1,7 @@
1
1
  import Plugin from "./plugin.class.js";
2
2
  import tryFn from "../concerns/try-fn.js";
3
3
  import { idGenerator } from "../concerns/id.js";
4
+ import { SchedulerError } from "./scheduler.errors.js";
4
5
 
5
6
  /**
6
7
  * SchedulerPlugin - Cron-based Task Scheduling System
@@ -183,21 +184,40 @@ export class SchedulerPlugin extends Plugin {
183
184
 
184
185
  _validateConfiguration() {
185
186
  if (Object.keys(this.config.jobs).length === 0) {
186
- throw new Error('SchedulerPlugin: At least one job must be defined');
187
+ throw new SchedulerError('At least one job must be defined', {
188
+ operation: 'validateConfiguration',
189
+ jobCount: 0,
190
+ suggestion: 'Provide at least one job in the jobs configuration: { jobs: { myJob: { schedule: "* * * * *", action: async () => {...} } } }'
191
+ });
187
192
  }
188
-
193
+
189
194
  for (const [jobName, job] of Object.entries(this.config.jobs)) {
190
195
  if (!job.schedule) {
191
- throw new Error(`SchedulerPlugin: Job '${jobName}' must have a schedule`);
196
+ throw new SchedulerError(`Job '${jobName}' must have a schedule`, {
197
+ operation: 'validateConfiguration',
198
+ taskId: jobName,
199
+ providedConfig: Object.keys(job),
200
+ suggestion: 'Add a schedule property with a valid cron expression: { schedule: "0 * * * *", action: async () => {...} }'
201
+ });
192
202
  }
193
-
203
+
194
204
  if (!job.action || typeof job.action !== 'function') {
195
- throw new Error(`SchedulerPlugin: Job '${jobName}' must have an action function`);
205
+ throw new SchedulerError(`Job '${jobName}' must have an action function`, {
206
+ operation: 'validateConfiguration',
207
+ taskId: jobName,
208
+ actionType: typeof job.action,
209
+ suggestion: 'Provide an action function: { schedule: "...", action: async (db, ctx) => {...} }'
210
+ });
196
211
  }
197
-
212
+
198
213
  // Validate cron expression
199
214
  if (!this._isValidCronExpression(job.schedule)) {
200
- throw new Error(`SchedulerPlugin: Job '${jobName}' has invalid cron expression: ${job.schedule}`);
215
+ throw new SchedulerError(`Job '${jobName}' has invalid cron expression`, {
216
+ operation: 'validateConfiguration',
217
+ taskId: jobName,
218
+ cronExpression: job.schedule,
219
+ suggestion: 'Use valid cron format (5 fields: minute hour day month weekday) or shortcuts (@hourly, @daily, @weekly, @monthly, @yearly)'
220
+ });
201
221
  }
202
222
  }
203
223
  }
@@ -592,11 +612,21 @@ export class SchedulerPlugin extends Plugin {
592
612
  async runJob(jobName, context = {}) {
593
613
  const job = this.jobs.get(jobName);
594
614
  if (!job) {
595
- throw new Error(`Job '${jobName}' not found`);
615
+ throw new SchedulerError(`Job '${jobName}' not found`, {
616
+ operation: 'runJob',
617
+ taskId: jobName,
618
+ availableJobs: Array.from(this.jobs.keys()),
619
+ suggestion: 'Check job name or use getAllJobsStatus() to list available jobs'
620
+ });
596
621
  }
597
622
 
598
623
  if (this.activeJobs.has(jobName)) {
599
- throw new Error(`Job '${jobName}' is already running`);
624
+ throw new SchedulerError(`Job '${jobName}' is already running`, {
625
+ operation: 'runJob',
626
+ taskId: jobName,
627
+ executionId: this.activeJobs.get(jobName),
628
+ suggestion: 'Wait for current execution to complete or check job status with getJobStatus()'
629
+ });
600
630
  }
601
631
 
602
632
  await this._executeJob(jobName);
@@ -608,12 +638,17 @@ export class SchedulerPlugin extends Plugin {
608
638
  enableJob(jobName) {
609
639
  const job = this.jobs.get(jobName);
610
640
  if (!job) {
611
- throw new Error(`Job '${jobName}' not found`);
641
+ throw new SchedulerError(`Job '${jobName}' not found`, {
642
+ operation: 'enableJob',
643
+ taskId: jobName,
644
+ availableJobs: Array.from(this.jobs.keys()),
645
+ suggestion: 'Check job name or use getAllJobsStatus() to list available jobs'
646
+ });
612
647
  }
613
-
648
+
614
649
  job.enabled = true;
615
650
  this._scheduleNextExecution(jobName);
616
-
651
+
617
652
  this.emit('job_enabled', { jobName });
618
653
  }
619
654
 
@@ -623,7 +658,12 @@ export class SchedulerPlugin extends Plugin {
623
658
  disableJob(jobName) {
624
659
  const job = this.jobs.get(jobName);
625
660
  if (!job) {
626
- throw new Error(`Job '${jobName}' not found`);
661
+ throw new SchedulerError(`Job '${jobName}' not found`, {
662
+ operation: 'disableJob',
663
+ taskId: jobName,
664
+ availableJobs: Array.from(this.jobs.keys()),
665
+ suggestion: 'Check job name or use getAllJobsStatus() to list available jobs'
666
+ });
627
667
  }
628
668
 
629
669
  job.enabled = false;
@@ -743,16 +783,31 @@ export class SchedulerPlugin extends Plugin {
743
783
  */
744
784
  addJob(jobName, jobConfig) {
745
785
  if (this.jobs.has(jobName)) {
746
- throw new Error(`Job '${jobName}' already exists`);
786
+ throw new SchedulerError(`Job '${jobName}' already exists`, {
787
+ operation: 'addJob',
788
+ taskId: jobName,
789
+ existingJobs: Array.from(this.jobs.keys()),
790
+ suggestion: 'Use a different job name or remove the existing job first with removeJob()'
791
+ });
747
792
  }
748
-
793
+
749
794
  // Validate job configuration
750
795
  if (!jobConfig.schedule || !jobConfig.action) {
751
- throw new Error('Job must have schedule and action');
796
+ throw new SchedulerError('Job must have schedule and action', {
797
+ operation: 'addJob',
798
+ taskId: jobName,
799
+ providedConfig: Object.keys(jobConfig),
800
+ suggestion: 'Provide both schedule and action: { schedule: "0 * * * *", action: async (db, ctx) => {...} }'
801
+ });
752
802
  }
753
-
803
+
754
804
  if (!this._isValidCronExpression(jobConfig.schedule)) {
755
- throw new Error(`Invalid cron expression: ${jobConfig.schedule}`);
805
+ throw new SchedulerError('Invalid cron expression', {
806
+ operation: 'addJob',
807
+ taskId: jobName,
808
+ cronExpression: jobConfig.schedule,
809
+ suggestion: 'Use valid cron format (5 fields) or shortcuts (@hourly, @daily, @weekly, @monthly, @yearly)'
810
+ });
756
811
  }
757
812
 
758
813
  const job = {
@@ -791,7 +846,12 @@ export class SchedulerPlugin extends Plugin {
791
846
  removeJob(jobName) {
792
847
  const job = this.jobs.get(jobName);
793
848
  if (!job) {
794
- throw new Error(`Job '${jobName}' not found`);
849
+ throw new SchedulerError(`Job '${jobName}' not found`, {
850
+ operation: 'removeJob',
851
+ taskId: jobName,
852
+ availableJobs: Array.from(this.jobs.keys()),
853
+ suggestion: 'Check job name or use getAllJobsStatus() to list available jobs'
854
+ });
795
855
  }
796
856
 
797
857
  // Cancel scheduled execution