@squiz/job-runner-lib 2.2.0 → 2.4.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/domains/component/errors/JobError.spec.d.ts +1 -0
  3. package/lib/domains/job/ParsePartitionKey/ParsePartitionKey.d.ts +2 -2
  4. package/lib/domains/job/ParsePartitionKey/ParsePartitionKey.types.d.ts +5 -0
  5. package/lib/domains/job/S3Repository/JobDetailsRepository.d.ts +1 -2
  6. package/lib/domains/job/S3Repository/JobDetailsRepository.spec.d.ts +1 -1
  7. package/lib/domains/job/S3Repository/JobUploadsRepository.d.ts +16 -0
  8. package/lib/domains/job/{services/JobQueueService/JobQueueService.spec.d.ts → S3Repository/JobUploadsRepository.spec.d.ts} +1 -1
  9. package/lib/domains/job/S3Repository/S3RepositoryBase.d.ts +12 -5
  10. package/lib/domains/job/S3Repository/index.d.ts +2 -1
  11. package/lib/domains/job/constants/InjectTokens.d.ts +8 -0
  12. package/lib/domains/job/constants/JobExecutionStatus.constants.d.ts +2 -1
  13. package/lib/domains/job/constants/Repository.constants.d.ts +3 -3
  14. package/lib/domains/job/constants/RetryConfig.constants.d.ts +21 -0
  15. package/lib/domains/job/index.d.ts +6 -4
  16. package/lib/domains/job/manifest/JobManifestTypes.d.ts +7 -6
  17. package/lib/domains/job/model/Base/Base.d.ts +10 -5
  18. package/lib/domains/job/model/Job/Job.d.ts +9 -6
  19. package/lib/domains/job/model/JobContext/JobContext.d.ts +6 -5
  20. package/lib/domains/job/model/JobExecution/JobExecution.d.ts +64 -9
  21. package/lib/domains/job/model/LogPayload/LogPayload.d.ts +21 -12
  22. package/lib/domains/job/model/LogPayload/index.d.ts +1 -0
  23. package/lib/domains/job/model/LogPayload/mocks/LogPayload.mock.d.ts +6 -0
  24. package/lib/domains/job/repository/AbstractRepository.d.ts +20 -11
  25. package/lib/domains/job/repository/JobContextRepository/JobContextRepository.d.ts +6 -7
  26. package/lib/domains/job/repository/JobExecutionRepository/JobExecutionRepository.d.ts +7 -17
  27. package/lib/domains/job/repository/JobExecutionRepository/JobExecutionRepository.spec.d.ts +1 -1
  28. package/lib/domains/job/repository/JobExecutionRepository/utils/convertDynamoDbItemToJobExecution/convertDynamoDbItemToJobExecution.d.ts +2 -0
  29. package/lib/domains/job/repository/JobExecutionRepository/utils/index.d.ts +1 -0
  30. package/lib/domains/job/repository/JobManifestRepository/JobManifestRepository.d.ts +5 -6
  31. package/lib/domains/job/repository/JobRepository/JobRepository.d.ts +6 -7
  32. package/lib/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.d.ts +9 -0
  33. package/lib/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.spec.d.ts +1 -0
  34. package/lib/domains/job/repository/index.d.ts +1 -0
  35. package/lib/domains/job/services/JobContextService/JobContextService.d.ts +6 -3
  36. package/lib/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.d.ts +10 -6
  37. package/lib/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.defs.d.ts +1 -3
  38. package/lib/domains/job/services/JobManifestService/JobManifestService.d.ts +0 -1
  39. package/lib/domains/job/services/JobStartQueueService/JobStartQueueService.d.ts +17 -0
  40. package/lib/domains/job/services/JobStartQueueService/JobStartQueueService.defs.d.ts +25 -0
  41. package/lib/domains/job/services/JobUploadService/utils/manifestValidator.d.ts +1 -2
  42. package/lib/domains/job/services/index.d.ts +2 -1
  43. package/lib/domains/manifest/services/ManifestServiceForDev.d.ts +0 -3
  44. package/lib/index.d.ts +1 -0
  45. package/lib/index.js +13 -13
  46. package/lib/index.js.map +4 -4
  47. package/lib/jest-global-setup.d.ts +6 -1
  48. package/lib/mocks/mockIoc.d.ts +3 -3
  49. package/lib/utils/buildJobRunnerModule/buildJobRunnerModule.d.ts +2 -0
  50. package/lib/utils/buildJobRunnerModule/buildJobRunnerModule.types.d.ts +14 -0
  51. package/lib/utils/index.d.ts +2 -0
  52. package/package.json +12 -11
  53. package/src/domains/component/errors/JobError.spec.ts +30 -0
  54. package/src/domains/component/errors/JobError.ts +8 -1
  55. package/src/domains/job/ParsePartitionKey/ParsePartitionKey.spec.ts +17 -8
  56. package/src/domains/job/ParsePartitionKey/ParsePartitionKey.ts +13 -16
  57. package/src/domains/job/ParsePartitionKey/ParsePartitionKey.types.ts +6 -0
  58. package/src/domains/job/S3Repository/JobDetailsRepository.spec.ts +47 -31
  59. package/src/domains/job/S3Repository/JobDetailsRepository.ts +7 -8
  60. package/src/domains/job/S3Repository/JobUploadsRepository.spec.ts +66 -0
  61. package/src/domains/job/S3Repository/JobUploadsRepository.ts +35 -0
  62. package/src/domains/job/S3Repository/S3RepositoryBase.spec.ts +13 -19
  63. package/src/domains/job/S3Repository/S3RepositoryBase.ts +24 -9
  64. package/src/domains/job/S3Repository/index.ts +2 -1
  65. package/src/domains/job/constants/InjectTokens.ts +8 -0
  66. package/src/domains/job/constants/JobExecutionStatus.constants.ts +1 -0
  67. package/src/domains/job/constants/Repository.constants.ts +3 -3
  68. package/src/domains/job/constants/RetryConfig.constants.ts +29 -0
  69. package/src/domains/job/core/pagination/services/PaginationService.spec.ts +3 -5
  70. package/src/domains/job/index.ts +6 -4
  71. package/src/domains/job/manifest/JobManifestTypes.ts +8 -6
  72. package/src/domains/job/manifest/mocks/JobManifest.mock.ts +2 -2
  73. package/src/domains/job/manifest/mocks/JobManifestDto.mock.ts +3 -2
  74. package/src/domains/job/manifest/mocks/JobManifestRecord.mock.ts +4 -3
  75. package/src/domains/job/model/Base/Base.ts +11 -5
  76. package/src/domains/job/model/Job/Job.ts +11 -6
  77. package/src/domains/job/model/Job/mocks/Job.mock.ts +2 -2
  78. package/src/domains/job/model/Job/mocks/JobDto.mock.ts +3 -2
  79. package/src/domains/job/model/Job/mocks/JobRecord.mock.ts +3 -2
  80. package/src/domains/job/model/JobContext/JobContext.ts +7 -5
  81. package/src/domains/job/model/JobContext/mocks/JobContext.mock.ts +2 -2
  82. package/src/domains/job/model/JobContext/mocks/JobContextDto.mock.ts +3 -2
  83. package/src/domains/job/model/JobContext/mocks/JobContextRecord.mock.ts +3 -2
  84. package/src/domains/job/model/JobExecution/JobExecution.ts +74 -9
  85. package/src/domains/job/model/JobExecution/mocks/JobExecution.mock.ts +7 -4
  86. package/src/domains/job/model/JobExecution/mocks/JobExecutionDto.mock.ts +32 -16
  87. package/src/domains/job/model/JobExecution/mocks/JobExecutionRecord.mock.ts +10 -4
  88. package/src/domains/job/model/LogPayload/LogPayload.ts +32 -11
  89. package/src/domains/job/model/LogPayload/index.ts +1 -0
  90. package/src/domains/job/model/LogPayload/mocks/LogPayload.mock.ts +21 -0
  91. package/src/domains/job/repository/AbstractRepository.ts +91 -59
  92. package/src/domains/job/repository/JobContextRepository/JobContextRepository.spec.ts +39 -49
  93. package/src/domains/job/repository/JobContextRepository/JobContextRepository.ts +33 -21
  94. package/src/domains/job/repository/JobExecutionRepository/JobExecutionRepository.spec.ts +343 -169
  95. package/src/domains/job/repository/JobExecutionRepository/JobExecutionRepository.ts +112 -141
  96. package/src/domains/job/repository/JobExecutionRepository/utils/convertDynamoDbItemToJobExecution/convertDynamoDbItemToJobExecution.ts +22 -0
  97. package/src/domains/job/repository/JobExecutionRepository/utils/index.ts +1 -0
  98. package/src/domains/job/repository/JobManifestRepository/JobManifestRepository.spec.ts +23 -38
  99. package/src/domains/job/repository/JobManifestRepository/JobManifestRepository.ts +29 -36
  100. package/src/domains/job/repository/JobRepository/JobRepository.spec.ts +72 -80
  101. package/src/domains/job/repository/JobRepository/JobRepository.ts +66 -70
  102. package/src/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.spec.ts +60 -0
  103. package/src/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.ts +42 -0
  104. package/src/domains/job/repository/index.ts +1 -0
  105. package/src/domains/job/services/JobContextService/JobContextService.spec.ts +73 -39
  106. package/src/domains/job/services/JobContextService/JobContextService.ts +37 -6
  107. package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.defs.ts +1 -3
  108. package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.spec.ts +113 -31
  109. package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.ts +101 -49
  110. package/src/domains/job/services/JobManifestService/JobManifestService.spec.ts +4 -41
  111. package/src/domains/job/services/JobManifestService/JobManifestService.ts +2 -8
  112. package/src/domains/job/services/JobStartQueueService/JobStartQueueService.defs.ts +27 -0
  113. package/src/domains/job/services/JobStartQueueService/JobStartQueueService.ts +42 -0
  114. package/src/domains/job/services/JobUploadService/JobUploadService.spec.ts +1 -1
  115. package/src/domains/job/services/JobUploadService/JobUploadService.ts +1 -1
  116. package/src/domains/job/services/JobUploadService/utils/manifestValidator.spec.ts +12 -25
  117. package/src/domains/job/services/JobUploadService/utils/manifestValidator.ts +2 -7
  118. package/src/domains/job/services/index.ts +2 -1
  119. package/src/domains/job/utils/SlackClient/SlackClient.spec.ts +0 -2
  120. package/src/domains/manifest/services/ManifestServiceForDev.spec.ts +1 -2
  121. package/src/domains/manifest/services/ManifestServiceForDev.ts +0 -4
  122. package/src/index.ts +1 -0
  123. package/src/jest-global-setup.ts +8 -4
  124. package/src/mocks/mockIoc.ts +18 -43
  125. package/src/utils/buildJobRunnerModule/buildJobRunnerModule.ts +81 -0
  126. package/src/utils/buildJobRunnerModule/buildJobRunnerModule.types.ts +13 -0
  127. package/src/utils/index.ts +2 -0
  128. package/lib/domains/job/S3Repository/S3Repository.d.ts +0 -15
  129. package/lib/domains/job/S3Repository/S3Repository.spec.d.ts +0 -5
  130. package/lib/domains/job/services/JobQueueService/JobQueueService.d.ts +0 -9
  131. package/lib/domains/job/services/JobQueueService/JobQueueService.defs.d.ts +0 -9
  132. package/src/custom-types.d.ts +0 -7
  133. package/src/domains/job/S3Repository/S3Repository.spec.ts +0 -50
  134. package/src/domains/job/S3Repository/S3Repository.ts +0 -27
  135. package/src/domains/job/services/JobQueueService/JobQueueService.defs.ts +0 -10
  136. package/src/domains/job/services/JobQueueService/JobQueueService.spec.ts +0 -190
  137. package/src/domains/job/services/JobQueueService/JobQueueService.ts +0 -60
