@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
@@ -3,56 +3,46 @@
3
3
  * Copyright Squiz Australia Pty Ltd. All Rights Reserved.
4
4
  */
5
5
 
6
- import * as dotenv from 'dotenv';
7
- dotenv.config();
8
-
9
- import {
10
- AttributeValue,
11
- DynamoDBClient,
12
- PutItemCommand,
13
- UpdateItemCommand,
14
- UpdateItemCommandInput,
15
- } from '@aws-sdk/client-dynamodb';
6
+ import { AttributeValue, DynamoDBClient, UpdateItemCommand, UpdateItemCommandInput } from '@aws-sdk/client-dynamodb';
16
7
  import { inject } from 'inversify';
17
8
  import { provideTransient } from '../../transientProvider';
18
9
  import { Repository } from '../AbstractRepository';
19
- import { PARTITION_KEYS } from '../../constants/Repository.constants';
20
- import { JobManifest } from '../../manifest/JobManifestTypes';
21
- import { JobManifestDto, CreateJobManifestRequest } from '../../manifest';
10
+ import { ENTITY_TYPES } from '../../constants/Repository.constants';
11
+ import {
12
+ CreateJobManifestRequest,
13
+ JobManifest,
14
+ JobManifestDto,
15
+ JobManifestSk,
16
+ UpdateJobManifestRequest,
17
+ } from '../../manifest';
18
+ import { InjectTokens } from '../../constants/InjectTokens';
22
19
  import { parsePartitionKey } from '../../ParsePartitionKey';
23
20
 
24
21
  @provideTransient(JobManifestRepository)
