@squiz/job-runner-lib 2.9.0 → 2.10.1
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/CHANGELOG.md +12 -0
- package/lib/domains/job/constants/RuntimeImage.constants.d.ts +3 -4
- package/lib/domains/job/constants/RuntimeImage.constants.spec.d.ts +5 -0
- package/lib/domains/job/errors/ConflictError.d.ts +14 -0
- package/lib/domains/job/errors/Error.constants.d.ts +4 -0
- package/lib/domains/job/errors/index.d.ts +1 -0
- package/lib/domains/job/model/Base/Base.d.ts +5 -0
- package/lib/domains/job/model/JobExecution/JobExecution.d.ts +15 -3
- package/lib/domains/job/repository/JobExecutionRepository/JobExecutionRepository.d.ts +3 -6
- package/lib/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.d.ts +1 -1
- package/lib/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.spec.d.ts +5 -0
- package/lib/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.types.d.ts +1 -1
- package/lib/index.js +11 -11
- package/lib/index.js.map +4 -4
- package/package.json +4 -3
- package/src/domains/job/constants/RuntimeImage.constants.spec.ts +26 -0
- package/src/domains/job/constants/RuntimeImage.constants.ts +5 -8
- package/src/domains/job/errors/ConflictError.ts +19 -0
- package/src/domains/job/errors/Error.constants.ts +4 -0
- package/src/domains/job/errors/index.ts +1 -0
- package/src/domains/job/manifest/mocks/JobManifest.mock.ts +2 -2
- package/src/domains/job/manifest/mocks/JobManifestDto.mock.ts +2 -2
- package/src/domains/job/manifest/mocks/JobManifestRecord.mock.ts +2 -2
- package/src/domains/job/manifest/mocks/JobManifestRequest.mock.ts +2 -2
- package/src/domains/job/model/Base/Base.ts +6 -0
- package/src/domains/job/model/JobExecution/JobExecution.ts +17 -3
- package/src/domains/job/model/JobExecution/mocks/JobExecution.mock.ts +1 -0
- package/src/domains/job/model/JobExecution/mocks/JobExecutionDto.mock.ts +3 -0
- package/src/domains/job/model/JobExecution/mocks/JobExecutionRecord.mock.ts +1 -0
- package/src/domains/job/repository/JobExecutionRepository/JobExecutionRepository.spec.ts +77 -16
- package/src/domains/job/repository/JobExecutionRepository/JobExecutionRepository.ts +19 -15
- package/src/domains/job/repository/JobExecutionRepository/utils/convertDynamoDbItemToJobExecution/convertDynamoDbItemToJobExecution.ts +1 -0
- package/src/domains/job/repository/JobManifestRepository/JobManifestRepository.spec.ts +24 -3
- package/src/domains/job/repository/JobRepository/JobRepository.spec.ts +24 -3
- package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.spec.ts +5 -6
- package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.ts +1 -1
- package/src/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.spec.ts +64 -0
- package/src/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.ts +2 -5
- package/src/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.types.ts +1 -1
- package/src/domains/manifest/utils/validateRuntimeImage.ts +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@squiz/job-runner-lib",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"private": false,
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@faker-js/faker": "8.0.2",
|
|
21
21
|
"@smithy/util-stream": "4.5.17",
|
|
22
|
-
"@squiz/virus-scanner-lib": "1.68.
|
|
22
|
+
"@squiz/virus-scanner-lib": "1.68.1",
|
|
23
23
|
"@types/cli-color": "2.0.6",
|
|
24
24
|
"@types/fs-extra": "11.0.1",
|
|
25
25
|
"@types/jest": "28.1.8",
|
|
@@ -34,11 +34,12 @@
|
|
|
34
34
|
"@aws-sdk/client-s3": "3.1003.0",
|
|
35
35
|
"@aws-sdk/client-sqs": "3.1003.0",
|
|
36
36
|
"@aws-sdk/s3-request-presigner": "3.1003.0",
|
|
37
|
+
"@aws-sdk/util-dynamodb": "3.996.2",
|
|
37
38
|
"@squiz/dx-common-lib": "1.66.1",
|
|
38
39
|
"@squiz/dx-json-schema-lib": "1.86.0",
|
|
39
40
|
"@squiz/dx-logger-lib": "1.64.0",
|
|
40
41
|
"@squiz/optimization-logger": "1.11.2",
|
|
41
|
-
"axios": "1.
|
|
42
|
+
"axios": "1.19.0",
|
|
42
43
|
"cli-color": "2.0.4",
|
|
43
44
|
"fs-extra": "11.1.0",
|
|
44
45
|
"immer": "10.1.3",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright Squiz Australia Pty Ltd. All Rights Reserved.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { RECOMMENDED_RUNTIME_IMAGE, getRuntimeImageOrDefault } from './RuntimeImage.constants';
|
|
7
|
+
|
|
8
|
+
describe('RuntimeImage.constants', () => {
|
|
9
|
+
describe('getRuntimeImageOrDefault', () => {
|
|
10
|
+
it('returns the recommended runtime image when image is undefined', () => {
|
|
11
|
+
expect(getRuntimeImageOrDefault(undefined)).toEqual(RECOMMENDED_RUNTIME_IMAGE);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('returns the recommended runtime image when image is null', () => {
|
|
15
|
+
expect(getRuntimeImageOrDefault(null)).toEqual(RECOMMENDED_RUNTIME_IMAGE);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('remaps deprecated node:20 to the recommended runtime image', () => {
|
|
19
|
+
expect(getRuntimeImageOrDefault('node:20')).toEqual(RECOMMENDED_RUNTIME_IMAGE);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns node:24 unchanged', () => {
|
|
23
|
+
expect(getRuntimeImageOrDefault('node:24')).toEqual('node:24');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -3,24 +3,21 @@
|
|
|
3
3
|
* Copyright Squiz Australia Pty Ltd. All Rights Reserved.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export const SUPPORTED_RUNTIME_IMAGES = ['node:
|
|
7
|
-
|
|
8
|
-
// Default for jobs that pre-date runtime image support.
|
|
9
|
-
export const LEGACY_RUNTIME_IMAGE = 'node:20';
|
|
6
|
+
export const SUPPORTED_RUNTIME_IMAGES = ['node:24'] as const;
|
|
10
7
|
|
|
11
8
|
export const RECOMMENDED_RUNTIME_IMAGE = 'node:24';
|
|
12
9
|
|
|
13
|
-
//
|
|
10
|
+
// Stored on old job records; remapped to RECOMMENDED_RUNTIME_IMAGE at read/execute time. Rejected on new uploads.
|
|
14
11
|
export const DEPRECATED_RUNTIME_IMAGES: readonly string[] = ['node:20'];
|
|
15
12
|
|
|
13
|
+
export const isDeprecatedRuntimeImage = (image: string): boolean => DEPRECATED_RUNTIME_IMAGES.includes(image);
|
|
14
|
+
|
|
16
15
|
export const getRuntimeImageOrDefault = (image: string | null | undefined): string =>
|
|
17
|
-
image === null || image === undefined ?
|
|
16
|
+
image === null || image === undefined || isDeprecatedRuntimeImage(image) ? RECOMMENDED_RUNTIME_IMAGE : image;
|
|
18
17
|
|
|
19
18
|
export const isSupportedRuntimeImage = (image: string): boolean =>
|
|
20
19
|
(SUPPORTED_RUNTIME_IMAGES as readonly string[]).includes(image);
|
|
21
20
|
|
|
22
|
-
export const isDeprecatedRuntimeImage = (image: string): boolean => DEPRECATED_RUNTIME_IMAGES.includes(image);
|
|
23
|
-
|
|
24
21
|
export const getRuntimeImagesMetadata = () => ({
|
|
25
22
|
supportedRuntimeImages: [...SUPPORTED_RUNTIME_IMAGES],
|
|
26
23
|
deprecatedRuntimeImages: [...DEPRECATED_RUNTIME_IMAGES],
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright Squiz Australia Pty Ltd. All Rights Reserved.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ERRORS } from './Error.constants';
|
|
7
|
+
import { ErrorWithHttpStatusCode } from './ErrorWithHttpStatusCode';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Conflict error (409).
|
|
11
|
+
* @export
|
|
12
|
+
* @class ConflictError
|
|
13
|
+
* @extends {ErrorWithHttpStatusCode}
|
|
14
|
+
*/
|
|
15
|
+
export class ConflictError extends ErrorWithHttpStatusCode {
|
|
16
|
+
constructor(details: string, message = ERRORS.conflict.name) {
|
|
17
|
+
super(ERRORS.conflict.code, message, details);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { faker } from '@faker-js/faker/locale/en';
|
|
7
7
|
import { ENTITY_TYPES } from '../../constants/Repository.constants';
|
|
8
|
-
import {
|
|
8
|
+
import { RECOMMENDED_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
|
|
9
9
|
import { JobManifest } from '../JobManifestTypes';
|
|
10
10
|
|
|
11
11
|
export const createMockJobManifest = (partialRecord: Partial<JobManifest> = {}): JobManifest => {
|
|
@@ -25,7 +25,7 @@ export const createMockJobManifest = (partialRecord: Partial<JobManifest> = {}):
|
|
|
25
25
|
name: faker.lorem.word(),
|
|
26
26
|
version,
|
|
27
27
|
timeoutSeconds: faker.number.int({ max: 28800, min: 5 }),
|
|
28
|
-
image:
|
|
28
|
+
image: RECOMMENDED_RUNTIME_IMAGE,
|
|
29
29
|
autoRetry: false,
|
|
30
30
|
type: ENTITY_TYPES.manifest,
|
|
31
31
|
...partialRecord,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { faker } from '@faker-js/faker/locale/en';
|
|
7
7
|
import { ENTITY_TYPES } from '../../constants/Repository.constants';
|
|
8
|
-
import {
|
|
8
|
+
import { RECOMMENDED_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
|
|
9
9
|
import { JobManifestDto, JobManifestRecord } from '../JobManifestTypes';
|
|
10
10
|
|
|
11
11
|
export const createMockJobManifestDto = (partialRecord: Partial<JobManifestRecord> = {}): JobManifestDto => {
|
|
@@ -40,7 +40,7 @@ export const createMockJobManifestDto = (partialRecord: Partial<JobManifestRecor
|
|
|
40
40
|
N: (partialRecord.timeoutSeconds ?? faker.number.int({ max: 28800, min: 5 })).toString(),
|
|
41
41
|
},
|
|
42
42
|
image: {
|
|
43
|
-
S: partialRecord.image ??
|
|
43
|
+
S: partialRecord.image ?? RECOMMENDED_RUNTIME_IMAGE,
|
|
44
44
|
},
|
|
45
45
|
autoRetry: {
|
|
46
46
|
BOOL: partialRecord.autoRetry ?? false,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { faker } from '@faker-js/faker/locale/en';
|
|
7
7
|
import { ENTITY_TYPES } from '../../constants/Repository.constants';
|
|
8
|
-
import {
|
|
8
|
+
import { RECOMMENDED_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
|
|
9
9
|
import { JobManifestRecord } from '../JobManifestTypes';
|
|
10
10
|
|
|
11
11
|
export const createMockJobManifestRecord = (partialRecord: Partial<JobManifestRecord> = {}): JobManifestRecord => {
|
|
@@ -29,7 +29,7 @@ export const createMockJobManifestRecord = (partialRecord: Partial<JobManifestRe
|
|
|
29
29
|
name,
|
|
30
30
|
version,
|
|
31
31
|
timeoutSeconds: faker.number.int({ max: 28800, min: 5 }),
|
|
32
|
-
image:
|
|
32
|
+
image: RECOMMENDED_RUNTIME_IMAGE,
|
|
33
33
|
autoRetry: false,
|
|
34
34
|
...partialRecord,
|
|
35
35
|
};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { faker } from '@faker-js/faker/locale/en';
|
|
7
|
-
import {
|
|
7
|
+
import { RECOMMENDED_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
|
|
8
8
|
import { CreateJobManifestRequest } from '../JobManifestTypes';
|
|
9
9
|
|
|
10
10
|
export const createMockCreateJobManifestRequest = (
|
|
@@ -26,7 +26,7 @@ export const createMockCreateJobManifestRequest = (
|
|
|
26
26
|
name: faker.lorem.word(),
|
|
27
27
|
version,
|
|
28
28
|
timeoutSeconds: faker.number.int({ max: 28800, min: 5 }),
|
|
29
|
-
image:
|
|
29
|
+
image: RECOMMENDED_RUNTIME_IMAGE,
|
|
30
30
|
autoRetry: false,
|
|
31
31
|
...partialRecord,
|
|
32
32
|
};
|
|
@@ -48,3 +48,9 @@ export type ListParameters<TFilters> = {
|
|
|
48
48
|
consistentRead?: boolean;
|
|
49
49
|
limit?: number;
|
|
50
50
|
};
|
|
51
|
+
|
|
52
|
+
export type UpdateOptions = {
|
|
53
|
+
conditionExpression?: string;
|
|
54
|
+
expressionAttributeValues?: Record<string, unknown>;
|
|
55
|
+
expressionAttributeNames?: Record<string, string>;
|
|
56
|
+
};
|
|
@@ -62,6 +62,13 @@ export interface JobExecution {
|
|
|
62
62
|
*/
|
|
63
63
|
ecsTaskId: string | null;
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The last version of the ECS task event which was handled.
|
|
67
|
+
* @type {(number | null)}
|
|
68
|
+
* @memberof JobExecution
|
|
69
|
+
*/
|
|
70
|
+
lastEcsTaskEventVersion: number | null;
|
|
71
|
+
|
|
65
72
|
/**
|
|
66
73
|
* The time the job run was started.
|
|
67
74
|
* @type {(string | null)}
|
|
@@ -184,21 +191,28 @@ export interface JobExecutionRecord {
|
|
|
184
191
|
|
|
185
192
|
/**
|
|
186
193
|
* The version of the job script to execute.
|
|
187
|
-
* @type
|
|
194
|
+
* @type {(string)}
|
|
188
195
|
* @memberof JobExecutionRecord
|
|
189
196
|
*/
|
|
190
197
|
version: string;
|
|
191
198
|
|
|
192
199
|
/**
|
|
193
200
|
* The ECS task ID of the job run.
|
|
194
|
-
* @type
|
|
201
|
+
* @type {(string | null)}
|
|
195
202
|
* @memberof JobExecutionRecord
|
|
196
203
|
*/
|
|
197
204
|
ecsTaskId: string | null;
|
|
198
205
|
|
|
206
|
+
/**
|
|
207
|
+
* The last version of the ECS task event which was handled.
|
|
208
|
+
* @type {(number | null)}
|
|
209
|
+
* @memberof JobExecution
|
|
210
|
+
*/
|
|
211
|
+
lastEcsTaskEventVersion: number | null;
|
|
212
|
+
|
|
199
213
|
/**
|
|
200
214
|
* The time the job run was started.
|
|
201
|
-
* @type
|
|
215
|
+
* @type {(string | null)}
|
|
202
216
|
* @memberof JobExecutionRecord
|
|
203
217
|
*/
|
|
204
218
|
timeStarted: string | null;
|
|
@@ -24,6 +24,7 @@ export const createMockJobExecution = (partialRecord: Partial<JobExecution> = {}
|
|
|
24
24
|
return {
|
|
25
25
|
context: faker.string.uuid(),
|
|
26
26
|
ecsTaskId: null,
|
|
27
|
+
lastEcsTaskEventVersion: null,
|
|
27
28
|
id: `${timeQueued}-${faker.string.uuid()}`,
|
|
28
29
|
jobName,
|
|
29
30
|
status: JOB_EXECUTION_STATUS.queued,
|
|
@@ -34,6 +34,9 @@ export const createMockJobExecutionDto = (partialRecord: Partial<JobExecutionRec
|
|
|
34
34
|
S: partialRecord.context ?? faker.string.uuid(),
|
|
35
35
|
},
|
|
36
36
|
ecsTaskId: partialRecord.ecsTaskId ? { S: partialRecord.ecsTaskId } : { NULL: true },
|
|
37
|
+
lastEcsTaskEventVersion: partialRecord.lastEcsTaskEventVersion
|
|
38
|
+
? { N: partialRecord.lastEcsTaskEventVersion.toString() }
|
|
39
|
+
: { NULL: true },
|
|
37
40
|
gsi1pk: {
|
|
38
41
|
S: `${ENTITY_TYPES.jobExecution}~${status}`,
|
|
39
42
|
},
|
|
@@ -28,6 +28,7 @@ export const createMockJobExecutionRecord = (partialRecord: Partial<JobExecution
|
|
|
28
28
|
return {
|
|
29
29
|
context: faker.string.uuid(),
|
|
30
30
|
ecsTaskId: null,
|
|
31
|
+
lastEcsTaskEventVersion: null,
|
|
31
32
|
gsi1pk: `${ENTITY_TYPES.jobExecution}~${status}`,
|
|
32
33
|
id: id,
|
|
33
34
|
jobName: jobName,
|
|
@@ -207,6 +207,7 @@ describe('JobExecutionRepository', (): void => {
|
|
|
207
207
|
it(`should return job execution by timeQueued and id and handle any null values`, async (): Promise<void> => {
|
|
208
208
|
const mockJobExecutionRecord = createMockJobExecutionDto({
|
|
209
209
|
ecsTaskId: 'ecs-task-id',
|
|
210
|
+
lastEcsTaskEventVersion: 1,
|
|
210
211
|
timeFinished: new Date().toISOString(),
|
|
211
212
|
timeStarted: new Date().toISOString(),
|
|
212
213
|
version: '1.0.0',
|
|
@@ -225,6 +226,7 @@ describe('JobExecutionRepository', (): void => {
|
|
|
225
226
|
expect(response).toStrictEqual({
|
|
226
227
|
context: mockJobExecutionRecord.context.S as string,
|
|
227
228
|
ecsTaskId: mockJobExecutionRecord.ecsTaskId.S as string,
|
|
229
|
+
lastEcsTaskEventVersion: parseInt(mockJobExecutionRecord.lastEcsTaskEventVersion.N as string),
|
|
228
230
|
id: mockJobExecutionRecord.id.S as string,
|
|
229
231
|
jobName: mockJobExecutionRecord.jobName.S as string,
|
|
230
232
|
status: mockJobExecutionRecord.status.S as string,
|
|
@@ -452,6 +454,7 @@ describe('JobExecutionRepository', (): void => {
|
|
|
452
454
|
const updatedAttributes: UpdateJobExecutionParameters = {
|
|
453
455
|
input: faker.lorem.words(),
|
|
454
456
|
ecsTaskId: faker.string.uuid(),
|
|
457
|
+
lastEcsTaskEventVersion: faker.number.int(),
|
|
455
458
|
version: faker.system.semver(),
|
|
456
459
|
timeStarted: faker.date.recent().toISOString(),
|
|
457
460
|
timeFinished: null,
|
|
@@ -471,20 +474,22 @@ describe('JobExecutionRepository', (): void => {
|
|
|
471
474
|
expect(result).toEqual(updatedJobExecution);
|
|
472
475
|
expect(dynamoClient).toHaveReceivedCommandWith(UpdateItemCommand, {
|
|
473
476
|
ExpressionAttributeNames: {
|
|
474
|
-
'#
|
|
475
|
-
'#
|
|
476
|
-
'#
|
|
477
|
-
'#
|
|
478
|
-
'#
|
|
479
|
-
'#
|
|
477
|
+
'#updateExpr0': 'input',
|
|
478
|
+
'#updateExpr1': 'ecsTaskId',
|
|
479
|
+
'#updateExpr2': 'lastEcsTaskEventVersion',
|
|
480
|
+
'#updateExpr3': 'version',
|
|
481
|
+
'#updateExpr4': 'timeStarted',
|
|
482
|
+
'#updateExpr5': 'timeFinished',
|
|
483
|
+
'#updateExpr6': 'context',
|
|
480
484
|
},
|
|
481
485
|
ExpressionAttributeValues: {
|
|
482
|
-
':
|
|
483
|
-
':
|
|
484
|
-
':
|
|
485
|
-
':
|
|
486
|
-
':
|
|
487
|
-
':
|
|
486
|
+
':updateExpr0': { S: updatedAttributes.input },
|
|
487
|
+
':updateExpr1': { S: updatedAttributes.ecsTaskId! },
|
|
488
|
+
':updateExpr2': { N: `${updatedAttributes.lastEcsTaskEventVersion}` },
|
|
489
|
+
':updateExpr3': { S: updatedAttributes.version! },
|
|
490
|
+
':updateExpr4': { S: updatedAttributes.timeStarted! },
|
|
491
|
+
':updateExpr5': { NULL: true },
|
|
492
|
+
':updateExpr6': { S: updatedAttributes.context! },
|
|
488
493
|
},
|
|
489
494
|
Key: {
|
|
490
495
|
pk: { S: `jobExecution~${tenant}` },
|
|
@@ -493,7 +498,7 @@ describe('JobExecutionRepository', (): void => {
|
|
|
493
498
|
ReturnValues: 'ALL_NEW',
|
|
494
499
|
TableName: tableName,
|
|
495
500
|
UpdateExpression:
|
|
496
|
-
'SET #
|
|
501
|
+
'SET #updateExpr0 = :updateExpr0, #updateExpr1 = :updateExpr1, #updateExpr2 = :updateExpr2, #updateExpr3 = :updateExpr3, #updateExpr4 = :updateExpr4, #updateExpr5 = :updateExpr5, #updateExpr6 = :updateExpr6',
|
|
497
502
|
});
|
|
498
503
|
});
|
|
499
504
|
|
|
@@ -517,12 +522,68 @@ describe('JobExecutionRepository', (): void => {
|
|
|
517
522
|
ExpressionAttributeNames: {
|
|
518
523
|
'#gsi1pk': 'gsi1pk',
|
|
519
524
|
'#lsi1sk': 'lsi1sk',
|
|
520
|
-
'#
|
|
525
|
+
'#updateExpr0': 'status',
|
|
521
526
|
},
|
|
522
527
|
ExpressionAttributeValues: {
|
|
523
528
|
':gsi1pk': { S: `jobExecution~${updatedJobExecution.status}` },
|
|
524
529
|
':lsi1sk': { S: `${updatedJobExecution.status}~${updatedJobExecution.jobName}` },
|
|
525
|
-
':
|
|
530
|
+
':updateExpr0': { S: updatedJobExecution.status },
|
|
531
|
+
},
|
|
532
|
+
Key: {
|
|
533
|
+
pk: { S: `jobExecution~${tenant}` },
|
|
534
|
+
sk: { S: `${jobExecution.jobName}~${jobExecution.id}` },
|
|
535
|
+
},
|
|
536
|
+
ReturnValues: 'ALL_NEW',
|
|
537
|
+
TableName: tableName,
|
|
538
|
+
UpdateExpression: 'SET #updateExpr0 = :updateExpr0, #lsi1sk = :lsi1sk, #gsi1pk = :gsi1pk',
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('Marshalls expression attribute values provided for conditional updates', async () => {
|
|
543
|
+
const jobExecution = createMockJobExecution({ status: JOB_EXECUTION_STATUS.queued });
|
|
544
|
+
const updatedJobExecution = createMockJobExecution({
|
|
545
|
+
...jobExecution,
|
|
546
|
+
status: JOB_EXECUTION_STATUS.running,
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
dynamoClient.on(UpdateItemCommand).resolvesOnce({
|
|
550
|
+
Attributes: createMockJobExecutionDto(updatedJobExecution),
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const result = await jobExecutionRepository.update(
|
|
554
|
+
jobExecution,
|
|
555
|
+
{
|
|
556
|
+
status: updatedJobExecution.status,
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
conditionExpression: '#ecsTaskId = :ecsTaskId AND #status = :status',
|
|
560
|
+
expressionAttributeNames: {
|
|
561
|
+
'#status': 'status',
|
|
562
|
+
'#ecsTaskId': 'ecsTaskId',
|
|
563
|
+
},
|
|
564
|
+
expressionAttributeValues: {
|
|
565
|
+
ecsTaskId: null,
|
|
566
|
+
status: JOB_EXECUTION_STATUS.queued,
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
expect(result).toEqual(updatedJobExecution);
|
|
572
|
+
expect(dynamoClient).toHaveReceivedCommandWith(UpdateItemCommand, {
|
|
573
|
+
ConditionExpression: '#ecsTaskId = :ecsTaskId AND #status = :status',
|
|
574
|
+
ExpressionAttributeNames: {
|
|
575
|
+
'#ecsTaskId': 'ecsTaskId',
|
|
576
|
+
'#gsi1pk': 'gsi1pk',
|
|
577
|
+
'#lsi1sk': 'lsi1sk',
|
|
578
|
+
'#status': 'status',
|
|
579
|
+
'#updateExpr0': 'status',
|
|
580
|
+
},
|
|
581
|
+
ExpressionAttributeValues: {
|
|
582
|
+
':gsi1pk': { S: 'jobExecution~running' },
|
|
583
|
+
':lsi1sk': { S: `running~${jobExecution.jobName}` },
|
|
584
|
+
':updateExpr0': { S: 'running' },
|
|
585
|
+
ecsTaskId: { NULL: true },
|
|
586
|
+
status: { S: 'queued' },
|
|
526
587
|
},
|
|
527
588
|
Key: {
|
|
528
589
|
pk: { S: `jobExecution~${tenant}` },
|
|
@@ -530,7 +591,7 @@ describe('JobExecutionRepository', (): void => {
|
|
|
530
591
|
},
|
|
531
592
|
ReturnValues: 'ALL_NEW',
|
|
532
593
|
TableName: tableName,
|
|
533
|
-
UpdateExpression: 'SET #
|
|
594
|
+
UpdateExpression: 'SET #updateExpr0 = :updateExpr0, #lsi1sk = :lsi1sk, #gsi1pk = :gsi1pk',
|
|
534
595
|
});
|
|
535
596
|
});
|
|
536
597
|
});
|
|
@@ -35,6 +35,8 @@ import { provideTransient } from '../../transientProvider';
|
|
|
35
35
|
import { Repository } from '../AbstractRepository';
|
|
36
36
|
import { InjectTokens } from '../../constants/InjectTokens';
|
|
37
37
|
import { convertDynamoDbItemToJobExecution } from './utils';
|
|
38
|
+
import { UpdateOptions } from '../../model/Base';
|
|
39
|
+
import { marshall } from '@aws-sdk/util-dynamodb';
|
|
38
40
|
|
|
39
41
|
@provideTransient(JobExecutionRepository)
|
|
40
42
|
export class JobExecutionRepository extends Repository<JobExecution, JobExecutionDto, JobExecutionSk> {
|
|
@@ -52,6 +54,7 @@ export class JobExecutionRepository extends Repository<JobExecution, JobExecutio
|
|
|
52
54
|
const newJobExecution: JobExecution = {
|
|
53
55
|
type: `${ENTITY_TYPES.jobExecution}-${createParams.jobName}`,
|
|
54
56
|
ecsTaskId: null,
|
|
57
|
+
lastEcsTaskEventVersion: null,
|
|
55
58
|
timeStarted: null,
|
|
56
59
|
timeFinished: null,
|
|
57
60
|
input: null,
|
|
@@ -240,11 +243,7 @@ export class JobExecutionRepository extends Repository<JobExecution, JobExecutio
|
|
|
240
243
|
async update(
|
|
241
244
|
sk: JobExecutionSk,
|
|
242
245
|
input: UpdateJobExecutionParameters,
|
|
243
|
-
options?:
|
|
244
|
-
ConditionExpression?: string;
|
|
245
|
-
ExpressionAttributeValues?: Record<string, AttributeValue>;
|
|
246
|
-
ExpressionAttributeNames?: Record<string, string>;
|
|
247
|
-
},
|
|
246
|
+
options?: UpdateOptions,
|
|
248
247
|
): Promise<JobExecution> {
|
|
249
248
|
// Initialise update command
|
|
250
249
|
const updateParams: UpdateItemCommandInput = {
|
|
@@ -259,11 +258,13 @@ export class JobExecutionRepository extends Repository<JobExecution, JobExecutio
|
|
|
259
258
|
const expressionAttributeNames: Record<string, string> = {};
|
|
260
259
|
const expressionAttributeValues: Record<string, AttributeValue> = {};
|
|
261
260
|
const updateExpressionPairs: Array<string> = [];
|
|
261
|
+
let index = 0;
|
|
262
262
|
|
|
263
263
|
// To avoid conflict with reserved keywords, refer to attributes with an additional '#' prefix
|
|
264
|
-
for (const
|
|
265
|
-
const
|
|
266
|
-
|
|
264
|
+
for (const property of Object.keys(input)) {
|
|
265
|
+
const key = `updateExpr${index++}`;
|
|
266
|
+
const val = input[property as keyof UpdateJobExecutionParameters];
|
|
267
|
+
expressionAttributeNames[`#${key}`] = property;
|
|
267
268
|
|
|
268
269
|
if (val === null) {
|
|
269
270
|
expressionAttributeValues[`:${key}`] = { NULL: true };
|
|
@@ -275,7 +276,7 @@ export class JobExecutionRepository extends Repository<JobExecution, JobExecutio
|
|
|
275
276
|
|
|
276
277
|
updateExpressionPairs.push(`#${key} = :${key}`);
|
|
277
278
|
|
|
278
|
-
if (
|
|
279
|
+
if (property === 'status') {
|
|
279
280
|
expressionAttributeNames[`#lsi1sk`] = 'lsi1sk';
|
|
280
281
|
expressionAttributeValues[`:lsi1sk`] = { S: `${val || ''}~${sk.jobName}` };
|
|
281
282
|
expressionAttributeNames[`#gsi1pk`] = 'gsi1pk';
|
|
@@ -290,23 +291,23 @@ export class JobExecutionRepository extends Repository<JobExecution, JobExecutio
|
|
|
290
291
|
updateParams.UpdateExpression = `SET ${updateExpressionPairs.join(', ')}`;
|
|
291
292
|
|
|
292
293
|
// Add condition expression if provided
|
|
293
|
-
if (options?.
|
|
294
|
-
updateParams.ConditionExpression = options.
|
|
294
|
+
if (options?.conditionExpression) {
|
|
295
|
+
updateParams.ConditionExpression = options.conditionExpression;
|
|
295
296
|
}
|
|
296
297
|
|
|
297
|
-
if (options?.
|
|
298
|
+
if (options?.expressionAttributeValues) {
|
|
298
299
|
// Merge existing expression attribute values with the ones from options
|
|
299
300
|
updateParams.ExpressionAttributeValues = {
|
|
300
301
|
...updateParams.ExpressionAttributeValues,
|
|
301
|
-
...options.
|
|
302
|
+
...marshall(options.expressionAttributeValues),
|
|
302
303
|
};
|
|
303
304
|
}
|
|
304
305
|
|
|
305
306
|
// Add expression attribute names if provided
|
|
306
|
-
if (options?.
|
|
307
|
+
if (options?.expressionAttributeNames) {
|
|
307
308
|
updateParams.ExpressionAttributeNames = {
|
|
308
309
|
...updateParams.ExpressionAttributeNames,
|
|
309
|
-
...options.
|
|
310
|
+
...options.expressionAttributeNames,
|
|
310
311
|
};
|
|
311
312
|
}
|
|
312
313
|
|
|
@@ -325,6 +326,9 @@ export class JobExecutionRepository extends Repository<JobExecution, JobExecutio
|
|
|
325
326
|
return {
|
|
326
327
|
context: { S: item.context },
|
|
327
328
|
ecsTaskId: item.ecsTaskId ? { S: item.ecsTaskId } : { NULL: true },
|
|
329
|
+
lastEcsTaskEventVersion: item.lastEcsTaskEventVersion
|
|
330
|
+
? { N: item.lastEcsTaskEventVersion.toString() }
|
|
331
|
+
: { NULL: true },
|
|
328
332
|
gsi1pk: { S: `${ENTITY_TYPES.jobExecution}~${item.status}` },
|
|
329
333
|
id: { S: item.id },
|
|
330
334
|
jobName: { S: item.jobName },
|
|
@@ -6,6 +6,7 @@ export const convertDynamoDbItemToJobExecution = (dbItem: JobExecutionDto): JobE
|
|
|
6
6
|
return {
|
|
7
7
|
context: dbItem.context.S as string,
|
|
8
8
|
ecsTaskId: dbItem.ecsTaskId.S ? (dbItem.ecsTaskId.S as string) : null,
|
|
9
|
+
lastEcsTaskEventVersion: dbItem.lastEcsTaskEventVersion.N ? parseInt(dbItem.lastEcsTaskEventVersion.N, 10) : null,
|
|
9
10
|
id: dbItem.id.S as string,
|
|
10
11
|
jobName: dbItem.jobName.S as string,
|
|
11
12
|
status: dbItem.status.S as JOB_EXECUTION_STATUS,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from '@aws-sdk/client-dynamodb';
|
|
14
14
|
import { initIocContainer, iocContainer } from '../../../../mocks/mockIoc';
|
|
15
15
|
import { ENTITY_TYPES } from '../../constants/Repository.constants';
|
|
16
|
-
import {
|
|
16
|
+
import { RECOMMENDED_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
|
|
17
17
|
import {
|
|
18
18
|
createMockClientGetResponse,
|
|
19
19
|
createMockClientPutResponse,
|
|
@@ -126,7 +126,7 @@ describe('JobManifestRepository', (): void => {
|
|
|
126
126
|
expect(mockSend).toBeCalledTimes(1);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
-
it(`should default missing image to
|
|
129
|
+
it(`should default missing image to the recommended runtime`, async (): Promise<void> => {
|
|
130
130
|
const mockJobManifestItem = createMockJobManifestDto();
|
|
131
131
|
delete (mockJobManifestItem as Partial<typeof mockJobManifestItem>).image;
|
|
132
132
|
|
|
@@ -143,12 +143,33 @@ describe('JobManifestRepository', (): void => {
|
|
|
143
143
|
});
|
|
144
144
|
expect(response).toEqual(
|
|
145
145
|
expect.objectContaining({
|
|
146
|
-
image:
|
|
146
|
+
image: RECOMMENDED_RUNTIME_IMAGE,
|
|
147
147
|
autoRetry: false,
|
|
148
148
|
}),
|
|
149
149
|
);
|
|
150
150
|
});
|
|
151
151
|
|
|
152
|
+
it('should remap deprecated node:20 runtime image to node:24', async (): Promise<void> => {
|
|
153
|
+
const mockJobManifestItem = createMockJobManifestDto({ image: 'node:20' });
|
|
154
|
+
|
|
155
|
+
mockSend.mockImplementationOnce(
|
|
156
|
+
(): GetItemCommandOutput =>
|
|
157
|
+
createMockClientGetResponse({
|
|
158
|
+
Item: mockJobManifestItem,
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const response = await jobManifestRepository.findOne({
|
|
163
|
+
name: mockJobManifestItem.name.S as string,
|
|
164
|
+
version: mockJobManifestItem.version.S as string,
|
|
165
|
+
});
|
|
166
|
+
expect(response).toEqual(
|
|
167
|
+
expect.objectContaining({
|
|
168
|
+
image: RECOMMENDED_RUNTIME_IMAGE,
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
152
173
|
it(`should default missing autoRetry to false`, async (): Promise<void> => {
|
|
153
174
|
const mockJobManifestItem = createMockJobManifestDto();
|
|
154
175
|
delete (mockJobManifestItem as Partial<typeof mockJobManifestItem>).autoRetry;
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { initIocContainer, iocContainer } from '../../../../mocks/mockIoc';
|
|
17
17
|
import { InjectTokens } from '../../constants/InjectTokens';
|
|
18
18
|
import { ENTITY_TYPES } from '../../constants/Repository.constants';
|
|
19
|
-
import {
|
|
19
|
+
import { RECOMMENDED_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
|
|
20
20
|
import {
|
|
21
21
|
createMockClientGetResponse,
|
|
22
22
|
createMockClientPutResponse,
|
|
@@ -176,7 +176,7 @@ describe('JobRepository', (): void => {
|
|
|
176
176
|
expect(mockSend).toBeCalledTimes(1);
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
-
it(`should default missing image to
|
|
179
|
+
it(`should default missing image to the recommended runtime`, async (): Promise<void> => {
|
|
180
180
|
const mockJobRecord = createMockJobDto();
|
|
181
181
|
delete (mockJobRecord as Partial<typeof mockJobRecord>).image;
|
|
182
182
|
mockSend.mockImplementationOnce(
|
|
@@ -193,7 +193,28 @@ describe('JobRepository', (): void => {
|
|
|
193
193
|
|
|
194
194
|
expect(response).toEqual(
|
|
195
195
|
expect.objectContaining({
|
|
196
|
-
image:
|
|
196
|
+
image: RECOMMENDED_RUNTIME_IMAGE,
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should remap deprecated node:20 runtime image to node:24', async (): Promise<void> => {
|
|
202
|
+
const mockJobRecord = createMockJobDto({ image: 'node:20' });
|
|
203
|
+
mockSend.mockImplementationOnce(
|
|
204
|
+
(): GetItemCommandOutput =>
|
|
205
|
+
createMockClientGetResponse({
|
|
206
|
+
Item: mockJobRecord,
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const response = await jobRepository.findOne({
|
|
211
|
+
name: mockJobRecord.name.S as string,
|
|
212
|
+
version: mockJobRecord.version.S as string,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(response).toEqual(
|
|
216
|
+
expect.objectContaining({
|
|
217
|
+
image: RECOMMENDED_RUNTIME_IMAGE,
|
|
197
218
|
}),
|
|
198
219
|
);
|
|
199
220
|
});
|