@@ -5,168 +5,205 @@
5
5
 
6
6
  import {
7
7
  AttributeValue,
8
+ BatchWriteItemCommand,
8
9
  DynamoDBClient,
9
- DynamoDBClientConfig,
10
- GetItemCommandOutput,
11
- PutItemCommandOutput,
10
+ GetItemCommand,
11
+ PutItemCommand,
12
+ QueryCommand,
12
13
  QueryCommandInput,
13
- ScanCommandOutput,
14
+ UpdateItemCommand,
14
15
  } from '@aws-sdk/client-dynamodb';
16
+ import { faker } from '@faker-js/faker';
17
+ import { AwsClientStub, mockClient } from 'aws-sdk-client-mock';
18
+ import 'aws-sdk-client-mock-jest';
15
19
  import { initIocContainer, iocContainer } from '../../../../mocks/mockIoc';
16
- import { testLogger } from '../../../../jest-global-setup';
20
+ import { InjectTokens } from '../../constants/InjectTokens';
21
+ import { JOB_EXECUTION_STATUS } from '../../constants/JobExecutionStatus.constants';
22
+ import { ENTITY_TYPES } from '../../constants/Repository.constants';
17
23
  import {
24
+ createMockClientGetResponse,
18
25
  createMockClientPutResponse,
19
26
  createMockClientScanResponse,
20
- createMockClientGetResponse,
21
27
  } from '../../core/dynamoClient.mock';
