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.
- package/dist/s3db.cjs.js +1177 -128
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1172 -129
- 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 +19 -4
- package/src/errors.js +306 -27
- 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.errors.js +47 -0
- package/src/plugins/cache.plugin.js +8 -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/stream/index.js +6 -1
- 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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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)
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|