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
@@ -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
@@ -0,0 +1,47 @@
1
+ import { S3dbError } from '../errors.js';
2
+
3
+ /**
4
+ * StateMachineError - Errors related to state machine operations
5
+ *
6
+ * Used for state machine operations including:
7
+ * - State transitions
8
+ * - State validation
9
+ * - Transition conditions
10
+ * - State machine configuration
11
+ * - Workflow execution
12
+ *
13
+ * @extends S3dbError
14
+ */
15
+ export class StateMachineError extends S3dbError {
16
+ constructor(message, details = {}) {
17
+ const { currentState, targetState, resourceName, operation = 'unknown', ...rest } = details;
18
+
19
+ let description = details.description;
20
+ if (!description) {
21
+ description = `
22
+ State Machine Operation Error
23
+
24
+ Operation: ${operation}
25
+ ${currentState ? `Current State: ${currentState}` : ''}
26
+ ${targetState ? `Target State: ${targetState}` : ''}
27
+ ${resourceName ? `Resource: ${resourceName}` : ''}
28
+
29
+ Common causes:
30
+ 1. Invalid state transition
31
+ 2. State machine not configured
32
+ 3. Transition conditions not met
33
+ 4. State not defined in configuration
34
+ 5. Missing transition handler
35
+
36
+ Solution:
37
+ Check state machine configuration and valid transitions.
38
+
39
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/state-machine.md
40
+ `.trim();
41
+ }
42
+
43
+ super(message, { ...rest, currentState, targetState, resourceName, operation, description });
44
+ }
45
+ }
46
+
47
+ export default StateMachineError;
@@ -1,5 +1,6 @@
1
1
  import Plugin from "./plugin.class.js";
2
2
  import tryFn from "../concerns/try-fn.js";
3
+ import { StateMachineError } from "./state-machine.errors.js";
3
4
 