22
- import { PARTITION_KEYS } from '../../constants/Repository.constants';
23
- import { JOB_EXECUTION_STATUS } from '../../constants/JobExecutionStatus.constants';
24
- import { JobExecution } from '../../model/JobExecution/JobExecution';
25
- import { createMockJobExecution } from '../../model/JobExecution/mocks/JobExecution.mock';
28
+ import { PaginationInfo } from '../../core/pagination/model/PaginationInfo';
29
+ import { PAGINATION_DIRECTION_TYPE_BEFORE } from '../../core/pagination/model/PaginationTypes';
30
+ import * as PaginationUtils from '../../core/pagination/utils/PaginationUtils';
31
+ import { JobExecution, UpdateJobExecutionParameters } from '../../model/JobExecution/JobExecution';
32
+ import { createMockJobExecution, createMockJobExecutions } from '../../model/JobExecution/mocks/JobExecution.mock';
26
33
  import {
27
34
  createMockJobExecutionDto,
28
35
  createMockJobExecutionDtos,
29
36
  } from '../../model/JobExecution/mocks/JobExecutionDto.mock';
30
37
  import { JobExecutionRepository } from './JobExecutionRepository';
31
- import * as PaginationUtils from '../../core/pagination/utils/PaginationUtils';
32
- import { PaginationInfo } from '../../core/pagination/model/PaginationInfo';
33
- import { PAGINATION_DIRECTION_TYPE_BEFORE } from '../../core/pagination/model/PaginationTypes';
34
38
 
35
- // Create a new container tsoa can use
36
- initIocContainer(testLogger);
39
+ initIocContainer();
37
40
 
