@defra-fish/connectors-lib 1.61.0-rc.8 → 1.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@defra-fish/connectors-lib",
3
- "version": "1.61.0-rc.8",
3
+ "version": "1.61.0",
4
4
  "description": "Shared connectors",
5
5
  "type": "module",
6
6
  "engines": {
7
- "node": ">=18.17"
7
+ "node": ">=20"
8
8
  },
9
9
  "keywords": [
10
10
  "rod",
@@ -35,12 +35,16 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@airbrake/node": "^2.1.7",
38
- "aws-sdk": "^2.1074.0",
38
+ "@aws-sdk/client-dynamodb": "^3.0.0",
39
+ "@aws-sdk/client-s3": "^3.0.0",
40
+ "@aws-sdk/client-secrets-manager": "^3.0.0",
41
+ "@aws-sdk/client-sqs": "^3.0.0",
42
+ "@aws-sdk/lib-dynamodb": "^3.0.0",
39
43
  "debug": "^4.3.3",
40
44
  "http-status-codes": "^2.3.0",
41
45
  "ioredis": "^4.28.5",
42
- "node-fetch": "^2.6.7",
46
+ "node-fetch": "^2.7.0",
43
47
  "redlock": "^4.2.0"
44
48
  },
45
- "gitHead": "6ee197c0caa5d83c4b111f6a382eee271d1e4f61"
49
+ "gitHead": "8d1841265a40178f285288e5323a36e887483434"
46
50
  }
@@ -0,0 +1,36 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`document client decorations createUpdateExpression transforms object to an update expression, with provided attribute names and values 1`] = `
4
+ Object {
5
+ "ExpressionAttributeNames": Object {
6
+ "#payload": "payload",
7
+ "#payment": "payment",
8
+ "#permissions": "permissions",
9
+ "#status": "status",
10
+ },
11
+ "ExpressionAttributeValues": Object {
12
+ ":payload": Object {
13
+ "id": "abc-123",
14
+ "name": "ABCDE-123JJ-ABK12",
15
+ "type": "ddd-111-ggg-888",
16
+ },
17
+ ":payment": Object {
18
+ "amount": 16.32,
19
+ "method": "barter",
20
+ "source": "credit",
21
+ "timestamp": "2025-04-09T11:53:17.854Z",
22
+ },
23
+ ":permissions": Array [
24
+ Object {
25
+ "id": "abc-123",
26
+ "name": "ABCDE-123JJ-ABK12",
27
+ "type": "ddd-111-ggg-888",
28
+ },
29
+ ],
30
+ ":status": Object {
31
+ "id": "finalised",
32
+ },
33
+ },
34
+ "UpdateExpression": "SET #payload = :payload,#permissions = :permissions,#status = :status,#payment = :payment",
35
+ }
36
+ `;
@@ -1,58 +1,112 @@
1
1
  import Config from '../config.js'
