account-lookup-service 17.8.0 → 17.9.0-snapshot.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 +3 -3
- package/src/lib/util.js +2 -2
- package/src/models/oracle/facade.js +186 -136
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "account-lookup-service",
|
3
3
|
"description": "Account Lookup Service is used to validate Party and Participant lookups.",
|
4
|
-
"version": "17.
|
4
|
+
"version": "17.9.0-snapshot.0",
|
5
5
|
"license": "Apache-2.0",
|
6
6
|
"author": "ModusBox",
|
7
7
|
"contributors": [
|
@@ -98,7 +98,7 @@
|
|
98
98
|
"@mojaloop/central-services-stream": "11.5.2",
|
99
99
|
"@mojaloop/database-lib": "11.1.4",
|
100
100
|
"@mojaloop/event-sdk": "14.4.0",
|
101
|
-
"@mojaloop/inter-scheme-proxy-cache-lib": "2.
|
101
|
+
"@mojaloop/inter-scheme-proxy-cache-lib": "2.5.0",
|
102
102
|
"@mojaloop/ml-schema-transformer-lib": "2.7.1",
|
103
103
|
"@mojaloop/sdk-standard-components": "19.11.3-snapshot.0",
|
104
104
|
"@now-ims/hapi-now-auth": "2.1.0",
|
@@ -106,7 +106,7 @@
|
|
106
106
|
"ajv-keywords": "5.1.0",
|
107
107
|
"blipp": "4.0.2",
|
108
108
|
"commander": "13.1.0",
|
109
|
-
"cron": "4.1.
|
109
|
+
"cron": "4.1.4",
|
110
110
|
"fast-safe-stringify": "^2.1.1",
|
111
111
|
"hapi-auth-bearer-token": "8.0.0",
|
112
112
|
"joi": "17.13.3",
|
package/src/lib/util.js
CHANGED
@@ -97,8 +97,8 @@ const rethrowDatabaseError = (error) => {
|
|
97
97
|
}
|
98
98
|
|
99
99
|
const countFspiopError = (error, options) => {
|
100
|
-
options.loggerOverride = logger
|
101
|
-
rethrow.countFspiopError(error, options)
|
100
|
+
options.loggerOverride = options?.log || logger
|
101
|
+
return rethrow.countFspiopError(error, options)
|
102
102
|
}
|
103
103
|
|
104
104
|
/**
|
@@ -29,18 +29,19 @@
|
|
29
29
|
const Mustache = require('mustache')
|
30
30
|
const request = require('@mojaloop/central-services-shared').Util.Request
|
31
31
|
const Enums = require('@mojaloop/central-services-shared').Enum
|
32
|
-
const Logger = require('@mojaloop/central-services-logger')
|
33
32
|
const ErrorHandler = require('@mojaloop/central-services-error-handling')
|
34
33
|
const Metrics = require('@mojaloop/central-services-metrics')
|
35
34
|
|
36
35
|
const Config = require('../../lib/config')
|
37
|
-
const
|
36
|
+
const { logger } = require('../../lib')
|
37
|
+
const { countFspiopError } = require('../../lib/util')
|
38
38
|
const { hubNameRegex } = require('../../lib/util').hubNameConfig
|
39
|
+
const oracleEndpointCached = require('../oracle/oracleEndpointCached')
|
40
|
+
|
41
|
+
const { Headers, RestMethods, ReturnCodes } = Enums.Http
|
39
42
|
|
40
43
|
/**
|
41
|
-
*
|
42
|
-
*
|
43
|
-
* @description This sends a request to the oracles that are registered to the ALS
|
44
|
+
* Sends a request to the oracles that are registered to the ALS
|
44
45
|
*
|
45
46
|
* @param {object} headers - incoming http request headers
|
46
47
|
* @param {string} method - incoming http request method
|
@@ -49,130 +50,134 @@ const { hubNameRegex } = require('../../lib/util').hubNameConfig
|
|
49
50
|
* @param {object} payload - payload of the request being sent out
|
50
51
|
* @param {object} assertPendingAcquire - flag to check DB pool pending acquire limit
|
51
52
|
*
|
52
|
-
* @returns {object}
|
53
|
+
* @returns {object} - response from the oracle
|
53
54
|
*/
|
54
|
-
|
55
|
+
const oracleRequest = async (headers, method, params = {}, query = {}, payload = undefined, cache, assertPendingAcquire) => {
|
56
|
+
const operation = oracleRequest.name
|
57
|
+
const log = logger.child({ component: operation, params })
|
58
|
+
let step = 'start'
|
59
|
+
|
55
60
|
try {
|
56
|
-
|
57
|
-
const
|
58
|
-
const
|
59
|
-
|
60
|
-
const partySubIdOrType = (params && params.SubId) ? params.SubId : (query && query.partySubIdOrType) ? query.partySubIdOrType : undefined
|
61
|
-
const isGetRequest = method.toUpperCase() === Enums.Http.RestMethods.GET
|
62
|
-
const isDeleteRequest = method.toUpperCase() === Enums.Http.RestMethods.DELETE
|
61
|
+
const source = headers[Headers.FSPIOP.SOURCE]
|
62
|
+
const destination = headers[Headers.FSPIOP.DESTINATION] || Config.HUB_NAME
|
63
|
+
const partySubIdOrType = params?.SubId || query?.partySubIdOrType
|
64
|
+
log.info('oracleRequest start...', { method, source, destination })
|
63
65
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
}
|
66
|
+
step = 'determineOracleEndpoint'
|
67
|
+
const url = await determineOracleEndpoint({
|
68
|
+
method, params, query, payload, assertPendingAcquire
|
69
|
+
})
|
70
|
+
log.verbose(`Oracle endpoint: ${url}`)
|
71
|
+
|
72
|
+
if (method.toUpperCase() === RestMethods.GET) {
|
73
|
+
step = 'sendOracleGetRequest'
|
74
|
+
return await sendOracleGetRequest({
|
75
|
+
url, source, destination, headers, method, params, cache
|
76
|
+
})
|
75
77
|
}
|
76
|
-
Logger.isDebugEnabled && Logger.debug(`Oracle endpoints: ${url}`)
|
77
|
-
const histTimerEnd = Metrics.getHistogram(
|
78
|
-
'egress_oracleRequest',
|
79
|
-
'Egress: oracleRequest',
|
80
|
-
['success', 'hit']
|
81
|
-
).startTimer()
|
82
|
-
try {
|
83
|
-
if (isGetRequest) {
|
84
|
-
let cachedOracleFspResponse
|
85
|
-
cachedOracleFspResponse = cache && cache.get(cache.createKey(`oracleSendRequest_${url}`))
|
86
|
-
if (!cachedOracleFspResponse) {
|
87
|
-
cachedOracleFspResponse = await request.sendRequest({
|
88
|
-
url,
|
89
|
-
headers,
|
90
|
-
source: headers[Enums.Http.Headers.FSPIOP.SOURCE],
|
91
|
-
destination: headers[Enums.Http.Headers.FSPIOP.DESTINATION] || Config.HUB_NAME,
|
92
|
-
method: method.toUpperCase(),
|
93
|
-
payload,
|
94
|
-
hubNameRegex
|
95
|
-
})
|
96
|
-
// Trying to cache the whole response object will fail because it contains circular references
|
97
|
-
// so we'll just cache the data property of the response.
|
98
|
-
cachedOracleFspResponse = {
|
99
|
-
data: cachedOracleFspResponse.data
|
100
|
-
}
|
101
|
-
cache && cache.set(
|
102
|
-
cache.createKey(`oracleSendRequest_${url}`),
|
103
|
-
cachedOracleFspResponse
|
104
|
-
)
|
105
|
-
histTimerEnd({ success: true, hit: false })
|
106
|
-
} else {
|
107
|
-
cachedOracleFspResponse = cachedOracleFspResponse.item
|
108
|
-
histTimerEnd({ success: true, hit: true })
|
109
|
-
Logger.isDebugEnabled && Logger.debug(`${new Date().toISOString()}, [oracleRequest]: cache hit for fsp for partyId lookup`)
|
110
|
-
}
|
111
78
|
|
112
|
-
|
113
|
-
}
|
79
|
+
if (partySubIdOrType && payload) payload.partySubIdOrType = partySubIdOrType
|
114
80
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
destination: headers[Enums.Http.Headers.FSPIOP.DESTINATION] || Config.HUB_NAME,
|
122
|
-
method: Enums.Http.RestMethods.GET,
|
123
|
-
payload,
|
124
|
-
hubNameRegex
|
125
|
-
})
|
81
|
+
if (method.toUpperCase() === RestMethods.DELETE && Config.DELETE_PARTICIPANT_VALIDATION_ENABLED) {
|
82
|
+
step = 'validatePartyDeletion'
|
83
|
+
await validatePartyDeletion({
|
84
|
+
url, source, destination, headers, method, params, payload
|
85
|
+
})
|
86
|
+
}
|
126
87
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.DELETE_PARTY_INFO_ERROR, `The party ${partyIdType}:${partyIdentifier} does not belong to the requesting FSP`)
|
143
|
-
}
|
144
|
-
}
|
145
|
-
}
|
88
|
+
step = 'sendRequest'
|
89
|
+
return await request.sendRequest({
|
90
|
+
url,
|
91
|
+
headers,
|
92
|
+
source,
|
93
|
+
destination,
|
94
|
+
method,
|
95
|
+
payload,
|
96
|
+
hubNameRegex
|
97
|
+
})
|
98
|
+
} catch (err) {
|
99
|
+
log.error('error in oracleRequest: ', err)
|
100
|
+
throw countFspiopError(err, { operation, step, log })
|
101
|
+
}
|
102
|
+
}
|
146
103
|
|
147
|
-
|
148
|
-
|
104
|
+
const determineOracleEndpoint = async ({
|
105
|
+
method, params, query, payload, assertPendingAcquire
|
106
|
+
}) => {
|
107
|
+
const partyIdType = params.Type
|
108
|
+
const partyIdentifier = params.ID
|
109
|
+
const partySubIdOrType = params?.SubId || query?.partySubIdOrType
|
110
|
+
const currency = payload?.currency || query?.currency
|
111
|
+
const isGetRequest = method.toUpperCase() === RestMethods.GET
|
112
|
+
let url
|
149
113
|
|
150
|
-
|
114
|
+
if (currency && partySubIdOrType && isGetRequest) {
|
115
|
+
url = await _getOracleEndpointByTypeCurrencyAndSubId(partyIdType, partyIdentifier, currency, partySubIdOrType, assertPendingAcquire)
|
116
|
+
} else if (currency && isGetRequest) {
|
117
|
+
url = await _getOracleEndpointByTypeAndCurrency(partyIdType, partyIdentifier, currency, assertPendingAcquire)
|
118
|
+
} else if (partySubIdOrType && isGetRequest) {
|
119
|
+
url = await _getOracleEndpointByTypeAndSubId(partyIdType, partyIdentifier, partySubIdOrType, assertPendingAcquire)
|
120
|
+
} else {
|
121
|
+
url = await _getOracleEndpointByType(partyIdType, partyIdentifier, assertPendingAcquire)
|
122
|
+
}
|
123
|
+
return url
|
124
|
+
}
|
125
|
+
|
126
|
+
const sendOracleGetRequest = async ({
|
127
|
+
url, source, destination, headers, method, params, cache
|
128
|
+
}) => {
|
129
|
+
const histTimerEnd = Metrics.getHistogram(
|
130
|
+
'egress_oracleRequest',
|
131
|
+
'Egress: oracleRequest',
|
132
|
+
['success', 'hit']
|
133
|
+
).startTimer()
|
134
|
+
const log = logger.child({ component: 'sendOracleGetRequest', params })
|
135
|
+
|
136
|
+
try {
|
137
|
+
let cachedOracleFspResponse
|
138
|
+
cachedOracleFspResponse = cache && cache.get(cache.createKey(`oracleSendRequest_${url}`))
|
139
|
+
|
140
|
+
if (!cachedOracleFspResponse) {
|
141
|
+
cachedOracleFspResponse = await request.sendRequest({
|
151
142
|
url,
|
152
143
|
headers,
|
153
|
-
source
|
154
|
-
destination
|
155
|
-
method
|
156
|
-
payload,
|
144
|
+
source,
|
145
|
+
destination,
|
146
|
+
method,
|
157
147
|
hubNameRegex
|
158
148
|
})
|
159
|
-
|
160
|
-
|
161
|
-
|
149
|
+
// Trying to cache the whole response object will fail because it contains circular references
|
150
|
+
// so we'll just cache the data property of the response.
|
151
|
+
cachedOracleFspResponse = {
|
152
|
+
data: cachedOracleFspResponse.data
|
153
|
+
}
|
154
|
+
cache && cache.set(
|
155
|
+
cache.createKey(`oracleSendRequest_${url}`),
|
156
|
+
cachedOracleFspResponse
|
157
|
+
)
|
158
|
+
histTimerEnd({ success: true, hit: false })
|
159
|
+
} else {
|
160
|
+
cachedOracleFspResponse = cachedOracleFspResponse.item
|
161
|
+
histTimerEnd({ success: true, hit: true })
|
162
|
+
logger.debug('[oracleRequest]: cache hit for fsp for partyId lookup')
|
162
163
|
}
|
164
|
+
|
165
|
+
return cachedOracleFspResponse
|
163
166
|
} catch (err) {
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
}]
|
168
|
-
Logger.isErrorEnabled && Logger.error(`error in oracleRequest: ${err?.stack}`)
|
167
|
+
log.warn('error in sendOracleGetRequest: ', err)
|
168
|
+
histTimerEnd({ success: false, hit: false })
|
169
|
+
|
169
170
|
// If the error was a 400 from the Oracle, we'll modify the error to generate a response to the
|
170
171
|
// initiator of the request.
|
171
172
|
if (
|
172
173
|
err.name === 'FSPIOPError' &&
|
173
174
|
err.apiErrorCode.code === ErrorHandler.Enums.FSPIOPErrorCodes.DESTINATION_COMMUNICATION_ERROR.code
|
174
175
|
) {
|
175
|
-
|
176
|
+
const extensions = [{
|
177
|
+
key: 'system',
|
178
|
+
value: '["@hapi/catbox-memory","http"]'
|
179
|
+
}]
|
180
|
+
if (err.extensions.some(ext => (ext.key === 'status' && ext.value === ReturnCodes.BADREQUEST.CODE))) {
|
176
181
|
throw ErrorHandler.Factory.createFSPIOPError(
|
177
182
|
ErrorHandler.Enums.FSPIOPErrorCodes.PARTY_NOT_FOUND,
|
178
183
|
undefined,
|
@@ -180,10 +185,11 @@ exports.oracleRequest = async (headers, method, params = {}, query = {}, payload
|
|
180
185
|
undefined,
|
181
186
|
extensions
|
182
187
|
)
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
188
|
+
}
|
189
|
+
// Added error 404 to cover a special case of the Mowali implementation
|
190
|
+
// which uses mojaloop/als-oracle-pathfinder and currently returns 404
|
191
|
+
// and in which case the Mowali implementation expects back `DESTINATION_FSP_ERROR`.
|
192
|
+
if (err.extensions.some(ext => (ext.key === 'status' && ext.value === ReturnCodes.NOTFOUND.CODE))) {
|
187
193
|
throw ErrorHandler.Factory.createFSPIOPError(
|
188
194
|
ErrorHandler.Enums.FSPIOPErrorCodes.DESTINATION_FSP_ERROR,
|
189
195
|
undefined,
|
@@ -193,13 +199,47 @@ exports.oracleRequest = async (headers, method, params = {}, query = {}, payload
|
|
193
199
|
)
|
194
200
|
}
|
195
201
|
}
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
+
|
203
|
+
throw err
|
204
|
+
}
|
205
|
+
}
|
206
|
+
|
207
|
+
const validatePartyDeletion = async ({
|
208
|
+
url, source, destination, headers, method, params, payload
|
209
|
+
}) => {
|
210
|
+
const log = logger.child({ component: 'validatePartyDeletion', params })
|
211
|
+
// If the request is a DELETE request, we need to ensure that the participant belongs to the requesting FSP
|
212
|
+
const getParticipantResponse = await request.sendRequest({
|
213
|
+
url,
|
214
|
+
headers,
|
215
|
+
source,
|
216
|
+
destination,
|
217
|
+
method,
|
218
|
+
payload,
|
219
|
+
hubNameRegex
|
220
|
+
})
|
221
|
+
|
222
|
+
if (getParticipantResponse.status !== ReturnCodes.OK.CODE) {
|
223
|
+
const errMessage = `Invalid getOracleResponse status code: ${getParticipantResponse.status}`
|
224
|
+
log.warn(errMessage)
|
225
|
+
throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.PARTY_NOT_FOUND, errMessage)
|
226
|
+
// todo: clarify if we need to throw PARTY_NOT_FOUND
|
227
|
+
}
|
228
|
+
|
229
|
+
const participant = getParticipantResponse.data
|
230
|
+
if (!Array.isArray(participant?.partyList) || participant.partyList.length === 0) {
|
231
|
+
const errMessage = 'No participant found for the party'
|
232
|
+
log.warn(errMessage)
|
233
|
+
throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.DELETE_PARTY_INFO_ERROR, errMessage)
|
234
|
+
}
|
235
|
+
|
236
|
+
const party = participant.partyList[0] // todo: clarify why we check only the first party?
|
237
|
+
if (party.fspId !== source) {
|
238
|
+
const errMessage = `The party ${params.Type}:${params.ID} does not belong to the requesting FSP`
|
239
|
+
log.warn(errMessage)
|
240
|
+
throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.DELETE_PARTY_INFO_ERROR, errMessage)
|
202
241
|
}
|
242
|
+
return true
|
203
243
|
}
|
204
244
|
|
205
245
|
/**
|
@@ -233,8 +273,10 @@ const _getOracleEndpointByTypeAndCurrency = async (partyIdType, partyIdentifier,
|
|
233
273
|
)
|
234
274
|
}
|
235
275
|
} else {
|
236
|
-
|
237
|
-
|
276
|
+
const errMessage = `Oracle type:${partyIdType} and currency:${currency} not found`
|
277
|
+
logger.error(errMessage)
|
278
|
+
throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ADD_PARTY_INFO_ERROR, errMessage)
|
279
|
+
.toApiErrorObject(Config.ERROR_HANDLING)
|
238
280
|
}
|
239
281
|
return url
|
240
282
|
}
|
@@ -269,8 +311,9 @@ const _getOracleEndpointByType = async (partyIdType, partyIdentifier, assertPend
|
|
269
311
|
)
|
270
312
|
}
|
271
313
|
} else {
|
272
|
-
|
273
|
-
|
314
|
+
const errMessage = `Oracle type:${partyIdType} not found`
|
315
|
+
logger.error(errMessage)
|
316
|
+
throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ADD_PARTY_INFO_ERROR, errMessage)
|
274
317
|
}
|
275
318
|
return url
|
276
319
|
}
|
@@ -306,8 +349,10 @@ const _getOracleEndpointByTypeAndSubId = async (partyIdType, partyIdentifier, pa
|
|
306
349
|
)
|
307
350
|
}
|
308
351
|
} else {
|
309
|
-
|
310
|
-
|
352
|
+
const errMessage = `Oracle type: ${partyIdType} and subId: ${partySubIdOrType} not found`
|
353
|
+
logger.error(errMessage)
|
354
|
+
throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ADD_PARTY_INFO_ERROR, errMessage)
|
355
|
+
.toApiErrorObject(Config.ERROR_HANDLING)
|
311
356
|
}
|
312
357
|
return url
|
313
358
|
}
|
@@ -344,16 +389,16 @@ const _getOracleEndpointByTypeCurrencyAndSubId = async (partyIdType, partyIdenti
|
|
344
389
|
)
|
345
390
|
}
|
346
391
|
} else {
|
347
|
-
|
348
|
-
|
392
|
+
const errMessage = `Oracle type: ${partyIdType}, currency: ${currency} and subId: ${partySubIdOrType} not found`
|
393
|
+
logger.error(errMessage)
|
394
|
+
throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ADD_PARTY_INFO_ERROR, errMessage)
|
395
|
+
.toApiErrorObject(Config.ERROR_HANDLING)
|
349
396
|
}
|
350
397
|
return url
|
351
398
|
}
|
352
399
|
|
353
400
|
/**
|
354
|
-
*
|
355
|
-
*
|
356
|
-
* @description This sends a request to the oracles that are registered to the ALS
|
401
|
+
* Sends a request to the oracles that are registered to the ALS
|
357
402
|
*
|
358
403
|
* @param {object} headers - incoming http request headers
|
359
404
|
* @param {object} method - incoming http request method
|
@@ -361,9 +406,9 @@ const _getOracleEndpointByTypeCurrencyAndSubId = async (partyIdType, partyIdenti
|
|
361
406
|
* @param {string} type - oracle type
|
362
407
|
* @param {object} payload - the payload to send in the request
|
363
408
|
*
|
364
|
-
* @returns {object}
|
409
|
+
* @returns {object} - response from the oracle
|
365
410
|
*/
|
366
|
-
|
411
|
+
const oracleBatchRequest = async (headers, method, requestPayload, type, payload) => {
|
367
412
|
try {
|
368
413
|
let oracleEndpointModel
|
369
414
|
let url
|
@@ -383,22 +428,27 @@ exports.oracleBatchRequest = async (headers, method, requestPayload, type, paylo
|
|
383
428
|
} else {
|
384
429
|
url = oracleEndpointModel[0].value + Enums.EndPoints.FspEndpointTemplates.ORACLE_PARTICIPANTS_BATCH
|
385
430
|
}
|
386
|
-
|
431
|
+
logger.debug(`Oracle endpoints: ${url}`)
|
387
432
|
return await request.sendRequest({
|
388
433
|
url,
|
389
434
|
headers,
|
390
|
-
source: headers[
|
391
|
-
destination: headers[
|
435
|
+
source: headers[Headers.FSPIOP.SOURCE],
|
436
|
+
destination: headers[Headers.FSPIOP.DESTINATION] || Config.HUB_NAME,
|
392
437
|
method,
|
393
438
|
payload,
|
394
439
|
hubNameRegex
|
395
440
|
})
|
396
441
|
} else {
|
397
|
-
|
442
|
+
logger.error(`Oracle type:${type} not found`)
|
398
443
|
throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ADD_PARTY_INFO_ERROR, `Oracle type:${type} not found`)
|
399
444
|
}
|
400
445
|
} catch (err) {
|
401
|
-
|
446
|
+
logger.error('error in oracleBatchRequest: ', err)
|
402
447
|
throw ErrorHandler.Factory.reformatFSPIOPError(err)
|
403
448
|
}
|
404
449
|
}
|
450
|
+
|
451
|
+
module.exports = {
|
452
|
+
oracleRequest,
|
453
|
+
oracleBatchRequest
|
454
|
+
}
|