@defra-fish/connectors-lib 1.61.0-rc.11 → 1.61.0-rc.13

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra-fish/connectors-lib",
3
- "version": "1.61.0-rc.11",
3
+ "version": "1.61.0-rc.13",
4
4
  "description": "Shared connectors",
5
5
  "type": "module",
6
6
  "engines": {
@@ -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
46
  "node-fetch": "^2.7.0",
43
47
  "redlock": "^4.2.0"
44
48
  },
45
- "gitHead": "ca0af7012d18aa9414afce422db4af34ed59dacd"
49
+ "gitHead": "fdd397ab2b9e6fa2346d765f4ada01ce5475dd10"
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,87 @@
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)
9
- })
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')
10
8
 
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)
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
+ )
15
23
  })
24
+ })
16
25
 
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')
26
+ describe.each`
27
+ name | clientName | configName | expectedAPIVersion
28
+ ${'ddb'} | ${'DynamoDB'} | ${''} | ${'2012-08-10'}
29
+ ${'sqs'} | ${'SQS'} | ${''} | ${'2012-11-05'}
30
+ ${'s3'} | ${'S3'} | ${''} | ${'2006-03-01'}
31
+ ${'secretsManager'} | ${'SecretsManager'} | ${'secretsManager'} | ${'2017-10-17'}
32
+ ${'docClient'} | ${'DynamoDBDocument'} | ${'dynamodb'} | ${'2012-08-10'}
33
+ `('AWS connectors for $clientName', ({ name, clientName, configName, expectedAPIVersion }) => {
34
+ beforeAll(() => {
35
+ createDocumentClient.mockImplementation(options => {
36
+ const client = new DynamoDB(options)
37
+ return DynamoDBDocument.from(client)
38
+ })
22
39
  })
23
40
 
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')
41
+ it(`exposes a ${clientName} client`, () => {
42
+ const { [name]: client } = AWS()
43
+ expect(client).toBeDefined()
29
44
  })
30
45
 
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()
46
+ it(`${name} client has type ${clientName}`, () => {
47
+ const { [name]: client } = AWS()
48
+ expect(client.constructor.name).toEqual(clientName)
36
49
  })
37
50
 
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()
51
+ it(`configures ${name} with a custom endpoint if one is defined in configuration`, async () => {
52
+ Config.aws[configName || clientName.toLowerCase()].endpoint = 'http://localhost:8080'
53
+
54
+ const { [name]: client } = AWS()
55
+ const endpoint = await client.config.endpoint()
56
+
57
+ expect(endpoint).toEqual(
58
+ expect.objectContaining({
59
+ hostname: 'localhost',
60
+ port: 8080,
61
+ protocol: 'http:',
62
+ path: '/'
63
+ })
64
+ )
44
65
  })
45
66
 
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)
67
+ it(`leaves endpoint undefined for ${clientName}, reverting to the internal handling for the default endpoint if it is not set in config`, async () => {
68
+ Config.aws[configName || clientName.toLowerCase()].endpoint = 'http://localhost:8080'
69
+
70
+ const { [name]: client } = AWS()
71
+ const endpoint = await client.config.endpoint()
72
+
73
+ expect(endpoint).toEqual(
74
+ expect.objectContaining({
75
+ hostname: 'localhost',
76
+ port: 8080,
77
+ protocol: 'http:',
78
+ path: '/'
79
+ })
80
+ )
50
81
  })
51
82
 
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')
83
+ it('uses expected apiVersion', () => {
84
+ const { [name]: client } = AWS()
85
+ expect(client.config.apiVersion).toEqual(expectedAPIVersion)
57
86
  })
58
87
  })
@@ -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
  })
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 { S3 } from '@aws-sdk/client-s3'
6
+ import { SecretsManager } from '@aws-sdk/client-secrets-manager'
5
7
 
6
8
  export default function () {
7
9
  return {
@@ -28,7 +30,7 @@ export default function () {
28
30
  apiVersion: '2006-03-01',
29
31
  ...(Config.aws.s3.endpoint && {
30
32
  endpoint: Config.aws.s3.endpoint,
31
- s3ForcePathStyle: true
33
+ forcePathStyle: true
32
34
  })
33
35
  }),
34
36
  secretsManager: new SecretsManager({
@@ -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))
@@ -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