4
5
  /**
5
6
  * StateMachinePlugin - Finite State Machine Management
@@ -120,20 +121,39 @@ export class StateMachinePlugin extends Plugin {
120
121
 
121
122
  _validateConfiguration() {
122
123
  if (!this.config.stateMachines || Object.keys(this.config.stateMachines).length === 0) {
123
- throw new Error('StateMachinePlugin: At least one state machine must be defined');
124
+ throw new StateMachineError('At least one state machine must be defined', {
125
+ operation: 'validateConfiguration',
126
+ machineCount: 0,
127
+ suggestion: 'Provide at least one state machine in the stateMachines configuration'
128
+ });
124
129
  }
125
130
 
126
131
  for (const [machineName, machine] of Object.entries(this.config.stateMachines)) {
127
132
  if (!machine.states || Object.keys(machine.states).length === 0) {
128
- throw new Error(`StateMachinePlugin: Machine '${machineName}' must have states defined`);
133
+ throw new StateMachineError(`Machine '${machineName}' must have states defined`, {
134
+ operation: 'validateConfiguration',
135
+ machineId: machineName,
136
+ suggestion: 'Define at least one state in the states configuration'
137
+ });
129
138
  }
130
-
139
+
131
140
  if (!machine.initialState) {
132
- throw new Error(`StateMachinePlugin: Machine '${machineName}' must have an initialState`);
141
+ throw new StateMachineError(`Machine '${machineName}' must have an initialState`, {
142
+ operation: 'validateConfiguration',
143
+ machineId: machineName,
144
+ availableStates: Object.keys(machine.states),
145
+ suggestion: 'Specify an initialState property matching one of the defined states'
146
+ });
133
147
  }
134
-
148
+
135
149
  if (!machine.states[machine.initialState]) {
136
- throw new Error(`StateMachinePlugin: Initial state '${machine.initialState}' not found in machine '${machineName}'`);
150
+ throw new StateMachineError(`Initial state '${machine.initialState}' not found in machine '${machineName}'`, {
151
+ operation: 'validateConfiguration',
152
+ machineId: machineName,
153
+ initialState: machine.initialState,
154
+ availableStates: Object.keys(machine.states),
155
+ suggestion: 'Set initialState to one of the defined states'
156
+ });
137
157
  }
138
158
  }
139
159
  }
@@ -200,14 +220,27 @@ export class StateMachinePlugin extends Plugin {
200
220
  async send(machineId, entityId, event, context = {}) {
201
221
  const machine = this.machines.get(machineId);
202
222
  if (!machine) {
203
- throw new Error(`State machine '${machineId}' not found`);
223
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
224
+ operation: 'send',
225
+ machineId,
226
+ availableMachines: Array.from(this.machines.keys()),
227
+ suggestion: 'Check machine ID or use getMachines() to list available machines'
228
+ });
204
229
  }
205
230
 
206
231
  const currentState = await this.getState(machineId, entityId);
207
232
  const stateConfig = machine.config.states[currentState];
208
-
233
+
209
234
  if (!stateConfig || !stateConfig.on || !stateConfig.on[event]) {
210
- throw new Error(`Event '${event}' not valid for state '${currentState}' in machine '${machineId}'`);
235
+ throw new StateMachineError(`Event '${event}' not valid for state '${currentState}' in machine '${machineId}'`, {
236
+ operation: 'send',
237
+ machineId,
238
+ entityId,
239
+ event,
240
+ currentState,
241
+ validEvents: stateConfig && stateConfig.on ? Object.keys(stateConfig.on) : [],
242
+ suggestion: 'Use getValidEvents() to check which events are valid for the current state'
243
+ });
211
244
  }
212
245
 
213
246
  const targetState = stateConfig.on[event];
@@ -218,12 +251,21 @@ export class StateMachinePlugin extends Plugin {
218
251
  const guard = this.config.guards[guardName];
219
252
 
220
253
  if (guard) {
221
- const [guardOk, guardErr, guardResult] = await tryFn(() =>
254
+ const [guardOk, guardErr, guardResult] = await tryFn(() =>
222
255
  guard(context, event, { database: this.database, machineId, entityId })
223
256
  );
224
-
257
+
225
258
  if (!guardOk || !guardResult) {
226
- throw new Error(`Transition blocked by guard '${guardName}': ${guardErr?.message || 'Guard returned false'}`);
259
+ throw new StateMachineError(`Transition blocked by guard '${guardName}'`, {
260
+ operation: 'send',
261
+ machineId,
262
+ entityId,
263
+ event,
264
+ currentState,
265
+ guardName,
266
+ guardError: guardErr?.message || 'Guard returned false',
267
+ suggestion: 'Check guard conditions or modify the context to satisfy guard requirements'
268
+ });
227
269
  }
228
270
  }
229
271
  }
@@ -363,7 +405,12 @@ export class StateMachinePlugin extends Plugin {
363
405
  async getState(machineId, entityId) {
364
406
  const machine = this.machines.get(machineId);
365
407
  if (!machine) {
366
- throw new Error(`State machine '${machineId}' not found`);
408
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
409
+ operation: 'getState',
410
+ machineId,
411
+ availableMachines: Array.from(this.machines.keys()),
412
+ suggestion: 'Check machine ID or use getMachines() to list available machines'
413
+ });
367
414
  }
368
415
 
369
416
  // Check in-memory cache first
@@ -397,7 +444,12 @@ export class StateMachinePlugin extends Plugin {
397
444
  async getValidEvents(machineId, stateOrEntityId) {
398
445
  const machine = this.machines.get(machineId);
399
446
  if (!machine) {
400
- throw new Error(`State machine '${machineId}' not found`);
447
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
448
+ operation: 'getValidEvents',
449
+ machineId,
450
+ availableMachines: Array.from(this.machines.keys()),
451
+ suggestion: 'Check machine ID or use getMachines() to list available machines'
452
+ });
401
453
  }
402
454
 
403
455
  let state;
@@ -458,7 +510,12 @@ export class StateMachinePlugin extends Plugin {
458
510
  async initializeEntity(machineId, entityId, context = {}) {
459
511
  const machine = this.machines.get(machineId);
460
512
  if (!machine) {
461
- throw new Error(`State machine '${machineId}' not found`);
513
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
514
+ operation: 'initializeEntity',
515
+ machineId,
516
+ availableMachines: Array.from(this.machines.keys()),
517
+ suggestion: 'Check machine ID or use getMachines() to list available machines'
518
+ });
462
519
  }
463
520
 
464
521
  const initialState = machine.config.initialState;
@@ -483,7 +540,14 @@ export class StateMachinePlugin extends Plugin {
483
540
 
484
541
  // Only throw if error is NOT "already exists"
485
542
  if (!ok && err && !err.message?.includes('already exists')) {
486
- throw new Error(`Failed to initialize entity state: ${err.message}`);
543
+ throw new StateMachineError('Failed to initialize entity state', {
544
+ operation: 'initializeEntity',
545
+ machineId,
546
+ entityId,
547
+ initialState,
548
+ original: err,
549
+ suggestion: 'Check state resource configuration and database permissions'
550
+ });
487
551
  }
488
552
  }
489
553
 
@@ -519,7 +583,12 @@ export class StateMachinePlugin extends Plugin {
519
583
  visualize(machineId) {
520
584
  const machine = this.machines.get(machineId);
521
585
  if (!machine) {
522
- throw new Error(`State machine '${machineId}' not found`);
586
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
587
+ operation: 'visualize',
588
+ machineId,
589
+ availableMachines: Array.from(this.machines.keys()),
590
+ suggestion: 'Check machine ID or use getMachines() to list available machines'
591
+ });
523
592
  }
524
593
 
525
594
  let dot = `digraph ${machineId} {\n`;
@@ -134,6 +134,7 @@ export class Resource extends AsyncEventEmitter {
134
134
  idGenerator: customIdGenerator,
135
135
  idSize = 22,
136
136
  versioningEnabled = false,
137
+ strictValidation = true,
137
138
  events = {},
138
139
  asyncEvents = true,
139
140
  asyncPartitions = true,
@@ -149,6 +150,7 @@ export class Resource extends AsyncEventEmitter {
149
150
  this.parallelism = parallelism;
150
151
  this.passphrase = passphrase ?? 'secret';
151
152
  this.versioningEnabled = versioningEnabled;
153
+ this.strictValidation = strictValidation;
152
154
 
153
155
  // Configure async events mode
154
156
  this.setAsyncMode(asyncEvents);
@@ -476,9 +478,14 @@ export class Resource extends AsyncEventEmitter {
476
478
 
477
479
  /**
478
480
  * Validate that all partition fields exist in current resource attributes
479
- * @throws {Error} If partition fields don't exist in current schema
481
+ * @throws {Error} If partition fields don't exist in current schema (only when strictValidation is true)
480
482
  */
