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.
- package/dist/s3db.cjs.js +1650 -136
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1644 -137
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/behaviors/enforce-limits.js +28 -4
- package/src/behaviors/index.js +6 -1
- package/src/client.class.js +11 -1
- package/src/concerns/partition-queue.js +7 -1
- package/src/concerns/plugin-storage.js +75 -13
- package/src/database.class.js +22 -4
- package/src/errors.js +414 -24
- package/src/partition-drivers/base-partition-driver.js +12 -2
- package/src/partition-drivers/index.js +7 -1
- package/src/partition-drivers/memory-partition-driver.js +20 -5
- package/src/partition-drivers/sqs-partition-driver.js +6 -1
- package/src/plugins/audit.errors.js +46 -0
- package/src/plugins/backup/base-backup-driver.class.js +36 -6
- package/src/plugins/backup/filesystem-backup-driver.class.js +55 -7
- package/src/plugins/backup/index.js +40 -9
- package/src/plugins/backup/multi-backup-driver.class.js +69 -9
- package/src/plugins/backup/s3-backup-driver.class.js +48 -6
- package/src/plugins/backup.errors.js +45 -0
- package/src/plugins/cache/cache.class.js +8 -1
- package/src/plugins/cache/memory-cache.class.js +216 -33
- package/src/plugins/cache.errors.js +47 -0
- package/src/plugins/cache.plugin.js +94 -3
- package/src/plugins/eventual-consistency/analytics.js +145 -0
- package/src/plugins/eventual-consistency/index.js +203 -1
- package/src/plugins/fulltext.errors.js +46 -0
- package/src/plugins/fulltext.plugin.js +15 -3
- package/src/plugins/metrics.errors.js +46 -0
- package/src/plugins/queue-consumer.plugin.js +31 -4
- package/src/plugins/queue.errors.js +46 -0
- package/src/plugins/replicator.errors.js +46 -0
- package/src/plugins/replicator.plugin.js +40 -5
- package/src/plugins/replicators/base-replicator.class.js +19 -3
- package/src/plugins/replicators/index.js +9 -3
- package/src/plugins/replicators/s3db-replicator.class.js +38 -8
- package/src/plugins/scheduler.errors.js +46 -0
- package/src/plugins/scheduler.plugin.js +79 -19
- package/src/plugins/state-machine.errors.js +47 -0
- package/src/plugins/state-machine.plugin.js +86 -17
- package/src/resource.class.js +8 -1
- package/src/stream/index.js +6 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`;
|
package/src/resource.class.js
CHANGED
|
@@ -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
|
}
|
package/src/stream/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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;
|