2
- const TEST_ENDPOINT = 'http://localhost:8080'
3
- jest.dontMock('aws-sdk')
4
- describe('aws connectors', () => {
5
- it('configures dynamodb with a custom endpoint if one is defined in configuration', async () => {
6
- Config.aws.dynamodb.endpoint = TEST_ENDPOINT
7
- const { ddb } = require('../aws.js').default()
8
- expect(ddb.config.endpoint).toEqual(TEST_ENDPOINT)
2
+ import AWS from '../aws.js'
3
+ import { DynamoDB } from '@aws-sdk/client-dynamodb'
4
+ import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb'
5
+ import { createDocumentClient } from '../documentclient-decorator.js'
6
+
7
+ jest.mock('../documentclient-decorator.js')
8
+
9
+ describe('AWS Special cases', () => {
10
+ it('document client passes convertEmptyValues flag', () => {
11
+ let spiedOptions
12
+ createDocumentClient.mockImplementation(options => {
13
+ spiedOptions = options
14
+ const client = new DynamoDB(options)
15
+ return DynamoDBDocument.from(client)
16
+ })
17
+ AWS()
18
+ expect(spiedOptions).toEqual(
19
+ expect.objectContaining({
20
+ convertEmptyValues: true
21
+ })
22
+ )
9
23
  })
10
24
 
11
- it('configures sqs with a custom endpoint if one is defined in configuration', async () => {
12
- Config.aws.sqs.endpoint = TEST_ENDPOINT
13
- const { sqs } = require('../aws.js').default()
14
- expect(sqs.config.endpoint).toEqual(TEST_ENDPOINT)
25
+ it('exports ListObjectsV2Command from S3 SDK', () => {
26
+ const { ListObjectsV2Command } = AWS()
27
+ expect(ListObjectsV2Command).toBeDefined()
15
28
  })
16
29
 
17
- it('uses the default dynamodb endpoint if it is not overridden in configuration', async () => {
18
- process.env.AWS_REGION = 'eu-west-2'
19
- delete Config.aws.dynamodb.endpoint
20
- const { ddb } = require('../aws.js').default()
21
- expect(ddb.config.endpoint).toEqual('dynamodb.eu-west-2.amazonaws.com')
30
+ describe('AWS connectors for S3Client', () => {
31
+ it('has region set to eu-west-2', async () => {
32
+ const { s3 } = AWS()
33
+ const region = await s3.config.region()
34
+ expect(region).toBe('eu-west-2')
35
+ })
36
+
37
+ it('sets forcePathStyle to true when endpoint is defined', () => {
38
+ Config.aws.s3.endpoint = 'http://localhost:8080'
39
+ const { s3 } = AWS()
40
+ expect(s3.config.forcePathStyle).toBe(true)
41
+ delete Config.aws.s3.endpoint
42
+ })
43
+
44
+ it('does not set forcePathStyle when no endpoint is defined', () => {
45
+ const { s3 } = AWS()
46
+ expect(s3.config.forcePathStyle).not.toBe(true)
47
+ })
22
48
  })
49
+ })
23
50
 
24
- it('uses the default sqs endpoint if it is not overridden in configuration', async () => {
25
- process.env.AWS_REGION = 'eu-west-2'
26
- delete Config.aws.sqs.endpoint
27
- const { sqs } = require('../aws.js').default()
28
- expect(sqs.config.endpoint).toEqual('sqs.eu-west-2.amazonaws.com')
51
+ describe.each`
52
+ name | clientName | configName | expectedAPIVersion
53
+ ${'ddb'} | ${'DynamoDB'} | ${''} | ${'2012-08-10'}
54
+ ${'sqs'} | ${'SQS'} | ${''} | ${'2012-11-05'}
55
+ ${'s3'} | ${'S3Client'} | ${'s3'} | ${'2006-03-01'}
56
+ ${'secretsManager'} | ${'SecretsManager'} | ${'secretsManager'} | ${'2017-10-17'}
57
+ ${'docClient'} | ${'DynamoDBDocument'} | ${'dynamodb'} | ${'2012-08-10'}
58
+ `('AWS connectors for $clientName', ({ name, clientName, configName, expectedAPIVersion }) => {
59
+ beforeAll(() => {
60
+ createDocumentClient.mockImplementation(options => {
61
+ const client = new DynamoDB(options)
62
+ return DynamoDBDocument.from(client)
63
+ })
29
64
  })
30
65
 
31
- it('configures s3 with a custom endpoint if one is defined in configuration', async () => {
32
- Config.aws.s3.endpoint = TEST_ENDPOINT
33
- const { s3 } = require('../aws.js').default()
34
- expect(s3.config.endpoint).toEqual(TEST_ENDPOINT)
35
- expect(s3.config.s3ForcePathStyle).toBeTruthy()
66
+ it(`exposes a ${clientName} client`, () => {
67
+ const { [name]: client } = AWS()
68
+ expect(client).toBeDefined()
36
69
  })
37
70
 
38
- it('uses default s3 settings if a custom endpoint is not defined', async () => {
39
- process.env.AWS_REGION = 'eu-west-2'
40
- delete Config.aws.s3.endpoint
41
- const { s3 } = require('../aws.js').default()
42
- expect(s3.config.endpoint).toEqual('s3.eu-west-2.amazonaws.com')
43
- expect(s3.config.s3ForcePathStyle).toBeFalsy()
71
+ it(`${name} client has type ${clientName}`, () => {
72
+ const { [name]: client } = AWS()
73
+ expect(client.constructor.name).toEqual(clientName)
44
74
  })
45
75
 
46
- it('configures secretsmanager with a custom endpoint if one is defined in configuration', async () => {
47
- Config.aws.secretsManager.endpoint = TEST_ENDPOINT
48
- const { secretsManager } = require('../aws.js').default()
49
- expect(secretsManager.config.endpoint).toEqual(TEST_ENDPOINT)
76
+ it(`configures ${name} with a custom endpoint if one is defined in configuration`, async () => {
77
+ Config.aws[configName || clientName.toLowerCase()].endpoint = 'http://localhost:8080'
78
+
79
+ const { [name]: client } = AWS()
80
+ const endpoint = await client.config.endpoint()
81
+
82
+ expect(endpoint).toEqual(
83
+ expect.objectContaining({
84
+ hostname: 'localhost',
85
+ port: 8080,
86
+ protocol: 'http:',
87
+ path: '/'
88
+ })
89
+ )
90
+ })
91
+
92
+ it(`leaves endpoint undefined for ${clientName}, reverting to the internal handling for the default endpoint if it is not set in config`, async () => {
93
+ Config.aws[configName || clientName.toLowerCase()].endpoint = 'http://localhost:8080'
94
+
95
+ const { [name]: client } = AWS()
96
+ const endpoint = await client.config.endpoint()
97
+
98
+ expect(endpoint).toEqual(
99
+ expect.objectContaining({
100
+ hostname: 'localhost',
101
+ port: 8080,
102
+ protocol: 'http:',
103
+ path: '/'
104
+ })
105
+ )
50
106
  })
51
107
 
52
- it('uses default secretsmanager settings if a custom endpoint is not defined', async () => {
53
- process.env.AWS_REGION = 'eu-west-2'
54
- delete Config.aws.secretsManager.endpoint
55
- const { secretsManager } = require('../aws.js').default()
56
- expect(secretsManager.config.endpoint).toEqual('secretsmanager.eu-west-2.amazonaws.com')
108
+ it('uses expected apiVersion', () => {
109
+ const { [name]: client } = AWS()
110
+ expect(client.config.apiVersion).toEqual(expectedAPIVersion)
57
111
  })
58
112
  })
@@ -1,161 +1,218 @@
1
- import AWSSdk from 'aws-sdk'
2
- import AWS from '../aws.js'
3
- const { docClient } = AWS()
1
+ import { createDocumentClient } from '../documentclient-decorator'
2
+ import { DynamoDB } from '@aws-sdk/client-dynamodb'
3
+ import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb'
4
+
5
+ jest.mock('@aws-sdk/client-dynamodb')
6
+ jest.mock('@aws-sdk/lib-dynamodb')
4
7
 
5
8
  describe('document client decorations', () => {
6
- it('deals with pagination where DynamoDB returns a LastEvaluatedKey in a query response', async () => {
7
- const testLastEvaluatedKey = { id: '16324258-85-92746491' }
8
-
9
- AWSSdk.DynamoDB.DocumentClient.__setNextResponses(
10
- 'query',
11
- {
12
- Items: [],
13
- LastEvaluatedKey: testLastEvaluatedKey
14
- },
15
- {
16
- Items: []
17
- }
18
- )
19
- await docClient.queryAllPromise({
20
- TableName: 'TEST'
9
+ beforeAll(() => {
10
+ jest.spyOn(global, 'setTimeout').mockImplementation(cb => cb())
11
+ DynamoDBDocument.from.mockReturnValue({
12
+ query: jest.fn().mockResolvedValue({ Items: [], lastEvaluatedKey: false }),
13
+ scan: jest.fn().mockResolvedValue({ Items: [], lastEvaluatedKey: false }),
14
+ batchWrite: jest.fn().mockResolvedValue({ UnprocessedItems: {} })
21
15
  })
22
- expect(AWSSdk.DynamoDB.DocumentClient.mockedMethods.query).toHaveBeenNthCalledWith(
23
- 1,
24
- expect.objectContaining({
25
- TableName: 'TEST'
26
- })
27
- )
28
- expect(AWSSdk.DynamoDB.DocumentClient.mockedMethods.query).toHaveBeenNthCalledWith(
29
- 2,
30
- expect.objectContaining({
31
- TableName: 'TEST',
32
- ExclusiveStartKey: testLastEvaluatedKey
33
- })
34
- )
35
16
  })
17
+ afterEach(jest.clearAllMocks)
36
18
 
37
- it('deals with pagination where DynamoDB returns a LastEvaluatedKey in a scan response', async () => {
38
- const testLastEvaluatedKey = { id: '16324258-85-92746491' }
39
-
40
- AWSSdk.DynamoDB.DocumentClient.__setNextResponses(
41
- 'scan',
42
- {
43
- Items: [],
44
- LastEvaluatedKey: testLastEvaluatedKey
45
- },
46
- {
47
- Items: []
48
- }
49
- )
50
- await docClient.scanAllPromise({
51
- TableName: 'TEST'
52
- })
53
- expect(AWSSdk.DynamoDB.DocumentClient.mockedMethods.scan).toHaveBeenNthCalledWith(
54
- 1,
55
- expect.objectContaining({
56
- TableName: 'TEST'
57
- })
58
- )
59
- expect(AWSSdk.DynamoDB.DocumentClient.mockedMethods.scan).toHaveBeenNthCalledWith(
60
- 2,
61
- expect.objectContaining({
62
- TableName: 'TEST',
63
- ExclusiveStartKey: testLastEvaluatedKey
64
- })
65
- )
19
+ it('passes options to DynamoDB constructor', () => {
20
+ const options = {
21
+ option1: '1',
22
+ option2: 2,
23
+ option3: Symbol('option3')
24
+ }
25
+ createDocumentClient(options)
26
+ expect(DynamoDB).toHaveBeenCalledWith(options)
66
27
  })
67
28
 
68
- it('deals with UnprocessedItems when making batchWrite requests to DynamoDB', async () => {
69
- AWSSdk.DynamoDB.DocumentClient.__setNextResponses(
70
- 'batchWrite',
71
- {
72
- UnprocessedItems: {
73
- NameOfTableToUpdate: [
74
- { PutRequest: { Item: { key: '1', field: 'data1' } } },
75
- { PutRequest: { Item: { key: '2', field: 'data2' } } }
76
- ]
77
- }
78
- },
79
- {
80
- UnprocessedItems: null
81
- }
82
- )
83
- await docClient.batchWriteAllPromise({
84
- RequestItems: {
85
- NameOfTableToUpdate: [
86
- { PutRequest: { Item: { key: '1', field: 'data1' } } },
87
- { PutRequest: { Item: { key: '2', field: 'data2' } } },
88
- { PutRequest: { Item: { key: '3', field: 'data3' } } }
89
- ]
90
- }
91
- })
92
- expect(AWSSdk.DynamoDB.DocumentClient.mockedMethods.batchWrite).toHaveBeenCalledTimes(2)
93
- expect(AWSSdk.DynamoDB.DocumentClient.mockedMethods.batchWrite).toHaveBeenNthCalledWith(
94
- 1,
95
- expect.objectContaining({
96
- RequestItems: {
97
- NameOfTableToUpdate: [
98
- { PutRequest: { Item: { key: '1', field: 'data1' } } },
99
- { PutRequest: { Item: { key: '2', field: 'data2' } } },
100
- { PutRequest: { Item: { key: '3', field: 'data3' } } }
101
- ]
102
- }
103
- })
104
- )
105
- expect(AWSSdk.DynamoDB.DocumentClient.mockedMethods.batchWrite).toHaveBeenNthCalledWith(
106
- 2,
29
+ it('creates DynamoDBDocument using client', () => {
30
+ createDocumentClient()
31
+ const [mockClient] = DynamoDB.mock.instances
32
+ expect(DynamoDBDocument.from).toHaveBeenCalledWith(mockClient, expect.any(Object))
33
+ })
34
+
35
+ it('Sets options to strip empty and undefined values when marshalling to DynamoDB lists, sets and values', () => {
36
+ createDocumentClient()
37
+ expect(DynamoDBDocument.from).toHaveBeenCalledWith(
38
+ expect.any(Object),
107
39
  expect.objectContaining({
108
- RequestItems: {
109
- NameOfTableToUpdate: [
110
- { PutRequest: { Item: { key: '1', field: 'data1' } } },
111
- { PutRequest: { Item: { key: '2', field: 'data2' } } }
112
- ]
40
+ marshallOptions: {
41
+ convertEmptyValues: true,
42
+ removeUndefinedValues: true
113
43
  }
114
44
  })
115
45
  )
116
46
  })
117
47
 
118
- it('deals with UnprocessedItems when making batchWrite requests to DynamoDB up to the given retry limit', async () => {
119
- const batchWriteResponses = Array(11).fill({
120
- UnprocessedItems: {
121
- NameOfTableToUpdate: [
122
- { PutRequest: { Item: { key: '1', field: 'data1' } } },
123
- { PutRequest: { Item: { key: '2', field: 'data2' } } }
124
- ]
48
+ describe.each`
49
+ aggregateMethod | baseMethod
50
+ ${'queryAllPromise'} | ${'query'}
51
+ ${'scanAllPromise'} | ${'scan'}
52
+ `('$aggregateMethod', ({ aggregateMethod, baseMethod }) => {
53
+ it('is added to document client', () => {
54
+ const docClient = createDocumentClient()
55
+ expect(docClient[aggregateMethod]).toBeDefined()
56
+ })
57
+
58
+ it(`passes arguments provided for ${aggregateMethod} to ${baseMethod}`, async () => {
59
+ const params = { TableName: 'TEST', KeyConditionExpression: 'id = :id', ExpressionAttributeValues: { ':id': 1 } }
60
+ const docClient = createDocumentClient()
61
+ await docClient[aggregateMethod](params)
62
+ expect(docClient[baseMethod]).toHaveBeenCalledWith(params)
63
+ })
64
+
65
+ it(`calls ${baseMethod} repeatedly until LastEvaluatedKey evaluates to false, concatenating all returned items`, async () => {
66
+ const expectedItems = [
67
+ { id: 1, data: Symbol('data1') },
68
+ { id: 2, data: Symbol('data2') },
69
+ { id: 3, data: Symbol('data3') },
70
+ { id: 4, data: Symbol('data4') },
71
+ { id: 5, data: Symbol('data5') }
72
+ ]
73
+ const docClient = createDocumentClient()
74
+ docClient[baseMethod]
75
+ .mockResolvedValueOnce({ Items: expectedItems.slice(0, 2), LastEvaluatedKey: true })
76
+ .mockResolvedValueOnce({ Items: expectedItems.slice(2, 4), LastEvaluatedKey: true })
77
+ .mockResolvedValueOnce({ Items: expectedItems.slice(4), LastEvaluatedKey: false })
78
+ const actualItems = await docClient[aggregateMethod]()
79
+ expect(actualItems).toEqual(expectedItems)
80
+ })
81
+
82
+ it(`whilst concatenating ${baseMethod} results, passes ExclusiveStartKey param`, async () => {
83
+ const expectedKey = Symbol('🔑')
84
+ const docClient = createDocumentClient()
85
+ docClient[baseMethod].mockResolvedValueOnce({ Items: [], LastEvaluatedKey: expectedKey }).mockResolvedValueOnce({ Items: [] })
86
+ await docClient[aggregateMethod]()
87
+ expect(docClient[baseMethod]).toHaveBeenNthCalledWith(
88
+ 2,
89
+ expect.objectContaining({
90
+ ExclusiveStartKey: expectedKey
91
+ })
92
+ )
93
+ })
94
+
95
+ it("omits ExclusiveStartKey if previous LastEvaluatedKey isn't available", async () => {
96
+ const docClient = createDocumentClient()
97
+ await docClient[aggregateMethod]()
98
+ expect(docClient[baseMethod]).toHaveBeenNthCalledWith(
99
+ 1,
100
+ expect.not.objectContaining({
101
+ ExclusiveStartKey: expect.anything()
102
+ })
103
+ )
104
+ })
105
+ })
106
+
107
+ describe('batchWriteAllPromise', () => {
108
+ it('is added to document client', () => {
109
+ const docClient = createDocumentClient()
110
+ expect(docClient.batchWriteAllPromise).toBeDefined()
111
+ })
112
+
113
+ it('passes arguments provided for batchWriteAllPromise to batchWrite', async () => {
114
+ const params = { RequestItems: { TEST: [{ PutRequest: { Item: { id: 1, data: Symbol('data1') } } }] } }
115
+ const docClient = createDocumentClient()
116
+ await docClient.batchWriteAllPromise(params)
117
+ expect(docClient.batchWrite).toHaveBeenCalledWith(params)
118
+ })
119
+
120
+ it('calls batchWrite repeatedly until UnprocessedItems is empty', async () => {
121
+ const docClient = createDocumentClient()
122
+ docClient.batchWrite
123
+ .mockResolvedValueOnce({ UnprocessedItems: { key: true } })
124
+ .mockResolvedValueOnce({ UnprocessedItems: { key: true } })
125
+ .mockResolvedValueOnce({ UnprocessedItems: { key: true } })
126
+ .mockResolvedValueOnce({ UnprocessedItems: {} })
127
+ await docClient.batchWriteAllPromise({ RequestItems: { key: true } })
128
+ expect(docClient.batchWrite).toHaveBeenCalledTimes(4)
129
+ })
130
+
131
+ it.each([
132
+ [1, 500],
133
+ [2, 750],
134
+ [3, 1125],
135
+ [4, 1687.5],
136
+ [5, 2500],
137
+ [6, 2500],
138
+ [7, 2500],
139
+ [8, 2500],
140
+ [9, 2500],
141
+ [10, 2500]
142
+ ])('retries %i times with %i ms delay on final retry', async (retries, delay) => {
143
+ const docClient = createDocumentClient()
144
+ for (let i = 0; i < retries; i++) {
145
+ docClient.batchWrite.mockResolvedValueOnce({ UnprocessedItems: { key: true } })
125
146
  }
147
+ await docClient.batchWriteAllPromise({ RequestItems: { key: true } })
148
+ expect(setTimeout).toHaveBeenNthCalledWith(retries, expect.any(Function), delay)
126
149
  })
127
- AWSSdk.DynamoDB.DocumentClient.__setNextResponses('batchWrite', ...batchWriteResponses)
128
- const request = {
129
- RequestItems: {
130
- NameOfTableToUpdate: [
131
- { PutRequest: { Item: { key: '1', field: 'data1' } } },
132
- { PutRequest: { Item: { key: '2', field: 'data2' } } },
133
- { PutRequest: { Item: { key: '3', field: 'data3' } } }
134
- ]
150
+
151
+ it('throws an error on the eleventh retry', async () => {
152
+ const docClient = createDocumentClient()
153
+ for (let i = 0; i < 11; i++) {
154
+ docClient.batchWrite.mockResolvedValueOnce({ UnprocessedItems: { key: true } })
135
155
  }
136
- }
137
- // Don't delay on setTimeouts!
138
- jest.spyOn(global, 'setTimeout').mockImplementation(cb => cb())
139
- await expect(docClient.batchWriteAllPromise(request)).rejects.toThrow(
140
- 'Failed to write items to DynamoDB using batch write. UnprocessedItems were returned and maxRetries has been reached.'
141
- )
156
+ await expect(() => docClient.batchWriteAllPromise({ RequestItems: { key: true } })).rejects.toThrow()
157
+ })
158
+
159
+ it('adds unprocessed items to batchWrite request', async () => {
160
+ const token = Symbol('token')
161
+ const firstCallSymbol = Symbol('first call')
162
+ const secondCallSymbol = Symbol('second call')
163
+ const docClient = createDocumentClient()
164
+ docClient.batchWrite.mockResolvedValueOnce({ UnprocessedItems: { secondCallSymbol } })
165
+ await docClient.batchWriteAllPromise({ token, RequestItems: { firstCallSymbol } })
166
+ expect(docClient.batchWrite).toHaveBeenNthCalledWith(
167
+ 2,
168
+ expect.objectContaining({
169
+ token,
170
+ RequestItems: { secondCallSymbol }
171
+ })
172
+ )
173
+ })
142
174
  })
143
175
 
144
- it('provides a convenience method to simplify building an update expression for DynamoDB', async () => {
145
- const test = {
146
- name: 'name-value',
147
- number: 123
148
- }
149
- expect(docClient.createUpdateExpression(test)).toStrictEqual({
150
- UpdateExpression: 'SET #name = :name,#number = :number',
151
- ExpressionAttributeNames: {
152
- '#name': 'name',
153
- '#number': 'number'
154
- },
155
- ExpressionAttributeValues: {
156
- ':name': 'name-value',
157
- ':number': 123
176
+ describe('createUpdateExpression', () => {
177
+ it('is added to document client', () => {
178
+ const docClient = createDocumentClient()
179
+ expect(docClient.createUpdateExpression).toBeDefined()
180
+ })
181
+
182
+ it('returns an object with UpdateExpression, ExpressionAttributeNames and ExpressionAttributeValues', () => {
183
+ const docClient = createDocumentClient()
184
+ const actual = docClient.createUpdateExpression({ id: 1, data: Symbol('data1') })
185
+ expect(actual).toEqual(
186
+ expect.objectContaining({
187
+ UpdateExpression: expect.any(String),
188
+ ExpressionAttributeNames: expect.any(Object),
189
+ ExpressionAttributeValues: expect.any(Object)
190
+ })
191
+ )
192
+ })
193
+
194
+ it('transforms object to an update expression, with provided attribute names and values', () => {
195
+ const permission = {
196
+ id: 'abc-123',
197
+ name: 'ABCDE-123JJ-ABK12',
198
+ type: 'ddd-111-ggg-888'
158
199
  }
200
+ const transaction = {
201
+ payload: permission,
202
+ permissions: [permission],
203
+ status: { id: 'finalised' },
204
+ payment: {
205
+ amount: 16.32,
206
+ method: 'barter',
207
+ source: 'credit',
208
+ timestamp: '2025-04-09T11:53:17.854Z'
209
+ }
210
+ }
211
+ const docClient = createDocumentClient()
212
+
213
+ const actual = docClient.createUpdateExpression(transaction)
214
+
215
+ expect(actual).toMatchSnapshot()
159
216
  })
160
217
  })
161
218
  })
@@ -706,52 +706,4 @@ describe('sales-api-connector', () => {
706
706
  })
707
707
  })
708
708
  })
709
-
710
- describe('linkRecurringPayments', () => {
711
- describe.each([
712
- ['existing-recurring-payment-id', 'agreement-id'],
713
- ['abc-123', 'def-456']
714
- ])("Processing payment for transaction id '%s'", (existingRecurringPaymentId, agreementId) => {
715
- beforeEach(() => {
716
- fetch.mockReturnValue({
717
- ok: true,
718
- status: 200,
719
- statusText: 'OK',
720
- text: async () => JSON.stringify({ existingRecurringPaymentId, agreementId })
721
- })
722
- })
723
-
724
- it('calls the endpoint with the correct parameters', async () => {
725
- await salesApi.linkRecurringPayments(existingRecurringPaymentId, agreementId)
726
-
727
- expect(fetch).toHaveBeenCalledWith(`http://0.0.0.0:4000/linkRecurringPayments/${existingRecurringPaymentId}/${agreementId}`, {
728
- method: 'get',
729
- headers: expect.any(Object),
730
- timeout: 20000
731
- })
732
- })
733
-
734
- it('returns the expected response data', async () => {
735
- const processedResult = await salesApi.linkRecurringPayments(existingRecurringPaymentId, agreementId)
736
-
737
- expect(processedResult).toEqual({ existingRecurringPaymentId, agreementId })
738
- })
739
- })
740
-
741
- it('throws an error on non-2xx response', async () => {
742
- fetch.mockReturnValue({
743
- ok: false,
744
- status: 500,
745
- statusText: 'Internal Server Error',
746
- text: async () => 'Server Error'
747
- })
748
-
749
- await expect(salesApi.linkRecurringPayments('existing-recurring-payment-id', 'agreement-id')).rejects.toThrow('Internal Server Error')
750
- expect(fetch).toHaveBeenCalledWith('http://0.0.0.0:4000/linkRecurringPayments/existing-recurring-payment-id/agreement-id', {
751
- method: 'get',
752
- headers: expect.any(Object),
753
- timeout: 20000
754
- })
755
- })
756
- })
757
709
  })
package/src/aws.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import Config from './config.js'
2
2
  import { createDocumentClient } from './documentclient-decorator.js'
3
- import AWS from 'aws-sdk'
4
- const { DynamoDB, SQS, S3, SecretsManager } = AWS
3
+ import { DynamoDB } from '@aws-sdk/client-dynamodb'
4
+ import { SQS } from '@aws-sdk/client-sqs'
5
+ import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3'
6
+ import { SecretsManager } from '@aws-sdk/client-secrets-manager'
5
7
 
6
8
  export default function () {
7
9
  return {
@@ -24,11 +26,12 @@ export default function () {
24
26
  endpoint: Config.aws.sqs.endpoint
25
27
  })
26
28
  }),
27
- s3: new S3({
29
+ s3: new S3Client({
30
+ region: 'eu-west-2',
28
31
  apiVersion: '2006-03-01',
29
32
  ...(Config.aws.s3.endpoint && {
30
33
  endpoint: Config.aws.s3.endpoint,
31
- s3ForcePathStyle: true
34
+ forcePathStyle: true
32
35
  })
33
36
  }),
34
37
  secretsManager: new SecretsManager({
@@ -36,6 +39,7 @@ export default function () {
36
39
  ...(Config.aws.secretsManager.endpoint && {
37
40
  endpoint: Config.aws.secretsManager.endpoint
38
41
  })
39
- })
42
+ }),
43
+ ListObjectsV2Command
40
44
  }
41
45
  }
@@ -1,10 +1,16 @@
1
1
  import db from 'debug'
2
- import AWS from 'aws-sdk'
3
- const { DynamoDB } = AWS
2
+ import { DynamoDB } from '@aws-sdk/client-dynamodb'
3
+ import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb'
4
4
  const debug = db('connectors:aws')
5
5
 
6
6
  export const createDocumentClient = options => {
7
- const docClient = new DynamoDB.DocumentClient(options)
7
+ const client = new DynamoDB(options)
8
+ const docClient = DynamoDBDocument.from(client, {
9
+ marshallOptions: {
10
+ convertEmptyValues: true,
11
+ removeUndefinedValues: true
12
+ }
13
+ })
8
14
 
9
15
  // Support for large query/scan operations which return results in pages
10
16
  const wrapPagedDocumentClientOperation = operationName => {
@@ -15,7 +21,7 @@ export const createDocumentClient = options => {
15
21
  const response = await docClient[operationName]({
16
22
  ...params,
17
23
  ...(lastEvaluatedKey && { ExclusiveStartKey: lastEvaluatedKey })
18
- }).promise()
24
+ })
19
25
  lastEvaluatedKey = response.LastEvaluatedKey
20
26
  response.Items && items.push(...response.Items)
21
27
  } while (lastEvaluatedKey)
@@ -26,9 +32,9 @@ export const createDocumentClient = options => {
26
32
  docClient.scanAllPromise = wrapPagedDocumentClientOperation('scan')
27
33
 
28
34
  /**
29
- * Handles batch writes which may return UnprocessedItems. If UnprocessedItems are returned then they will be retried with exponential backoff
35
+ * Handles batch writes which may return UnprocessedItems. If UnprocessedItems are returned then they will be retried with exponential backoff
30
36
  *
31
- * @param {DocumentClient.BatchWriteItemInput} params as per DynamoDB.DocumentClient.batchWrite
37
+ * @param {DocumentClient.BatchWriteCommandInput} params as per DynamoDB.DocumentClient.batchWrite
32
38
  * @returns {Promise<void>}
33
39
  */
34
40
  docClient.batchWriteAllPromise = async params => {
@@ -37,13 +43,13 @@ export const createDocumentClient = options => {
37
43
  let unprocessedItemsDelay = 500
38
44
  let maxRetries = 10
39
45
  while (hasUnprocessedItems) {
40
- const result = await docClient.batchWrite(request).promise()
46
+ const result = await docClient.batchWrite(request)
41
47
  hasUnprocessedItems = !!Object.keys(result.UnprocessedItems ?? {}).length
42
48
  if (hasUnprocessedItems) {
43
49
  request = { ...params, RequestItems: result.UnprocessedItems }
44
50
  if (maxRetries-- === 0) {
45
51
  throw new Error(
46
- 'Failed to write items to DynamoDB using batch write. UnprocessedItems were returned and maxRetries has been reached.'
52
+ 'Failed to write items to DynamoDB using batch write. UnprocessedItems were returned and maxRetries has been reached.'
47
53
  )
48
54
  }
49
55
  await new Promise(resolve => setTimeout(resolve, unprocessedItemsDelay))
@@ -308,15 +308,3 @@ export const preparePermissionDataForRenewal = async referenceNumber =>
308
308
  export const processRPResult = async (transactionId, paymentId, createdDate) => {
309
309
  return exec2xxOrThrow(call(new URL(`/processRPResult/${transactionId}/${paymentId}/${createdDate}`, urlBase), 'get'))
310
310
  }
311
-
312
- /**
313
- * Link an old RecurringPayment to its replacement
314
- *
315
- * @param existingRecurringPaymentId
316
- * @param agreementId
317
- * @returns {Promise<*>}
318
- * @throws on a non-2xx response
319
- */
320
- export const linkRecurringPayments = async (existingRecurringPaymentId, agreementId) => {
321
- return exec2xxOrThrow(call(new URL(`/linkRecurringPayments/${existingRecurringPaymentId}/${agreementId}`, urlBase), 'get'))
322
- }
@@ -1,73 +0,0 @@
1
- import cloneDeep from 'clone-deep'
2
- export const configureAwsSdkMock = (AwsSdk = jest.genMockFromModule('aws-sdk')) => {
3
- const configuredMocks = []
4
- AwsSdk.__resetAll = () => configuredMocks.forEach(c => c.__init())
5
-
6
- const configureMock = (awsClass, methodNames, defaultResponse) => {
7
- configuredMocks.push(awsClass)
8
- awsClass.mockedMethods = createHandlers(awsClass, methodNames, defaultResponse)
9
-
10
- awsClass.mockImplementation(() => ({ ...awsClass.mockedMethods }))
11
- awsClass.expectedResponses = {}
12
- awsClass.nextResponses = {}
13
- awsClass.expectedErrors = {}
14
- awsClass.__init = (
15
- { expectedResponses = {}, nextResponses = [], expectedErrors = {} } = {
16
- expectedResponses: {},
17
- nextResponses: {},
18
- expectedErrors: {}
19
- }
20
- ) => {
21
- awsClass.expectedResponses = expectedResponses
22
- awsClass.nextResponses = nextResponses
23
- awsClass.expectedErrors = expectedErrors
24
- }
25
- awsClass.__setResponse = (methodName, response) => {
26
- awsClass.expectedResponses[methodName] = cloneDeep(response)
27
- }
28
- awsClass.__setNextResponses = (methodName, ...responses) => {
29
- awsClass.nextResponses[methodName] = cloneDeep(responses)
30
- }
31
- awsClass.__throwWithErrorOn = (methodName, error = new Error('Test error')) => {
32
- awsClass.expectedErrors[methodName] = error
33
- }
34
- }
35
-
36
- const createHandlers = (awsClass, names, defaultResponse) => {
37
- return names.reduce((acc, name) => {
38
- acc[name] = jest.fn(() => ({
39
- promise: jest.fn(async () => {
40
- if (awsClass.expectedErrors[name]) {
41
- throw awsClass.expectedErrors[name]
42
- }
43
- if (awsClass.nextResponses[name] && awsClass.nextResponses[name].length) {
44
- return awsClass.nextResponses[name].shift()
45
- }
46
- return awsClass.expectedResponses[name] || defaultResponse
47
- })
48
- }))
49
- return acc
50
- }, {})
51
- }
52
-
53
- configureMock(AwsSdk.SQS, [
54
- 'listQueues',
55
- 'createQueue',
56
- 'deleteQueue',
57
- 'purgeQueue',
58
- 'sendMessage',
59
- 'receiveMessage',
60
- 'deleteMessage',
61
- 'deleteMessageBatch'
62
- ])
63
- configureMock(AwsSdk.DynamoDB, ['listTables', 'describeTable', 'getItem', 'putItem', 'query', 'scan'], {})
64
- configureMock(
65
- AwsSdk.DynamoDB.DocumentClient,
66
- ['get', 'put', 'update', 'query', 'scan', 'delete', 'createSet', 'batchGet', 'batchWrite'],
67
- {}
68
- )
69
- configureMock(AwsSdk.S3, ['listObjectsV2', 'getObject', 'putObject', 'headObject', 'deleteObject', 'upload', 'listBuckets', 'headBucket'])
70
- configureMock(AwsSdk.SecretsManager, ['getSecretValue'])
71
-
72
- return AwsSdk
73
- }
@@ -1,3 +0,0 @@
1
- import { configureAwsSdkMock } from './aws-mock-helper.js'
2
- const AwsSdk = configureAwsSdkMock()
3
- export default AwsSdk