@squiz/optimization-utils 2.0.2 → 3.0.0
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/array/chunkify.d.ts +1 -0
- package/dist/array/chunkify.js +12 -0
- package/dist/array/chunkify.js.map +1 -0
- package/dist/array/uniqueWith.d.ts +1 -0
- package/dist/array/uniqueWith.js +6 -0
- package/dist/array/uniqueWith.js.map +1 -0
- package/dist/index.d.ts +3 -13
- package/dist/index.js +3 -13
- package/dist/index.js.map +1 -1
- package/dist/{typesUtils → types}/utilities.js.map +1 -1
- package/package.json +3 -16
- package/CHANGELOG.md +0 -43
- package/dist/cloudflare/CloudflareKVHttpService.d.ts +0 -11
- package/dist/cloudflare/CloudflareKVHttpService.js +0 -19
- package/dist/cloudflare/CloudflareKVHttpService.js.map +0 -1
- package/dist/cloudflare/ImplCloudflareKVHttpService.d.ts +0 -22
- package/dist/cloudflare/ImplCloudflareKVHttpService.js +0 -94
- package/dist/cloudflare/ImplCloudflareKVHttpService.js.map +0 -1
- package/dist/config/ConfigurationLoader.d.ts +0 -23
- package/dist/config/ConfigurationLoader.js +0 -98
- package/dist/config/ConfigurationLoader.js.map +0 -1
- package/dist/date/DateManipulator.d.ts +0 -6
- package/dist/date/DateManipulator.js +0 -21
- package/dist/date/DateManipulator.js.map +0 -1
- package/dist/event/AggregateRoot.d.ts +0 -4
- package/dist/event/AggregateRoot.js +0 -3
- package/dist/event/AggregateRoot.js.map +0 -1
- package/dist/event/DomainEvent.d.ts +0 -14
- package/dist/event/DomainEvent.js +0 -28
- package/dist/event/DomainEvent.js.map +0 -1
- package/dist/event/DynamoDBEventMapper.d.ts +0 -39
- package/dist/event/DynamoDBEventMapper.js +0 -58
- package/dist/event/DynamoDBEventMapper.js.map +0 -1
- package/dist/event/EventHandler.d.ts +0 -14
- package/dist/event/EventHandler.js +0 -43
- package/dist/event/EventHandler.js.map +0 -1
- package/dist/scheduler/EventBridgeScheduler.d.ts +0 -21
- package/dist/scheduler/EventBridgeScheduler.js +0 -130
- package/dist/scheduler/EventBridgeScheduler.js.map +0 -1
- package/dist/scheduler/Scheduler.d.ts +0 -21
- package/dist/scheduler/Scheduler.js +0 -17
- package/dist/scheduler/Scheduler.js.map +0 -1
- package/dist/testing/mock.d.ts +0 -4
- package/dist/testing/mock.js +0 -17
- package/dist/testing/mock.js.map +0 -1
- package/dist/typesUtils/DynamoDB.d.ts +0 -4
- package/dist/typesUtils/DynamoDB.js +0 -3
- package/dist/typesUtils/DynamoDB.js.map +0 -1
- package/src/cloudflare/CloudflareKVHttpService.ts +0 -20
- package/src/cloudflare/ImplCloudflareKVHttpService.ts +0 -129
- package/src/cloudflare/__tests__/ImplCloudflareKVHttpService.spec.ts +0 -178
- package/src/config/ConfigurationLoader.ts +0 -74
- package/src/config/__tests__/ConfigurationLoader.spec.ts +0 -62
- package/src/date/DateManipulator.ts +0 -29
- package/src/date/__tests__/DateManipulator.spec.ts +0 -64
- package/src/event/AggregateRoot.ts +0 -5
- package/src/event/DomainEvent.ts +0 -53
- package/src/event/DynamoDBEventMapper.ts +0 -75
- package/src/event/EventHandler.ts +0 -57
- package/src/event/__tests__/DynamoDBEventMapper.spec.ts +0 -121
- package/src/index.ts +0 -21
- package/src/object/__tests__/getProperty.spec.ts +0 -17
- package/src/object/getProperty.ts +0 -21
- package/src/scheduler/EventBridgeScheduler.ts +0 -172
- package/src/scheduler/Scheduler.ts +0 -32
- package/src/scheduler/__tests__/EventBridgeScheduler.spec.ts +0 -308
- package/src/testing/mock.ts +0 -15
- package/src/typesUtils/DynamoDB.ts +0 -17
- package/src/typesUtils/utilities.ts +0 -11
- package/tsconfig.json +0 -13
- /package/dist/{typesUtils → types}/utilities.d.ts +0 -0
- /package/dist/{typesUtils → types}/utilities.js +0 -0
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
EventDynamoDBModel,
|
|
3
|
-
eventToDynamoDB,
|
|
4
|
-
toEventFromPlain,
|
|
5
|
-
toEventFromDynamoDb,
|
|
6
|
-
} from '../DynamoDBEventMapper';
|
|
7
|
-
import { faker } from '@faker-js/faker';
|
|
8
|
-
import { marshall } from '@aws-sdk/util-dynamodb';
|
|
9
|
-
import { TenantIdValue } from '@squiz/optimization-value-objects';
|
|
10
|
-
import { AsDomainEvent, DomainEvent } from '../DomainEvent';
|
|
11
|
-
|
|
12
|
-
type ExampleEventDetail = {
|
|
13
|
-
experimentId: string;
|
|
14
|
-
tenantId: TenantIdValue;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
@AsDomainEvent()
|
|
18
|
-
class ExampleEvent implements DomainEvent<{}> {
|
|
19
|
-
readonly detail!: ExampleEventDetail;
|
|
20
|
-
readonly eventId!: string;
|
|
21
|
-
readonly name!: string;
|
|
22
|
-
readonly time!: Date;
|
|
23
|
-
readonly version!: string;
|
|
24
|
-
readonly entityId: string;
|
|
25
|
-
|
|
26
|
-
constructor(data: Omit<ExampleEvent, 'name'>) {
|
|
27
|
-
this.detail = data.detail;
|
|
28
|
-
this.eventId = data.eventId;
|
|
29
|
-
this.name = ExampleEvent.name;
|
|
30
|
-
this.time = data.time;
|
|
31
|
-
this.version = data.version;
|
|
32
|
-
this.entityId = data.entityId;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const createPlainExampleEvent: () => {
|
|
37
|
-
eventId: string;
|
|
38
|
-
name: string;
|
|
39
|
-
detail: ExampleEventDetail;
|
|
40
|
-
time: string;
|
|
41
|
-
version: string;
|
|
42
|
-
entityId: string;
|
|
43
|
-
} = () => {
|
|
44
|
-
return {
|
|
45
|
-
detail: {
|
|
46
|
-
experimentId: faker.string.uuid(),
|
|
47
|
-
tenantId: faker.string.uuid(),
|
|
48
|
-
},
|
|
49
|
-
eventId: faker.string.uuid(),
|
|
50
|
-
time: faker.date.past().toISOString(),
|
|
51
|
-
version: faker.number.int().toString(),
|
|
52
|
-
name: ExampleEvent.name,
|
|
53
|
-
ttl: faker.date.past().getTime() / 1000,
|
|
54
|
-
entityId: 'example-id',
|
|
55
|
-
};
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
describe('toEventEntityFromDynamoDb', () => {
|
|
59
|
-
it('should map dynamo item to event instance', () => {
|
|
60
|
-
const event = createPlainExampleEvent();
|
|
61
|
-
|
|
62
|
-
const result = toEventFromDynamoDb<ExampleEvent>(marshall(event));
|
|
63
|
-
|
|
64
|
-
expect(result).toEqual(
|
|
65
|
-
new ExampleEvent({
|
|
66
|
-
detail: event.detail,
|
|
67
|
-
eventId: event.eventId,
|
|
68
|
-
time: new Date(event.time),
|
|
69
|
-
version: event.version,
|
|
70
|
-
entityId: 'example-id',
|
|
71
|
-
}),
|
|
72
|
-
);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe('toEventFromPlain', () => {
|
|
77
|
-
const event = createPlainExampleEvent();
|
|
78
|
-
|
|
79
|
-
it('should map the plain object to event instance', () => {
|
|
80
|
-
const result = toEventFromPlain<ExampleEvent>(event);
|
|
81
|
-
|
|
82
|
-
expect(result).toEqual(
|
|
83
|
-
new ExampleEvent({
|
|
84
|
-
detail: event.detail,
|
|
85
|
-
eventId: event.eventId,
|
|
86
|
-
time: new Date(event.time),
|
|
87
|
-
version: event.version,
|
|
88
|
-
entityId: 'example-id',
|
|
89
|
-
}),
|
|
90
|
-
);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe('toDynamoDB', () => {
|
|
95
|
-
it('should map event entity to dynamo item', () => {
|
|
96
|
-
const event = new ExampleEvent({
|
|
97
|
-
detail: {
|
|
98
|
-
experimentId: faker.string.uuid(),
|
|
99
|
-
tenantId: faker.string.uuid(),
|
|
100
|
-
},
|
|
101
|
-
eventId: faker.string.uuid(),
|
|
102
|
-
time: new Date('2020-01-01T00:00:00.000Z'),
|
|
103
|
-
version: faker.number.int().toString(),
|
|
104
|
-
entityId: 'example-id',
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const result = eventToDynamoDB(event);
|
|
108
|
-
|
|
109
|
-
expect(result).toEqual(
|
|
110
|
-
marshall({
|
|
111
|
-
detail: event.detail,
|
|
112
|
-
eventId: event.eventId,
|
|
113
|
-
time: event.time.toISOString(),
|
|
114
|
-
version: event.version,
|
|
115
|
-
name: 'ExampleEvent',
|
|
116
|
-
ttl: new Date('2020-01-31T00:00:00.000Z').getTime() / 1000,
|
|
117
|
-
entityId: 'example-id',
|
|
118
|
-
} as EventDynamoDBModel),
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export * from './typesUtils/utilities';
|
|
2
|
-
export * from './typesUtils/DynamoDB';
|
|
3
|
-
|
|
4
|
-
export * from './testing/mock';
|
|
5
|
-
|
|
6
|
-
export * from './scheduler/Scheduler';
|
|
7
|
-
export * from './scheduler/EventBridgeScheduler';
|
|
8
|
-
|
|
9
|
-
export * from './event/DynamoDBEventMapper';
|
|
10
|
-
export * from './event/DomainEvent';
|
|
11
|
-
export * from './event/AggregateRoot';
|
|
12
|
-
|
|
13
|
-
export * from './date/DateManipulator';
|
|
14
|
-
export * from './event/EventHandler';
|
|
15
|
-
|
|
16
|
-
export * from './config/ConfigurationLoader';
|
|
17
|
-
|
|
18
|
-
export * from './cloudflare/ImplCloudflareKVHttpService';
|
|
19
|
-
export * from './cloudflare/CloudflareKVHttpService';
|
|
20
|
-
|
|
21
|
-
export * from './object/getProperty';
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { getProperty } from '../getProperty';
|
|
2
|
-
|
|
3
|
-
it('should return a passed property', () => {
|
|
4
|
-
const object = { a: [{ b: { c: 3 } }] };
|
|
5
|
-
|
|
6
|
-
const result = getProperty(object, 'a[0].b.c');
|
|
7
|
-
|
|
8
|
-
expect(result).toBe(3);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('should return undefined if property does not exist', () => {
|
|
12
|
-
const object = { a: [{ b: { c: 3 } }] };
|
|
13
|
-
|
|
14
|
-
const result = getProperty(object, 'a[0].z.c');
|
|
15
|
-
|
|
16
|
-
expect(result).toBeUndefined();
|
|
17
|
-
});
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export const getProperty = (
|
|
2
|
-
obj: Record<string | symbol | number, unknown>,
|
|
3
|
-
path: string,
|
|
4
|
-
defaultValue: unknown = undefined,
|
|
5
|
-
): unknown => {
|
|
6
|
-
const travel = (regexp: RegExp) =>
|
|
7
|
-
path
|
|
8
|
-
.split(regexp)
|
|
9
|
-
.filter(Boolean)
|
|
10
|
-
.reduce<unknown>(
|
|
11
|
-
(res, key) =>
|
|
12
|
-
res !== null && res !== undefined
|
|
13
|
-
? (res as Record<string, unknown>)[key]
|
|
14
|
-
: res,
|
|
15
|
-
obj,
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
const result = travel(/[,[\]]+?/) || travel(/[,[\].]+?/);
|
|
19
|
-
|
|
20
|
-
return result === undefined || result === obj ? defaultValue : result;
|
|
21
|
-
};
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import { injectable } from 'inversify';
|
|
2
|
-
import {
|
|
3
|
-
CreateScheduleCommand,
|
|
4
|
-
DeleteScheduleCommand,
|
|
5
|
-
FlexibleTimeWindowMode,
|
|
6
|
-
GetScheduleCommand,
|
|
7
|
-
ResourceNotFoundException,
|
|
8
|
-
SchedulerClient,
|
|
9
|
-
} from '@aws-sdk/client-scheduler';
|
|
10
|
-
import { DomainEvent } from '../event/DomainEvent';
|
|
11
|
-
import { PutItemCommandInput } from '@aws-sdk/client-dynamodb';
|
|
12
|
-
import { eventToDynamoDB } from '../event/DynamoDBEventMapper';
|
|
13
|
-
import { randomUUID } from 'crypto';
|
|
14
|
-
import { ScheduleName, Scheduler, SchedulerOptions } from './Scheduler';
|
|
15
|
-
import { marshall } from '@aws-sdk/util-dynamodb';
|
|
16
|
-
import { createLog, Logger, LogMessage } from '@squiz/optimization-logger';
|
|
17
|
-
|
|
18
|
-
export type EventBridgeSchedulerConfig = {
|
|
19
|
-
region: string;
|
|
20
|
-
groupName: string;
|
|
21
|
-
eventTableName: string;
|
|
22
|
-
schedulerRole: string;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
@injectable()
|
|
26
|
-
export class EventBridgeScheduler implements Scheduler {
|
|
27
|
-
constructor(
|
|
28
|
-
private readonly config: EventBridgeSchedulerConfig,
|
|
29
|
-
private readonly client: SchedulerClient,
|
|
30
|
-
private readonly logger: Logger,
|
|
31
|
-
) {}
|
|
32
|
-
|
|
33
|
-
async schedule(
|
|
34
|
-
domainEvent: DomainEvent,
|
|
35
|
-
options: SchedulerOptions,
|
|
36
|
-
): Promise<void> {
|
|
37
|
-
const logMessage = createLog()
|
|
38
|
-
.attachEventName(domainEvent.name)
|
|
39
|
-
.create(this);
|
|
40
|
-
|
|
41
|
-
this.logger.debug(...logMessage('started scheduling'));
|
|
42
|
-
// I putted here "the schedule attributes" because in that way we ensure that every PutItem command
|
|
43
|
-
// it will create always a new record in dynamoDB
|
|
44
|
-
const putItemCommand: PutItemCommandInput = {
|
|
45
|
-
TableName: this.config.eventTableName,
|
|
46
|
-
Item: {
|
|
47
|
-
...eventToDynamoDB(domainEvent),
|
|
48
|
-
// https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-schedule-context-attributes.html
|
|
49
|
-
...marshall({
|
|
50
|
-
eventId: '<aws.scheduler.execution-id>',
|
|
51
|
-
time: '<aws.scheduler.scheduled-time>',
|
|
52
|
-
}),
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
this.logger.debug(...logMessage('finished mapping to PutItemCommandInput'));
|
|
57
|
-
|
|
58
|
-
const scheduleName = this.createScheduleName(options.name);
|
|
59
|
-
const scheduleExpression = this.createScheduleExpression(options);
|
|
60
|
-
|
|
61
|
-
this.logger.debug(
|
|
62
|
-
...logMessage(
|
|
63
|
-
`started calling AWS Scheduler with ScheduleName: ` +
|
|
64
|
-
`${scheduleName}, ScheduleExpression: ${scheduleExpression}`,
|
|
65
|
-
),
|
|
66
|
-
);
|
|
67
|
-
await this._deleteSchedule(scheduleName, logMessage);
|
|
68
|
-
const command = new CreateScheduleCommand({
|
|
69
|
-
Name: scheduleName,
|
|
70
|
-
GroupName: this.config.groupName,
|
|
71
|
-
ScheduleExpression: scheduleExpression,
|
|
72
|
-
ScheduleExpressionTimezone: 'UTC',
|
|
73
|
-
State: 'ENABLED',
|
|
74
|
-
FlexibleTimeWindow: {
|
|
75
|
-
Mode: FlexibleTimeWindowMode.OFF,
|
|
76
|
-
},
|
|
77
|
-
ActionAfterCompletion: 'DELETE',
|
|
78
|
-
Target: {
|
|
79
|
-
Arn: 'arn:aws:scheduler:::aws-sdk:dynamodb:putItem',
|
|
80
|
-
Input: JSON.stringify(putItemCommand),
|
|
81
|
-
RoleArn: this.config.schedulerRole,
|
|
82
|
-
},
|
|
83
|
-
ClientToken: randomUUID(),
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
await this.client.send(command);
|
|
87
|
-
this.logger.debug(...logMessage('finished calling AWS Scheduler'));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
private createScheduleName(scheduleName: ScheduleName): string {
|
|
91
|
-
const truncateString = (str: string, maxLength: number): string =>
|
|
92
|
-
str.length > maxLength ? str.substring(0, maxLength) : str;
|
|
93
|
-
const maxLengthOfScheduleName = 64;
|
|
94
|
-
|
|
95
|
-
return truncateString(scheduleName, maxLengthOfScheduleName);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
private createScheduleExpression(options: SchedulerOptions): string {
|
|
99
|
-
switch (options.type) {
|
|
100
|
-
case 'at': {
|
|
101
|
-
const dateString = options.value.toISOString().replace(/\..*/, '');
|
|
102
|
-
|
|
103
|
-
return `at(${dateString})`;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
case 'cron':
|
|
107
|
-
return `cron(${options.value})`;
|
|
108
|
-
case 'rate':
|
|
109
|
-
return `rate(${options.value} ${options.unit})`;
|
|
110
|
-
default:
|
|
111
|
-
throw new Error(`Passed unresolved ScheduleOption type.`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async deleteSchedule(scheduleName: ScheduleName): Promise<void> {
|
|
116
|
-
const logMessage = createLog()
|
|
117
|
-
.attachMetadata({ scheduleName })
|
|
118
|
-
.create(this);
|
|
119
|
-
|
|
120
|
-
return this._deleteSchedule(scheduleName, logMessage);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
private async _deleteSchedule(
|
|
124
|
-
scheduleName: ScheduleName,
|
|
125
|
-
logMessage: LogMessage,
|
|
126
|
-
): Promise<void> {
|
|
127
|
-
const trimmedScheduleName = this.createScheduleName(scheduleName);
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
this.logger.debug(
|
|
131
|
-
...logMessage(
|
|
132
|
-
`started getting schedule, scheduleName: ${trimmedScheduleName}`,
|
|
133
|
-
),
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
const getCommand = new GetScheduleCommand({
|
|
137
|
-
GroupName: this.config.groupName,
|
|
138
|
-
Name: trimmedScheduleName,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
await this.client.send(getCommand);
|
|
142
|
-
|
|
143
|
-
this.logger.debug(
|
|
144
|
-
...logMessage(
|
|
145
|
-
`finished getting schedule, scheduleName: ${trimmedScheduleName}`,
|
|
146
|
-
),
|
|
147
|
-
);
|
|
148
|
-
} catch (e) {
|
|
149
|
-
if (e instanceof ResourceNotFoundException) {
|
|
150
|
-
this.logger.debug(
|
|
151
|
-
...logMessage(
|
|
152
|
-
`the schedule does not exist, skipped deleting existing schedule, scheduleName: ${trimmedScheduleName}`,
|
|
153
|
-
),
|
|
154
|
-
);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
throw e;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
this.logger.debug(...logMessage('started deleting schedule'));
|
|
162
|
-
|
|
163
|
-
const deleteCommand = new DeleteScheduleCommand({
|
|
164
|
-
GroupName: this.config.groupName,
|
|
165
|
-
Name: trimmedScheduleName,
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
await this.client.send(deleteCommand);
|
|
169
|
-
|
|
170
|
-
this.logger.debug(...logMessage('finished deleting schedule'));
|
|
171
|
-
}
|
|
172
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { DomainEvent } from './../event/DomainEvent';
|
|
2
|
-
import { injectable } from 'inversify';
|
|
3
|
-
|
|
4
|
-
export type CronExpression =
|
|
5
|
-
`${string} ${string} ${string} ${string} ${string} ${string}`;
|
|
6
|
-
export type RateUnit = 'minutes' | 'hours' | 'days';
|
|
7
|
-
export type ScheduleName = string;
|
|
8
|
-
export type SchedulerOptions = (
|
|
9
|
-
| {
|
|
10
|
-
type: 'at';
|
|
11
|
-
value: Date;
|
|
12
|
-
}
|
|
13
|
-
| {
|
|
14
|
-
type: 'cron';
|
|
15
|
-
value: CronExpression;
|
|
16
|
-
}
|
|
17
|
-
| {
|
|
18
|
-
type: 'rate';
|
|
19
|
-
value: number;
|
|
20
|
-
unit: RateUnit;
|
|
21
|
-
}
|
|
22
|
-
) & { name: ScheduleName };
|
|
23
|
-
|
|
24
|
-
@injectable()
|
|
25
|
-
export abstract class Scheduler {
|
|
26
|
-
abstract schedule(
|
|
27
|
-
domainEvent: DomainEvent,
|
|
28
|
-
options: SchedulerOptions,
|
|
29
|
-
): Promise<void>;
|
|
30
|
-
|
|
31
|
-
abstract deleteSchedule(scheduleName: ScheduleName): Promise<void>;
|
|
32
|
-
}
|
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
import { Container } from 'inversify';
|
|
2
|
-
import {
|
|
3
|
-
CreateScheduleCommand,
|
|
4
|
-
CreateScheduleCommandInput,
|
|
5
|
-
DeleteScheduleCommand,
|
|
6
|
-
DeleteScheduleCommandInput,
|
|
7
|
-
GetScheduleCommand,
|
|
8
|
-
GetScheduleCommandInput,
|
|
9
|
-
ResourceNotFoundException,
|
|
10
|
-
SchedulerClient,
|
|
11
|
-
} from '@aws-sdk/client-scheduler';
|
|
12
|
-
import { randomUUID } from 'crypto';
|
|
13
|
-
import { faker } from '@faker-js/faker';
|
|
14
|
-
import { ExceptionOptionType } from '@smithy/smithy-client/dist-types/exceptions';
|
|
15
|
-
import { SchedulerServiceException } from '@aws-sdk/client-scheduler/dist-types/models/SchedulerServiceException';
|
|
16
|
-
import { Logger, createLoggerMock } from '@squiz/optimization-logger';
|
|
17
|
-
import { createSchedulerClientMock } from '../../testing/mock';
|
|
18
|
-
import { eventToDynamoDB } from '../../event/DynamoDBEventMapper';
|
|
19
|
-
import { Scheduler } from '../Scheduler';
|
|
20
|
-
import {
|
|
21
|
-
EventBridgeScheduler,
|
|
22
|
-
EventBridgeSchedulerConfig,
|
|
23
|
-
} from '../EventBridgeScheduler';
|
|
24
|
-
import { DomainEvent } from '../../event/DomainEvent';
|
|
25
|
-
|
|
26
|
-
jest.mock('@aws-sdk/client-scheduler', () => {
|
|
27
|
-
return {
|
|
28
|
-
...jest.requireActual('@aws-sdk/client-scheduler'),
|
|
29
|
-
CreateScheduleCommand: jest.fn(),
|
|
30
|
-
GetScheduleCommand: jest.fn(),
|
|
31
|
-
DeleteScheduleCommand: jest.fn(),
|
|
32
|
-
};
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
class ExampleEvent implements DomainEvent {
|
|
36
|
-
readonly detail: Record<string, unknown> = {};
|
|
37
|
-
readonly eventId: string = randomUUID();
|
|
38
|
-
readonly name: string = ExampleEvent.name;
|
|
39
|
-
readonly time: Date = new Date();
|
|
40
|
-
readonly version: string = '1';
|
|
41
|
-
readonly entityId = 'uuid';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
describe('EventBridgeScheduler', () => {
|
|
45
|
-
let scheduler: Scheduler;
|
|
46
|
-
let schedulerClient: SchedulerClient;
|
|
47
|
-
let event: ExampleEvent;
|
|
48
|
-
const config: EventBridgeSchedulerConfig = {
|
|
49
|
-
eventTableName: 'event-table',
|
|
50
|
-
groupName: 'experiment',
|
|
51
|
-
region: 'eu-north-1',
|
|
52
|
-
schedulerRole: 'example-arn',
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
beforeEach(() => {
|
|
56
|
-
jest.resetAllMocks();
|
|
57
|
-
|
|
58
|
-
const container = new Container({ defaultScope: 'Singleton' });
|
|
59
|
-
|
|
60
|
-
container.bind(Scheduler).toDynamicValue(({ container: c }) => {
|
|
61
|
-
return new EventBridgeScheduler(
|
|
62
|
-
config,
|
|
63
|
-
c.get(SchedulerClient),
|
|
64
|
-
c.get(Logger),
|
|
65
|
-
);
|
|
66
|
-
});
|
|
67
|
-
container
|
|
68
|
-
.bind(SchedulerClient)
|
|
69
|
-
.toConstantValue(createSchedulerClientMock());
|
|
70
|
-
container.bind(Logger).toConstantValue(createLoggerMock());
|
|
71
|
-
|
|
72
|
-
event = new ExampleEvent();
|
|
73
|
-
schedulerClient = container.get(SchedulerClient);
|
|
74
|
-
scheduler = container.get(Scheduler);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe('schedule', () => {
|
|
78
|
-
it('should call the CreateScheduleCommand with the PutItemCommandInput', async () => {
|
|
79
|
-
await scheduler.schedule(event, {
|
|
80
|
-
type: 'at',
|
|
81
|
-
value: faker.date.future(),
|
|
82
|
-
name: randomUUID(),
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
expect(schedulerClient.send).toHaveBeenCalledWith(
|
|
86
|
-
expect.any(CreateScheduleCommand),
|
|
87
|
-
);
|
|
88
|
-
expect(CreateScheduleCommand).toHaveBeenCalledWith(
|
|
89
|
-
expect.objectContaining({
|
|
90
|
-
Target: {
|
|
91
|
-
Arn: 'arn:aws:scheduler:::aws-sdk:dynamodb:putItem',
|
|
92
|
-
Input: JSON.stringify({
|
|
93
|
-
TableName: config.eventTableName,
|
|
94
|
-
Item: {
|
|
95
|
-
...eventToDynamoDB(event),
|
|
96
|
-
eventId: {
|
|
97
|
-
S: '<aws.scheduler.execution-id>',
|
|
98
|
-
},
|
|
99
|
-
time: {
|
|
100
|
-
S: '<aws.scheduler.scheduled-time>',
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
}),
|
|
104
|
-
RoleArn: config.schedulerRole,
|
|
105
|
-
},
|
|
106
|
-
} as CreateScheduleCommandInput),
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should schedule with the "at" schedule expression', async () => {
|
|
111
|
-
await scheduler.schedule(event, {
|
|
112
|
-
type: 'at',
|
|
113
|
-
value: new Date('2023-01-01T00:00:00.000Z'),
|
|
114
|
-
name: randomUUID(),
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
expect(schedulerClient.send).toHaveBeenCalledWith(
|
|
118
|
-
expect.any(CreateScheduleCommand),
|
|
119
|
-
);
|
|
120
|
-
expect(CreateScheduleCommand).toHaveBeenCalledWith(
|
|
121
|
-
expect.objectContaining({
|
|
122
|
-
ScheduleExpression: 'at(2023-01-01T00:00:00)',
|
|
123
|
-
ScheduleExpressionTimezone: 'UTC',
|
|
124
|
-
} as CreateScheduleCommandInput),
|
|
125
|
-
);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('should schedule with the "rate" schedule expression', async () => {
|
|
129
|
-
await scheduler.schedule(event, {
|
|
130
|
-
type: 'rate',
|
|
131
|
-
value: 15,
|
|
132
|
-
unit: 'days',
|
|
133
|
-
name: randomUUID(),
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
expect(schedulerClient.send).toHaveBeenCalledWith(
|
|
137
|
-
expect.any(CreateScheduleCommand),
|
|
138
|
-
);
|
|
139
|
-
expect(CreateScheduleCommand).toHaveBeenCalledWith(
|
|
140
|
-
expect.objectContaining({
|
|
141
|
-
ScheduleExpression: 'rate(15 days)',
|
|
142
|
-
ScheduleExpressionTimezone: 'UTC',
|
|
143
|
-
} as CreateScheduleCommandInput),
|
|
144
|
-
);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('should schedule with the "cron" schedule expression', async () => {
|
|
148
|
-
await scheduler.schedule(event, {
|
|
149
|
-
type: 'cron',
|
|
150
|
-
value: '* * * * ? *',
|
|
151
|
-
name: randomUUID(),
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
expect(schedulerClient.send).toHaveBeenCalledWith(
|
|
155
|
-
expect.any(CreateScheduleCommand),
|
|
156
|
-
);
|
|
157
|
-
expect(CreateScheduleCommand).toHaveBeenCalledWith(
|
|
158
|
-
expect.objectContaining({
|
|
159
|
-
ScheduleExpression: 'cron(* * * * ? *)',
|
|
160
|
-
ScheduleExpressionTimezone: 'UTC',
|
|
161
|
-
} as CreateScheduleCommandInput),
|
|
162
|
-
);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('should create a schedule name which includes the event name', async () => {
|
|
166
|
-
await scheduler.schedule(event, {
|
|
167
|
-
type: 'cron',
|
|
168
|
-
value: '* * * * ? *',
|
|
169
|
-
name: 'ExampleSchedule',
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
expect(schedulerClient.send).toHaveBeenCalledWith(
|
|
173
|
-
expect.any(CreateScheduleCommand),
|
|
174
|
-
);
|
|
175
|
-
expect(CreateScheduleCommand).toHaveBeenCalledWith(
|
|
176
|
-
expect.objectContaining({
|
|
177
|
-
Name: 'ExampleSchedule',
|
|
178
|
-
} as CreateScheduleCommandInput),
|
|
179
|
-
);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('should trim end if the name contains more than 64 characters', async () => {
|
|
183
|
-
await scheduler.schedule(event, {
|
|
184
|
-
type: 'cron',
|
|
185
|
-
value: '* * * * ? *',
|
|
186
|
-
name: 'pVlH%46KTS).@KFhaumnG!#>dcZ#FTh=yBwD$rwDz@s41BO5C=?>gr9J.#[)!T^4Y',
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
expect(schedulerClient.send).toHaveBeenCalledWith(
|
|
190
|
-
expect.any(CreateScheduleCommand),
|
|
191
|
-
);
|
|
192
|
-
expect(CreateScheduleCommand).toHaveBeenCalledWith(
|
|
193
|
-
expect.objectContaining({
|
|
194
|
-
Name: 'pVlH%46KTS).@KFhaumnG!#>dcZ#FTh=yBwD$rwDz@s41BO5C=?>gr9J.#[)!T^4',
|
|
195
|
-
} as CreateScheduleCommandInput),
|
|
196
|
-
);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('should call CreateScheduleCommand with correct params', async () => {
|
|
200
|
-
await scheduler.schedule(event, {
|
|
201
|
-
type: 'cron',
|
|
202
|
-
value: '* * * * ? *',
|
|
203
|
-
name: randomUUID(),
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
expect(schedulerClient.send).toHaveBeenCalledWith(
|
|
207
|
-
expect.any(CreateScheduleCommand),
|
|
208
|
-
);
|
|
209
|
-
expect(CreateScheduleCommand).toHaveBeenCalledWith(
|
|
210
|
-
expect.objectContaining({
|
|
211
|
-
GroupName: config.groupName,
|
|
212
|
-
State: 'ENABLED',
|
|
213
|
-
FlexibleTimeWindow: {
|
|
214
|
-
Mode: 'OFF',
|
|
215
|
-
},
|
|
216
|
-
ActionAfterCompletion: 'DELETE',
|
|
217
|
-
ClientToken: expect.any(String),
|
|
218
|
-
} as CreateScheduleCommandInput),
|
|
219
|
-
);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('should remove the existing schedule if it exists', async () => {
|
|
223
|
-
const scheduleName = randomUUID();
|
|
224
|
-
|
|
225
|
-
await scheduler.schedule(event, {
|
|
226
|
-
type: 'at',
|
|
227
|
-
value: faker.date.future(),
|
|
228
|
-
name: scheduleName,
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
expect(GetScheduleCommand).toHaveBeenCalledWith({
|
|
232
|
-
GroupName: config.groupName,
|
|
233
|
-
Name: scheduleName,
|
|
234
|
-
} as GetScheduleCommandInput);
|
|
235
|
-
expect(DeleteScheduleCommand).toHaveBeenCalledWith({
|
|
236
|
-
GroupName: config.groupName,
|
|
237
|
-
Name: scheduleName,
|
|
238
|
-
} as DeleteScheduleCommandInput);
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
describe('deleteSchedule', () => {
|
|
243
|
-
it('should delete schedule if it exists', async () => {
|
|
244
|
-
const scheduleName = 'example-schedule';
|
|
245
|
-
|
|
246
|
-
await scheduler.deleteSchedule(scheduleName);
|
|
247
|
-
|
|
248
|
-
expect(GetScheduleCommand).toHaveBeenCalledWith({
|
|
249
|
-
GroupName: config.groupName,
|
|
250
|
-
Name: scheduleName,
|
|
251
|
-
} as GetScheduleCommandInput);
|
|
252
|
-
expect(DeleteScheduleCommand).toHaveBeenCalledWith({
|
|
253
|
-
GroupName: config.groupName,
|
|
254
|
-
Name: scheduleName,
|
|
255
|
-
} as DeleteScheduleCommandInput);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('should skip deleting schedule if it does not exist', async () => {
|
|
259
|
-
(schedulerClient.send as jest.Mock).mockRejectedValueOnce(
|
|
260
|
-
new ResourceNotFoundException(
|
|
261
|
-
{} as ExceptionOptionType<
|
|
262
|
-
ResourceNotFoundException,
|
|
263
|
-
SchedulerServiceException
|
|
264
|
-
>,
|
|
265
|
-
),
|
|
266
|
-
);
|
|
267
|
-
const scheduleName = randomUUID();
|
|
268
|
-
|
|
269
|
-
await scheduler.schedule(event, {
|
|
270
|
-
type: 'at',
|
|
271
|
-
value: faker.date.future(),
|
|
272
|
-
name: scheduleName,
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
expect(GetScheduleCommand).toHaveBeenCalledTimes(1);
|
|
276
|
-
expect(DeleteScheduleCommand).toHaveBeenCalledTimes(0);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it('should rethrow exception if the GetScheduleCommand return exception other than ResourceNotFound', async () => {
|
|
280
|
-
(schedulerClient.send as jest.Mock).mockRejectedValueOnce(new Error());
|
|
281
|
-
const scheduleName = randomUUID();
|
|
282
|
-
|
|
283
|
-
await expect(() =>
|
|
284
|
-
scheduler.deleteSchedule(scheduleName),
|
|
285
|
-
).rejects.toThrow();
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it('should trim the scheduleName up to 64 characters', async () => {
|
|
289
|
-
const scheduleNameWith65Characters =
|
|
290
|
-
'pVlH%46KTS).@KFhaumnG!#>dcZ#FTh=yBwD$rwDz@s41BO5C=?>gr9J.#[)!T^4Y';
|
|
291
|
-
const expectedScheduleName =
|
|
292
|
-
'pVlH%46KTS).@KFhaumnG!#>dcZ#FTh=yBwD$rwDz@s41BO5C=?>gr9J.#[)!T^4';
|
|
293
|
-
|
|
294
|
-
await scheduler.deleteSchedule(scheduleNameWith65Characters);
|
|
295
|
-
|
|
296
|
-
expect(GetScheduleCommand).toHaveBeenCalledWith(
|
|
297
|
-
expect.objectContaining({
|
|
298
|
-
Name: expectedScheduleName,
|
|
299
|
-
}),
|
|
300
|
-
);
|
|
301
|
-
expect(DeleteScheduleCommand).toHaveBeenCalledWith(
|
|
302
|
-
expect.objectContaining({
|
|
303
|
-
Name: expectedScheduleName,
|
|
304
|
-
}),
|
|
305
|
-
);
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
});
|