@defra-fish/dynamics-lib 1.73.0-sales-api-refactor → 1.73.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 +4 -3
- package/src/__mocks__/dynamics-web-api-mock-helper.js +6 -6
- package/src/__mocks__/dynamics-web-api.js +4 -2
- package/src/client/__tests__/dynamics-client.spec.js +138 -32
- package/src/client/__tests__/entity-manager.spec.js +27 -27
- package/src/client/dynamics-client.js +7 -5
- package/src/client/entity-manager.js +12 -9
- package/src/entities/base.entity.js +1 -1
- package/src/queries/__tests__/contact.queries.spec.js +7 -5
- package/src/queries/__tests__/recurring-payments-queries.spec.js +4 -45
- package/src/queries/recurring-payments.queries.js +0 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra-fish/dynamics-lib",
|
|
3
|
-
"version": "1.73.0
|
|
3
|
+
"version": "1.73.0",
|
|
4
4
|
"description": "Framework to support integration with dynamics",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -36,11 +36,12 @@
|
|
|
36
36
|
"cache-manager": "3.6.0",
|
|
37
37
|
"cache-manager-ioredis": "2.1.0",
|
|
38
38
|
"debug": "4.3.3",
|
|
39
|
-
"dynamics-web-api": "
|
|
39
|
+
"dynamics-web-api": "2.4.0",
|
|
40
40
|
"joi": "17.13.3",
|
|
41
41
|
"moment": "2.29.1",
|
|
42
42
|
"pluralize": "8.0.0",
|
|
43
43
|
"simple-oauth2": "4.3.0",
|
|
44
44
|
"uuid": "8.3.2"
|
|
45
|
-
}
|
|
45
|
+
},
|
|
46
|
+
"gitHead": "128d150e6bd30468bca4e39fb44dd11c5598eb43"
|
|
46
47
|
}
|
|
@@ -3,7 +3,7 @@ const { readFileSync } = jest.requireActual('fs')
|
|
|
3
3
|
const Path = jest.requireActual('path')
|
|
4
4
|
const optionSetDataPath = Path.join(Project.root, 'src', '__mocks__', 'option-set-data.json')
|
|
5
5
|
|
|
6
|
-
export const configureDynamicsWebApiMock =
|
|
6
|
+
export const configureDynamicsWebApiMock = DynamicsWebApi => {
|
|
7
7
|
let expectedResponse = {}
|
|
8
8
|
let nextResponses = {}
|
|
9
9
|
let callError = {}
|
|
@@ -53,12 +53,12 @@ export const configureDynamicsWebApiMock = (DynamicsWebApi = jest.genMockFromMod
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
DynamicsWebApi.prototype.
|
|
57
|
-
DynamicsWebApi.prototype.
|
|
58
|
-
DynamicsWebApi.prototype.
|
|
59
|
-
DynamicsWebApi.prototype.
|
|
56
|
+
DynamicsWebApi.prototype.retrieve = jest.fn(async () => responseCapableMethod('retrieve'))
|
|
57
|
+
DynamicsWebApi.prototype.create = jest.fn(async () => responseCapableMethod('create'))
|
|
58
|
+
DynamicsWebApi.prototype.update = jest.fn(async () => responseCapableMethod('update'))
|
|
59
|
+
DynamicsWebApi.prototype.retrieveMultiple = jest.fn(async () => responseCapableMethod('retrieveMultiple'))
|
|
60
60
|
DynamicsWebApi.prototype.retrieveGlobalOptionSets = jest.fn(async () => responseCapableMethod('retrieveGlobalOptionSets'))
|
|
61
|
-
DynamicsWebApi.prototype.
|
|
61
|
+
DynamicsWebApi.prototype.callFunction = jest.fn(async functionName => {
|
|
62
62
|
let returnValue = null
|
|
63
63
|
if (functionName === 'RetrieveVersion') {
|
|
64
64
|
returnValue = {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { configureDynamicsWebApiMock } from './dynamics-web-api-mock-helper.js'
|
|
2
|
-
const DynamicsWebApi = jest.
|
|
3
|
-
|
|
2
|
+
const DynamicsWebApi = jest.fn()
|
|
3
|
+
DynamicsWebApi.prototype = {}
|
|
4
|
+
configureDynamicsWebApiMock(DynamicsWebApi)
|
|
5
|
+
export { DynamicsWebApi }
|
|
@@ -1,39 +1,145 @@
|
|
|
1
1
|
import { config } from '../dynamics-client.js'
|
|
2
2
|
import SimpleOAuth2 from 'simple-oauth2'
|
|
3
3
|
|
|
4
|
+
const MOCK_SCOPE = 'https://resource/.default'
|
|
5
|
+
const PREEMPTIVE_TOKEN_EXPIRY_SECONDS = 60
|
|
6
|
+
|
|
7
|
+
const setRequiredEnv = () => {
|
|
8
|
+
process.env.DYNAMICS_API_PATH = 'https://test-server'
|
|
9
|
+
process.env.DYNAMICS_API_VERSION = '9.1'
|
|
10
|
+
process.env.DYNAMICS_API_TIMEOUT = 60000
|
|
11
|
+
process.env.OAUTH_AUTHORITY_HOST_URL = 'https://test-authority/'
|
|
12
|
+
process.env.OAUTH_TENANT = 'tenant'
|
|
13
|
+
process.env.OAUTH_CLIENT_ID = 'clientId'
|
|
14
|
+
process.env.OAUTH_CLIENT_SECRET = 'clientSecret'
|
|
15
|
+
process.env.OAUTH_SCOPE = MOCK_SCOPE
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const createConfiguredClient = () => {
|
|
19
|
+
const dynamicsApiConfig = config()
|
|
20
|
+
const oauthClient = SimpleOAuth2.ClientCredentials.mock.results.at(-1).value
|
|
21
|
+
return { dynamicsApiConfig, oauthClient }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const seedCachedToken = async (dynamicsApiConfig, token = 'MOCK TOKEN') => {
|
|
25
|
+
SimpleOAuth2.__setMockTokenReturnValue(token)
|
|
26
|
+
await dynamicsApiConfig.onTokenRefresh()
|
|
27
|
+
}
|
|
28
|
+
|
|
4
29
|
describe('dynamics-client', () => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
jest.clearAllMocks()
|
|
32
|
+
SimpleOAuth2.__setMockTokenReturnValue('MOCK TOKEN')
|
|
33
|
+
SimpleOAuth2.__setMockTokenExpired(false)
|
|
34
|
+
setRequiredEnv()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('exposes serverUrl from DYNAMICS_API_PATH', () => {
|
|
38
|
+
const { dynamicsApiConfig } = createConfiguredClient()
|
|
39
|
+
|
|
40
|
+
expect(dynamicsApiConfig.serverUrl).toBe(process.env.DYNAMICS_API_PATH)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('exposes dataApi.version from DYNAMICS_API_VERSION', () => {
|
|
44
|
+
const { dynamicsApiConfig } = createConfiguredClient()
|
|
45
|
+
|
|
46
|
+
expect(dynamicsApiConfig.dataApi.version).toBe(process.env.DYNAMICS_API_VERSION)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('exposes timeout from DYNAMICS_API_TIMEOUT', () => {
|
|
50
|
+
const { dynamicsApiConfig } = createConfiguredClient()
|
|
51
|
+
|
|
52
|
+
expect(dynamicsApiConfig.timeout).toBe(`${process.env.DYNAMICS_API_TIMEOUT}`)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('exposes onTokenRefresh as a function', () => {
|
|
56
|
+
const { dynamicsApiConfig } = createConfiguredClient()
|
|
57
|
+
|
|
58
|
+
expect(typeof dynamicsApiConfig.onTokenRefresh).toBe('function')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('returns getToken access token when access token cache is empty', async () => {
|
|
62
|
+
SimpleOAuth2.__setMockTokenReturnValue('FIRST TOKEN')
|
|
63
|
+
const { dynamicsApiConfig } = createConfiguredClient()
|
|
64
|
+
|
|
65
|
+
const token = await dynamicsApiConfig.onTokenRefresh()
|
|
66
|
+
|
|
67
|
+
expect(token).toBe('FIRST TOKEN')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('calls getToken once when access token cache is empty', async () => {
|
|
71
|
+
const { dynamicsApiConfig, oauthClient } = createConfiguredClient()
|
|
72
|
+
|
|
73
|
+
await dynamicsApiConfig.onTokenRefresh()
|
|
74
|
+
|
|
75
|
+
expect(oauthClient.getToken).toHaveBeenCalledTimes(1)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('calls getToken with scope when access token cache is empty', async () => {
|
|
79
|
+
const { dynamicsApiConfig, oauthClient } = createConfiguredClient()
|
|
80
|
+
|
|
81
|
+
await dynamicsApiConfig.onTokenRefresh()
|
|
82
|
+
|
|
83
|
+
expect(oauthClient.getToken).toHaveBeenCalledWith({ scope: MOCK_SCOPE })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('calls accessToken.expired with PREEMPTIVE_TOKEN_EXPIRY_SECONDS when token is cached', async () => {
|
|
87
|
+
const { dynamicsApiConfig, oauthClient } = createConfiguredClient()
|
|
88
|
+
await seedCachedToken(dynamicsApiConfig)
|
|
89
|
+
const cachedAccessToken = await oauthClient.getToken.mock.results[0].value
|
|
90
|
+
|
|
91
|
+
await dynamicsApiConfig.onTokenRefresh()
|
|
92
|
+
|
|
93
|
+
expect(cachedAccessToken.expired).toHaveBeenCalledWith(PREEMPTIVE_TOKEN_EXPIRY_SECONDS)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('returns cached token when cached access token is not expired', async () => {
|
|
97
|
+
const { dynamicsApiConfig } = createConfiguredClient()
|
|
98
|
+
await seedCachedToken(dynamicsApiConfig, 'FIRST TOKEN')
|
|
99
|
+
SimpleOAuth2.__setMockTokenReturnValue('SECOND TOKEN')
|
|
100
|
+
|
|
101
|
+
const token = await dynamicsApiConfig.onTokenRefresh()
|
|
102
|
+
|
|
103
|
+
expect(token).toBe('FIRST TOKEN')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('does not call getToken again when cached access token is not expired', async () => {
|
|
107
|
+
const { dynamicsApiConfig, oauthClient } = createConfiguredClient()
|
|
108
|
+
await seedCachedToken(dynamicsApiConfig)
|
|
109
|
+
|
|
110
|
+
await dynamicsApiConfig.onTokenRefresh()
|
|
111
|
+
|
|
112
|
+
expect(oauthClient.getToken).toHaveBeenCalledTimes(1)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('returns refreshed token when cached access token is expired', async () => {
|
|
116
|
+
const { dynamicsApiConfig } = createConfiguredClient()
|
|
117
|
+
await seedCachedToken(dynamicsApiConfig, 'FIRST TOKEN')
|
|
118
|
+
SimpleOAuth2.__setMockTokenReturnValue('NEW TOKEN')
|
|
35
119
|
SimpleOAuth2.__setMockTokenExpired(true)
|
|
36
|
-
|
|
37
|
-
|
|
120
|
+
|
|
121
|
+
const token = await dynamicsApiConfig.onTokenRefresh()
|
|
122
|
+
|
|
123
|
+
expect(token).toBe('NEW TOKEN')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('calls getToken again when cached access token is expired', async () => {
|
|
127
|
+
const { dynamicsApiConfig, oauthClient } = createConfiguredClient()
|
|
128
|
+
await seedCachedToken(dynamicsApiConfig)
|
|
129
|
+
SimpleOAuth2.__setMockTokenExpired(true)
|
|
130
|
+
|
|
131
|
+
await dynamicsApiConfig.onTokenRefresh()
|
|
132
|
+
|
|
133
|
+
expect(oauthClient.getToken).toHaveBeenCalledTimes(2)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('calls getToken with scope when cached access token is expired', async () => {
|
|
137
|
+
const { dynamicsApiConfig, oauthClient } = createConfiguredClient()
|
|
138
|
+
await seedCachedToken(dynamicsApiConfig)
|
|
139
|
+
SimpleOAuth2.__setMockTokenExpired(true)
|
|
140
|
+
|
|
141
|
+
await dynamicsApiConfig.onTokenRefresh()
|
|
142
|
+
|
|
143
|
+
expect(oauthClient.getToken).toHaveBeenLastCalledWith({ scope: MOCK_SCOPE })
|
|
38
144
|
})
|
|
39
145
|
})
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from '../../index.js'
|
|
14
14
|
import TestEntity from '../../__mocks__/TestEntity.js'
|
|
15
15
|
import { v4 as uuidv4 } from 'uuid'
|
|
16
|
-
import MockDynamicsWebApi from 'dynamics-web-api'
|
|
16
|
+
import { DynamicsWebApi as MockDynamicsWebApi } from 'dynamics-web-api'
|
|
17
17
|
import { PredefinedQuery } from '../../queries/predefined-query.js'
|
|
18
18
|
import { BaseEntity, EntityDefinition } from '../../entities/base.entity.js'
|
|
19
19
|
|
|
@@ -33,7 +33,7 @@ describe('entity manager', () => {
|
|
|
33
33
|
t.boolVal = true
|
|
34
34
|
|
|
35
35
|
const result = await persist([t])
|
|
36
|
-
expect(MockDynamicsWebApi.prototype.
|
|
36
|
+
expect(MockDynamicsWebApi.prototype.create).toHaveBeenCalled()
|
|
37
37
|
expect(result).toHaveLength(1)
|
|
38
38
|
expect(result[0]).toEqual(resultUuid)
|
|
39
39
|
})
|
|
@@ -54,7 +54,7 @@ describe('entity manager', () => {
|
|
|
54
54
|
)
|
|
55
55
|
|
|
56
56
|
const result = await persist([t])
|
|
57
|
-
expect(MockDynamicsWebApi.prototype.
|
|
57
|
+
expect(MockDynamicsWebApi.prototype.update).toHaveBeenCalled()
|
|
58
58
|
expect(result).toHaveLength(1)
|
|
59
59
|
expect(result[0]).toEqual(resultUuid)
|
|
60
60
|
})
|
|
@@ -69,7 +69,7 @@ describe('entity manager', () => {
|
|
|
69
69
|
t.boolVal = true
|
|
70
70
|
|
|
71
71
|
const result = await persist([t], 'foo')
|
|
72
|
-
expect(MockDynamicsWebApi.prototype.
|
|
72
|
+
expect(MockDynamicsWebApi.prototype.create).toHaveBeenCalled()
|
|
73
73
|
expect(MockDynamicsWebApi.prototype.executeBatch).toHaveBeenCalledWith(
|
|
74
74
|
expect.objectContaining({
|
|
75
75
|
impersonateAAD: 'foo'
|
|
@@ -94,10 +94,10 @@ describe('entity manager', () => {
|
|
|
94
94
|
{}
|
|
95
95
|
)
|
|
96
96
|
await expect(persist([newEntity, existingEntity])).rejects.toThrow('Test error')
|
|
97
|
-
// Expect the console error to contain details of the batch data (one
|
|
97
|
+
// Expect the console error to contain details of the batch data (one create, one update plus the exception object)
|
|
98
98
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
99
99
|
expect.stringMatching('Error persisting batch. Data: %j, Exception: %o'),
|
|
100
|
-
expect.arrayContaining([{
|
|
100
|
+
expect.arrayContaining([{ create: expect.any(Object) }, { update: expect.any(Object) }]),
|
|
101
101
|
expect.any(Error)
|
|
102
102
|
)
|
|
103
103
|
})
|
|
@@ -332,14 +332,14 @@ describe('entity manager', () => {
|
|
|
332
332
|
|
|
333
333
|
describe('findById', () => {
|
|
334
334
|
it('finds by a primary key guid', async () => {
|
|
335
|
-
MockDynamicsWebApi.__setResponse('
|
|
335
|
+
MockDynamicsWebApi.__setResponse('retrieve', {
|
|
336
336
|
'@odata.etag': 'W/"202465000"',
|
|
337
337
|
idval: '9f1b34a0-0c66-e611-80dc-c4346bad0190',
|
|
338
338
|
strval: 'example'
|
|
339
339
|
})
|
|
340
340
|
|
|
341
341
|
const result = await findById(TestEntity, '9f1b34a0-0c66-e611-80dc-c4346bad0190')
|
|
342
|
-
expect(MockDynamicsWebApi.prototype.
|
|
342
|
+
expect(MockDynamicsWebApi.prototype.retrieve).toBeCalledWith({
|
|
343
343
|
key: '9f1b34a0-0c66-e611-80dc-c4346bad0190',
|
|
344
344
|
collection: TestEntity.definition.dynamicsCollection,
|
|
345
345
|
select: TestEntity.definition.select
|
|
@@ -350,13 +350,13 @@ describe('entity manager', () => {
|
|
|
350
350
|
it('returns null if not found', async () => {
|
|
351
351
|
const notFoundError = new Error('Not found')
|
|
352
352
|
notFoundError.status = 404
|
|
353
|
-
MockDynamicsWebApi.__throwWithErrorOn('
|
|
353
|
+
MockDynamicsWebApi.__throwWithErrorOn('retrieve', notFoundError)
|
|
354
354
|
const result = await findById(TestEntity, '9f1b34a0-0c66-e611-80dc-c4346bad0190')
|
|
355
355
|
expect(result).toEqual(null)
|
|
356
356
|
})
|
|
357
357
|
|
|
358
358
|
it('throws an exception on general errors', async () => {
|
|
359
|
-
MockDynamicsWebApi.__throwWithErrorOn('
|
|
359
|
+
MockDynamicsWebApi.__throwWithErrorOn('retrieve')
|
|
360
360
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
|
|
361
361
|
await expect(findById(TestEntity, '9f1b34a0-0c66-e611-80dc-c4346bad0190')).rejects.toThrow('Test error')
|
|
362
362
|
expect(consoleErrorSpy).toHaveBeenCalled()
|
|
@@ -365,13 +365,13 @@ describe('entity manager', () => {
|
|
|
365
365
|
|
|
366
366
|
describe('findByAlternateKey', () => {
|
|
367
367
|
it('finds by an alternate key', async () => {
|
|
368
|
-
MockDynamicsWebApi.__setResponse('
|
|
368
|
+
MockDynamicsWebApi.__setResponse('retrieve', {
|
|
369
369
|
'@odata.etag': 'W/"202465000"',
|
|
370
370
|
idval: '9f1b34a0-0c66-e611-80dc-c4346bad0190',
|
|
371
371
|
strval: 'example'
|
|
372
372
|
})
|
|
373
373
|
const result = await findByAlternateKey(TestEntity, 'example')
|
|
374
|
-
expect(MockDynamicsWebApi.prototype.
|
|
374
|
+
expect(MockDynamicsWebApi.prototype.retrieve).toBeCalledWith({
|
|
375
375
|
key: "strval='example'",
|
|
376
376
|
collection: TestEntity.definition.dynamicsCollection,
|
|
377
377
|
select: TestEntity.definition.select
|
|
@@ -381,13 +381,13 @@ describe('entity manager', () => {
|
|
|
381
381
|
})
|
|
382
382
|
|
|
383
383
|
it('escapes special characters in the key', async () => {
|
|
384
|
-
MockDynamicsWebApi.__setResponse('
|
|
384
|
+
MockDynamicsWebApi.__setResponse('retrieve', {
|
|
385
385
|
'@odata.etag': 'W/"202465000"',
|
|
386
386
|
idval: '9f1b34a0-0c66-e611-80dc-c4346bad0190',
|
|
387
387
|
strval: 'example'
|
|
388
388
|
})
|
|
389
389
|
const result = await findByAlternateKey(TestEntity, "test & example'")
|
|
390
|
-
expect(MockDynamicsWebApi.prototype.
|
|
390
|
+
expect(MockDynamicsWebApi.prototype.retrieve).toBeCalledWith({
|
|
391
391
|
key: "strval='test %26 example'''",
|
|
392
392
|
collection: TestEntity.definition.dynamicsCollection,
|
|
393
393
|
select: TestEntity.definition.select
|
|
@@ -399,7 +399,7 @@ describe('entity manager', () => {
|
|
|
399
399
|
|
|
400
400
|
describe('findByExample', () => {
|
|
401
401
|
it('builds a select statement appropriate to the type definition for each field', async () => {
|
|
402
|
-
MockDynamicsWebApi.__setResponse('
|
|
402
|
+
MockDynamicsWebApi.__setResponse('retrieveMultiple', { value: [{}] })
|
|
403
403
|
|
|
404
404
|
const lookup = new TestEntity()
|
|
405
405
|
lookup.strVal = 'StringData'
|
|
@@ -412,7 +412,7 @@ describe('entity manager', () => {
|
|
|
412
412
|
const expectedLookupSelect =
|
|
413
413
|
"strval eq 'StringData' and intval eq 123 and decval eq 123.45 and boolval eq true and dateval eq 1946-01-01 and datetimeval eq 1946-01-01T01:02:03Z and optionsetval eq 910400000"
|
|
414
414
|
const result = await findByExample(lookup)
|
|
415
|
-
expect(MockDynamicsWebApi.prototype.
|
|
415
|
+
expect(MockDynamicsWebApi.prototype.retrieveMultiple).toBeCalledWith({
|
|
416
416
|
collection: TestEntity.definition.dynamicsCollection,
|
|
417
417
|
select: TestEntity.definition.select,
|
|
418
418
|
filter: expect.stringMatching(`${TestEntity.definition.defaultFilter} and ${expectedLookupSelect}`)
|
|
@@ -422,7 +422,7 @@ describe('entity manager', () => {
|
|
|
422
422
|
})
|
|
423
423
|
|
|
424
424
|
it('does not require the entity to define a default filter', async () => {
|
|
425
|
-
MockDynamicsWebApi.__setResponse('
|
|
425
|
+
MockDynamicsWebApi.__setResponse('retrieveMultiple', { value: [{}] })
|
|
426
426
|
|
|
427
427
|
class SameEntity extends BaseEntity {
|
|
428
428
|
static _definition = new EntityDefinition(() => ({
|
|
@@ -458,7 +458,7 @@ describe('entity manager', () => {
|
|
|
458
458
|
lookup.testVal = 'StringData'
|
|
459
459
|
const expectedLookupSelect = "testval eq 'StringData'"
|
|
460
460
|
const result = await findByExample(lookup)
|
|
461
|
-
expect(MockDynamicsWebApi.prototype.
|
|
461
|
+
expect(MockDynamicsWebApi.prototype.retrieveMultiple).toBeCalledWith({
|
|
462
462
|
collection: SameEntity.definition.dynamicsCollection,
|
|
463
463
|
select: SameEntity.definition.select,
|
|
464
464
|
filter: expect.stringMatching(`${expectedLookupSelect}`)
|
|
@@ -468,13 +468,13 @@ describe('entity manager', () => {
|
|
|
468
468
|
})
|
|
469
469
|
|
|
470
470
|
it('only serializes fields into the select statement if they are set', async () => {
|
|
471
|
-
MockDynamicsWebApi.__setResponse('
|
|
471
|
+
MockDynamicsWebApi.__setResponse('retrieveMultiple', { value: [{}] })
|
|
472
472
|
|
|
473
473
|
const lookup = new TestEntity()
|
|
474
474
|
lookup.strVal = 'StringData'
|
|
475
475
|
const expectedLookupSelect = "strval eq 'StringData'"
|
|
476
476
|
const result = await findByExample(lookup)
|
|
477
|
-
expect(MockDynamicsWebApi.prototype.
|
|
477
|
+
expect(MockDynamicsWebApi.prototype.retrieveMultiple).toBeCalledWith({
|
|
478
478
|
collection: TestEntity.definition.dynamicsCollection,
|
|
479
479
|
select: TestEntity.definition.select,
|
|
480
480
|
filter: expect.stringMatching(`${TestEntity.definition.defaultFilter} and ${expectedLookupSelect}`)
|
|
@@ -484,7 +484,7 @@ describe('entity manager', () => {
|
|
|
484
484
|
})
|
|
485
485
|
|
|
486
486
|
it('throws an error object on failure', async () => {
|
|
487
|
-
MockDynamicsWebApi.__throwWithErrorOn('
|
|
487
|
+
MockDynamicsWebApi.__throwWithErrorOn('retrieveMultiple')
|
|
488
488
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
|
|
489
489
|
await expect(findByExample(new TestEntity())).rejects.toThrow('Test error')
|
|
490
490
|
expect(consoleErrorSpy).toHaveBeenCalled()
|
|
@@ -492,8 +492,8 @@ describe('entity manager', () => {
|
|
|
492
492
|
})
|
|
493
493
|
|
|
494
494
|
describe('executeQuery', () => {
|
|
495
|
-
it('calls
|
|
496
|
-
MockDynamicsWebApi.__setResponse('
|
|
495
|
+
it('calls retrieveMultiple on the Dynamics API with the request data', async () => {
|
|
496
|
+
MockDynamicsWebApi.__setResponse('retrieveMultiple', {
|
|
497
497
|
value: [
|
|
498
498
|
{
|
|
499
499
|
'@odata.etag': 'W/"202465000"',
|
|
@@ -504,7 +504,7 @@ describe('entity manager', () => {
|
|
|
504
504
|
})
|
|
505
505
|
|
|
506
506
|
const result = await executeQuery(new PredefinedQuery({ root: TestEntity, filter: "strval eq 'example'" }))
|
|
507
|
-
expect(MockDynamicsWebApi.prototype.
|
|
507
|
+
expect(MockDynamicsWebApi.prototype.retrieveMultiple).toBeCalledWith({
|
|
508
508
|
collection: TestEntity.definition.dynamicsCollection,
|
|
509
509
|
filter: "strval eq 'example'",
|
|
510
510
|
select: TestEntity.definition.select
|
|
@@ -516,7 +516,7 @@ describe('entity manager', () => {
|
|
|
516
516
|
})
|
|
517
517
|
|
|
518
518
|
it('throws an error object on failure', async () => {
|
|
519
|
-
MockDynamicsWebApi.__throwWithErrorOn('
|
|
519
|
+
MockDynamicsWebApi.__throwWithErrorOn('retrieveMultiple')
|
|
520
520
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
|
|
521
521
|
await expect(executeQuery(new PredefinedQuery({ root: TestEntity, filter: "strval eq 'example'" }))).rejects.toThrow('Test error')
|
|
522
522
|
expect(consoleErrorSpy).toHaveBeenCalled()
|
|
@@ -527,7 +527,7 @@ describe('entity manager', () => {
|
|
|
527
527
|
beforeEach(async () => {
|
|
528
528
|
MockDynamicsWebApi.__reset()
|
|
529
529
|
MockDynamicsWebApi.__setNextResponses(
|
|
530
|
-
'
|
|
530
|
+
'retrieveMultiple',
|
|
531
531
|
{
|
|
532
532
|
value: [
|
|
533
533
|
{
|
|
@@ -592,7 +592,7 @@ describe('entity manager', () => {
|
|
|
592
592
|
})
|
|
593
593
|
|
|
594
594
|
it('throws an error object on failure', async () => {
|
|
595
|
-
MockDynamicsWebApi.__throwWithErrorOn('
|
|
595
|
+
MockDynamicsWebApi.__throwWithErrorOn('retrieveMultiple')
|
|
596
596
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
|
|
597
597
|
await expect(
|
|
598
598
|
executePagedQuery(new PredefinedQuery({ root: TestEntity, filter: "strval eq 'example'" }), () => {}, 1)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import DynamicsWebApi from 'dynamics-web-api'
|
|
1
|
+
import { DynamicsWebApi } from 'dynamics-web-api'
|
|
2
2
|
import SimpleOAuth2 from 'simple-oauth2'
|
|
3
3
|
const PREEMPTIVE_TOKEN_EXPIRY_SECONDS = 60
|
|
4
4
|
|
|
@@ -21,14 +21,16 @@ export function config () {
|
|
|
21
21
|
|
|
22
22
|
let accessToken = null
|
|
23
23
|
return {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
serverUrl: process.env.DYNAMICS_API_PATH,
|
|
25
|
+
dataApi: {
|
|
26
|
+
version: process.env.DYNAMICS_API_VERSION
|
|
27
|
+
},
|
|
26
28
|
timeout: process.env.DYNAMICS_API_TIMEOUT || 90000,
|
|
27
|
-
onTokenRefresh: async
|
|
29
|
+
onTokenRefresh: async () => {
|
|
28
30
|
if (!accessToken || accessToken.expired(PREEMPTIVE_TOKEN_EXPIRY_SECONDS)) {
|
|
29
31
|
accessToken = await oauthClient.getToken({ scope: process.env.OAUTH_SCOPE })
|
|
30
32
|
}
|
|
31
|
-
|
|
33
|
+
return accessToken.token.access_token
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
}
|
|
@@ -14,9 +14,9 @@ export async function persist (entities, createdBy) {
|
|
|
14
14
|
dynamicsClient.startBatch()
|
|
15
15
|
entities.forEach(entity => {
|
|
16
16
|
if (entity.isNew()) {
|
|
17
|
-
dynamicsClient.
|
|
17
|
+
dynamicsClient.create(entity.toPersistRequest())
|
|
18
18
|
} else {
|
|
19
|
-
dynamicsClient.
|
|
19
|
+
dynamicsClient.update(entity.toPersistRequest())
|
|
20
20
|
}
|
|
21
21
|
})
|
|
22
22
|
return await dynamicsClient.executeBatch({
|
|
@@ -24,7 +24,7 @@ export async function persist (entities, createdBy) {
|
|
|
24
24
|
})
|
|
25
25
|
} catch (e) {
|
|
26
26
|
const error = e.length ? e[0] : e
|
|
27
|
-
const requestDetails = entities.map(entity => ({ [entity.isNew() ? '
|
|
27
|
+
const requestDetails = entities.map(entity => ({ [entity.isNew() ? 'create' : 'update']: entity.toPersistRequest() }))
|
|
28
28
|
console.error('Error persisting batch. Data: %j, Exception: %o', requestDetails, error)
|
|
29
29
|
throw error
|
|
30
30
|
}
|
|
@@ -33,7 +33,7 @@ export async function persist (entities, createdBy) {
|
|
|
33
33
|
const retrieveMultipleFetchOperation = async entityClasses => {
|
|
34
34
|
try {
|
|
35
35
|
dynamicsClient.startBatch()
|
|
36
|
-
entityClasses.forEach(cls => dynamicsClient.
|
|
36
|
+
entityClasses.forEach(cls => dynamicsClient.retrieveMultiple(cls.definition.toRetrieveRequest()))
|
|
37
37
|
return await dynamicsClient.executeBatch()
|
|
38
38
|
} catch (e) {
|
|
39
39
|
const error = e.length ? e[0] : e
|
|
@@ -98,7 +98,10 @@ export function retrieveGlobalOptionSets () {
|
|
|
98
98
|
'dynamics_optionsetmap',
|
|
99
99
|
async () => {
|
|
100
100
|
try {
|
|
101
|
-
const data = await dynamicsClient.retrieveGlobalOptionSets(
|
|
101
|
+
const data = await dynamicsClient.retrieveGlobalOptionSets({
|
|
102
|
+
castType: 'Microsoft.Dynamics.CRM.OptionSetMetadata',
|
|
103
|
+
select: ['Name', 'Options']
|
|
104
|
+
})
|
|
102
105
|
return data.value.reduce((acc, { Name: name, Options: options }) => {
|
|
103
106
|
acc[name] = {
|
|
104
107
|
name,
|
|
@@ -132,7 +135,7 @@ export function retrieveGlobalOptionSets () {
|
|
|
132
135
|
*/
|
|
133
136
|
export async function findById (entityType, key) {
|
|
134
137
|
try {
|
|
135
|
-
const record = await dynamicsClient.
|
|
138
|
+
const record = await dynamicsClient.retrieve({ key: key, ...entityType.definition.toRetrieveRequest(null) })
|
|
136
139
|
const optionSetData = await retrieveGlobalOptionSets().cached()
|
|
137
140
|
return entityType.fromResponse(record, optionSetData)
|
|
138
141
|
} catch (e) {
|
|
@@ -180,7 +183,7 @@ export async function findByExample (entity) {
|
|
|
180
183
|
return acc
|
|
181
184
|
}, [])
|
|
182
185
|
].join(' and ')
|
|
183
|
-
const results = await dynamicsClient.
|
|
186
|
+
const results = await dynamicsClient.retrieveMultiple(entity.constructor.definition.toRetrieveRequest(filter))
|
|
184
187
|
const optionSetData = await retrieveGlobalOptionSets().cached()
|
|
185
188
|
return results.value.map(result => entity.constructor.fromResponse(result, optionSetData))
|
|
186
189
|
} catch (e) {
|
|
@@ -200,7 +203,7 @@ export async function findByExample (entity) {
|
|
|
200
203
|
*/
|
|
201
204
|
export async function executeQuery (query) {
|
|
202
205
|
try {
|
|
203
|
-
const response = await dynamicsClient.
|
|
206
|
+
const response = await dynamicsClient.retrieveMultiple(query.toRetrieveRequest())
|
|
204
207
|
const optionSetData = await retrieveGlobalOptionSets().cached()
|
|
205
208
|
return query.fromResponse(response.value, optionSetData)
|
|
206
209
|
} catch (e) {
|
|
@@ -227,7 +230,7 @@ export async function executePagedQuery (query, onPageReceived, maxPages) {
|
|
|
227
230
|
const optionSetData = await retrieveGlobalOptionSets().cached()
|
|
228
231
|
let nextLink = null
|
|
229
232
|
do {
|
|
230
|
-
const response = await dynamicsClient.
|
|
233
|
+
const response = await dynamicsClient.retrieveMultiple(query.toRetrieveRequest(), nextLink)
|
|
231
234
|
nextLink = response.oDataNextLink
|
|
232
235
|
await onPageReceived(query.fromResponse(response.value, optionSetData))
|
|
233
236
|
processed += response.value.length
|
|
@@ -4,11 +4,13 @@ import { Permission } from '../../entities/permission.entity.js'
|
|
|
4
4
|
import { PredefinedQuery } from '../predefined-query.js'
|
|
5
5
|
|
|
6
6
|
jest.mock('dynamics-web-api', () => {
|
|
7
|
-
return
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
return {
|
|
8
|
+
DynamicsWebApi: jest.fn().mockImplementation(() => {
|
|
9
|
+
return {
|
|
10
|
+
callAction: jest.fn()
|
|
11
|
+
}
|
|
12
|
+
})
|
|
13
|
+
}
|
|
12
14
|
})
|
|
13
15
|
|
|
14
16
|
describe('Contact Queries', () => {
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
findDueRecurringPayments,
|
|
3
3
|
findRecurringPaymentsByAgreementId,
|
|
4
|
-
findRecurringPaymentByPermissionId
|
|
5
|
-
findPermissionByRecurringPaymentId
|
|
4
|
+
findRecurringPaymentByPermissionId
|
|
6
5
|
} from '../recurring-payments.queries.js'
|
|
7
|
-
import { RecurringPayment } from '../../entities/recurring-payment.entity.js'
|
|
8
6
|
|
|
9
7
|
describe('Recurring Payment Queries', () => {
|
|
10
8
|
const baseSelection = () => [
|
|
@@ -21,7 +19,7 @@ describe('Recurring Payment Queries', () => {
|
|
|
21
19
|
]
|
|
22
20
|
|
|
23
21
|
describe('findDueRecurringPayments', () => {
|
|
24
|
-
it('builds
|
|
22
|
+
it('builds a query to retrieve active recurring payments', () => {
|
|
25
23
|
const date = new Date('2023-11-08')
|
|
26
24
|
|
|
27
25
|
const query = findDueRecurringPayments(date)
|
|
@@ -37,7 +35,7 @@ describe('Recurring Payment Queries', () => {
|
|
|
37
35
|
})
|
|
38
36
|
|
|
39
37
|
describe('findRecurringPaymentsByAgreementId', () => {
|
|
40
|
-
it('builds
|
|
38
|
+
it('builds a query to retrieve active recurring payments', () => {
|
|
41
39
|
const agreementId = 'abc123'
|
|
42
40
|
|
|
43
41
|
const query = findRecurringPaymentsByAgreementId(agreementId)
|
|
@@ -51,7 +49,7 @@ describe('Recurring Payment Queries', () => {
|
|
|
51
49
|
})
|
|
52
50
|
|
|
53
51
|
describe('findRecurringPaymentByPermissionId', () => {
|
|
54
|
-
it('builds
|
|
52
|
+
it('builds a query to retrieve recurring payments by permissionId', () => {
|
|
55
53
|
const permissionId = 'perm-123'
|
|
56
54
|
|
|
57
55
|
const query = findRecurringPaymentByPermissionId(permissionId)
|
|
@@ -64,43 +62,4 @@ describe('Recurring Payment Queries', () => {
|
|
|
64
62
|
})
|
|
65
63
|
})
|
|
66
64
|
})
|
|
67
|
-
|
|
68
|
-
describe('findPermissionByRecurringPaymentId', () => {
|
|
69
|
-
const createMockDefinition = defaultFilter => ({
|
|
70
|
-
defaultFilter,
|
|
71
|
-
relationships: {
|
|
72
|
-
activePermission: { property: 'defra_ActivePermission' }
|
|
73
|
-
},
|
|
74
|
-
toRetrieveRequest: filter => ({
|
|
75
|
-
collection: 'defra_recurringpayments',
|
|
76
|
-
select: baseSelection(),
|
|
77
|
-
filter
|
|
78
|
-
})
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it.each(['rcp-456', 'rcp-789'])('builds full filter with conjunctions when recurring payment id is %s', recurringPaymentId => {
|
|
82
|
-
const mockDefaultFilter = 'mock_default_filter'
|
|
83
|
-
const definitionSpy = jest.spyOn(RecurringPayment, 'definition', 'get').mockReturnValue(createMockDefinition(mockDefaultFilter))
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
const request = findPermissionByRecurringPaymentId(recurringPaymentId).toRetrieveRequest()
|
|
87
|
-
expect(request.filter).toBe(
|
|
88
|
-
`_defra_activepermission_value ne null and defra_recurringpaymentid eq ${recurringPaymentId} and ${mockDefaultFilter}`
|
|
89
|
-
)
|
|
90
|
-
} finally {
|
|
91
|
-
definitionSpy.mockRestore()
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('expands active permission relationship', () => {
|
|
96
|
-
const definitionSpy = jest.spyOn(RecurringPayment, 'definition', 'get').mockReturnValue(createMockDefinition('mock_default_filter'))
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const request = findPermissionByRecurringPaymentId('rcp-456').toRetrieveRequest()
|
|
100
|
-
expect(request.expand).toContainEqual({ property: 'defra_ActivePermission' })
|
|
101
|
-
} finally {
|
|
102
|
-
definitionSpy.mockRestore()
|
|
103
|
-
}
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
65
|
})
|
|
@@ -59,20 +59,3 @@ export const findRecurringPaymentByPermissionId = permissionId => {
|
|
|
59
59
|
expand: [activePermission]
|
|
60
60
|
})
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Builds a query to retrieve the active permission linked to a recurring payment
|
|
65
|
-
*
|
|
66
|
-
* @param recurringPaymentId the id of the recurring payment
|
|
67
|
-
* @returns {PredefinedQuery}
|
|
68
|
-
*/
|
|
69
|
-
export const findPermissionByRecurringPaymentId = recurringPaymentId => {
|
|
70
|
-
const { activePermission } = RecurringPayment.definition.relationships
|
|
71
|
-
const filter = `_defra_activepermission_value ne null and defra_recurringpaymentid eq ${recurringPaymentId} and ${RecurringPayment.definition.defaultFilter}`
|
|
72
|
-
|
|
73
|
-
return new PredefinedQuery({
|
|
74
|
-
root: RecurringPayment,
|
|
75
|
-
filter,
|
|
76
|
-
expand: [activePermission]
|
|
77
|
-
})
|
|
78
|
-
}
|