481
483
  validatePartitions() {
484
+ // Skip validation if strictValidation is disabled
485
+ if (!this.strictValidation) {
486
+ return;
487
+ }
488
+
482
489
  if (!this.config.partitions) {
483
490
  return; // No partitions to validate
484
491
  }
@@ -3,10 +3,15 @@ export * from "./resource-writer.class.js"
3
3
  export * from "./resource-ids-reader.class.js"
4
4
  export * from "./resource-ids-page-reader.class.js"
5
5
 
6
+ import { StreamError } from '../errors.js';
7
+
6
8
  export function streamToString(stream) {
7
9
  return new Promise((resolve, reject) => {
8
10
  if (!stream) {
9
- return reject(new Error('streamToString: stream is undefined'));
11
+ return reject(new StreamError('Stream is undefined', {
12
+ operation: 'streamToString',
13
+ suggestion: 'Ensure a valid stream is passed to streamToString()'
14
+ }));
10
15
  }
11
16
  const chunks = [];
12
17
  stream.on('data', (chunk) => chunks.push(chunk));
@@ -4,13 +4,18 @@ import { PromisePool } from "@supercharge/promise-pool";
4
4
 
5
5
  import { ResourceIdsPageReader } from "./resource-ids-page-reader.class.js"
6
6
  import tryFn from "../concerns/try-fn.js";
7
+ import { StreamError } from '../errors.js';
7
8
 
8
9
  export class ResourceReader extends EventEmitter {
9
10
  constructor({ resource, batchSize = 10, concurrency = 5 }) {
10
11
  super()
11
12
 
12
13
  if (!resource) {
13
- throw new Error("Resource is required for ResourceReader");
14
+ throw new StreamError('Resource is required for ResourceReader', {
15
+ operation: 'constructor',
16
+ resource: resource?.name,
17
+ suggestion: 'Pass a valid Resource instance when creating ResourceReader'
18
+ });
14
19
  }
15
20
 
16
21
  this.resource = resource;