diff --git a/.changeset/ready-cities-wonder.md b/.changeset/ready-cities-wonder.md new file mode 100644 index 00000000000..44478c0f6d7 --- /dev/null +++ b/.changeset/ready-cities-wonder.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-function': minor +--- + +feat: add timezone support to scheduling Lambda functions diff --git a/packages/backend-function/API.md b/packages/backend-function/API.md index ee433c60f33..140295de1ed 100644 --- a/packages/backend-function/API.md +++ b/packages/backend-function/API.md @@ -34,7 +34,10 @@ export type AddEnvironmentFactory = { }; // @public (undocumented) -export type CronSchedule = `${string} ${string} ${string} ${string} ${string}` | `${string} ${string} ${string} ${string} ${string} ${string}`; +export type CronSchedule = CronScheduleExpression | ZonedCronSchedule; + +// @public (undocumented) +export type CronScheduleExpression = `${string} ${string} ${string} ${string} ${string}` | `${string} ${string} ${string} ${string} ${string} ${string}`; // @public (undocumented) type DataClientConfig = { @@ -141,7 +144,22 @@ type ResourceConfig = { }; // @public (undocumented) -export type TimeInterval = `every ${number}m` | `every ${number}h` | `every day` | `every week` | `every month` | `every year`; +export type TimeInterval = ZonedTimeInterval | TimeIntervalExpression; + +// @public (undocumented) +export type TimeIntervalExpression = `every ${number}m` | `every ${number}h` | `every day` | `every week` | `every month` | `every year`; + +// @public (undocumented) +export type ZonedCronSchedule = { + cron: CronScheduleExpression; + timezone: string; +}; + +// @public (undocumented) +export type ZonedTimeInterval = { + rate: TimeIntervalExpression; + timezone: string; +}; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-function/src/factory.test.ts b/packages/backend-function/src/factory.test.ts index f3dbed08c9b..37bb032788c 100644 --- a/packages/backend-function/src/factory.test.ts +++ b/packages/backend-function/src/factory.test.ts @@ -495,17 +495,34 @@ void describe('AmplifyFunctionFactory', () => { }).getInstance(getInstanceProps); const template = Template.fromStack(lambda.stack); - template.hasResourceProperties('AWS::Events::Rule', { + template.hasResourceProperties('AWS::Scheduler::Schedule', { ScheduleExpression: 'cron(*/5 * * * ? *)', - Targets: [ - { - Arn: { - // eslint-disable-next-line spellcheck/spell-checker - 'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'], - }, - Id: 'Target0', + ScheduleExpressionTimezone: 'UTC', + Target: { + Arn: { + // eslint-disable-next-line spellcheck/spell-checker + 'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'], }, - ], + }, + }); + }); + + void it('sets valid schedule - rate with timezone', () => { + const lambda = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + schedule: { rate: 'every 5m', timezone: 'America/New_York' }, + }).getInstance(getInstanceProps); + const template = Template.fromStack(lambda.stack); + + template.hasResourceProperties('AWS::Scheduler::Schedule', { + ScheduleExpression: 'cron(*/5 * * * ? *)', + ScheduleExpressionTimezone: 'America/New_York', + Target: { + Arn: { + // eslint-disable-next-line spellcheck/spell-checker + 'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'], + }, + }, }); }); @@ -516,17 +533,34 @@ void describe('AmplifyFunctionFactory', () => { }).getInstance(getInstanceProps); const template = Template.fromStack(lambda.stack); - template.hasResourceProperties('AWS::Events::Rule', { + template.hasResourceProperties('AWS::Scheduler::Schedule', { ScheduleExpression: 'cron(0 1 * * ? *)', - Targets: [ - { - Arn: { - // eslint-disable-next-line spellcheck/spell-checker - 'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'], - }, - Id: 'Target0', + ScheduleExpressionTimezone: 'UTC', + Target: { + Arn: { + // eslint-disable-next-line spellcheck/spell-checker + 'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'], }, - ], + }, + }); + }); + + void it('sets valid schedule - cron with timezone', () => { + const lambda = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + schedule: { cron: '0 1 * * ?', timezone: 'America/New_York' }, + }).getInstance(getInstanceProps); + const template = Template.fromStack(lambda.stack); + + template.hasResourceProperties('AWS::Scheduler::Schedule', { + ScheduleExpression: 'cron(0 1 * * ? *)', + ScheduleExpressionTimezone: 'America/New_York', + Target: { + Arn: { + // eslint-disable-next-line spellcheck/spell-checker + 'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'], + }, + }, }); }); @@ -537,14 +571,16 @@ void describe('AmplifyFunctionFactory', () => { }).getInstance(getInstanceProps); const template = Template.fromStack(lambda.stack); - template.resourceCountIs('AWS::Events::Rule', 2); + template.resourceCountIs('AWS::Scheduler::Schedule', 2); - template.hasResourceProperties('AWS::Events::Rule', { + template.hasResourceProperties('AWS::Scheduler::Schedule', { ScheduleExpression: 'cron(0 1 * * ? *)', + ScheduleExpressionTimezone: 'UTC', }); - template.hasResourceProperties('AWS::Events::Rule', { + template.hasResourceProperties('AWS::Scheduler::Schedule', { ScheduleExpression: 'cron(*/5 * * * ? *)', + ScheduleExpressionTimezone: 'UTC', }); }); @@ -554,7 +590,7 @@ void describe('AmplifyFunctionFactory', () => { }).getInstance(getInstanceProps); const template = Template.fromStack(lambda.stack); - template.resourceCountIs('AWS::Events::Rule', 0); + template.resourceCountIs('AWS::Scheduler::Schedule', 0); }); }); diff --git a/packages/backend-function/src/factory.ts b/packages/backend-function/src/factory.ts index b24d42913ea..5d16f70e052 100644 --- a/packages/backend-function/src/factory.ts +++ b/packages/backend-function/src/factory.ts @@ -23,8 +23,8 @@ import { StackProvider, } from '@aws-amplify/plugin-types'; import { Duration, Size, Stack, Tags } from 'aws-cdk-lib'; -import { Rule } from 'aws-cdk-lib/aws-events'; -import * as targets from 'aws-cdk-lib/aws-events-targets'; +import * as scheduler from 'aws-cdk-lib/aws-scheduler'; +import * as targets from 'aws-cdk-lib/aws-scheduler-targets'; import { Architecture, CfnFunction, @@ -47,7 +47,7 @@ import { FunctionEnvironmentTranslator } from './function_env_translator.js'; import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js'; import { FunctionLayerArnParser } from './layer_parser.js'; import { convertLoggingOptionsToCDK } from './logging_options_parser.js'; -import { convertFunctionSchedulesToRuleSchedules } from './schedule_parser.js'; +import { convertFunctionSchedulesToScheduleExpressions } from './schedule_parser.js'; import { ProvidedFunctionFactory, ProvidedFunctionProps, @@ -59,16 +59,32 @@ export type AddEnvironmentFactory = { addEnvironment: (key: string, value: string | BackendSecret) => void; }; -export type CronSchedule = +export type CronScheduleExpression = | `${string} ${string} ${string} ${string} ${string}` | `${string} ${string} ${string} ${string} ${string} ${string}`; -export type TimeInterval = + +export type ZonedCronSchedule = { + cron: CronScheduleExpression; + timezone: string; +}; + +export type CronSchedule = CronScheduleExpression | ZonedCronSchedule; + +export type TimeIntervalExpression = | `every ${number}m` | `every ${number}h` | `every day` | `every week` | `every month` | `every year`; + +export type ZonedTimeInterval = { + rate: TimeIntervalExpression; + timezone: string; +}; + +export type TimeInterval = ZonedTimeInterval | TimeIntervalExpression; + export type FunctionSchedule = TimeInterval | CronSchedule; export type FunctionLogLevel = Extract< @@ -626,19 +642,19 @@ class AmplifyFunction } try { - const schedules = convertFunctionSchedulesToRuleSchedules( + const expressions = convertFunctionSchedulesToScheduleExpressions( functionLambda, props.schedule, ); - const lambdaTarget = new targets.LambdaFunction(functionLambda); - schedules.forEach((schedule, index) => { - // Lambda name will be prepended to rule id, so only using index here for uniqueness - const rule = new Rule(functionLambda, `schedule${index}`, { - schedule, - }); + const lambdaTarget = new targets.LambdaInvoke(functionLambda); - rule.addTarget(lambdaTarget); + expressions.forEach((expression, index) => { + // Lambda name will be prepended to schedule id, so only using index here for uniqueness + new scheduler.Schedule(functionLambda, `schedule${index}`, { + schedule: expression, + target: lambdaTarget, + }); }); } catch (error) { throw new AmplifyUserError( diff --git a/packages/backend-function/src/schedule_parser.test.ts b/packages/backend-function/src/schedule_parser.test.ts index d383d101ca4..39bdc30d22c 100644 --- a/packages/backend-function/src/schedule_parser.test.ts +++ b/packages/backend-function/src/schedule_parser.test.ts @@ -1,145 +1,215 @@ import { describe, it } from 'node:test'; import { App, Duration, Stack } from 'aws-cdk-lib'; import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda'; -import { convertFunctionSchedulesToRuleSchedules } from './schedule_parser.js'; +import { convertFunctionSchedulesToScheduleExpressions } from './schedule_parser.js'; import { FunctionSchedule } from './factory.js'; import assert from 'node:assert'; import os from 'os'; void describe('ScheduleParser', () => { - void it('creates EventBridge rule for natural language', () => { + void it('creates EventBridge Schedule expression for natural language', () => { const schedule: FunctionSchedule[] = ['every day']; const expectedScheduleExpression = 'cron(0 0 * * ? *)'; // every day at 00:00 const testLambda = getTestLambda(); - const schedules = convertFunctionSchedulesToRuleSchedules( + const schedules = convertFunctionSchedulesToScheduleExpressions( testLambda, schedule, ); assert.equal(schedules.length, 1); assert.equal(schedules[0].expressionString, expectedScheduleExpression); + assert.equal(schedules[0].timeZone?.timezoneName, 'UTC'); }); - void it('creates EventBridge rule for natural language with number value', () => { + void it('creates EventBridge Schedule expression for natural language with timezone', () => { + const schedule: FunctionSchedule[] = [ + { rate: 'every day', timezone: 'America/New_York' }, + ]; + const expectedScheduleExpression = 'cron(0 0 * * ? *)'; // every day at 00:00 + const testLambda = getTestLambda(); + + const schedules = convertFunctionSchedulesToScheduleExpressions( + testLambda, + schedule, + ); + + assert.equal(schedules.length, 1); + assert.equal(schedules[0].expressionString, expectedScheduleExpression); + assert.equal(schedules[0].timeZone?.timezoneName, 'America/New_York'); + }); + + void it('creates EventBridge Schedule expression for natural language with number value', () => { const schedule: FunctionSchedule[] = ['every 5m']; const expectedScheduleExpression = 'cron(*/5 * * * ? *)'; const testLambda = getTestLambda(); - const schedules = convertFunctionSchedulesToRuleSchedules( + const schedules = convertFunctionSchedulesToScheduleExpressions( testLambda, schedule, ); assert.equal(schedules.length, 1); assert.equal(schedules[0].expressionString, expectedScheduleExpression); + assert.equal(schedules[0].timeZone?.timezoneName, 'UTC'); }); - void it('creates EventBridge rule for cron expression', () => { + void it('creates EventBridge Schedule expression for cron expression', () => { const schedule: FunctionSchedule[] = ['* * * * ?']; const expectedScheduleExpression = 'cron(* * * * ? *)'; const testLambda = getTestLambda(); - const schedules = convertFunctionSchedulesToRuleSchedules( + const schedules = convertFunctionSchedulesToScheduleExpressions( testLambda, schedule, ); assert.equal(schedules.length, 1); assert.equal(schedules[0].expressionString, expectedScheduleExpression); + assert.equal(schedules[0].timeZone?.timezoneName, 'UTC'); }); - void it('creates EventBridge rule for cron expression with /', () => { + void it('creates EventBridge Schedule expression for cron expression with timezone', () => { + const schedule: FunctionSchedule[] = [ + { cron: '* * * * ?', timezone: 'America/New_York' }, + ]; + const expectedScheduleExpression = 'cron(* * * * ? *)'; + const testLambda = getTestLambda(); + + const schedules = convertFunctionSchedulesToScheduleExpressions( + testLambda, + schedule, + ); + + assert.equal(schedules.length, 1); + assert.equal(schedules[0].expressionString, expectedScheduleExpression); + assert.equal(schedules[0].timeZone?.timezoneName, 'America/New_York'); + }); + + void it('creates EventBridge Schedule expression for cron expression with /', () => { const schedule: FunctionSchedule[] = ['*/5 * * * ?']; const expectedScheduleExpression = 'cron(*/5 * * * ? *)'; const testLambda = getTestLambda(); - const schedules = convertFunctionSchedulesToRuleSchedules( + const schedules = convertFunctionSchedulesToScheduleExpressions( testLambda, schedule, ); assert.equal(schedules.length, 1); assert.equal(schedules[0].expressionString, expectedScheduleExpression); + assert.equal(schedules[0].timeZone?.timezoneName, 'UTC'); }); - void it('creates EventBridge rule for cron expression with ,', () => { + void it('creates EventBridge Schedule expression for cron expression with ,', () => { const schedule: FunctionSchedule[] = ['0 1,2 * * ?']; const expectedScheduleExpression = 'cron(0 1,2 * * ? *)'; const testLambda = getTestLambda(); - const schedules = convertFunctionSchedulesToRuleSchedules( + const schedules = convertFunctionSchedulesToScheduleExpressions( testLambda, schedule, ); assert.equal(schedules.length, 1); assert.equal(schedules[0].expressionString, expectedScheduleExpression); + assert.equal(schedules[0].timeZone?.timezoneName, 'UTC'); }); - void it('creates EventBridge rule for cron expression with -', () => { + void it('creates EventBridge Schedule expression for cron expression with -', () => { const schedule: FunctionSchedule[] = ['0 1-5 * * ?']; const expectedScheduleExpression = 'cron(0 1-5 * * ? *)'; const testLambda = getTestLambda(); - const schedules = convertFunctionSchedulesToRuleSchedules( + const schedules = convertFunctionSchedulesToScheduleExpressions( testLambda, schedule, ); assert.equal(schedules.length, 1); assert.equal(schedules[0].expressionString, expectedScheduleExpression); + assert.equal(schedules[0].timeZone?.timezoneName, 'UTC'); }); - void it('creates EventBridge rules for an array of natural language', () => { + void it('creates EventBridge Schedule expressions for an array of natural language', () => { const functionSchedules: FunctionSchedule[] = ['every 2h', 'every month']; const expectedExpressionTwoHours = 'cron(0 */2 * * ? *)'; const expectedExpressionEveryMonth = 'cron(0 0 1 * ? *)'; const testLambda = getTestLambda(); - const schedules = convertFunctionSchedulesToRuleSchedules( + const schedules = convertFunctionSchedulesToScheduleExpressions( testLambda, functionSchedules, ); assert.equal(schedules.length, 2); assert.equal(schedules[0].expressionString, expectedExpressionTwoHours); + assert.equal(schedules[0].timeZone?.timezoneName, 'UTC'); assert.equal(schedules[1].expressionString, expectedExpressionEveryMonth); + assert.equal(schedules[1].timeZone?.timezoneName, 'UTC'); }); - void it('creates EventBridge rules for an array of cron expressions', () => { + void it('creates EventBridge Schedule expressions for an array of cron expressions', () => { const functionSchedules: FunctionSchedule[] = ['* * * * ?', '0 0 * * ?']; const expectedExpressionEveryMinute = 'cron(* * * * ? *)'; const expectedExpressionEveryMidnight = 'cron(0 0 * * ? *)'; const testLambda = getTestLambda(); - const schedules = convertFunctionSchedulesToRuleSchedules( + const schedules = convertFunctionSchedulesToScheduleExpressions( testLambda, functionSchedules, ); assert.equal(schedules.length, 2); assert.equal(schedules[0].expressionString, expectedExpressionEveryMinute); + assert.equal(schedules[0].timeZone?.timezoneName, 'UTC'); assert.equal( schedules[1].expressionString, expectedExpressionEveryMidnight, ); + assert.equal(schedules[1].timeZone?.timezoneName, 'UTC'); }); - void it('creates EventBridge rules for an array of both natural language and cron expressions', () => { + void it('creates EventBridge Schedule expressions for an array of both natural language and cron expressions', () => { const functionSchedules: FunctionSchedule[] = ['* * * * ?', 'every week']; const expectedExpressionEveryMinute = 'cron(* * * * ? *)'; const expectedExpressionEveryWeek = 'cron(0 0 ? * 1 *)'; const testLambda = getTestLambda(); - const schedules = convertFunctionSchedulesToRuleSchedules( + const schedules = convertFunctionSchedulesToScheduleExpressions( testLambda, functionSchedules, ); assert.equal(schedules.length, 2); assert.equal(schedules[0].expressionString, expectedExpressionEveryMinute); + assert.equal(schedules[0].timeZone?.timezoneName, 'UTC'); assert.equal(schedules[1].expressionString, expectedExpressionEveryWeek); + assert.equal(schedules[1].timeZone?.timezoneName, 'UTC'); + }); + + void it('creates EventBridge Schedule expressions for an array of both expression with timezone and cron expression without timezone', () => { + const functionSchedules: FunctionSchedule[] = [ + '* * * * ?', + { cron: '0 0 * * ?', timezone: 'Asia/Tokyo' }, + ]; + const expectedExpressionEveryMinute = 'cron(* * * * ? *)'; + const expectedExpressionEveryMidnight = 'cron(0 0 * * ? *)'; + const testLambda = getTestLambda(); + + const schedules = convertFunctionSchedulesToScheduleExpressions( + testLambda, + functionSchedules, + ); + + assert.equal(schedules.length, 2); + assert.equal(schedules[0].expressionString, expectedExpressionEveryMinute); + assert.equal(schedules[0].timeZone?.timezoneName, 'UTC'); + assert.equal( + schedules[1].expressionString, + expectedExpressionEveryMidnight, + ); + assert.equal(schedules[1].timeZone?.timezoneName, 'Asia/Tokyo'); }); void it('throws if rate is a negative number', () => { @@ -148,7 +218,7 @@ void describe('ScheduleParser', () => { assert.throws( () => { - convertFunctionSchedulesToRuleSchedules(testLambda, schedule); + convertFunctionSchedulesToScheduleExpressions(testLambda, schedule); }, { message: @@ -163,7 +233,7 @@ void describe('ScheduleParser', () => { assert.throws( () => { - convertFunctionSchedulesToRuleSchedules(testLambda, schedule); + convertFunctionSchedulesToScheduleExpressions(testLambda, schedule); }, { message: @@ -187,7 +257,7 @@ void describe('ScheduleParser', () => { assert.throws( () => { - convertFunctionSchedulesToRuleSchedules(testLambda, schedule); + convertFunctionSchedulesToScheduleExpressions(testLambda, schedule); }, { message: expectedErrors.join(os.EOL), @@ -214,7 +284,7 @@ void describe('ScheduleParser', () => { assert.throws( () => { - convertFunctionSchedulesToRuleSchedules(testLambda, schedule); + convertFunctionSchedulesToScheduleExpressions(testLambda, schedule); }, { message: expectedErrors.join(os.EOL), @@ -237,7 +307,7 @@ void describe('ScheduleParser', () => { assert.throws( () => { - convertFunctionSchedulesToRuleSchedules(testLambda, schedule); + convertFunctionSchedulesToScheduleExpressions(testLambda, schedule); }, { message: expectedErrors.join(os.EOL), @@ -260,7 +330,7 @@ void describe('ScheduleParser', () => { assert.throws( () => { - convertFunctionSchedulesToRuleSchedules(testLambda, schedule); + convertFunctionSchedulesToScheduleExpressions(testLambda, schedule); }, { message: expectedErrors.join(os.EOL), @@ -274,7 +344,7 @@ void describe('ScheduleParser', () => { assert.throws( () => { - convertFunctionSchedulesToRuleSchedules(testLambda, schedule); + convertFunctionSchedulesToScheduleExpressions(testLambda, schedule); }, { message: diff --git a/packages/backend-function/src/schedule_parser.ts b/packages/backend-function/src/schedule_parser.ts index fafebea7127..82d2654b720 100644 --- a/packages/backend-function/src/schedule_parser.ts +++ b/packages/backend-function/src/schedule_parser.ts @@ -1,9 +1,15 @@ -import { CronOptions, Schedule } from 'aws-cdk-lib/aws-events'; +import { + CronOptionsWithTimezone, + ScheduleExpression, +} from 'aws-cdk-lib/aws-scheduler'; +import { TimeZone } from 'aws-cdk-lib'; import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; import type { - CronSchedule, + CronScheduleExpression, FunctionSchedule, - TimeInterval, + TimeIntervalExpression, + ZonedCronSchedule, + ZonedTimeInterval, } from './factory.js'; import os from 'os'; @@ -15,23 +21,52 @@ type CronPart = | 'day-of-week' | 'year'; +type ZonedSchedule = ZonedCronSchedule | ZonedTimeInterval; + +type Writable = { + -readonly [P in K]: T[P]; +} & Omit; + +const DEFAULT_TIMEZONE = 'UTC'; + +const hydrateDefaults = (schedule: FunctionSchedule): ZonedSchedule => { + if (typeof schedule === 'string') { + return schedule.includes('every') + ? { rate: schedule as TimeIntervalExpression, timezone: DEFAULT_TIMEZONE } + : { + cron: schedule as CronScheduleExpression, + timezone: DEFAULT_TIMEZONE, + }; + } else if ('rate' in schedule) { + return { ...schedule, timezone: schedule.timezone ?? DEFAULT_TIMEZONE }; + } else if ('cron' in schedule) { + return { ...schedule, timezone: schedule.timezone ?? DEFAULT_TIMEZONE }; + } + throw new Error("Could not determine the function's schedule type"); +}; + /** - * Parses function schedule props in order to create EventBridge rules. + * Converts function schedules to schedule expressions. + * @param lambda The Lambda function to associate with the schedules. + * @param functionSchedules The function schedules to convert. + * @returns An array of schedule expressions. */ -export const convertFunctionSchedulesToRuleSchedules = ( +export const convertFunctionSchedulesToScheduleExpressions = ( lambda: NodejsFunction, functionSchedules: FunctionSchedule | FunctionSchedule[], -) => { +): ScheduleExpression[] => { const errors: string[] = []; - const ruleSchedules: Schedule[] = []; + const scheduleExpressions: ScheduleExpression[] = []; const schedules = Array.isArray(functionSchedules) ? functionSchedules : [functionSchedules]; - schedules.forEach((schedule) => { + const zonedSchedules = schedules.map(hydrateDefaults); + + zonedSchedules.forEach((schedule) => { if (isTimeInterval(schedule)) { - const { value, unit } = parseTimeInterval(schedule); + const { value, unit } = parseTimeInterval(schedule.rate); if (value && !isPositiveWholeNumber(value)) { errors.push( @@ -51,7 +86,7 @@ export const convertFunctionSchedulesToRuleSchedules = ( ); } } else { - const cronErrors = validateCron(schedule); + const cronErrors = validateCron(schedule.cron); if (cronErrors.length > 0) { errors.push(...cronErrors); @@ -59,7 +94,9 @@ export const convertFunctionSchedulesToRuleSchedules = ( } if (errors.length === 0) { - ruleSchedules.push(Schedule.cron(translateToCronOptions(schedule))); + scheduleExpressions.push( + ScheduleExpression.cron(translateToCronOptionsWithTimezone(schedule)), + ); } }); @@ -67,13 +104,14 @@ export const convertFunctionSchedulesToRuleSchedules = ( throw new Error(errors.join(os.EOL)); } - return ruleSchedules; + return scheduleExpressions; }; const isTimeInterval = ( - schedule: FunctionSchedule, -): schedule is TimeInterval => { - const parts = schedule.split(' '); + schedule: ZonedSchedule, +): schedule is ZonedTimeInterval => { + const expression = 'rate' in schedule ? schedule.rate : ''; + const parts = expression.split(' '); return ( parts[0] === 'every' && @@ -84,7 +122,7 @@ const isTimeInterval = ( ); }; -const parseTimeInterval = (timeInterval: TimeInterval) => { +const parseTimeInterval = (timeInterval: TimeIntervalExpression) => { const part = timeInterval.split(' ')[1]; const value = part.match(/-?\d+\.?\d*/); const unit = part.match(/[a-zA-Z]+/); @@ -97,7 +135,7 @@ const parseTimeInterval = (timeInterval: TimeInterval) => { const isPositiveWholeNumber = (test: number) => test > 0 && test % 1 === 0; -const validateCron = (cron: CronSchedule) => { +const validateCron = (cron: CronScheduleExpression) => { const errors: string[] = []; const cronParts = cron.split(' '); @@ -270,20 +308,45 @@ const isWholeNumberBetweenInclusive = ( max: number, ) => min <= test && test <= max && test % 1 === 0; -const translateToCronOptions = (schedule: FunctionSchedule): CronOptions => { +const translateToCronOptionsWithTimezone = ( + schedule: ZonedSchedule, +): CronOptionsWithTimezone => { if (isTimeInterval(schedule)) { - const { value, unit } = parseTimeInterval(schedule); + const { value, unit } = parseTimeInterval(schedule.rate); switch (unit) { case 'm': - return { minute: `*/${value}` }; + return { + minute: `*/${value}`, + timeZone: TimeZone.of(schedule.timezone), + }; case 'h': - return { minute: '0', hour: `*/${value}` }; + return { + minute: '0', + hour: `*/${value}`, + timeZone: TimeZone.of(schedule.timezone), + }; case 'day': - return { minute: '0', hour: '0', day: `*` }; + return { + minute: '0', + hour: '0', + day: `*`, + timeZone: TimeZone.of(schedule.timezone), + }; case 'week': - return { minute: '0', hour: '0', weekDay: `1` }; + return { + minute: '0', + hour: '0', + weekDay: `1`, + timeZone: TimeZone.of(schedule.timezone), + }; case 'month': - return { minute: '0', hour: '0', day: '1', month: `*` }; + return { + minute: '0', + hour: '0', + day: '1', + month: `*`, + timeZone: TimeZone.of(schedule.timezone), + }; case 'year': return { minute: '0', @@ -291,6 +354,7 @@ const translateToCronOptions = (schedule: FunctionSchedule): CronOptions => { day: '1', month: '1', year: `*`, + timeZone: TimeZone.of(schedule.timezone), }; default: // This should never happen with strict types @@ -299,12 +363,13 @@ const translateToCronOptions = (schedule: FunctionSchedule): CronOptions => { ); } } else { - const cronArray = schedule.split(' '); - const result: Record = { + const cronArray = schedule.cron.split(' '); + const result: Writable = { minute: cronArray[0], hour: cronArray[1], month: cronArray[3], year: cronArray.length === 6 ? cronArray[5] : '*', + timeZone: TimeZone.of(schedule.timezone), }; // Branching logic here is because we cannot supply both day and weekDay