@forzalabs/remora 0.1.5-nasco.3 → 0.1.7-nasco.3
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/Constants.js +1 -1
- package/database/DatabaseEngine.js +17 -14
- package/definitions/json_schemas/consumer-schema.json +68 -3
- package/drivers/DeltaShareDriver.js +9 -5
- package/drivers/LocalDriver.js +59 -0
- package/drivers/S3Driver.js +51 -0
- package/engines/Environment.js +4 -0
- package/engines/consumer/ConsumerOnFinishManager.js +188 -0
- package/engines/dataset/Dataset.js +7 -4
- package/engines/dataset/DatasetRecord.js +1 -1
- package/engines/dataset/ParallelDataset.js +3 -0
- package/engines/execution/ExecutionEnvironment.js +11 -0
- package/engines/execution/ExecutionPlanner.js +3 -0
- package/engines/producer/ProducerEngine.js +1 -0
- package/engines/scheduler/CronScheduler.js +215 -0
- package/engines/scheduler/QueueManager.js +307 -0
- package/engines/transform/TransformationEngine.js +2 -2
- package/engines/transform/TypeCaster.js +12 -4
- package/engines/usage/UsageDataManager.js +41 -0
- package/package.json +3 -1
|
@@ -13,6 +13,7 @@ class ExecutionPlannerClas {
|
|
|
13
13
|
switch (engine) {
|
|
14
14
|
case 'aws-dynamodb': return 'no-sql';
|
|
15
15
|
case 'aws-redshift':
|
|
16
|
+
case 'delta-share':
|
|
16
17
|
case 'postgres': return 'sql';
|
|
17
18
|
case 'aws-s3': return 'file';
|
|
18
19
|
case 'local': return 'local';
|
|
@@ -65,6 +66,8 @@ class ExecutionPlannerClas {
|
|
|
65
66
|
default:
|
|
66
67
|
throw new Error(`Output format "${output.format}" not supported`);
|
|
67
68
|
}
|
|
69
|
+
if (output.onSuccess && output.onSuccess.length > 0)
|
|
70
|
+
plan.push({ type: 'perform-on-success-actions', output });
|
|
68
71
|
}
|
|
69
72
|
plan.push({ type: 'clean-datasets' });
|
|
70
73
|
plan.push({ type: 'save-execution-stats' });
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
const cron = __importStar(require("node-cron"));
|
|
49
|
+
const Environment_1 = __importDefault(require("../Environment"));
|
|
50
|
+
const ConsumerEngine_1 = __importDefault(require("../consumer/ConsumerEngine"));
|
|
51
|
+
const UserManager_1 = __importDefault(require("../UserManager"));
|
|
52
|
+
class CronScheduler {
|
|
53
|
+
constructor() {
|
|
54
|
+
this.scheduledJobs = new Map();
|
|
55
|
+
this.isInitialized = false;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Initialize the CRON scheduler by scanning all consumers and scheduling those with CRON triggers
|
|
59
|
+
*/
|
|
60
|
+
initialize() {
|
|
61
|
+
if (this.isInitialized) {
|
|
62
|
+
console.log('CRON scheduler already initialized');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
console.log('Initializing CRON scheduler...');
|
|
66
|
+
try {
|
|
67
|
+
const consumers = Environment_1.default.getAllConsumers();
|
|
68
|
+
let cronJobCount = 0;
|
|
69
|
+
for (const consumer of consumers) {
|
|
70
|
+
if (this.hasCronTrigger(consumer)) {
|
|
71
|
+
this.scheduleConsumer(consumer);
|
|
72
|
+
cronJobCount++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.isInitialized = true;
|
|
76
|
+
console.log(`CRON scheduler initialized with ${cronJobCount} scheduled jobs`);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error('Failed to initialize CRON scheduler:', error);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Check if a consumer has any CRON triggers configured
|
|
85
|
+
*/
|
|
86
|
+
hasCronTrigger(consumer) {
|
|
87
|
+
return consumer.outputs.some(output => { var _a; return ((_a = output.trigger) === null || _a === void 0 ? void 0 : _a.type) === 'CRON' && output.trigger.value; });
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Schedule a consumer with CRON triggers
|
|
91
|
+
*/
|
|
92
|
+
scheduleConsumer(consumer) {
|
|
93
|
+
consumer.outputs.forEach((output, index) => {
|
|
94
|
+
var _a;
|
|
95
|
+
if (((_a = output.trigger) === null || _a === void 0 ? void 0 : _a.type) === 'CRON' && output.trigger.value) {
|
|
96
|
+
const jobKey = `${consumer.name}_output_${index}`;
|
|
97
|
+
const cronExpression = output.trigger.value;
|
|
98
|
+
try {
|
|
99
|
+
// Validate CRON expression
|
|
100
|
+
if (!cron.validate(cronExpression)) {
|
|
101
|
+
console.error(`Invalid CRON expression for consumer ${consumer.name}: ${cronExpression}`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Schedule the job
|
|
105
|
+
const task = cron.schedule(cronExpression, () => __awaiter(this, void 0, void 0, function* () {
|
|
106
|
+
yield this.executeConsumerOutput(consumer, output, index);
|
|
107
|
+
}));
|
|
108
|
+
// Don't start the task immediately, we'll start it manually
|
|
109
|
+
task.stop();
|
|
110
|
+
this.scheduledJobs.set(jobKey, task);
|
|
111
|
+
task.start();
|
|
112
|
+
console.log(`Scheduled CRON job for consumer "${consumer.name}" output ${index} with expression: ${cronExpression}`);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error(`Failed to schedule CRON job for consumer ${consumer.name}:`, error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Execute a consumer output when triggered by CRON
|
|
122
|
+
*/
|
|
123
|
+
executeConsumerOutput(consumer, output, outputIndex) {
|
|
124
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
125
|
+
try {
|
|
126
|
+
console.log(`Executing CRON job for consumer "${consumer.name}" output ${outputIndex}`);
|
|
127
|
+
const user = UserManager_1.default.getUser();
|
|
128
|
+
// Execute the consumer with default options
|
|
129
|
+
const result = yield ConsumerEngine_1.default.execute(consumer, {}, user);
|
|
130
|
+
console.log(`CRON job completed successfully for consumer "${consumer.name}" output ${outputIndex}`);
|
|
131
|
+
// Log execution statistics
|
|
132
|
+
if (result && result._stats) {
|
|
133
|
+
console.log(`CRON job stats: ${result._stats.elapsedMS}ms, size: ${result._stats.size}, cycles: ${result._stats.cycles}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
console.error(`CRON job failed for consumer "${consumer.name}" output ${outputIndex}:`, error);
|
|
138
|
+
// Optionally, you could implement error handling strategies here:
|
|
139
|
+
// - Send notifications
|
|
140
|
+
// - Log to a monitoring system
|
|
141
|
+
// - Retry logic
|
|
142
|
+
// - Disable the job after repeated failures
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Add or update a CRON job for a specific consumer
|
|
148
|
+
*/
|
|
149
|
+
updateConsumerSchedule(consumer) {
|
|
150
|
+
// First, remove any existing schedules for this consumer
|
|
151
|
+
this.removeConsumerSchedule(consumer.name);
|
|
152
|
+
// Then, add new schedules if they have CRON triggers
|
|
153
|
+
if (this.hasCronTrigger(consumer)) {
|
|
154
|
+
this.scheduleConsumer(consumer);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Remove all scheduled jobs for a consumer
|
|
159
|
+
*/
|
|
160
|
+
removeConsumerSchedule(consumerName) {
|
|
161
|
+
const jobsToRemove = Array.from(this.scheduledJobs.keys()).filter(key => key.startsWith(`${consumerName}_output_`));
|
|
162
|
+
jobsToRemove.forEach(jobKey => {
|
|
163
|
+
const task = this.scheduledJobs.get(jobKey);
|
|
164
|
+
if (task) {
|
|
165
|
+
task.stop();
|
|
166
|
+
task.destroy();
|
|
167
|
+
this.scheduledJobs.delete(jobKey);
|
|
168
|
+
console.log(`Removed CRON job: ${jobKey}`);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get information about all scheduled jobs
|
|
174
|
+
*/
|
|
175
|
+
getScheduledJobs() {
|
|
176
|
+
return Array.from(this.scheduledJobs.entries()).map(([jobKey, task]) => ({
|
|
177
|
+
jobKey,
|
|
178
|
+
isRunning: task.getStatus() === 'scheduled'
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Stop all scheduled jobs
|
|
183
|
+
*/
|
|
184
|
+
stopAllJobs() {
|
|
185
|
+
console.log('Stopping all CRON jobs...');
|
|
186
|
+
this.scheduledJobs.forEach((task, jobKey) => {
|
|
187
|
+
task.stop();
|
|
188
|
+
task.destroy();
|
|
189
|
+
console.log(`Stopped CRON job: ${jobKey}`);
|
|
190
|
+
});
|
|
191
|
+
this.scheduledJobs.clear();
|
|
192
|
+
this.isInitialized = false;
|
|
193
|
+
console.log('All CRON jobs stopped');
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Restart the scheduler (useful for configuration reloads)
|
|
197
|
+
*/
|
|
198
|
+
restart() {
|
|
199
|
+
console.log('Restarting CRON scheduler...');
|
|
200
|
+
this.stopAllJobs();
|
|
201
|
+
this.initialize();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Get the scheduler status
|
|
205
|
+
*/
|
|
206
|
+
getStatus() {
|
|
207
|
+
return {
|
|
208
|
+
initialized: this.isInitialized,
|
|
209
|
+
jobCount: this.scheduledJobs.size,
|
|
210
|
+
jobs: this.getScheduledJobs()
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Export a singleton instance
|
|
215
|
+
exports.default = new CronScheduler();
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const client_sqs_1 = require("@aws-sdk/client-sqs");
|
|
16
|
+
const Environment_1 = __importDefault(require("../Environment"));
|
|
17
|
+
const ConsumerEngine_1 = __importDefault(require("../consumer/ConsumerEngine"));
|
|
18
|
+
const UserManager_1 = __importDefault(require("../UserManager"));
|
|
19
|
+
const SecretManager_1 = __importDefault(require("../SecretManager"));
|
|
20
|
+
class QueueManager {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.queueMappings = new Map();
|
|
23
|
+
this.pollingIntervals = new Map();
|
|
24
|
+
this.isInitialized = false;
|
|
25
|
+
this.POLLING_INTERVAL_MS = 5000; // Poll every 5 seconds
|
|
26
|
+
this.MAX_MESSAGES = 10; // Maximum messages to receive in one poll
|
|
27
|
+
// Initialize SQS client with default configuration
|
|
28
|
+
// Will be reconfigured when we know the specific queue details
|
|
29
|
+
this.sqsClient = new client_sqs_1.SQSClient({});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Initialize the Queue Manager by scanning all consumers and setting up queue listeners for those with QUEUE triggers
|
|
33
|
+
*/
|
|
34
|
+
initialize() {
|
|
35
|
+
if (this.isInitialized) {
|
|
36
|
+
console.log('Queue Manager already initialized');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
console.log('Initializing Queue Manager...');
|
|
40
|
+
try {
|
|
41
|
+
const consumers = Environment_1.default.getAllConsumers();
|
|
42
|
+
let queueTriggerCount = 0;
|
|
43
|
+
for (const consumer of consumers) {
|
|
44
|
+
if (this.hasQueueTrigger(consumer)) {
|
|
45
|
+
this.setupQueueListeners(consumer);
|
|
46
|
+
queueTriggerCount++;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
this.isInitialized = true;
|
|
50
|
+
console.log(`Queue Manager initialized with ${queueTriggerCount} queue triggers`);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.error('Failed to initialize Queue Manager:', error);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if a consumer has any QUEUE triggers configured
|
|
59
|
+
*/
|
|
60
|
+
hasQueueTrigger(consumer) {
|
|
61
|
+
return consumer.outputs.some(output => { var _a; return ((_a = output.trigger) === null || _a === void 0 ? void 0 : _a.type) === 'QUEUE' && output.trigger.value; });
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Setup queue listeners for a consumer with QUEUE triggers
|
|
65
|
+
*/
|
|
66
|
+
setupQueueListeners(consumer) {
|
|
67
|
+
consumer.outputs.forEach((output, index) => {
|
|
68
|
+
var _a;
|
|
69
|
+
if (((_a = output.trigger) === null || _a === void 0 ? void 0 : _a.type) === 'QUEUE' && output.trigger.value) {
|
|
70
|
+
try {
|
|
71
|
+
const queueConfig = this.parseQueueConfig(output.trigger.value, output.trigger.metadata);
|
|
72
|
+
const mapping = {
|
|
73
|
+
consumer,
|
|
74
|
+
outputIndex: index,
|
|
75
|
+
queueUrl: queueConfig.queueUrl,
|
|
76
|
+
messageType: queueConfig.messageType
|
|
77
|
+
};
|
|
78
|
+
// Add to mappings
|
|
79
|
+
if (!this.queueMappings.has(queueConfig.queueUrl)) {
|
|
80
|
+
this.queueMappings.set(queueConfig.queueUrl, []);
|
|
81
|
+
}
|
|
82
|
+
this.queueMappings.get(queueConfig.queueUrl).push(mapping);
|
|
83
|
+
// Start polling for this queue if not already started
|
|
84
|
+
if (!this.pollingIntervals.has(queueConfig.queueUrl)) {
|
|
85
|
+
this.startQueuePolling(queueConfig.queueUrl, queueConfig.region, queueConfig.credentials);
|
|
86
|
+
}
|
|
87
|
+
console.log(`Setup queue listener for consumer "${consumer.name}" output ${index} on queue: ${queueConfig.queueUrl}`);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
console.error(`Failed to setup queue listener for consumer ${consumer.name}:`, error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Parse queue configuration from trigger value and metadata
|
|
97
|
+
*/
|
|
98
|
+
parseQueueConfig(triggerValue, metadata) {
|
|
99
|
+
// triggerValue should be the queue URL or queue name
|
|
100
|
+
let queueUrl = triggerValue;
|
|
101
|
+
// If it's not a full URL, construct it
|
|
102
|
+
if (!queueUrl.startsWith('https://')) {
|
|
103
|
+
const region = (metadata === null || metadata === void 0 ? void 0 : metadata.region) || process.env.AWS_DEFAULT_REGION || 'us-east-1';
|
|
104
|
+
const accountId = (metadata === null || metadata === void 0 ? void 0 : metadata.accountId) || process.env.AWS_ACCOUNT_ID;
|
|
105
|
+
if (!accountId) {
|
|
106
|
+
throw new Error('AWS Account ID is required for queue trigger. Set it in metadata.accountId or AWS_ACCOUNT_ID environment variable');
|
|
107
|
+
}
|
|
108
|
+
queueUrl = `https://sqs.${region}.amazonaws.com/${accountId}/${triggerValue}`;
|
|
109
|
+
}
|
|
110
|
+
// Extract region from URL if not provided in metadata
|
|
111
|
+
const urlParts = queueUrl.match(/https:\/\/sqs\.([^.]+)\.amazonaws\.com\//);
|
|
112
|
+
const region = (metadata === null || metadata === void 0 ? void 0 : metadata.region) || (urlParts === null || urlParts === void 0 ? void 0 : urlParts[1]) || 'us-east-1';
|
|
113
|
+
// Get credentials from metadata or environment
|
|
114
|
+
let credentials;
|
|
115
|
+
const accessKeyId = (metadata === null || metadata === void 0 ? void 0 : metadata.accessKeyId) || process.env.AWS_ACCESS_KEY_ID;
|
|
116
|
+
const secretAccessKey = (metadata === null || metadata === void 0 ? void 0 : metadata.secretAccessKey) || process.env.AWS_SECRET_ACCESS_KEY;
|
|
117
|
+
const sessionToken = (metadata === null || metadata === void 0 ? void 0 : metadata.sessionToken) || process.env.AWS_SESSION_TOKEN;
|
|
118
|
+
if (accessKeyId && secretAccessKey) {
|
|
119
|
+
credentials = {
|
|
120
|
+
accessKeyId: SecretManager_1.default.replaceSecret(accessKeyId),
|
|
121
|
+
secretAccessKey: SecretManager_1.default.replaceSecret(secretAccessKey),
|
|
122
|
+
sessionToken: sessionToken ? SecretManager_1.default.replaceSecret(sessionToken) : undefined
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
queueUrl,
|
|
127
|
+
messageType: metadata === null || metadata === void 0 ? void 0 : metadata.messageType,
|
|
128
|
+
region,
|
|
129
|
+
credentials
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Start polling a specific queue for messages
|
|
134
|
+
*/
|
|
135
|
+
startQueuePolling(queueUrl, region, credentials) {
|
|
136
|
+
// Create SQS client for this specific queue
|
|
137
|
+
const sqsClient = new client_sqs_1.SQSClient({
|
|
138
|
+
region,
|
|
139
|
+
credentials
|
|
140
|
+
});
|
|
141
|
+
const pollQueue = () => __awaiter(this, void 0, void 0, function* () {
|
|
142
|
+
try {
|
|
143
|
+
const command = new client_sqs_1.ReceiveMessageCommand({
|
|
144
|
+
QueueUrl: queueUrl,
|
|
145
|
+
MaxNumberOfMessages: this.MAX_MESSAGES,
|
|
146
|
+
WaitTimeSeconds: 20, // Long polling
|
|
147
|
+
VisibilityTimeout: 300 // 5 minutes to process the message
|
|
148
|
+
});
|
|
149
|
+
const response = yield sqsClient.send(command);
|
|
150
|
+
if (response.Messages && response.Messages.length > 0) {
|
|
151
|
+
console.log(`Received ${response.Messages.length} messages from queue: ${queueUrl}`);
|
|
152
|
+
for (const message of response.Messages) {
|
|
153
|
+
yield this.processMessage(queueUrl, message, sqsClient);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.error(`Error polling queue ${queueUrl}:`, error);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
// Start continuous polling
|
|
162
|
+
const interval = setInterval(pollQueue, this.POLLING_INTERVAL_MS);
|
|
163
|
+
this.pollingIntervals.set(queueUrl, interval);
|
|
164
|
+
// Start immediately
|
|
165
|
+
pollQueue();
|
|
166
|
+
console.log(`Started polling queue: ${queueUrl}`);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Process a message from the queue
|
|
170
|
+
*/
|
|
171
|
+
processMessage(queueUrl, message, sqsClient) {
|
|
172
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
173
|
+
try {
|
|
174
|
+
const mappings = this.queueMappings.get(queueUrl);
|
|
175
|
+
if (!mappings || mappings.length === 0) {
|
|
176
|
+
console.log(`No consumer mappings found for queue: ${queueUrl}`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Parse message body
|
|
180
|
+
let messageData;
|
|
181
|
+
try {
|
|
182
|
+
messageData = JSON.parse(message.Body || '{}');
|
|
183
|
+
}
|
|
184
|
+
catch (_a) {
|
|
185
|
+
console.warn(`Failed to parse message body as JSON for queue ${queueUrl}. Using raw body.`);
|
|
186
|
+
messageData = { body: message.Body };
|
|
187
|
+
}
|
|
188
|
+
let messageProcessedByAnyConsumer = false;
|
|
189
|
+
// Process message for each mapped consumer that matches the message criteria
|
|
190
|
+
for (const mapping of mappings) {
|
|
191
|
+
try {
|
|
192
|
+
// Check if message type matches (if specified)
|
|
193
|
+
if (mapping.messageType) {
|
|
194
|
+
const messageType = messageData.type || messageData.messageType || messageData.eventType;
|
|
195
|
+
if (messageType !== mapping.messageType) {
|
|
196
|
+
console.log(`Message type ${messageType} does not match expected ${mapping.messageType} for consumer ${mapping.consumer.name} - skipping`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
console.log(`Processing queue message for consumer "${mapping.consumer.name}" output ${mapping.outputIndex}`);
|
|
201
|
+
const user = UserManager_1.default.getUser();
|
|
202
|
+
// Execute the consumer with default options
|
|
203
|
+
const result = yield ConsumerEngine_1.default.execute(mapping.consumer, {}, user);
|
|
204
|
+
console.log(`Queue trigger completed successfully for consumer "${mapping.consumer.name}" output ${mapping.outputIndex}`);
|
|
205
|
+
// Log execution statistics
|
|
206
|
+
if (result && result._stats) {
|
|
207
|
+
console.log(`Queue trigger stats: ${result._stats.elapsedMS}ms, size: ${result._stats.size}, cycles: ${result._stats.cycles}`);
|
|
208
|
+
}
|
|
209
|
+
messageProcessedByAnyConsumer = true;
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
console.error(`Queue trigger failed for consumer "${mapping.consumer.name}" output ${mapping.outputIndex}:`, error);
|
|
213
|
+
// Continue processing for other consumers even if one fails
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Only delete message from queue if it was processed by at least one consumer
|
|
217
|
+
// This ensures messages intended for other consumers or systems remain in the queue
|
|
218
|
+
if (messageProcessedByAnyConsumer && message.ReceiptHandle) {
|
|
219
|
+
yield sqsClient.send(new client_sqs_1.DeleteMessageCommand({
|
|
220
|
+
QueueUrl: queueUrl,
|
|
221
|
+
ReceiptHandle: message.ReceiptHandle
|
|
222
|
+
}));
|
|
223
|
+
console.log(`Deleted processed message ${message.MessageId} from queue`);
|
|
224
|
+
}
|
|
225
|
+
else if (!messageProcessedByAnyConsumer) {
|
|
226
|
+
console.log(`Message ${message.MessageId} was not processed by any consumer - leaving in queue for other consumers or systems`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
console.error(`Error processing message from queue ${queueUrl}:`, error);
|
|
231
|
+
// Message will remain in queue and be retried or go to DLQ based on queue configuration
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Add or update queue listeners for a specific consumer
|
|
237
|
+
*/
|
|
238
|
+
updateConsumerSchedule(consumer) {
|
|
239
|
+
// First, remove any existing listeners for this consumer
|
|
240
|
+
this.removeConsumerSchedule(consumer.name);
|
|
241
|
+
// Then, add new listeners if they have QUEUE triggers
|
|
242
|
+
if (this.hasQueueTrigger(consumer)) {
|
|
243
|
+
this.setupQueueListeners(consumer);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Remove all queue listeners for a consumer
|
|
248
|
+
*/
|
|
249
|
+
removeConsumerSchedule(consumerName) {
|
|
250
|
+
// Remove mappings for this consumer
|
|
251
|
+
for (const [queueUrl, mappings] of this.queueMappings.entries()) {
|
|
252
|
+
const updatedMappings = mappings.filter(mapping => mapping.consumer.name !== consumerName);
|
|
253
|
+
if (updatedMappings.length === 0) {
|
|
254
|
+
// No more consumers listening to this queue, stop polling
|
|
255
|
+
const interval = this.pollingIntervals.get(queueUrl);
|
|
256
|
+
if (interval) {
|
|
257
|
+
clearInterval(interval);
|
|
258
|
+
this.pollingIntervals.delete(queueUrl);
|
|
259
|
+
console.log(`Stopped polling queue: ${queueUrl}`);
|
|
260
|
+
}
|
|
261
|
+
this.queueMappings.delete(queueUrl);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
this.queueMappings.set(queueUrl, updatedMappings);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Stop all queue polling
|
|
270
|
+
*/
|
|
271
|
+
stopAllQueues() {
|
|
272
|
+
console.log('Stopping all queue polling...');
|
|
273
|
+
this.pollingIntervals.forEach((interval, queueUrl) => {
|
|
274
|
+
clearInterval(interval);
|
|
275
|
+
console.log(`Stopped polling queue: ${queueUrl}`);
|
|
276
|
+
});
|
|
277
|
+
this.pollingIntervals.clear();
|
|
278
|
+
this.queueMappings.clear();
|
|
279
|
+
this.isInitialized = false;
|
|
280
|
+
console.log('All queue polling stopped');
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Restart the queue manager (useful for configuration reloads)
|
|
284
|
+
*/
|
|
285
|
+
restart() {
|
|
286
|
+
console.log('Restarting Queue Manager...');
|
|
287
|
+
this.stopAllQueues();
|
|
288
|
+
this.initialize();
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Get the queue manager status
|
|
292
|
+
*/
|
|
293
|
+
getStatus() {
|
|
294
|
+
const queues = Array.from(this.queueMappings.entries()).map(([queueUrl, mappings]) => ({
|
|
295
|
+
queueUrl,
|
|
296
|
+
consumerCount: mappings.length
|
|
297
|
+
}));
|
|
298
|
+
return {
|
|
299
|
+
initialized: this.isInitialized,
|
|
300
|
+
queueCount: this.queueMappings.size,
|
|
301
|
+
consumerCount: Array.from(this.queueMappings.values()).flat().length,
|
|
302
|
+
queues
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Export a singleton instance
|
|
307
|
+
exports.default = new QueueManager();
|
|
@@ -283,11 +283,11 @@ class TransformationEngineClass {
|
|
|
283
283
|
}
|
|
284
284
|
// Single transformation
|
|
285
285
|
if ('cast' in transformations) {
|
|
286
|
-
const { cast } = transformations;
|
|
286
|
+
const { cast, format } = transformations;
|
|
287
287
|
let oldDimension = dataset.getDimensions().find(x => x.name === field.key);
|
|
288
288
|
if (!oldDimension)
|
|
289
289
|
oldDimension = dataset.getDimensions().find(x => x.key === field.key);
|
|
290
|
-
const newDimension = Object.assign(Object.assign({}, structuredClone(oldDimension)), { type: cast });
|
|
290
|
+
const newDimension = Object.assign(Object.assign({}, structuredClone(oldDimension)), { type: cast, format: format });
|
|
291
291
|
dataset.setSingleDimension(newDimension, oldDimension);
|
|
292
292
|
}
|
|
293
293
|
return dataset;
|
|
@@ -28,11 +28,19 @@ class TypeCasterClass {
|
|
|
28
28
|
case 'datetime':
|
|
29
29
|
case 'date': {
|
|
30
30
|
let dateValue = null;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
try {
|
|
32
|
+
if (format && typeof value === 'string')
|
|
33
|
+
dateValue = dayjs_1.default.utc(value, format, true).toDate();
|
|
34
|
+
else
|
|
35
|
+
dateValue = new Date(value);
|
|
36
|
+
return dateValue.toISOString();
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
34
39
|
dateValue = new Date(value);
|
|
35
|
-
|
|
40
|
+
if (!isNaN(dateValue))
|
|
41
|
+
return dateValue.toISOString();
|
|
42
|
+
throw new Error(`Error casting "${value}" to date with format "${format}": ${error}`);
|
|
43
|
+
}
|
|
36
44
|
}
|
|
37
45
|
case 'number': {
|
|
38
46
|
if (typeof value === 'number')
|
|
@@ -106,5 +106,46 @@ class UsageDataManager {
|
|
|
106
106
|
};
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
|
+
getFilteredUsageDetails(from, to, filters) {
|
|
110
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
111
|
+
const now = DSTE_1.default.now();
|
|
112
|
+
const fromDate = from || new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
113
|
+
const toDate = to || now;
|
|
114
|
+
const collection = 'usage';
|
|
115
|
+
// Build match criteria
|
|
116
|
+
const matchCriteria = {
|
|
117
|
+
startedAt: { $gte: fromDate, $lte: toDate }
|
|
118
|
+
};
|
|
119
|
+
if (filters === null || filters === void 0 ? void 0 : filters.consumer) {
|
|
120
|
+
matchCriteria.consumer = filters.consumer;
|
|
121
|
+
}
|
|
122
|
+
if (filters === null || filters === void 0 ? void 0 : filters.user) {
|
|
123
|
+
matchCriteria['executedBy.name'] = filters.user;
|
|
124
|
+
}
|
|
125
|
+
if (filters === null || filters === void 0 ? void 0 : filters.status) {
|
|
126
|
+
matchCriteria.status = filters.status;
|
|
127
|
+
}
|
|
128
|
+
// Query with filters and sorting
|
|
129
|
+
const usageDetails = yield DatabaseEngine_1.default.query(collection, matchCriteria, {
|
|
130
|
+
sort: { startedAt: -1 },
|
|
131
|
+
limit: 1000 // Reasonable limit to prevent large result sets
|
|
132
|
+
});
|
|
133
|
+
return usageDetails;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
getUsageById(id) {
|
|
137
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
138
|
+
const collection = 'usage';
|
|
139
|
+
try {
|
|
140
|
+
const usageDetail = yield DatabaseEngine_1.default.get(collection, id);
|
|
141
|
+
return usageDetail;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
// If document not found or invalid ID, return null
|
|
145
|
+
console.error('Error fetching usage by ID:', error);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
109
150
|
}
|
|
110
151
|
exports.default = new UsageDataManager();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forzalabs/remora",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7-nasco.3",
|
|
4
4
|
"description": "A powerful CLI tool for seamless data translation.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"private": false,
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@aws-sdk/client-redshift-data": "^3.699.0",
|
|
37
37
|
"@aws-sdk/client-s3": "^3.701.0",
|
|
38
|
+
"@aws-sdk/client-sqs": "^3.886.0",
|
|
38
39
|
"adm-zip": "^0.5.16",
|
|
39
40
|
"ajv": "^8.17.1",
|
|
40
41
|
"ajv-formats": "^3.0.1",
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
"knex": "^2.4.2",
|
|
55
56
|
"mongodb": "^6.15.0",
|
|
56
57
|
"next": "^13.4.1",
|
|
58
|
+
"node-cron": "^4.2.1",
|
|
57
59
|
"ora": "^5.4.1",
|
|
58
60
|
"react": "^18.2.0",
|
|
59
61
|
"react-dom": "^18.2.0",
|