@friggframework/core 2.0.0-next.69 → 2.0.0-next.70

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.
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Scheduler Commands
3
+ *
4
+ * Application Layer - Command pattern for scheduling operations.
5
+ *
6
+ * Follows hexagonal architecture:
7
+ * - Receives SchedulerServiceInterface via dependency injection
8
+ * - Contains business logic (validation, logging, error mapping)
9
+ * - Protocol-agnostic (doesn't know about HTTP/Lambda)
10
+ *
11
+ * @example
12
+ * const schedulerCommands = createSchedulerCommands({ integrationName: 'zoho' });
13
+ * await schedulerCommands.scheduleJob({
14
+ * jobId: 'zoho-notif-renewal-abc123',
15
+ * scheduledAt: new Date(Date.now() + 6 * 24 * 60 * 60 * 1000), // 6 days
16
+ * event: 'REFRESH_WEBHOOK',
17
+ * payload: { integrationId: 'abc123' },
18
+ * queueUrl: process.env.ZOHO_QUEUE_URL,
19
+ * });
20
+ */
21
+
22
+ const { createSchedulerService } = require('../../infrastructure/scheduler');
23
+
24
+ /**
25
+ * Derive SQS ARN from SQS URL
26
+ *
27
+ * SQS URL format: https://sqs.{region}.amazonaws.com/{account-id}/{queue-name}
28
+ * SQS ARN format: arn:aws:sqs:{region}:{account-id}:{queue-name}
29
+ *
30
+ * @param {string} queueUrl - SQS queue URL
31
+ * @returns {string} SQS queue ARN
32
+ */
33
+ function deriveArnFromQueueUrl(queueUrl) {
34
+ try {
35
+ const url = new URL(queueUrl);
36
+ const region = url.hostname.split('.')[1];
37
+ const pathParts = url.pathname.split('/').filter(Boolean);
38
+ const accountId = pathParts[0];
39
+ const queueName = pathParts[1];
40
+ return `arn:aws:sqs:${region}:${accountId}:${queueName}`;
41
+ } catch (error) {
42
+ throw new Error(`Invalid SQS queue URL: ${queueUrl}`);
43
+ }
44
+ }
45
+
46
+ const ERROR_CODE_MAP = {
47
+ SCHEDULER_NOT_CONFIGURED: 503,
48
+ INVALID_JOB_DATA: 400,
49
+ SCHEDULE_NOT_FOUND: 404,
50
+ };
51
+
52
+ function mapErrorToResponse(error) {
53
+ const status = ERROR_CODE_MAP[error?.code] || 500;
54
+ return {
55
+ error: status,
56
+ reason: error?.message,
57
+ code: error?.code,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Create scheduler commands for an integration
63
+ *
64
+ * @param {Object} params
65
+ * @param {string} params.integrationName - Name of the integration (used for logging)
66
+ * @param {SchedulerServiceInterface} [params.schedulerService] - Optional injected scheduler service
67
+ * @returns {Object} Scheduler commands object
68
+ */
69
+ function createSchedulerCommands({ integrationName, schedulerService }) {
70
+ if (!integrationName) {
71
+ throw new Error('integrationName is required');
72
+ }
73
+
74
+ // Support both dependency injection and lazy creation
75
+ // DI is preferred for testability, lazy creation for convenience
76
+ let _schedulerService = schedulerService || null;
77
+
78
+ function getSchedulerService() {
79
+ if (!_schedulerService) {
80
+ try {
81
+ _schedulerService = createSchedulerService();
82
+ } catch (error) {
83
+ console.warn(
84
+ `[${integrationName}] Scheduler service not available: ${error.message}`
85
+ );
86
+ return null;
87
+ }
88
+ }
89
+ return _schedulerService;
90
+ }
91
+
92
+ return {
93
+ /**
94
+ * Schedule a one-time job to be executed at a specific time
95
+ *
96
+ * @param {Object} params
97
+ * @param {string} params.jobId - Unique identifier for the job
98
+ * @param {Date} params.scheduledAt - When to execute the job
99
+ * @param {string} params.event - Event name to trigger
100
+ * @param {Object} params.payload - Additional payload data
101
+ * @param {string} params.queueUrl - Target SQS queue URL (ARN is derived internally)
102
+ * @returns {Promise<{jobArn: string, scheduledAt: string} | {error: number, reason: string}>}
103
+ */
104
+ async scheduleJob({ jobId, scheduledAt, event, payload, queueUrl }) {
105
+ try {
106
+ if (!jobId) {
107
+ const error = new Error('jobId is required');
108
+ error.code = 'INVALID_JOB_DATA';
109
+ throw error;
110
+ }
111
+
112
+ if (!scheduledAt || !(scheduledAt instanceof Date)) {
113
+ const error = new Error('scheduledAt must be a valid Date');
114
+ error.code = 'INVALID_JOB_DATA';
115
+ throw error;
116
+ }
117
+
118
+ if (!event) {
119
+ const error = new Error('event is required');
120
+ error.code = 'INVALID_JOB_DATA';
121
+ throw error;
122
+ }
123
+
124
+ if (!queueUrl) {
125
+ const error = new Error('queueUrl is required');
126
+ error.code = 'INVALID_JOB_DATA';
127
+ throw error;
128
+ }
129
+
130
+ // Derive ARN from URL (business logic - transformation)
131
+ const queueArn = deriveArnFromQueueUrl(queueUrl);
132
+
133
+ // Get scheduler service (via DI or factory)
134
+ const service = getSchedulerService();
135
+ if (!service) {
136
+ console.warn(
137
+ `[${integrationName}] Scheduler not configured, skipping job schedule`
138
+ );
139
+ return {
140
+ jobId,
141
+ jobArn: null,
142
+ scheduledAt: null,
143
+ warning: 'Scheduler not configured',
144
+ };
145
+ }
146
+
147
+ // Build the SQS message payload (business logic - assembly)
148
+ const sqsPayload = {
149
+ event,
150
+ integrationName,
151
+ data: payload || {},
152
+ scheduledAt: scheduledAt.toISOString(),
153
+ createdAt: new Date().toISOString(),
154
+ };
155
+
156
+ // Delegate to service (Port interface)
157
+ const result = await service.scheduleOneTime({
158
+ scheduleName: jobId,
159
+ scheduleAt: scheduledAt,
160
+ queueResourceId: queueArn,
161
+ payload: sqsPayload,
162
+ });
163
+
164
+ console.log(
165
+ `[${integrationName}] Scheduled job ${jobId} for ${result.scheduledAt}`
166
+ );
167
+
168
+ return {
169
+ jobId,
170
+ jobArn: result.scheduledJobId,
171
+ scheduledAt: result.scheduledAt,
172
+ };
173
+ } catch (error) {
174
+ console.error(
175
+ `[${integrationName}] Failed to schedule job ${jobId}:`,
176
+ error.message
177
+ );
178
+ return mapErrorToResponse(error);
179
+ }
180
+ },
181
+
182
+ /**
183
+ * Delete a scheduled job
184
+ *
185
+ * @param {string} jobId - Job ID to delete
186
+ * @returns {Promise<{success: boolean, jobId: string} | {error: number, reason: string}>}
187
+ */
188
+ async deleteJob(jobId) {
189
+ try {
190
+ if (!jobId) {
191
+ const error = new Error('jobId is required');
192
+ error.code = 'INVALID_JOB_DATA';
193
+ throw error;
194
+ }
195
+
196
+ const service = getSchedulerService();
197
+ if (!service) {
198
+ console.warn(
199
+ `[${integrationName}] Scheduler not configured, skipping job deletion`
200
+ );
201
+ return {
202
+ success: true,
203
+ jobId,
204
+ warning: 'Scheduler not configured',
205
+ };
206
+ }
207
+
208
+ await service.deleteSchedule(jobId);
209
+
210
+ console.log(`[${integrationName}] Deleted scheduled job ${jobId}`);
211
+
212
+ return {
213
+ success: true,
214
+ jobId,
215
+ };
216
+ } catch (error) {
217
+ console.error(
218
+ `[${integrationName}] Failed to delete job ${jobId}:`,
219
+ error.message
220
+ );
221
+ return mapErrorToResponse(error);
222
+ }
223
+ },
224
+
225
+ /**
226
+ * Get the status of a scheduled job
227
+ *
228
+ * @param {string} jobId - Job ID to check
229
+ * @returns {Promise<{exists: boolean, scheduledAt?: string, state?: string} | {error: number, reason: string}>}
230
+ */
231
+ async getJobStatus(jobId) {
232
+ try {
233
+ if (!jobId) {
234
+ const error = new Error('jobId is required');
235
+ error.code = 'INVALID_JOB_DATA';
236
+ throw error;
237
+ }
238
+
239
+ const service = getSchedulerService();
240
+ if (!service) {
241
+ return {
242
+ exists: false,
243
+ warning: 'Scheduler not configured',
244
+ };
245
+ }
246
+
247
+ const status = await service.getScheduleStatus(jobId);
248
+
249
+ return status;
250
+ } catch (error) {
251
+ console.error(
252
+ `[${integrationName}] Failed to get job status ${jobId}:`,
253
+ error.message
254
+ );
255
+ return mapErrorToResponse(error);
256
+ }
257
+ },
258
+ };
259
+ }
260
+
261
+ module.exports = {
262
+ createSchedulerCommands,
263
+ };
@@ -7,6 +7,9 @@ const { createEntityCommands } = require('./commands/entity-commands');
7
7
  const {
8
8
  createCredentialCommands,
9
9
  } = require('./commands/credential-commands');
10
+ const {
11
+ createSchedulerCommands,
12
+ } = require('./commands/scheduler-commands');
10
13
 
11
14
  /**
12
15
  * Create a unified command factory with all CRUD operations
@@ -57,6 +60,7 @@ module.exports = {
57
60
  createUserCommands,
58
61
  createEntityCommands,
59
62
  createCredentialCommands,
63
+ createSchedulerCommands,
60
64
 
61
65
  // Legacy standalone function
62
66
  findIntegrationContextByExternalEntityId,
package/index.js CHANGED
@@ -166,6 +166,7 @@ module.exports = {
166
166
  createUserCommands: application.createUserCommands,
167
167
  createEntityCommands: application.createEntityCommands,
168
168
  createCredentialCommands: application.createCredentialCommands,
169
+ createSchedulerCommands: application.createSchedulerCommands,
169
170
  findIntegrationContextByExternalEntityId:
170
171
  application.findIntegrationContextByExternalEntityId,
171
172
  integrationCommands: application.integrationCommands,
@@ -0,0 +1,184 @@
1
+ /**
2
+ * EventBridge Scheduler Adapter
3
+ *
4
+ * Infrastructure Layer - Hexagonal Architecture
5
+ *
6
+ * Responsible for:
7
+ * - Creating one-time EventBridge Scheduler schedules
8
+ * - Deleting schedules when no longer needed
9
+ * - Checking schedule status
10
+ *
11
+ * This adapter implements SchedulerServiceInterface for AWS EventBridge Scheduler.
12
+ */
13
+
14
+ const {
15
+ SchedulerClient,
16
+ CreateScheduleCommand,
17
+ DeleteScheduleCommand,
18
+ GetScheduleCommand,
19
+ ResourceNotFoundException,
20
+ } = require('@aws-sdk/client-scheduler');
21
+
22
+ const { SchedulerServiceInterface } = require('./scheduler-service-interface');
23
+
24
+ class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
25
+ constructor({ region } = {}) {
26
+ super();
27
+ this.client = new SchedulerClient({
28
+ region: region || process.env.AWS_REGION || 'us-east-1',
29
+ });
30
+ this.scheduleGroupName =
31
+ process.env.SCHEDULE_GROUP_NAME || 'frigg-integration-schedules';
32
+ this.roleArn = process.env.SCHEDULER_ROLE_ARN;
33
+ }
34
+
35
+ /**
36
+ * Create a one-time schedule that sends a message to SQS
37
+ *
38
+ * @param {Object} params
39
+ * @param {string} params.scheduleName - Unique name for the schedule
40
+ * @param {Date} params.scheduleAt - When to trigger the schedule
41
+ * @param {string} params.queueResourceId - Queue resource identifier (ARN) to send message to
42
+ * @param {Object} params.payload - Message payload
43
+ * @returns {Promise<{scheduledJobId: string, scheduledAt: string}>}
44
+ */
45
+ async scheduleOneTime({ scheduleName, scheduleAt, queueResourceId, payload }) {
46
+ if (!scheduleName) {
47
+ throw new Error('scheduleName is required');
48
+ }
49
+ if (!scheduleAt || !(scheduleAt instanceof Date)) {
50
+ throw new Error('scheduleAt must be a valid Date object');
51
+ }
52
+ if (!queueResourceId) {
53
+ throw new Error('queueResourceId is required');
54
+ }
55
+ if (!this.roleArn) {
56
+ throw new Error(
57
+ 'SCHEDULER_ROLE_ARN environment variable is not set'
58
+ );
59
+ }
60
+
61
+ // Format date to AWS schedule expression (at(yyyy-mm-ddThh:mm:ss))
62
+ const scheduleExpression = `at(${scheduleAt.toISOString().replace(/\.\d{3}Z$/, '')})`;
63
+
64
+ const command = new CreateScheduleCommand({
65
+ Name: scheduleName,
66
+ GroupName: this.scheduleGroupName,
67
+ ScheduleExpression: scheduleExpression,
68
+ ScheduleExpressionTimezone: 'UTC',
69
+ FlexibleTimeWindow: {
70
+ Mode: 'OFF',
71
+ },
72
+ Target: {
73
+ Arn: queueResourceId,
74
+ RoleArn: this.roleArn,
75
+ Input: JSON.stringify(payload),
76
+ },
77
+ ActionAfterCompletion: 'DELETE', // Auto-cleanup after execution
78
+ });
79
+
80
+ try {
81
+ const response = await this.client.send(command);
82
+ console.log(
83
+ `[Scheduler] Created schedule ${scheduleName} for ${scheduleAt.toISOString()}`
84
+ );
85
+
86
+ return {
87
+ scheduledJobId: response.ScheduleArn,
88
+ scheduledAt: scheduleAt.toISOString(),
89
+ };
90
+ } catch (error) {
91
+ console.error(
92
+ `[Scheduler] Failed to create schedule ${scheduleName}:`,
93
+ error.message
94
+ );
95
+ throw error;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Delete a schedule
101
+ *
102
+ * @param {string} scheduleName - Name of the schedule to delete
103
+ * @returns {Promise<void>}
104
+ */
105
+ async deleteSchedule(scheduleName) {
106
+ if (!scheduleName) {
107
+ throw new Error('scheduleName is required');
108
+ }
109
+
110
+ const command = new DeleteScheduleCommand({
111
+ Name: scheduleName,
112
+ GroupName: this.scheduleGroupName,
113
+ });
114
+
115
+ try {
116
+ await this.client.send(command);
117
+ console.log(`[Scheduler] Deleted schedule ${scheduleName}`);
118
+ } catch (error) {
119
+ if (error instanceof ResourceNotFoundException) {
120
+ console.log(
121
+ `[Scheduler] Schedule ${scheduleName} not found (already deleted or executed)`
122
+ );
123
+ return; // Graceful handling - schedule doesn't exist
124
+ }
125
+ console.error(
126
+ `[Scheduler] Failed to delete schedule ${scheduleName}:`,
127
+ error.message
128
+ );
129
+ throw error;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Get schedule status
135
+ *
136
+ * @param {string} scheduleName - Name of the schedule
137
+ * @returns {Promise<{exists: boolean, scheduledAt?: string, state?: string}>}
138
+ */
139
+ async getScheduleStatus(scheduleName) {
140
+ if (!scheduleName) {
141
+ throw new Error('scheduleName is required');
142
+ }
143
+
144
+ const command = new GetScheduleCommand({
145
+ Name: scheduleName,
146
+ GroupName: this.scheduleGroupName,
147
+ });
148
+
149
+ try {
150
+ const response = await this.client.send(command);
151
+
152
+ // Parse the schedule expression to get the scheduled time
153
+ // Format: at(yyyy-mm-ddThh:mm:ss)
154
+ let scheduledAt = null;
155
+ if (response.ScheduleExpression) {
156
+ const match = response.ScheduleExpression.match(
157
+ /^at\((.+)\)$/
158
+ );
159
+ if (match) {
160
+ scheduledAt = new Date(match[1] + 'Z').toISOString();
161
+ }
162
+ }
163
+
164
+ return {
165
+ exists: true,
166
+ scheduledAt,
167
+ state: response.State,
168
+ };
169
+ } catch (error) {
170
+ if (error instanceof ResourceNotFoundException) {
171
+ return {
172
+ exists: false,
173
+ };
174
+ }
175
+ console.error(
176
+ `[Scheduler] Failed to get schedule ${scheduleName}:`,
177
+ error.message
178
+ );
179
+ throw error;
180
+ }
181
+ }
182
+ }
183
+
184
+ module.exports = { EventBridgeSchedulerAdapter };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Scheduler Infrastructure
3
+ *
4
+ * Provides scheduling capabilities for one-time jobs.
5
+ * Follows hexagonal architecture with interface + adapters pattern.
6
+ *
7
+ * Providers:
8
+ * - eventbridge: AWS EventBridge Scheduler (production)
9
+ * - mock: In-memory mock scheduler (local development)
10
+ */
11
+
12
+ const { SchedulerServiceInterface } = require('./scheduler-service-interface');
13
+ const { EventBridgeSchedulerAdapter } = require('./eventbridge-scheduler-adapter');
14
+ const { MockSchedulerAdapter } = require('./mock-scheduler-adapter');
15
+ const {
16
+ createSchedulerService,
17
+ SCHEDULER_PROVIDERS,
18
+ determineProvider,
19
+ } = require('./scheduler-service-factory');
20
+
21
+ module.exports = {
22
+ // Interface (Port)
23
+ SchedulerServiceInterface,
24
+
25
+ // Adapters
26
+ EventBridgeSchedulerAdapter,
27
+ MockSchedulerAdapter,
28
+
29
+ // Factory
30
+ createSchedulerService,
31
+ SCHEDULER_PROVIDERS,
32
+ determineProvider,
33
+ };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Mock Scheduler Adapter for Local Development
3
+ *
4
+ * Stores schedules in memory and logs instead of creating real EventBridge schedules.
5
+ * Used when SCHEDULER_PROVIDER=mock or in local/dev/test environments.
6
+ *
7
+ * This adapter implements SchedulerServiceInterface for local development and testing.
8
+ */
9
+
10
+ const { SchedulerServiceInterface } = require('./scheduler-service-interface');
11
+
12
+ class MockSchedulerAdapter extends SchedulerServiceInterface {
13
+ constructor(options = {}) {
14
+ super();
15
+ this.verbose = options.verbose || false;
16
+ this.schedules = new Map();
17
+ }
18
+
19
+ /**
20
+ * Schedule a one-time job to be executed at a specific time
21
+ *
22
+ * @param {Object} params
23
+ * @param {string} params.scheduleName - Unique name for the schedule
24
+ * @param {Date} params.scheduleAt - When to trigger the schedule
25
+ * @param {string} params.queueResourceId - Queue resource identifier to send message to
26
+ * @param {Object} params.payload - JSON payload to send
27
+ * @returns {Promise<{scheduledJobId: string, scheduledAt: string}>}
28
+ */
29
+ async scheduleOneTime({ scheduleName, scheduleAt, queueResourceId, payload }) {
30
+ if (!scheduleName) {
31
+ throw new Error('scheduleName is required');
32
+ }
33
+ if (!scheduleAt || !(scheduleAt instanceof Date)) {
34
+ throw new Error('scheduleAt must be a valid Date object');
35
+ }
36
+ if (!queueResourceId) {
37
+ throw new Error('queueResourceId is required');
38
+ }
39
+
40
+ const scheduleData = {
41
+ scheduleName,
42
+ scheduledAt: scheduleAt.toISOString(),
43
+ queueResourceId,
44
+ payload,
45
+ createdAt: new Date().toISOString(),
46
+ state: 'ENABLED',
47
+ };
48
+
49
+ this.schedules.set(scheduleName, scheduleData);
50
+
51
+ console.log(`[MockScheduler] Created schedule: ${scheduleName}`);
52
+ console.log(`[MockScheduler] Scheduled for: ${scheduleAt.toISOString()}`);
53
+ console.log(`[MockScheduler] Target: ${queueResourceId}`);
54
+ if (this.verbose) {
55
+ console.log(`[MockScheduler] Payload:`, JSON.stringify(payload, null, 2));
56
+ }
57
+
58
+ return {
59
+ scheduledJobId: `mock-job-${scheduleName}`,
60
+ scheduledAt: scheduleAt.toISOString(),
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Delete a scheduled job
66
+ *
67
+ * @param {string} scheduleName - Name of the schedule to delete
68
+ * @returns {Promise<void>}
69
+ */
70
+ async deleteSchedule(scheduleName) {
71
+ if (!scheduleName) {
72
+ throw new Error('scheduleName is required');
73
+ }
74
+
75
+ const existed = this.schedules.has(scheduleName);
76
+ this.schedules.delete(scheduleName);
77
+
78
+ console.log(`[MockScheduler] Deleted schedule: ${scheduleName} (existed: ${existed})`);
79
+ }
80
+
81
+ /**
82
+ * Get the status of a scheduled job
83
+ *
84
+ * @param {string} scheduleName - Name of the schedule
85
+ * @returns {Promise<{exists: boolean, scheduledAt?: string, state?: string}>}
86
+ */
87
+ async getScheduleStatus(scheduleName) {
88
+ if (!scheduleName) {
89
+ throw new Error('scheduleName is required');
90
+ }
91
+
92
+ const schedule = this.schedules.get(scheduleName);
93
+
94
+ if (!schedule) {
95
+ return { exists: false };
96
+ }
97
+
98
+ return {
99
+ exists: true,
100
+ scheduledAt: schedule.scheduledAt,
101
+ state: schedule.state,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Get all scheduled jobs (helper for testing)
107
+ *
108
+ * @returns {Object} Map of all schedules as plain object
109
+ */
110
+ _getSchedules() {
111
+ return Object.fromEntries(this.schedules);
112
+ }
113
+
114
+ /**
115
+ * Clear all schedules (helper for testing)
116
+ */
117
+ _clearSchedules() {
118
+ const count = this.schedules.size;
119
+ this.schedules.clear();
120
+ console.log(`[MockScheduler] Cleared ${count} schedules`);
121
+ }
122
+
123
+ /**
124
+ * Simulate triggering a schedule (helper for testing)
125
+ *
126
+ * @param {string} scheduleName - Name of the schedule to trigger
127
+ * @returns {Object|null} The payload that would be sent, or null if not found
128
+ */
129
+ _simulateTrigger(scheduleName) {
130
+ const schedule = this.schedules.get(scheduleName);
131
+ if (!schedule) {
132
+ console.log(`[MockScheduler] Cannot trigger - schedule not found: ${scheduleName}`);
133
+ return null;
134
+ }
135
+
136
+ console.log(`[MockScheduler] Simulating trigger for: ${scheduleName}`);
137
+ console.log(`[MockScheduler] Payload:`, JSON.stringify(schedule.payload, null, 2));
138
+
139
+ return schedule.payload;
140
+ }
141
+ }
142
+
143
+ module.exports = { MockSchedulerAdapter };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Scheduler Service Factory
3
+ *
4
+ * Creates scheduler service instances based on configuration.
5
+ * Returns implementations of SchedulerServiceInterface.
6
+ *
7
+ * Environment Detection:
8
+ * - SCHEDULER_PROVIDER=eventbridge -> Use AWS EventBridge Scheduler
9
+ * - SCHEDULER_PROVIDER=mock -> Use in-memory mock scheduler
10
+ * - Default in dev/test/local stages -> Mock scheduler
11
+ * - Default in other stages -> EventBridge scheduler
12
+ */
13
+
14
+ const { EventBridgeSchedulerAdapter } = require('./eventbridge-scheduler-adapter');
15
+ const { MockSchedulerAdapter } = require('./mock-scheduler-adapter');
16
+
17
+ const SCHEDULER_PROVIDERS = {
18
+ EVENTBRIDGE: 'eventbridge',
19
+ MOCK: 'mock',
20
+ };
21
+
22
+ const LOCAL_STAGES = ['dev', 'test', 'local'];
23
+
24
+ /**
25
+ * Determine the scheduler provider based on environment
26
+ *
27
+ * @returns {string} Provider name
28
+ */
29
+ function determineProvider() {
30
+ const explicitProvider = process.env.SCHEDULER_PROVIDER;
31
+ if (explicitProvider) {
32
+ return explicitProvider;
33
+ }
34
+
35
+ const stage = process.env.STAGE || 'dev';
36
+ if (LOCAL_STAGES.includes(stage)) {
37
+ return SCHEDULER_PROVIDERS.MOCK;
38
+ }
39
+
40
+ return SCHEDULER_PROVIDERS.EVENTBRIDGE;
41
+ }
42
+
43
+ /**
44
+ * Create a scheduler service instance
45
+ *
46
+ * @param {Object} options
47
+ * @param {string} options.provider - Scheduler provider ('eventbridge' or 'mock')
48
+ * @param {string} options.region - AWS region (for EventBridge)
49
+ * @param {boolean} options.verbose - Verbose logging (for Mock)
50
+ * @returns {SchedulerServiceInterface} Implementation of scheduler interface
51
+ */
52
+ function createSchedulerService(options = {}) {
53
+ const provider = options.provider || determineProvider();
54
+
55
+ switch (provider) {
56
+ case SCHEDULER_PROVIDERS.EVENTBRIDGE:
57
+ return new EventBridgeSchedulerAdapter({
58
+ region: options.region,
59
+ });
60
+ case SCHEDULER_PROVIDERS.MOCK:
61
+ return new MockSchedulerAdapter({
62
+ verbose: options.verbose,
63
+ });
64
+ default:
65
+ throw new Error(`Unknown scheduler provider: ${provider}`);
66
+ }
67
+ }
68
+
69
+ module.exports = {
70
+ createSchedulerService,
71
+ SCHEDULER_PROVIDERS,
72
+ determineProvider,
73
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Scheduler Service Interface (Port)
3
+ *
4
+ * Defines the contract for scheduling one-time jobs.
5
+ * All scheduler adapters must extend this interface.
6
+ *
7
+ * Following Frigg's hexagonal architecture pattern:
8
+ * - Port defines WHAT the service does (contract)
9
+ * - Adapters implement HOW (AWS EventBridge, Mock, etc.)
10
+ */
11
+ class SchedulerServiceInterface {
12
+ /**
13
+ * Schedule a one-time job to be executed at a specific time
14
+ *
15
+ * @param {Object} params
16
+ * @param {string} params.scheduleName - Unique name for the schedule
17
+ * @param {Date} params.scheduleAt - When to trigger the schedule
18
+ * @param {string} params.queueResourceId - Queue resource identifier to send message to
19
+ * @param {Object} params.payload - JSON payload to send
20
+ * @returns {Promise<{scheduledJobId: string, scheduledAt: string}>}
21
+ */
22
+ async scheduleOneTime({ scheduleName, scheduleAt, queueResourceId, payload }) {
23
+ throw new Error('Method scheduleOneTime must be implemented by subclass');
24
+ }
25
+
26
+ /**
27
+ * Delete a scheduled job
28
+ *
29
+ * @param {string} scheduleName - Name of the schedule to delete
30
+ * @returns {Promise<void>}
31
+ */
32
+ async deleteSchedule(scheduleName) {
33
+ throw new Error('Method deleteSchedule must be implemented by subclass');
34
+ }
35
+
36
+ /**
37
+ * Get the status of a scheduled job
38
+ *
39
+ * @param {string} scheduleName - Name of the schedule
40
+ * @returns {Promise<{exists: boolean, scheduledAt?: string, state?: string}>}
41
+ */
42
+ async getScheduleStatus(scheduleName) {
43
+ throw new Error('Method getScheduleStatus must be implemented by subclass');
44
+ }
45
+ }
46
+
47
+ module.exports = { SchedulerServiceInterface };
@@ -115,7 +115,10 @@ class OAuth2Requester extends Requester {
115
115
  */
116
116
  async setTokens(params) {
117
117
  this.access_token = get(params, 'access_token');
118
- this.refresh_token = get(params, 'refresh_token', null);
118
+ const newRefreshToken = get(params, 'refresh_token', null);
119
+ if (newRefreshToken !== null) {
120
+ this.refresh_token = newRefreshToken;
121
+ }
119
122
  const accessExpiresIn = get(params, 'expires_in', null);
120
123
  const refreshExpiresIn = get(
121
124
  params,
@@ -124,7 +127,9 @@ class OAuth2Requester extends Requester {
124
127
  );
125
128
 
126
129
  this.accessTokenExpire = new Date(Date.now() + accessExpiresIn * 1000);
127
- this.refreshTokenExpire = new Date(Date.now() + refreshExpiresIn * 1000);
130
+ if (refreshExpiresIn !== null) {
131
+ this.refreshTokenExpire = new Date(Date.now() + refreshExpiresIn * 1000);
132
+ }
128
133
 
129
134
  await this.notify(this.DLGT_TOKEN_UPDATE);
130
135
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0-next.69",
4
+ "version": "2.0.0-next.70",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -38,9 +38,9 @@
38
38
  }
39
39
  },
40
40
  "devDependencies": {
41
- "@friggframework/eslint-config": "2.0.0-next.69",
42
- "@friggframework/prettier-config": "2.0.0-next.69",
43
- "@friggframework/test": "2.0.0-next.69",
41
+ "@friggframework/eslint-config": "2.0.0-next.70",
42
+ "@friggframework/prettier-config": "2.0.0-next.70",
43
+ "@friggframework/test": "2.0.0-next.70",
44
44
  "@prisma/client": "^6.17.0",
45
45
  "@types/lodash": "4.17.15",
46
46
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -80,5 +80,5 @@
80
80
  "publishConfig": {
81
81
  "access": "public"
82
82
  },
83
- "gitHead": "018c93f98b5f14786d016a4f621adef10ad27597"
83
+ "gitHead": "dee1112300e01813e68b2598c3a722d1a31e1677"
84
84
  }