25
- export class JobManifestRepository extends Repository<JobManifest, JobManifestDto> {
26
- public constructor(@inject(DynamoDBClient) protected dynamoDbClient: DynamoDBClient) {
27
- super(dynamoDbClient);
28
- this.pk = PARTITION_KEYS.manifest;
29
- this.tableName = process.env.DYNAMODB_TABLE as string;
22
+ export class JobManifestRepository extends Repository<JobManifest, JobManifestDto, JobManifestSk> {
23
+ public constructor(
24
+ @inject(DynamoDBClient) dynamoDbClient: DynamoDBClient,
25
+ @inject(InjectTokens.DynamoTableName) tableName: string,
26
+ @inject(InjectTokens.Tenant) tenant: string,
27
+ ) {
28
+ super(dynamoDbClient, tableName, `${ENTITY_TYPES.manifest}~${tenant}`);
30
29
  }
31
30
 
32
31
  public async create(input: CreateJobManifestRequest): Promise<JobManifest> {
33
32
  const newJobManifest: JobManifest = {
34
33
  ...input,
35
- type: this.pk,
34
+ type: ENTITY_TYPES.manifest,
36
35
  };
37
36
 
38
- const dynamoDbRecord = this.convertToDynamoDbItem(newJobManifest);
39
-
40
- await this.dynamoDbClient.send(
41
- new PutItemCommand({
42
- Item: dynamoDbRecord,
43
- TableName: this.tableName,
44
- }),
45
- );
46
-
47
- return newJobManifest;
37
+ return super.create(newJobManifest);
48
38
  }
49
39
 
50
- async update(sk: string, input: Partial<JobManifest>): Promise<JobManifest | null> {
40
+ async update(sk: JobManifestSk, input: UpdateJobManifestRequest): Promise<JobManifest> {
51
41
  // Initialise update command
52
42
  const updateParams: UpdateItemCommandInput = {
53
43
  Key: {
54
44
  pk: { S: this.pk },
55
- sk: { S: sk },
45
+ sk: { S: this.buildSk(sk) },
56
46
  },
57
47
  ReturnValues: 'ALL_NEW',
58
48
  TableName: this.tableName,
@@ -88,8 +78,7 @@ export class JobManifestRepository extends Repository<JobManifest, JobManifestDt
88
78
  const updateItemCommand = new UpdateItemCommand(updateParams);
89
79
  const updatedJob = await this.dynamoDbClient.send(updateItemCommand);
90
80
 
91
- /* istanbul ignore next */
92
- return updatedJob.Attributes ? this.convertToItem(updatedJob.Attributes as JobManifestDto) : null;
81
+ return this.convertToItem(updatedJob.Attributes as JobManifestDto);
93
82
  }
94
83
 
95
84
  convertToDynamoDbItem(item: JobManifest): JobManifestDto {
@@ -99,8 +88,8 @@ export class JobManifestRepository extends Repository<JobManifest, JobManifestDt
99
88
  entryFunction: { S: JSON.stringify(item.entryFunction) },
100
89
  name: { S: item.name },
101
90
  version: { S: item.version },
102
- pk: { S: item.type },
103
- sk: { S: `${item.name}-${item.version}` },
91
+ pk: { S: this.pk },
92
+ sk: { S: `${item.name}~${item.version}` },
104
93
  timeoutSeconds: { N: item.timeoutSeconds.toString() },
105
94
  };
106
95
  }
@@ -114,7 +103,11 @@ export class JobManifestRepository extends Repository<JobManifest, JobManifestDt
114
103
  name: name.S as string,
115
104
  version: version.S as string,
116
105
  timeoutSeconds: parseInt(timeoutSeconds.N as string),
117
- type: parsePartitionKey(pk.S as string),
106
+ type: parsePartitionKey(pk.S as string).entityType,
118
107
  };
119
108
  }
109
+
110
+ protected buildSk(sk: JobManifestSk): string {
111
+ return `${sk.name}~${sk.version}`;
112
+ }
120
113
  }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import {
7
7
  AttributeValue,
8
+ DeleteItemCommandOutput,
8
9
  DynamoDBClient,
9
10
  DynamoDBClientConfig,
10
11
  GetItemCommandOutput,
@@ -13,13 +14,12 @@ import {
13
14
  ScanCommandOutput,
14
15
  } from '@aws-sdk/client-dynamodb';
15
16
  import { initIocContainer, iocContainer } from '../../../../mocks/mockIoc';
16
- import { testLogger } from '../../../../jest-global-setup';
17
17
  import {
18
18
  createMockClientPutResponse,
19
19
  createMockClientScanResponse,
20
20
  createMockClientGetResponse,
21
21
  } from '../../core/dynamoClient.mock';
22
- import { PARTITION_KEYS } from '../../constants/Repository.constants';
22
+ import { ENTITY_TYPES } from '../../constants/Repository.constants';
23
23
  import { PaginationInfo } from '../../core/pagination/model/PaginationInfo';
24
24
  import { PAGINATION_DIRECTION_TYPE_BEFORE } from '../../core/pagination/model/PaginationTypes';
25
25
  import * as PaginationUtils from '../../core/pagination/utils/PaginationUtils';
@@ -27,8 +27,10 @@ import { Job } from '../../model/Job/Job';
27
27
  import { createMockJobDto, createMockJobDtos } from '../../model/Job/mocks/JobDto.mock';
28
28
  import { createMockCreateJobRequest } from '../../model/Job/mocks/JobRequest.mock';
29
29
  import { JobRepository } from './JobRepository';
30
- // Create a new container tsoa can use
31
- initIocContainer(testLogger);
30
+ import { createMockJob, createMockJobRecord } from '../../model/Job';
31
+ import { InjectTokens } from '../../constants/InjectTokens';
32
+
33
+ initIocContainer();
32
34
 
33
35
  // Mock DynamoDB Client
34
36
  const mockOptions = {} as DynamoDBClientConfig;
@@ -39,18 +41,18 @@ mockClient.send = mockSend;
39
41
  // Inject JobRepository with a mocked client
40
42
  iocContainer.rebind<DynamoDBClient>(DynamoDBClient).toConstantValue(mockClient);
41
43
 
42
- let jobRepository: JobRepository;
43
-
44
44
  describe('JobRepository', (): void => {
45
+ let jobRepository: JobRepository;
46
+ let tenant: string;
47
+ let tableName: string;
48
+
45
49
  beforeAll(() => {
46
50
  jobRepository = iocContainer.get(JobRepository);
51
+ tenant = iocContainer.get(InjectTokens.Tenant);
52
+ tableName = iocContainer.get(InjectTokens.DynamoTableName);
47
53
  });
48
54
 
49
55
  describe('list', () => {
50
- beforeEach(() => {
51
- jest.resetAllMocks();
52
- });
53
-
54
56
  it(`should return an empty list`, async (): Promise<void> => {
55
57
  mockSend.mockImplementationOnce((): ScanCommandOutput => createMockClientScanResponse());
56
58
 
@@ -87,7 +89,9 @@ describe('JobRepository', (): void => {
87
89
  }),
88
90
  );
89
91
 
90
- const response = await jobRepository.list({ name: mockName });
92
+ const response = await jobRepository.list({
93
+ filters: { name: mockName },
94
+ });
91
95
  expect(mockSend).toBeCalledTimes(1);
92
96
  expect(response).toHaveLength(mockRecords.length);
93
97
  });
@@ -104,14 +108,16 @@ describe('JobRepository', (): void => {
104
108
  }),
105
109
  );
106
110
 
107
- const response = await jobRepository.list({ version: mockVersion });
111
+ const response = await jobRepository.list({
112
+ filters: { version: mockVersion },
113
+ });
108
114
  expect(mockSend).toBeCalledTimes(1);
109
115
  expect(response).toHaveLength(mockRecords.length);
110
116
  });
111
117
 
112
118
  it(`should paginate results to return a list of existing jobs`, async (): Promise<void> => {
113
- const mockRecordsPage1 = createMockJobDtos();
114
- const mockRecordsPage2 = createMockJobDtos();
119
+ const mockRecordsPage1 = createMockJobDtos(2);
120
+ const mockRecordsPage2 = createMockJobDtos(2);
115
121
 
116
122
  mockSend
117
123
  .mockImplementationOnce(
@@ -129,9 +135,11 @@ describe('JobRepository', (): void => {
129
135
  }),
130
136
  );
131
137
 
132
- const response = await jobRepository.list();
138
+ const response = await jobRepository.list({
139
+ limit: 3,
140
+ });
133
141
  expect(mockSend).toBeCalledTimes(2);
134
- expect(response).toHaveLength(mockRecordsPage1.length + mockRecordsPage2.length);
142
+ expect(response).toHaveLength(3);
135
143
  });
136
144
 
137
145
  it(`should stop paginating if unexpect result is returned`, async (): Promise<void> => {
@@ -144,10 +152,6 @@ describe('JobRepository', (): void => {
144
152
  });
145
153
 
146
154
  describe('findOne', () => {
147
- beforeEach(() => {
148
- jest.resetAllMocks();
149
- });
150
-
151
155
  it(`should return job by name`, async (): Promise<void> => {
152
156
  const mockJobRecord = createMockJobDto();
153
157
  mockSend.mockImplementationOnce(
@@ -157,7 +161,10 @@ describe('JobRepository', (): void => {
157
161
  }),
158
162
  );
159
163
 
160
- const response = await jobRepository.findOne(mockJobRecord.sk.S as string);
164
+ const response = await jobRepository.findOne({
165
+ name: mockJobRecord.name.S as string,
166
+ version: mockJobRecord.version.S as string,
167
+ });
161
168
  expect(response).toStrictEqual({
162
169
  name: mockJobRecord.name.S as string,
163
170
  type: `job`,
@@ -175,22 +182,18 @@ describe('JobRepository', (): void => {
175
182
  }),
176
183
  );
177
184
 
178
- const response = await jobRepository.findOne(`null`);
185
+ const response = await jobRepository.findOne({ name: 'unknown', version: '0.0.1' });
179
186
  expect(mockSend).toBeCalledTimes(1);
180
187
  expect(response).toBe(null);
181
188
  });
182
189
  });
183
190
 
184
191
  describe('create', () => {
185
- beforeEach(() => {
186
- jest.resetAllMocks();
187
- });
188
-
189
192
  it(`should create a new job record and return job in the response`, async (): Promise<void> => {
190
193
  const newJobToCreate = createMockCreateJobRequest();
191
194
  const mockReponse = createMockClientPutResponse({
192
195
  Attributes: {
193
- pk: { S: PARTITION_KEYS.job },
196
+ pk: { S: ENTITY_TYPES.job },
194
197
  sk: { S: `${newJobToCreate.name}-${newJobToCreate.version}` },
195
198
  name: { S: newJobToCreate.name },
196
199
  version: { S: newJobToCreate.version },
@@ -203,7 +206,7 @@ describe('JobRepository', (): void => {
203
206
  expect(mockSend).toBeCalledTimes(1);
204
207
  expect(response).toStrictEqual({
205
208
  ...newJobToCreate,
206
- type: PARTITION_KEYS.job,
209
+ type: ENTITY_TYPES.job,
207
210
  });
208
211
  });
209
212
  });
@@ -215,7 +218,7 @@ describe('JobRepository', (): void => {
215
218
 
216
219
  beforeEach(() => {
217
220
  mockExclusiveStartKey = {
218
- pk: { S: PARTITION_KEYS.job },
221
+ pk: { S: `${ENTITY_TYPES.job}~${tenant}` },
219
222
  sk: { S: 'mockSk' },
220
223
  };
221
224
  mockPaginationInfo = {
@@ -227,11 +230,11 @@ describe('JobRepository', (): void => {
227
230
  mockParams = {
228
231
  ExclusiveStartKey: mockExclusiveStartKey,
229
232
  ExpressionAttributeValues: {
230
- ':pk': { S: PARTITION_KEYS.job },
233
+ ':pk': { S: `${ENTITY_TYPES.job}~${tenant}` },
231
234
  },
232
235
  KeyConditionExpression: 'pk = :pk',
233
236
  ScanIndexForward: false,
234
- TableName: process.env.DYNAMODB_TABLE,
237
+ TableName: tableName,
235
238
  };
236
239
 
237
240
  jest.spyOn(PaginationUtils, 'decodeTokenString').mockReturnValue(mockExclusiveStartKey);
@@ -261,7 +264,7 @@ describe('JobRepository', (): void => {
261
264
  it('should return an array of jobs if found', async () => {
262
265
  const mockJobRecords = createMockJobDtos();
263
266
  const mockLastEvaluatedKey = {
264
- pk: { S: PARTITION_KEYS.job },
267
+ pk: { S: ENTITY_TYPES.job },
265
268
  sk: { S: 'mockSk' },
266
269
  };
267
270
 
@@ -286,52 +289,41 @@ describe('JobRepository', (): void => {
286
289
  });
287
290
  });
288
291
 
289
- // describe('update', () => {
290
- // beforeEach(() => {
291
- // jest.resetAllMocks();
292
- // });
293
-
294
- // it(`should update a job record`, async (): Promise<void> => {
295
- // const jobToUpdate = createMockJob();
296
- // const updatedJob: Job = {
297
- // ...jobToUpdate,
298
- // name: `new-name`,
299
- // version: `9.9.9`,
300
- // };
301
- // const updatedJobRecord = createMockJobRecord({
302
- // sk: updatedJob.name,
303
- // version: updatedJob.version,
304
- // });
305
- // const updatedJobRecordDto = createMockJobDto({
306
- // ...updatedJobRecord,
307
- // });
308
- // const putResponse = createMockClientPutResponse({
309
- // Attributes: { ...updatedJobRecordDto },
310
- // });
311
-
312
- // mockSend.mockImplementation((): PutItemCommandOutput => putResponse);
313
-
314
- // const response = await jobRepository.update(jobToUpdate.name, updatedJobRecord);
315
- // expect(response).toStrictEqual(updatedJob);
316
- // expect(mockSend).toBeCalledTimes(1);
317
- // });
318
- // });
319
-
320
- // describe('delete', () => {
321
- // beforeEach(() => {
322
- // jest.resetAllMocks();
323
- // });
324
-
325
- // it(`should return undefined when calling the delete operation`, async (): Promise<void> => {
326
- // mockSend.mockImplementationOnce((): DeleteItemCommandOutput => {
327
- // return {
328
- // $metadata: {},
329
- // };
330
- // });
331
-
332
- // const response = await jobRepository.delete(`name`);
333
- // expect(response).toBe(undefined);
334
- // expect(mockSend).toBeCalledTimes(1);
335
- // });
336
- // });
292
+ describe('update', () => {
293
+ it(`should update a job record`, async (): Promise<void> => {
294
+ const jobToUpdate = createMockJob();
295
+ const updatedJob: Job = {
296
+ ...jobToUpdate,
297
+ description: `updated description`,
298
+ };
299
+ const updatedJobRecord = createMockJobRecord(updatedJob);
300
+ const updatedJobRecordDto = createMockJobDto(updatedJobRecord);
301
+ const putResponse = createMockClientPutResponse({
302
+ Attributes: { ...updatedJobRecordDto },
303
+ });
304
+
305
+ mockSend.mockImplementation((): PutItemCommandOutput => putResponse);
306
+
307
+ const response = await jobRepository.update(
308
+ { name: jobToUpdate.name, version: jobToUpdate.version },
309
+ updatedJobRecord,
310
+ );
311
+ expect(response).toStrictEqual(updatedJob);
312
+ expect(mockSend).toBeCalledTimes(1);
313
+ });
314
+ });
315
+
316
+ describe('delete', () => {
317
+ it(`should return undefined when calling the delete operation`, async (): Promise<void> => {
318
+ mockSend.mockImplementationOnce((): DeleteItemCommandOutput => {
319
+ return {
320
+ $metadata: {},
321
+ };
322
+ });
323
+
324
+ const response = await jobRepository.delete({ name: `name`, version: '1.0.0' });
325
+ expect(response).toBe(undefined);
326
+ expect(mockSend).toBeCalledTimes(1);
327
+ });
328
+ });
337
329
  });
@@ -3,53 +3,44 @@
3
3
  * Copyright Squiz Australia Pty Ltd. All Rights Reserved.
4
4
  */
5
5
 
6
- import * as dotenv from 'dotenv';
7
- dotenv.config();
8
-
9
6
  import {
10
7
  AttributeValue,
11
8
  DynamoDBClient,
12
- PutItemCommand,
13
9
  QueryCommand,
14
10
  QueryCommandInput,
15
- ScanCommand,
16
- ScanCommandOutput,
11
+ QueryCommandOutput,
17
12
  } from '@aws-sdk/client-dynamodb';
18
13
  import { inject } from 'inversify';
19
14
  import { provideTransient } from '../../transientProvider';
20
- import { Repository } from '../../repository/AbstractRepository';
21
- import { PARTITION_KEYS } from '../../constants/Repository.constants';
15
+ import { Repository } from '../AbstractRepository';
16
+ import { ENTITY_TYPES } from '../../constants/Repository.constants';
22
17
  import { parsePartitionKey } from '../../ParsePartitionKey';
23
- import { CreateJobRequest, Job, JobDto, ListJobParameters } from '../../model/Job/Job';
24
- import { PaginationInfo } from '../../core/pagination/model/PaginationInfo';
25
- import { PaginationQueryResponse } from '../../core/pagination/model/PaginationQueryResponse';
26
- import { decodeTokenString } from '../../core/pagination/utils/PaginationUtils';
27
- import { PAGINATION_DIRECTION_TYPE_BEFORE } from '../../core/pagination/model/PaginationTypes';
18
+ import { CreateJobRequest, Job, JobDto, JobSk, ListJobParameters } from '../../model/Job';
19
+ import {
20
+ decodeTokenString,
21
+ PAGINATION_DIRECTION_TYPE_BEFORE,
22
+ PaginationInfo,
23
+ PaginationQueryResponse,
24
+ } from '../../core/pagination';
25
+ import { InjectTokens } from '../../constants/InjectTokens';
28
26
 
29
27
  @provideTransient(JobRepository)
30
- export class JobRepository extends Repository<Job, JobDto> {
31
- public constructor(@inject(DynamoDBClient) protected dynamoDbClient: DynamoDBClient) {
32
- super(dynamoDbClient);
33
- this.pk = PARTITION_KEYS.job;
34
- this.tableName = process.env.DYNAMODB_TABLE as string;
28
+ export class JobRepository extends Repository<Job, JobDto, JobSk> {
29
+ public constructor(
30
+ @inject(DynamoDBClient) dynamoDbClient: DynamoDBClient,
31
+ @inject(InjectTokens.DynamoTableName) tableName: string,
32
+ @inject(InjectTokens.Tenant) tenant: string,
33
+ ) {
34
+ super(dynamoDbClient, tableName, `${ENTITY_TYPES.job}~${tenant}`);
35
35
  }
36
36
 
37
37
  public async create(input: CreateJobRequest): Promise<Job> {
38
38
  const newJob: Job = {
39
39
  ...input,
40
- type: this.pk,
40
+ type: ENTITY_TYPES.job,
41
41
  };
42
42
 
43
- const dynamoDbRecord = this.convertToDynamoDbItem(newJob);
44
-
45
- await this.dynamoDbClient.send(
46
- new PutItemCommand({
47
- Item: dynamoDbRecord,
48
- TableName: this.tableName,
49
- }),
50
- );
51
-
52
- return newJob;
43
+ return super.create(newJob);
53
44
  }
54
45
 
55
46
  /**
@@ -58,7 +49,9 @@ export class JobRepository extends Repository<Job, JobDto> {
58
49
  * @returns {Promise<Array<T>>} The retrieved records.
59
50
  * @memberof Repository
60
51
  */
61
- async list(parameters?: ListJobParameters): Promise<Array<Job>> {
52
+ async list(parameters: ListJobParameters = {}): Promise<Array<Job>> {
53
+ const keyConditionExpressions = ['#pk = :pk'];
54
+ const filterExpressions: Array<string> = [];
62
55
  const expressionAttributeNames: Record<string, string> = {
63
56
  '#pk': 'pk',
64
57
  };
@@ -66,26 +59,21 @@ export class JobRepository extends Repository<Job, JobDto> {
66
59
  ':pk': { S: this.pk },
67
60
  };
68
61
 
69
- const params = {
70
- ExpressionAttributeNames: expressionAttributeNames,
71
- ExpressionAttributeValues: expressionAttributeValues,
72
- FilterExpression: '#pk = :pk',
73
- TableName: this.tableName,
74
- };
75
-
76
- if (parameters) {
77
- const { name, version } = parameters;
78
-
79
- if (name) {
80
- params.ExpressionAttributeNames['#name'] = 'name';
81
- params.ExpressionAttributeValues[':name'] = { S: name };
82
- params.FilterExpression += ` and #name = :name`;
83
- }
84
-
85
- if (version) {
86
- params.ExpressionAttributeNames['#version'] = 'version';
87
- params.ExpressionAttributeValues[':version'] = { S: version };
88
- params.FilterExpression += ` and #version = :version`;
62
+ if (parameters.filters) {
63
+ const { name, version } = parameters.filters;
64
+
65
+ if (name && version) {
66
+ expressionAttributeNames['#sk'] = 'sk';
67
+ expressionAttributeValues[':sk'] = { S: `${name}~${version}` };
68
+ keyConditionExpressions.push(`#sk = :sk`);
69
+ } else if (name) {
70
+ expressionAttributeNames['#sk'] = 'sk';
71
+ expressionAttributeValues[':skPrefix'] = { S: `${name}~` };
72
+ keyConditionExpressions.push(`begins_with(#sk, :skPrefix)`);
73
+ } else if (version) {
74
+ expressionAttributeNames['#version'] = 'version';
75
+ expressionAttributeValues[':version'] = { S: version };
76
+ filterExpressions.push(`#version = :version`);
89
77
  }
90
78
  }
91
79
 
@@ -93,29 +81,33 @@ export class JobRepository extends Repository<Job, JobDto> {
93
81
  // By default, the Scan operation processes data sequentially.
94
82
  // DynamoDB returns data in increments of 1 MB increments.
95
83
 
96
- let startKey; /* Indicator of where to paginate from. */
84
+ let startKey: Record<string, AttributeValue> | undefined = undefined; /* Indicator of where to paginate from. */
97
85
  const results: Array<Record<string, AttributeValue>> = [];
98
86
 
99
87
  // Iterate through pages until no more results are found.
100
- while (startKey !== null) {
101
- const res: ScanCommandOutput = await this.dynamoDbClient.send(
102
- new ScanCommand({ ...params, ExclusiveStartKey: startKey }),
88
+ do {
89
+ const res: QueryCommandOutput = await this.dynamoDbClient.send(
90
+ new QueryCommand({
91
+ ExpressionAttributeNames: expressionAttributeNames,
92
+ ExpressionAttributeValues: expressionAttributeValues,
93
+ KeyConditionExpression: keyConditionExpressions.join(' and '),
94
+ FilterExpression: filterExpressions.length > 0 ? filterExpressions.join(' and ') : undefined,
95
+ TableName: this.tableName,
96
+ ExclusiveStartKey: startKey,
97
+ Limit: parameters.limit,
98
+ ConsistentRead: parameters.consistentRead,
99
+ }),
103
100
  );
104
101
 
105
- if (!res) {
106
- // Stop paginating if result is unexpected.
107
- startKey = null;
108
- } else {
109
- results.push(
110
- ...(res.Items ??
111
- /* istanbul ignore next */
112
- []),
113
- );
114
-
115
- // If no more results, set key to null to terminate loop
116
- startKey = res.LastEvaluatedKey ?? null;
102
+ results.push(...(res?.Items || []));
103
+ startKey = res?.LastEvaluatedKey;
104
+
105
+ if (parameters.limit !== undefined && results.length >= parameters.limit) {
106
+ // Truncate list to max requested items, stop querying.
107
+ results.length = parameters.limit;
108
+ break;
117
109
  }
118
- }
110
+ } while (startKey);
119
111
 
120
112
  return results.length ? results.map((item) => this.convertToItem(item as JobDto)) : [];
121
113
  }
@@ -160,8 +152,8 @@ export class JobRepository extends Repository<Job, JobDto> {
160
152
 
161
153
  convertToDynamoDbItem(item: Job): JobDto {
162
154
  return {
163
- pk: { S: item.type },
164
- sk: { S: `${item.name}-${item.version}` },
155
+ pk: { S: this.pk },
156
+ sk: { S: this.buildSk(item) },
165
157
  name: { S: item.name },
166
158
  description: item.description ? { S: item.description } : { NULL: true },
167
159
  version: { S: item.version },
@@ -181,8 +173,12 @@ export class JobRepository extends Repository<Job, JobDto> {
181
173
  return {
182
174
  name: name.S as string,
183
175
  description: description.S ? (description.S as string) : '',
184
- type: parsePartitionKey(pk.S as string),
176
+ type: parsePartitionKey(pk.S as string).entityType,
185
177
  version: version.S as string,
186
178
  };
187
179
  }
180
+
181
+ protected buildSk(sk: JobSk): string {
182
+ return `${sk.name}~${sk.version}`;
183
+ }
188
184
  }
@@ -0,0 +1,60 @@
1
+ import 'aws-sdk-client-mock-jest';
2
+ import { mockClient } from 'aws-sdk-client-mock';
3
+ import { DynamoDBClient, QueryCommand, QueryCommandInput } from '@aws-sdk/client-dynamodb';
4
+ import { faker } from '@faker-js/faker';
5
+ import { createMockJobExecution, createMockJobExecutionDto } from '../../model/JobExecution';
6
+ import { MultiTenantJobExecutionRepository } from './MultiTenantJobExecutionRepository';
7
+ import { JOB_EXECUTION_STATUS } from '../../constants/JobExecutionStatus.constants';
8
+ import { ENTITY_TYPES } from '../../constants/Repository.constants';
9
+
10
+ describe('MultiTenantJobExecutionRepository', () => {
11
+ const mockDynamoClient = mockClient(DynamoDBClient);
12
+ const tableName = faker.lorem.words();
13
+ let repository: MultiTenantJobExecutionRepository;
14
+
15
+ beforeEach(() => {
16
+ mockDynamoClient.reset();
17
+ repository = new MultiTenantJobExecutionRepository(new DynamoDBClient(), tableName);
18
+ });
19
+
20
+ describe('findByStatus', () => {
21
+ it('Queries job executions by status', async () => {
22
+ const tenant1 = faker.string.uuid();
23
+ const tenant2 = faker.string.uuid();
24
+ const executions = [createMockJobExecution(), createMockJobExecution(), createMockJobExecution()];
25
+ const expectedQueryInput: QueryCommandInput = {
26
+ KeyConditionExpression: 'gsi1pk = :gsi1pk',
27
+ ExpressionAttributeValues: {
28
+ ':gsi1pk': { S: `${ENTITY_TYPES.jobExecution}~running` },
29
+ },
30
+ TableName: tableName,
31
+ IndexName: 'gsi1',
32
+ };
33
+
34
+ mockDynamoClient
35
+ .on(QueryCommand)
36
+ .resolvesOnce({
37
+ Items: [createMockJobExecutionDto({ pk: `${ENTITY_TYPES.jobExecution}~${tenant1}`, ...executions[0] })],
38
+ LastEvaluatedKey: { pk: { S: 'page-2' } },
39
+ })
40
+ .resolvesOnce({
41
+ Items: [
42
+ createMockJobExecutionDto({ pk: `${ENTITY_TYPES.jobExecution}~${tenant1}`, ...executions[1] }),
43
+ createMockJobExecutionDto({ pk: `${ENTITY_TYPES.jobExecution}~${tenant2}`, ...executions[2] }),
44
+ ],
45
+ });
46
+
47
+ const results = await repository.findByStatus(JOB_EXECUTION_STATUS.running);
48
+
49
+ expect(results).toEqual({
50
+ [tenant1]: [executions[0], executions[1]],
51
+ [tenant2]: [executions[2]],
52
+ });
53
+ expect(mockDynamoClient).toHaveReceivedNthCommandWith(1, QueryCommand, expectedQueryInput);
54
+ expect(mockDynamoClient).toHaveReceivedNthCommandWith(2, QueryCommand, {
55
+ ...expectedQueryInput,
56
+ ExclusiveStartKey: { pk: { S: 'page-2' } },
57
+ });
58
+ });
59
+ });
60
+ });