38
- // Mock DynamoDB Client
39
- const mockOptions = {} as DynamoDBClientConfig;
40
- const mockClient = new DynamoDBClient(mockOptions);
41
- const mockSend = jest.fn();
42
- mockClient.send = mockSend;
43
-
44
- // Inject JobRepository with a mocked client
45
- iocContainer.rebind<DynamoDBClient>(DynamoDBClient).toConstantValue(mockClient);
41
+ describe('JobExecutionRepository', (): void => {
42
+ let dynamoClient: AwsClientStub<DynamoDBClient>;
43
+ let jobExecutionRepository: JobExecutionRepository;
44
+ let tenant: string;
45
+ let tableName: string;
46
46
 
47
- let jobExecutionRepository: JobExecutionRepository;
47
+ beforeEach(() => {
48
+ dynamoClient = mockClient(DynamoDBClient);
49
+ dynamoClient.reset();
48
50
 
49
- describe('JobExecutionRepository', (): void => {
50
- beforeAll(() => {
51
51
  jobExecutionRepository = iocContainer.get(JobExecutionRepository);
52
+ tenant = iocContainer.get(InjectTokens.Tenant);
53
+ tableName = iocContainer.get(InjectTokens.DynamoTableName);
52
54
  });
53
55
 
54
56
  describe('list', () => {
55
- beforeEach(() => {
56
- jest.resetAllMocks();
57
- });
58
-
59
57
  it(`should return an empty list`, async (): Promise<void> => {
60
- mockSend.mockImplementationOnce((): ScanCommandOutput => createMockClientScanResponse());
58
+ dynamoClient.on(QueryCommand).resolvesOnce(createMockClientScanResponse());
61
59
 
62
- const response = await jobExecutionRepository.list({ jobName: 'does-not-exist' });
63
- expect(mockSend).toBeCalled();
60
+ const response = await jobExecutionRepository.list({
61
+ filters: { jobName: 'does-not-exist' },
62
+ });
64
63
  expect(response).toStrictEqual([]);
64
+ expect(dynamoClient).toHaveReceivedCommandTimes(QueryCommand, 1);
65
65
  });
66
66
 
67
67
  it(`should return a list of existing job executions`, async (): Promise<void> => {
68
- const mockRecord = createMockJobExecutionDto();
68
+ const mockJobExecution = createMockJobExecution();
69
69
 
70
- mockSend.mockImplementationOnce(
71
- (): ScanCommandOutput =>
72
- createMockClientScanResponse({
73
- Items: [mockRecord],
74
- LastEvaluatedKey: undefined,
75
- }),
70
+ dynamoClient.on(QueryCommand).resolvesOnce(
71
+ createMockClientScanResponse({
72
+ Items: [createMockJobExecutionDto(mockJobExecution)],
73
+ LastEvaluatedKey: undefined,
74
+ }),
76
75
  );
77
76
 
78
77
  const response = await jobExecutionRepository.list();
79
- expect(mockSend).toBeCalledTimes(1);
80
- expect(response).toHaveLength(1);
78
+ expect(response).toEqual([mockJobExecution]);
79
+ expect(dynamoClient).toHaveReceivedCommandWith(QueryCommand, {
80
+ ExclusiveStartKey: undefined,
81
+ ExpressionAttributeNames: { '#pk': 'pk' },
82
+ ExpressionAttributeValues: { ':pk': { S: `jobExecution~${tenant}` } },
83
+ IndexName: undefined,
84
+ KeyConditionExpression: '#pk = :pk',
85
+ ScanIndexForward: false,
86
+ TableName: tableName,
87
+ });
81
88
  });
82
89
 
83
90
  it(`should return a list of existing job executions by name`, async (): Promise<void> => {
84
91
  const mockName = `mock`;
85
- const mockRecords = [
86
- createMockJobExecutionDto({ jobName: mockName }),
87
- createMockJobExecutionDto({ jobName: mockName }),
92
+ const mockJobExecutions = [
93
+ createMockJobExecution({ jobName: mockName }),
94
+ createMockJobExecution({ jobName: mockName }),
88
95
  ];
89
96
 
90
- mockSend.mockImplementationOnce(
91
- (): ScanCommandOutput =>
92
- createMockClientScanResponse({
93
- Items: mockRecords,
94
- LastEvaluatedKey: undefined,
95
- }),
97
+ dynamoClient.on(QueryCommand).resolvesOnce(
98
+ createMockClientScanResponse({
99
+ Items: mockJobExecutions.map((execution) => createMockJobExecutionDto(execution)),
100
+ LastEvaluatedKey: undefined,
101
+ }),
96
102
  );
97
103
 
98
- const response = await jobExecutionRepository.list({ jobName: mockName });
99
- expect(mockSend).toBeCalledTimes(1);
100
- expect(response).toHaveLength(mockRecords.length);
104
+ const response = await jobExecutionRepository.list({
105
+ filters: { jobName: mockName },
106
+ });
107
+ expect(response).toEqual(mockJobExecutions);
108
+ expect(dynamoClient).toHaveReceivedCommandWith(QueryCommand, {
109
+ ExclusiveStartKey: undefined,
110
+ ExpressionAttributeNames: { '#pk': 'pk', '#sk': 'sk' },
111
+ ExpressionAttributeValues: {
112
+ ':pk': { S: `jobExecution~${tenant}` },
113
+ ':skPrefix': { S: `${mockName}~` },
114
+ },
115
+ IndexName: undefined,
116
+ KeyConditionExpression: '#pk = :pk and begins_with(#sk, :skPrefix)',
117
+ ScanIndexForward: false,
118
+ TableName: tableName,
119
+ });
101
120
  });
102
121
 
103
122
  it(`should return a list of existing job executions by status`, async (): Promise<void> => {
104
123
  const mockStatus = JOB_EXECUTION_STATUS.running;
105
- const mockRecords = [
106
- createMockJobExecutionDto({ status: mockStatus }),
107
- createMockJobExecutionDto({ status: mockStatus }),
124
+ const mockJobExecutions = [
125
+ createMockJobExecution({ status: mockStatus }),
126
+ createMockJobExecution({ status: mockStatus }),
108
127
  ];
109
128
 
110
- mockSend.mockImplementationOnce(
111
- (): ScanCommandOutput =>
112
- createMockClientScanResponse({
113
- Items: mockRecords,
114
- LastEvaluatedKey: undefined,
115
- }),
129
+ dynamoClient.on(QueryCommand).resolvesOnce(
130
+ createMockClientScanResponse({
131
+ Items: mockJobExecutions.map((execution) => createMockJobExecutionDto(execution)),
132
+ LastEvaluatedKey: undefined,
133
+ }),
116
134
  );
117
135
 
118
- const response = await jobExecutionRepository.list({ status: mockStatus });
119
- expect(mockSend).toBeCalledTimes(1);
120
- expect(response).toHaveLength(mockRecords.length);
136
+ const response = await jobExecutionRepository.list({
137
+ filters: { status: mockStatus },
138
+ });
139
+ expect(response).toEqual(mockJobExecutions);
140
+ expect(dynamoClient).toHaveReceivedCommandWith(QueryCommand, {
141
+ ExclusiveStartKey: undefined,
142
+ ExpressionAttributeNames: {
143
+ '#lsi1sk': 'lsi1sk',
144
+ '#pk': 'pk',
145
+ },
146
+ ExpressionAttributeValues: {
147
+ ':lsi1skPrefix': { S: `${mockStatus}~` },
148
+ ':pk': { S: `jobExecution~${tenant}` },
149
+ },
150
+ IndexName: 'lsi1',
151
+ KeyConditionExpression: '#pk = :pk and begins_with(#lsi1sk, :lsi1skPrefix)',
152
+ ScanIndexForward: false,
153
+ TableName: tableName,
154
+ });
121
155
  });
122
156
 
123
157
  it(`should paginate results to return a list of existing job executions`, async (): Promise<void> => {
124
158
  const mockRecordsPage1 = [
125
- createMockJobExecutionDto({ jobName: 'testJob1' }),
126
- createMockJobExecutionDto({ jobName: 'testJob1' }),
127
- createMockJobExecutionDto({ jobName: 'testJob1' }),
159
+ createMockJobExecution({ jobName: 'testJob1' }),
160
+ createMockJobExecution({ jobName: 'testJob1' }),
161
+ createMockJobExecution({ jobName: 'testJob1' }),
128
162
  ];
129
163
  const mockRecordsPage2 = [
130
- createMockJobExecutionDto({ jobName: 'testJob2' }),
131
- createMockJobExecutionDto({ jobName: 'testJob2' }),
132
- createMockJobExecutionDto({ jobName: 'testJob2' }),
164
+ createMockJobExecution({ jobName: 'testJob2' }),
165
+ createMockJobExecution({ jobName: 'testJob2' }),
166
+ createMockJobExecution({ jobName: 'testJob2' }),
133
167
  ];
134
168
 
135
- mockSend
136
- .mockImplementationOnce(
137
- (): ScanCommandOutput =>
138
- createMockClientScanResponse({
139
- Items: mockRecordsPage1,
140
- LastEvaluatedKey: mockRecordsPage1.slice(-1)[0],
141
- }),
169
+ dynamoClient
170
+ .on(QueryCommand)
171
+ .resolvesOnce(
172
+ createMockClientScanResponse({
173
+ Items: mockRecordsPage1.map((execution) => createMockJobExecutionDto(execution)),
174
+ LastEvaluatedKey: createMockJobExecutionDto(mockRecordsPage1.slice(-1)[0]),
175
+ }),
142
176
  )
143
- .mockImplementationOnce(
144
- (): ScanCommandOutput =>
145
- createMockClientScanResponse({
146
- Items: mockRecordsPage2,
147
- LastEvaluatedKey: undefined,
148
- }),
177
+ .resolvesOnce(
178
+ createMockClientScanResponse({
179
+ Items: mockRecordsPage2.map((execution) => createMockJobExecutionDto(execution)),
180
+ LastEvaluatedKey: undefined,
181
+ }),
149
182
  );
150
183
 
151
- const response = await jobExecutionRepository.list({ jobName: 'testJob1' });
152
- expect(mockSend).toBeCalledTimes(2);
153
- expect(response).toHaveLength(mockRecordsPage1.length + mockRecordsPage2.length);
184
+ const response = await jobExecutionRepository.list({
185
+ filters: { jobName: 'testJob1' },
186
+ limit: 4,
187
+ });
188
+ expect(response).toEqual([mockRecordsPage1[0], mockRecordsPage1[1], mockRecordsPage1[2], mockRecordsPage2[0]]);
189
+ expect(dynamoClient).toHaveReceivedCommandTimes(QueryCommand, 2);
190
+ expect(dynamoClient).toHaveReceivedCommandWith(QueryCommand, {
191
+ Limit: 4,
192
+ });
154
193
  });
155
194
 
156
195
  it(`should stop paginating if unexpected result is returned`, async (): Promise<void> => {
157
- mockSend.mockImplementationOnce(() => null);
196
+ dynamoClient.on(QueryCommand).resolvesOnce({});
158
197
 
159
- const response = await jobExecutionRepository.list({ jobName: 'testJob1' });
160
- expect(mockSend).toBeCalledTimes(1);
161
- expect(response).toHaveLength(0);
198
+ const response = await jobExecutionRepository.list({
199
+ filters: { jobName: 'testJob1' },
200
+ });
201
+ expect(response).toEqual([]);
202
+ expect(dynamoClient).toHaveReceivedCommandTimes(QueryCommand, 1);
162
203
  });
163
204
  });
164
205
 
165
206
  describe('findOne', () => {
166
- beforeEach(() => {
167
- jest.resetAllMocks();
168
- });
169
-
170
207
  it(`should return job execution by timeQueued and id and handle any null values`, async (): Promise<void> => {
171
208
  const mockJobExecutionRecord = createMockJobExecutionDto({
172
209
  ecsTaskId: 'ecs-task-id',
@@ -175,79 +212,70 @@ describe('JobExecutionRepository', (): void => {
175
212
  version: '1.0.0',
176
213
  });
177
214
 
178
- mockSend.mockImplementationOnce(
179
- (): GetItemCommandOutput =>
180
- createMockClientGetResponse({
181
- Item: mockJobExecutionRecord,
182
- }),
215
+ dynamoClient.on(GetItemCommand).resolvesOnce(
216
+ createMockClientGetResponse({
217
+ Item: mockJobExecutionRecord,
218
+ }),
183
219
  );
184
220
 
185
- const response = await jobExecutionRepository.findOne(
186
- `${mockJobExecutionRecord.timeQueued.S as string}-${mockJobExecutionRecord.id.S as string}`,
187
- mockJobExecutionRecord.jobName.S as string,
188
- );
221
+ const response = await jobExecutionRepository.findOne({
222
+ jobName: mockJobExecutionRecord.jobName.S as string,
223
+ id: mockJobExecutionRecord.id.S as string,
224
+ });
189
225
  expect(response).toStrictEqual({
190
226
  context: mockJobExecutionRecord.context.S as string,
191
- ecsTaskId: mockJobExecutionRecord.ecsTaskId.NULL ? null : (mockJobExecutionRecord.ecsTaskId.S as string),
227
+ ecsTaskId: mockJobExecutionRecord.ecsTaskId.S as string,
192
228
  id: mockJobExecutionRecord.id.S as string,
193
229
  jobName: mockJobExecutionRecord.jobName.S as string,
194
230
  status: mockJobExecutionRecord.status.S as string,
195
- timeFinished: mockJobExecutionRecord.timeFinished.NULL
196
- ? null
197
- : (mockJobExecutionRecord.timeFinished.S as string),
231
+ timeFinished: mockJobExecutionRecord.timeFinished.S as string,
198
232
  timeQueued: mockJobExecutionRecord.timeQueued.S as string,
199
- timeStarted: mockJobExecutionRecord.timeStarted.NULL ? null : (mockJobExecutionRecord.timeStarted.S as string),
233
+ timeStarted: mockJobExecutionRecord.timeStarted.S as string,
200
234
  type: `jobExecution-${mockJobExecutionRecord.jobName.S as string}`,
201
- version: mockJobExecutionRecord.version.NULL ? null : (mockJobExecutionRecord.version.S as string),
235
+ version: mockJobExecutionRecord.version.S as string,
202
236
  input: mockJobExecutionRecord.input.NULL ? null : JSON.parse(mockJobExecutionRecord.input.S as string),
237
+ retryCount: 0,
238
+ lastFailureReason: null,
239
+ retryAfter: null,
203
240
  } as JobExecution);
204
- expect(mockSend).toBeCalledTimes(1);
241
+ expect(dynamoClient).toHaveReceivedCommandWith(GetItemCommand, {});
205
242
  });
206
243
 
207
244
  it(`should return null if job execution does not exist`, async (): Promise<void> => {
208
- mockSend.mockImplementationOnce(
209
- (): ScanCommandOutput =>
210
- createMockClientScanResponse({
211
- Items: [],
212
- }),
245
+ dynamoClient.on(GetItemCommand).resolvesOnce(
246
+ createMockClientGetResponse({
247
+ Item: undefined,
248
+ }),
213
249
  );
214
250
 
215
- const response = await jobExecutionRepository.findOne(`null`, `null`);
216
- expect(mockSend).toBeCalledTimes(1);
251
+ const response = await jobExecutionRepository.findOne({ jobName: 'null', id: 'null' });
217
252
  expect(response).toBe(null);
253
+ expect(dynamoClient).toHaveReceivedCommandTimes(GetItemCommand, 1);
218
254
  });
219
255
  });
220
256
 
221
257
  describe('create', () => {
222
- beforeEach(() => {
223
- jest.resetAllMocks();
224
- });
225
-
226
258
  it(`should create a new job execution record and return job execution record in the response`, async (): Promise<void> => {
227
259
  const newJobExecutionToCreate = createMockJobExecution();
228
260
  const mockResponse = createMockClientPutResponse({
229
261
  Attributes: {
230
262
  jobName: { S: newJobExecutionToCreate.jobName },
231
- pk: { S: PARTITION_KEYS.jobExecution },
263
+ pk: { S: ENTITY_TYPES.jobExecution },
232
264
  sk: { S: `${newJobExecutionToCreate.jobName}` },
233
265
  },
234
266
  });
235
267
 
236
- mockSend.mockImplementationOnce((): PutItemCommandOutput => mockResponse);
268
+ dynamoClient.on(PutItemCommand).resolvesOnce(mockResponse);
237
269
 
238
270
  const response = await jobExecutionRepository.create(newJobExecutionToCreate);
239
- expect(mockSend).toBeCalledTimes(1);
240
271
  expect(response).toStrictEqual({
241
272
  ...newJobExecutionToCreate,
242
273
  });
274
+ expect(dynamoClient).toHaveReceivedCommandWith(PutItemCommand, {});
243
275
  });
244
276
  });
245
277
 
246
278
  describe('begin', () => {
247
- beforeEach(() => {
248
- jest.resetAllMocks();
249
- });
250
-
251
279
  it(`should create a new queued job record`, async (): Promise<void> => {
252
280
  const newJobExecutionToCreate = createMockJobExecution({
253
281
  status: JOB_EXECUTION_STATUS.queued,
@@ -257,7 +285,7 @@ describe('JobExecutionRepository', (): void => {
257
285
  });
258
286
  const mockReponse = createMockClientPutResponse({
259
287
  Attributes: {
260
- pk: { S: `${PARTITION_KEYS.jobExecution}-${newJobExecutionToCreate.jobName}` },
288
+ pk: { S: `${ENTITY_TYPES.jobExecution}-${newJobExecutionToCreate.jobName}` },
261
289
  sk: { S: `${newJobExecutionToCreate.timeQueued}-${newJobExecutionToCreate.id}` },
262
290
  name: { S: newJobExecutionToCreate.jobName },
263
291
  context: { S: newJobExecutionToCreate.context },
@@ -268,7 +296,7 @@ describe('JobExecutionRepository', (): void => {
268
296
  },
269
297
  });
270
298
 
271
- mockSend.mockImplementationOnce(() => Promise.resolve(mockReponse));
299
+ dynamoClient.on(PutItemCommand).resolvesOnce(mockReponse);
272
300
 
273
301
  const response = await jobExecutionRepository.begin({
274
302
  context: newJobExecutionToCreate.context,
@@ -276,7 +304,6 @@ describe('JobExecutionRepository', (): void => {
276
304
  version: newJobExecutionToCreate.version,
277
305
  input: newJobExecutionToCreate.input,
278
306
  });
279
- expect(mockSend).toBeCalledTimes(1);
280
307
  expect(response.id).not.toBeNull();
281
308
  expect(response.timeQueued).not.toBeNull();
282
309
  expect(response.ecsTaskId).toBeNull();
@@ -286,18 +313,21 @@ describe('JobExecutionRepository', (): void => {
286
313
  expect(response.jobName).toBe(newJobExecutionToCreate.jobName);
287
314
  expect(response.version).toBe(newJobExecutionToCreate.version);
288
315
  expect(response.input).toBe(newJobExecutionToCreate.input);
316
+ expect(dynamoClient).toHaveReceivedCommandWith(PutItemCommand, {});
289
317
  });
290
318
  });
291
319
 
292
320
  describe('getExecutions', () => {
321
+ const jobName = 'something';
322
+ const jobExecutionId = faker.string.uuid();
293
323
  let mockExclusiveStartKey: Record<string, AttributeValue>;
294
324
  let mockPaginationInfo: PaginationInfo;
295
325
  let mockParams: QueryCommandInput;
296
326
 
297
327
  beforeEach(() => {
298
328
  mockExclusiveStartKey = {
299
- pk: { S: `${PARTITION_KEYS.jobExecution}-something` },
300
- sk: { S: 'mockSk' },
329
+ pk: { S: `${ENTITY_TYPES.jobExecution}~${tenant}` },
330
+ sk: { S: `${jobName}~${jobExecutionId}` },
301
331
  };
302
332
  mockPaginationInfo = {
303
333
  direction: PAGINATION_DIRECTION_TYPE_BEFORE,
@@ -308,61 +338,205 @@ describe('JobExecutionRepository', (): void => {
308
338
  mockParams = {
309
339
  ExclusiveStartKey: mockExclusiveStartKey,
310
340
  ExpressionAttributeValues: {
311
- ':pk': { S: `${PARTITION_KEYS.jobExecution}-something` },
341
+ ':pk': { S: `${ENTITY_TYPES.jobExecution}~${tenant}` },
342
+ ':skPrefix': { S: `${jobName}~` },
312
343
  },
313
- KeyConditionExpression: 'pk = :pk',
314
- TableName: process.env.DYNAMODB_TABLE,
344
+ KeyConditionExpression: 'pk = :pk and begins_with(sk, :skPrefix)',
345
+ TableName: tableName,
315
346
  };
316
347
 
317
348
  jest.spyOn(PaginationUtils, 'decodeTokenString').mockReturnValue(mockExclusiveStartKey);
318
349
  });
319
350
 
320
351
  it('should return an empty array if no executions are found', async () => {
321
- mockSend.mockImplementationOnce(() =>
322
- Promise.resolve({
323
- Items: [],
324
- LastEvaluatedKey: null,
325
- }),
326
- );
327
-
328
- const mockRequestData: PaginationInfo = mockPaginationInfo;
352
+ dynamoClient.on(QueryCommand).resolvesOnce({
353
+ Items: [],
354
+ LastEvaluatedKey: undefined,
355
+ });
329
356
 
330
- const result = await jobExecutionRepository.getExecutions(mockRequestData, 'something');
357
+ const result = await jobExecutionRepository.getExecutions(mockPaginationInfo, 'something');
331
358
 
332
- expect(mockSend).toHaveBeenCalledWith(
333
- expect.objectContaining({
334
- input: { ...mockParams },
335
- }),
336
- );
359
+ expect(dynamoClient).toHaveReceivedCommandWith(QueryCommand, mockParams);
337
360
  expect(result.items).toEqual([]);
338
- expect(result.lastEvaluatedKey).toBe(null);
361
+ expect(result.lastEvaluatedKey).toBe(undefined);
339
362
  });
340
363
 
341
- it('should return an array of jobs if found', async () => {
364
+ it('should return an array of job executions if found', async () => {
342
365
  const mockJobExecutionRecords = createMockJobExecutionDtos();
343
366
  const mockLastEvaluatedKey = {
344
- pk: { S: `${PARTITION_KEYS.job}-${'something'}` },
367
+ pk: { S: `${ENTITY_TYPES.job}-${'something'}` },
345
368
  sk: { S: 'mockSk' },
346
369
  };
347
370
 
348
- mockSend.mockImplementationOnce(() =>
349
- Promise.resolve({
350
- Items: mockJobExecutionRecords,
351
- LastEvaluatedKey: mockLastEvaluatedKey,
352
- }),
353
- );
354
-
355
- const mockRequestData: PaginationInfo = mockPaginationInfo;
371
+ dynamoClient.on(QueryCommand).resolvesOnce({
372
+ Items: mockJobExecutionRecords,
373
+ LastEvaluatedKey: mockLastEvaluatedKey,
374
+ });
356
375
 
357
- const result = await jobExecutionRepository.getExecutions(mockRequestData, 'something');
376
+ const result = await jobExecutionRepository.getExecutions(mockPaginationInfo, 'something');
358
377
 
359
- expect(mockSend).toHaveBeenCalledWith(
360
- expect.objectContaining({
361
- input: { ...mockParams },
362
- }),
363
- );
378
+ expect(dynamoClient).toHaveReceivedCommandWith(QueryCommand, mockParams);
364
379
  expect(result.items).toHaveLength(mockJobExecutionRecords.length);
365
380
  expect(result.lastEvaluatedKey).toStrictEqual(mockLastEvaluatedKey);
366
381
  });
367
382
  });
383
+
384
+ describe('update', () => {
385
+ it('Updates the record properties', async () => {
386
+ const jobExecution = createMockJobExecution({ status: JOB_EXECUTION_STATUS.queued });
387
+ const updatedAttributes: UpdateJobExecutionParameters = {
388
+ input: faker.lorem.words(),
389
+ ecsTaskId: faker.string.uuid(),
390
+ version: faker.system.semver(),
391
+ timeStarted: faker.date.recent().toISOString(),
392
+ timeFinished: null,
393
+ context: faker.lorem.words(),
394
+ };
395
+ const updatedJobExecution = createMockJobExecution({
396
+ ...jobExecution,
397
+ ...updatedAttributes,
398
+ });
399
+
400
+ dynamoClient.on(UpdateItemCommand).resolvesOnce({
401
+ Attributes: createMockJobExecutionDto(updatedJobExecution),
402
+ });
403
+
404
+ const result = await jobExecutionRepository.update(jobExecution, updatedAttributes);
405
+
406
+ expect(result).toEqual(updatedJobExecution);
407
+ expect(dynamoClient).toHaveReceivedCommandWith(UpdateItemCommand, {
408
+ ExpressionAttributeNames: {
409
+ '#context': 'context',
410
+ '#ecsTaskId': 'ecsTaskId',
411
+ '#input': 'input',
412
+ '#timeFinished': 'timeFinished',
413
+ '#timeStarted': 'timeStarted',
414
+ '#version': 'version',
415
+ },
416
+ ExpressionAttributeValues: {
417
+ ':context': { S: updatedAttributes.context! },
418
+ ':ecsTaskId': { S: updatedAttributes.ecsTaskId! },
419
+ ':input': { S: updatedAttributes.input },
420
+ ':timeFinished': { NULL: true },
421
+ ':timeStarted': { S: updatedAttributes.timeStarted! },
422
+ ':version': { S: updatedAttributes.version! },
423
+ },
424
+ Key: {
425
+ pk: { S: `jobExecution~${tenant}` },
426
+ sk: { S: `${jobExecution.jobName}~${jobExecution.id}` },
427
+ },
428
+ ReturnValues: 'ALL_NEW',
429
+ TableName: tableName,
430
+ UpdateExpression:
431
+ 'SET #input = :input, #ecsTaskId = :ecsTaskId, #version = :version, #timeStarted = :timeStarted, #timeFinished = :timeFinished, #context = :context',
432
+ });
433
+ });
434
+
435
+ it('Updates the record keys when the status is updated', async () => {
436
+ const jobExecution = createMockJobExecution({ status: JOB_EXECUTION_STATUS.queued });
437
+ const updatedJobExecution = createMockJobExecution({
438
+ ...jobExecution,
439
+ status: JOB_EXECUTION_STATUS.running,
440
+ });
441
+
442
+ dynamoClient.on(UpdateItemCommand).resolvesOnce({
443
+ Attributes: createMockJobExecutionDto(updatedJobExecution),
444
+ });
445
+
446
+ const result = await jobExecutionRepository.update(jobExecution, {
447
+ status: updatedJobExecution.status,
448
+ });
449
+
450
+ expect(result).toEqual(updatedJobExecution);
451
+ expect(dynamoClient).toHaveReceivedCommandWith(UpdateItemCommand, {
452
+ ExpressionAttributeNames: {
453
+ '#gsi1pk': 'gsi1pk',
454
+ '#lsi1sk': 'lsi1sk',
455
+ '#status': 'status',
456
+ },
457
+ ExpressionAttributeValues: {
458
+ ':gsi1pk': { S: `jobExecution~${updatedJobExecution.status}` },
459
+ ':lsi1sk': { S: `${updatedJobExecution.status}~${updatedJobExecution.jobName}` },
460
+ ':status': { S: updatedJobExecution.status },
461
+ },
462
+ Key: {
463
+ pk: { S: `jobExecution~${tenant}` },
464
+ sk: { S: `${jobExecution.jobName}~${jobExecution.id}` },
465
+ },
466
+ ReturnValues: 'ALL_NEW',
467
+ TableName: tableName,
468
+ UpdateExpression: 'SET #status = :status, #lsi1sk = :lsi1sk, #gsi1pk = :gsi1pk',
469
+ });
470
+ });
471
+ });
472
+
473
+ describe('batchDelete', () => {
474
+ it('should delete the requested dynamo records', async () => {
475
+ dynamoClient.on(BatchWriteItemCommand).resolves({});
476
+
477
+ const jobExecutions = createMockJobExecutions(3);
478
+ const result = await jobExecutionRepository.batchDelete(jobExecutions);
479
+
480
+ expect(result).toEqual({ unprocessedItems: [] });
481
+ expect(dynamoClient).toHaveReceivedCommandWith(BatchWriteItemCommand, {
482
+ RequestItems: {
483
+ [tableName]: [
484
+ {
485
+ DeleteRequest: {
486
+ Key: {
487
+ pk: { S: `jobExecution~${tenant}` },
488
+ sk: { S: `${jobExecutions[0].jobName}~${jobExecutions[0].id}` },
489
+ },
490
+ },
491
+ },
492
+ {
493
+ DeleteRequest: {
494
+ Key: {
495
+ pk: { S: `jobExecution~${tenant}` },
496
+ sk: { S: `${jobExecutions[1].jobName}~${jobExecutions[1].id}` },
497
+ },
498
+ },
499
+ },
500
+ {
501
+ DeleteRequest: {
502
+ Key: {
503
+ pk: { S: `jobExecution~${tenant}` },
504
+ sk: { S: `${jobExecutions[2].jobName}~${jobExecutions[2].id}` },
505
+ },
506
+ },
507
+ },
508
+ ],
509
+ },
510
+ });
511
+ });
512
+
513
+ it('should return without attempting to delete if an empty array of SK is provided', async () => {
514
+ const result = await jobExecutionRepository.batchDelete([]);
515
+
516
+ expect(result).toEqual({ unprocessedItems: [] });
517
+ expect(dynamoClient).not.toHaveReceivedCommand(BatchWriteItemCommand);
518
+ });
519
+
520
+ it('should return the unprocessed SK items if some items fail to delete', async () => {
521
+ const jobExecutions = createMockJobExecutions(3);
522
+
523
+ dynamoClient.on(BatchWriteItemCommand).resolves({
524
+ UnprocessedItems: {
525
+ [tableName]: [
526
+ {
527
+ DeleteRequest: {
528
+ Key: {
529
+ pk: { S: `jobExecution~${tenant}` },
530
+ sk: { S: `${jobExecutions[1].jobName}~${jobExecutions[1].id}` },
531
+ },
532
+ },
533
+ },
534
+ ],
535
+ },
536
+ });
537
+
538
+ const result = await jobExecutionRepository.batchDelete(jobExecutions);
539
+ expect(result).toEqual({ unprocessedItems: [jobExecutions[1]] });
540
+ });
541
+ });
368
542
  });