@open-xchange/soap-client 0.1.5 → 0.2.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/CHANGELOG.md +31 -1
- package/README.md +23 -0
- package/client.js +72 -0
- package/package.json +18 -7
- package/services/common/context.js +144 -131
- package/services/common/user.js +121 -100
- package/services/common/util.js +23 -9
- package/services/reseller/oxaas.js +23 -11
- package/services/reseller/resellerContext.js +45 -29
- package/services/reseller/resellerUser.js +81 -63
- package/services/secondaryAccount.js +61 -46
- package/services/sharedAccount.js +120 -104
- package/soap.js +169 -51
- package/test/soap.test.js +0 -226
- package/vitest.config.js +0 -8
|
@@ -1,52 +1,67 @@
|
|
|
1
|
-
import { createClientAsync } from '../soap.js'
|
|
2
|
-
|
|
3
|
-
let OXSecondaryAccountService
|
|
4
|
-
function getClient () {
|
|
5
|
-
if (!OXSecondaryAccountService) OXSecondaryAccountService = createClientAsync('OXSecondaryAccountService')
|
|
6
|
-
return OXSecondaryAccountService
|
|
7
|
-
}
|
|
1
|
+
import { createClientAsync, memoizeClient } from '../soap.js'
|
|
8
2
|
|
|
9
3
|
/**
|
|
10
|
-
*
|
|
11
|
-
* @
|
|
12
|
-
* @
|
|
13
|
-
* @param {Object} context - The context object where the account will be created
|
|
14
|
-
* @param {Array<Object>} users - Array of users to associate with the secondary account
|
|
15
|
-
* @param {Array<Object>} groups - Array of groups to associate with the secondary account
|
|
16
|
-
* @returns {Promise<Object>} The created secondary account object
|
|
17
|
-
* @throws {Error} If the account creation fails
|
|
4
|
+
* Build a secondary-account service bound to a SOAP client constructor.
|
|
5
|
+
* @param {(type: string) => Promise<Object>} createClient
|
|
6
|
+
* @returns {Object} The secondary account service.
|
|
18
7
|
*/
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
accountDataOnCreate: accountData,
|
|
22
|
-
context: { id: context.id },
|
|
23
|
-
users: users || [],
|
|
24
|
-
auth: context.admin
|
|
25
|
-
})
|
|
26
|
-
}
|
|
8
|
+
export function createSecondaryAccountService (createClient) {
|
|
9
|
+
const getClient = memoizeClient(createClient, 'OXSecondaryAccountService')
|
|
27
10
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
11
|
+
/**
|
|
12
|
+
* Creates a secondary account using the OX Secondary Account Service
|
|
13
|
+
* @async
|
|
14
|
+
* @param {Object} accountData - The data for the secondary account to be created
|
|
15
|
+
* @param {Object} context - The context object where the account will be created
|
|
16
|
+
* @param {Array<Object>} users - Array of users to associate with the secondary account
|
|
17
|
+
* @param {Array<Object>} groups - Array of groups to associate with the secondary account
|
|
18
|
+
* @returns {Promise<Object>} The created secondary account object
|
|
19
|
+
* @throws {Error} If the account creation fails
|
|
20
|
+
*/
|
|
21
|
+
async function create (accountData, context, users = [], groups = []) {
|
|
22
|
+
return (await getClient()).createAsync({
|
|
23
|
+
accountDataOnCreate: accountData,
|
|
24
|
+
context: { id: context.id },
|
|
25
|
+
users: users || [],
|
|
26
|
+
auth: context.admin
|
|
27
|
+
})
|
|
28
|
+
}
|
|
42
29
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
30
|
+
async function list (context) {
|
|
31
|
+
return (await getClient()).listAsync({
|
|
32
|
+
context: { id: context.id },
|
|
33
|
+
auth: context.admin
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function remove (primaryAddress, context, users, groups) {
|
|
38
|
+
return (await getClient()).deleteAsync({
|
|
39
|
+
primaryAddress,
|
|
40
|
+
context: { id: context.id },
|
|
41
|
+
users,
|
|
42
|
+
auth: context.admin
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function update (primaryAddress, accountData, context, users, groups) {
|
|
47
|
+
return (await getClient()).updateAsync({
|
|
48
|
+
primaryAddress,
|
|
49
|
+
accountDataUpdate: accountData,
|
|
50
|
+
context: { id: context.id },
|
|
51
|
+
users,
|
|
52
|
+
groups,
|
|
53
|
+
auth: context.admin
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { create, list, remove, update }
|
|
52
58
|
}
|
|
59
|
+
|
|
60
|
+
// Classic module exports, bound to the env-configured default endpoint.
|
|
61
|
+
let defaultService
|
|
62
|
+
const service = () => (defaultService ??= createSecondaryAccountService(createClientAsync))
|
|
63
|
+
|
|
64
|
+
export const create = (...args) => service().create(...args)
|
|
65
|
+
export const list = (...args) => service().list(...args)
|
|
66
|
+
export const remove = (...args) => service().remove(...args)
|
|
67
|
+
export const update = (...args) => service().update(...args)
|
|
@@ -18,116 +18,132 @@
|
|
|
18
18
|
* Any use of the work other than as authorized under this license or copyright law is prohibited.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { createClientAsync } from '../soap.js'
|
|
22
|
-
|
|
23
|
-
let OXSharedAccountService
|
|
24
|
-
function getClient () {
|
|
25
|
-
if (!OXSharedAccountService) OXSharedAccountService = createClientAsync('OXSharedAccountService')
|
|
26
|
-
return OXSharedAccountService
|
|
27
|
-
}
|
|
21
|
+
import { createClientAsync, memoizeClient } from '../soap.js'
|
|
28
22
|
|
|
29
23
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* @
|
|
33
|
-
* @param {Object} sharedAccountData - The data for the shared account to be created
|
|
34
|
-
* @returns {Promise<Object>} The created shared account object
|
|
35
|
-
* @throws {Error} If the shared account creation fails
|
|
24
|
+
* Build a shared-account service bound to a SOAP client constructor.
|
|
25
|
+
* @param {(type: string) => Promise<Object>} createClient
|
|
26
|
+
* @returns {Object} The shared account service.
|
|
36
27
|
*/
|
|
37
|
-
export
|
|
38
|
-
|
|
39
|
-
ctx: { id: context.id },
|
|
40
|
-
sharedAccountData, // display_name, given_name, imapServer, language, mailenabled, name, sur_name, primaryEmail, email1, smtpServer, timezone, password
|
|
41
|
-
auth: context.admin
|
|
42
|
-
})
|
|
43
|
-
}
|
|
28
|
+
export function createSharedAccountService (createClient) {
|
|
29
|
+
const getClient = memoizeClient(createClient, 'OXSharedAccountService')
|
|
44
30
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
})
|
|
61
|
-
}
|
|
31
|
+
/**
|
|
32
|
+
* Creates a shared account using the OX Shared Account Service
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} context - The context object where the account will be created
|
|
35
|
+
* @param {Object} sharedAccountData - The data for the shared account to be created
|
|
36
|
+
* @returns {Promise<Object>} The created shared account object
|
|
37
|
+
* @throws {Error} If the shared account creation fails
|
|
38
|
+
*/
|
|
39
|
+
async function create (context, sharedAccountData) {
|
|
40
|
+
return (await getClient()).createAsync({
|
|
41
|
+
ctx: { id: context.id },
|
|
42
|
+
sharedAccountData, // display_name, given_name, imapServer, language, mailenabled, name, sur_name, primaryEmail, email1, smtpServer, timezone, password
|
|
43
|
+
auth: context.admin
|
|
44
|
+
})
|
|
45
|
+
}
|
|
62
46
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
capabilities: {
|
|
81
|
-
grantedCapability: permissions.capabilities?.grantedCapability ?? [],
|
|
82
|
-
deniedCapability: permissions.capabilities?.deniedCapability ?? [],
|
|
83
|
-
},
|
|
84
|
-
mailConfig: permissions.mailConfig,
|
|
85
|
-
calendarConfig: permissions.calendarConfig,
|
|
86
|
-
auth: context.admin
|
|
87
|
-
})
|
|
88
|
-
}
|
|
47
|
+
/**
|
|
48
|
+
* Converts an existing user to a shared account using the OX Shared Account Service
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} context - The context of the user to be converted
|
|
51
|
+
* @param {number} userId - ID of the user to be converted
|
|
52
|
+
* @param {string} password - The password for the new shared account
|
|
53
|
+
* @returns {Promise<void>}
|
|
54
|
+
* @throws {Error} If the conversion fails
|
|
55
|
+
*/
|
|
56
|
+
async function convertUserToSharedAccount (context, userId, password) {
|
|
57
|
+
return (await getClient()).convertUserToSharedAccountAsync({
|
|
58
|
+
ctx: { id: context.id },
|
|
59
|
+
user: { id: userId },
|
|
60
|
+
password,
|
|
61
|
+
auth: context.admin
|
|
62
|
+
})
|
|
63
|
+
}
|
|
89
64
|
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
65
|
+
/**
|
|
66
|
+
* Add users and groups with specified permissions and capabilities to an existing shared account using the OX Shared Account Service.
|
|
67
|
+
*
|
|
68
|
+
* @param {Object} context - The context of the users and groups
|
|
69
|
+
* @param {Object} sharedAccountContext - The context of the shared account
|
|
70
|
+
* @param {number} sharedAccountId - ID of the shared account
|
|
71
|
+
* @param {Object} permissions - The users, groups, capabilities, and module-specific permissions.
|
|
72
|
+
* @returns {Promise<Object>} The shared account object
|
|
73
|
+
* @throws {Error} If the creation of the shared account permissions fails
|
|
74
|
+
*/
|
|
75
|
+
async function createSharedAccountPermissions (context, sharedAccountContext, sharedAccountId, permissions) {
|
|
76
|
+
return (await getClient()).createSharedAccountPermissionsAsync({
|
|
77
|
+
ctx: { id: context.id },
|
|
78
|
+
sharedAccountCtx: { id: sharedAccountContext.id },
|
|
79
|
+
sharedAccount: { id: sharedAccountId },
|
|
80
|
+
groups: permissions.groups ?? [],
|
|
81
|
+
users: permissions.users?.map(user => ({ id: user.userdata.id })) ?? [],
|
|
82
|
+
capabilities: {
|
|
83
|
+
grantedCapability: permissions.capabilities?.grantedCapability ?? [],
|
|
84
|
+
deniedCapability: permissions.capabilities?.deniedCapability ?? [],
|
|
85
|
+
},
|
|
86
|
+
mailConfig: permissions.mailConfig,
|
|
87
|
+
calendarConfig: permissions.calendarConfig,
|
|
88
|
+
auth: context.admin
|
|
89
|
+
})
|
|
90
|
+
}
|
|
104
91
|
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
92
|
+
/**
|
|
93
|
+
* List all shared accounts in the specified context
|
|
94
|
+
*
|
|
95
|
+
* @param {Object} context - The context object whose shared account will be listed
|
|
96
|
+
* @param {*} pattern - An optional search pattern for the contexts
|
|
97
|
+
* @returns {Promise<Object[]>} The list of shared account objects
|
|
98
|
+
*/
|
|
99
|
+
async function list (context, pattern = '*') {
|
|
100
|
+
return (await getClient()).listAsync({
|
|
101
|
+
ctx: { id: context.id },
|
|
102
|
+
search_pattern: pattern,
|
|
103
|
+
auth: context.admin
|
|
104
|
+
})
|
|
105
|
+
}
|
|
119
106
|
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Get all data of a specified shared account in the specified context
|
|
109
|
+
*
|
|
110
|
+
* @param {*} context - The context where the shared account exists
|
|
111
|
+
* @param {number} sharedAccountId - ID of the requested shared account
|
|
112
|
+
* @returns {Promise<Object>} The shared account object
|
|
113
|
+
*/
|
|
114
|
+
async function getData (context, sharedAccountId) {
|
|
115
|
+
return (await getClient()).getDataAsync({
|
|
116
|
+
ctx: { id: context.id },
|
|
117
|
+
sharedAccount: { id: sharedAccountId },
|
|
118
|
+
auth: context.admin
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Remove a specified shared account in the specified context
|
|
124
|
+
*
|
|
125
|
+
* @param {*} context - The context object where the account will be removed
|
|
126
|
+
* @param {number} sharedAccountId - ID of the shared account to be removed
|
|
127
|
+
* @returns {Promise<void>}
|
|
128
|
+
*/
|
|
129
|
+
async function remove (context, sharedAccountId) {
|
|
130
|
+
return (await getClient()).deleteAsync({
|
|
131
|
+
ctx: { id: context.id },
|
|
132
|
+
sharedAccount: { id: sharedAccountId },
|
|
133
|
+
auth: context.admin
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { create, convertUserToSharedAccount, createSharedAccountPermissions, list, getData, remove }
|
|
133
138
|
}
|
|
139
|
+
|
|
140
|
+
// Classic module exports, bound to the env-configured default endpoint.
|
|
141
|
+
let defaultService
|
|
142
|
+
const service = () => (defaultService ??= createSharedAccountService(createClientAsync))
|
|
143
|
+
|
|
144
|
+
export const create = (...args) => service().create(...args)
|
|
145
|
+
export const convertUserToSharedAccount = (...args) => service().convertUserToSharedAccount(...args)
|
|
146
|
+
export const createSharedAccountPermissions = (...args) => service().createSharedAccountPermissions(...args)
|
|
147
|
+
export const list = (...args) => service().list(...args)
|
|
148
|
+
export const getData = (...args) => service().getData(...args)
|
|
149
|
+
export const remove = (...args) => service().remove(...args)
|
package/soap.js
CHANGED
|
@@ -40,12 +40,9 @@ const RETRY_OPTIONS = {
|
|
|
40
40
|
randomize: true // jitter to avoid thundering herd across parallel shards
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// This flag enables debug output including timing information.
|
|
48
|
-
const debug = process.env.DEBUG_SOAP === 'true'
|
|
43
|
+
// This flag enables debug output including timing information. Read lazily so
|
|
44
|
+
// importing the module has no environment-dependent behavior.
|
|
45
|
+
const debug = () => process.env.DEBUG_SOAP === 'true'
|
|
49
46
|
|
|
50
47
|
/**
|
|
51
48
|
* Walk one layer of common error-wrapper shapes:
|
|
@@ -91,44 +88,44 @@ function describeError (err) {
|
|
|
91
88
|
'unknown'
|
|
92
89
|
}
|
|
93
90
|
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
password: process.env.E2E_ADMIN_PW
|
|
113
|
-
}
|
|
91
|
+
// Swallow specifically timeout-related unhandled promise rejections as
|
|
92
|
+
// fallback; log everything else, as those indicate real problems. Installed
|
|
93
|
+
// on demand (idempotent) — importing the module must not touch process-global
|
|
94
|
+
// state. The env-configured default client installs it for backwards
|
|
95
|
+
// compatibility with the CLI and test-runner consumers; factory clients from
|
|
96
|
+
// createClientFactory never do, so embedding applications keep their own
|
|
97
|
+
// rejection policy.
|
|
98
|
+
let rejectionFilterInstalled = false
|
|
99
|
+
function installTimeoutRejectionFilter () {
|
|
100
|
+
if (rejectionFilterInstalled) return
|
|
101
|
+
rejectionFilterInstalled = true
|
|
102
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
103
|
+
const r = unwrapError(reason)
|
|
104
|
+
if (r?.code === 'ECONNABORTED' && (r?.message?.includes('timeout') || r?.message?.includes('exceeded'))) {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
console.error('Unhandled promise rejection:', describeError(reason))
|
|
108
|
+
})
|
|
114
109
|
}
|
|
115
110
|
|
|
116
111
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* This is used for performance tracking of the SOAP methods.
|
|
112
|
+
* Lazily create the PerformanceObserver that logs SOAP method timings when
|
|
113
|
+
* DEBUG_SOAP is enabled. One observer per process, created on first client
|
|
114
|
+
* construction rather than at import time.
|
|
122
115
|
* @see https://nodejs.org/api/perf_hooks.html
|
|
123
|
-
* @params {PerformanceObserverEntryList} items The list of performance entries.
|
|
124
116
|
* @returns {void}
|
|
125
117
|
*/
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
118
|
+
let debugObserverInstalled = false
|
|
119
|
+
function ensureDebugObserver () {
|
|
120
|
+
if (debugObserverInstalled || !debug()) return
|
|
121
|
+
debugObserverInstalled = true
|
|
122
|
+
const obs = new PerformanceObserver((items) => {
|
|
123
|
+
items.getEntries().forEach((entry) => {
|
|
124
|
+
console.log(`${entry.name} took ${Math.round(entry.duration)}ms`)
|
|
125
|
+
})
|
|
129
126
|
})
|
|
130
|
-
})
|
|
131
|
-
|
|
127
|
+
obs.observe({ entryTypes: ['measure'] })
|
|
128
|
+
}
|
|
132
129
|
|
|
133
130
|
async function logSoapError (e) {
|
|
134
131
|
console.error(describeError(e))
|
|
@@ -218,15 +215,134 @@ function shouldAbortRetry (error) {
|
|
|
218
215
|
}
|
|
219
216
|
|
|
220
217
|
/**
|
|
221
|
-
*
|
|
218
|
+
* p-retry v8 `onFailedAttempt` handler, shared by both retry sites below.
|
|
219
|
+
*
|
|
220
|
+
* IMPORTANT: p-retry v8 invokes `onFailedAttempt` with a *context object*
|
|
221
|
+
* `{ error, attemptNumber, retriesLeft, retriesConsumed, retryDelay }` — NOT
|
|
222
|
+
* the error itself (that was the pre-v7 signature). So when we abort, we must
|
|
223
|
+
* pass `context.error` (the real Error) to `AbortError`, not `context`.
|
|
224
|
+
*
|
|
225
|
+
* Why it matters: `new AbortError(nonError)` sets `AbortError.message` to the
|
|
226
|
+
* value verbatim, and p-retry does not unwrap an AbortError thrown *by this
|
|
227
|
+
* callback* — it rejects with it as-is. Passing `context` would therefore
|
|
228
|
+
* reject with an Error whose `.message` is an object, which later blows up
|
|
229
|
+
* consumers doing `error.message.includes(...)` (appsuite-codeceptjs
|
|
230
|
+
* contexts.js) with "error.message.includes is not a function", masking the
|
|
231
|
+
* real SOAP fault. Passing `context.error` keeps `.message` a string and lets
|
|
232
|
+
* `wrapSoapFault` recover the fault downstream.
|
|
233
|
+
*
|
|
234
|
+
* @param {object} context p-retry attempt context.
|
|
235
|
+
* @param {string} label Human-readable label for the retry log line.
|
|
236
|
+
*/
|
|
237
|
+
function handleFailedAttempt (context, label) {
|
|
238
|
+
if (shouldAbortRetry(context)) throw new AbortError(context.error)
|
|
239
|
+
console.log(`Retrying ${label} in ${context.retriesLeft} attempts (${describeError(context)})`)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @typedef {object} TransportOptions
|
|
244
|
+
* @property {string} url Base URL of the provisioning API (e.g. the core-mw
|
|
245
|
+
* admin Service), without the `/webservices` suffix.
|
|
246
|
+
* @property {{login: string, password: string}} auth Master admin credentials
|
|
247
|
+
* injected into every SOAP call unless the call passes its own `auth`.
|
|
248
|
+
*/
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Create a per-endpoint SOAP client constructor. Multiple factories coexist in
|
|
252
|
+
* one process, each bound to its own endpoint and credentials — nothing is
|
|
253
|
+
* read from the environment and no process-global state is touched.
|
|
254
|
+
* @param {TransportOptions} options
|
|
255
|
+
* @returns {(type: string) => Promise<Object>} createClientAsync bound to the endpoint.
|
|
256
|
+
*/
|
|
257
|
+
function createClientFactory ({ url, auth }) {
|
|
258
|
+
if (!url) throw new TypeError('createClientFactory: url is required')
|
|
259
|
+
if (!auth?.login || !auth?.password) throw new TypeError('createClientFactory: auth {login, password} is required')
|
|
260
|
+
const baseUrl = String(url).replace(/\/$/, '')
|
|
261
|
+
const defaultAuth = { auth: { login: auth.login, password: auth.password } }
|
|
262
|
+
return (type) => createTypedClientAsync(type, baseUrl, defaultAuth)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Memoize one SOAP client per service type, but only once it resolves.
|
|
267
|
+
*
|
|
268
|
+
* A plain `promise ??= createClient(type)` caches a rejection for the lifetime
|
|
269
|
+
* of the process: if the WSDL fetch exhausts its retry schedule or hits a
|
|
270
|
+
* non-retryable fault, every later call re-throws that stale error without ever
|
|
271
|
+
* touching the network again. That is fatal for a long-running embedder, where
|
|
272
|
+
* one bad startup would poison the service until the process restarts. Clearing
|
|
273
|
+
* the slot on failure keeps the happy path a single shared client while letting
|
|
274
|
+
* the next call re-attempt construction.
|
|
275
|
+
*
|
|
276
|
+
* @param {(type: string) => Promise<Object>} createClient
|
|
277
|
+
* @param {string} type The name of the service type.
|
|
278
|
+
* @returns {() => Promise<Object>} Getter returning the memoized client.
|
|
279
|
+
*/
|
|
280
|
+
function memoizeClient (createClient, type) {
|
|
281
|
+
let clientPromise
|
|
282
|
+
return () => (clientPromise ??= createClient(type).catch(e => {
|
|
283
|
+
clientPromise = undefined
|
|
284
|
+
throw e
|
|
285
|
+
}))
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// The env-configured default transport behind the classic module exports.
|
|
289
|
+
// Built lazily on first use: loads .env files, installs the timeout-rejection
|
|
290
|
+
// filter (CLI/test-runner compatibility), and binds PROVISIONING_URL +
|
|
291
|
+
// E2E_ADMIN_USER/PW. Factory transports never take this path.
|
|
292
|
+
let defaultCreateClient
|
|
293
|
+
function getDefaultCreateClient () {
|
|
294
|
+
if (!defaultCreateClient) {
|
|
295
|
+
for (const envFile of ['.env', '.env.defaults']) {
|
|
296
|
+
try { process.loadEnvFile(envFile) } catch {}
|
|
297
|
+
}
|
|
298
|
+
installTimeoutRejectionFilter()
|
|
299
|
+
// Report the missing *environment variables*, not createClientFactory's
|
|
300
|
+
// parameter names: a caller of the classic module exports never touched
|
|
301
|
+
// that function and would otherwise get an error naming an API it has
|
|
302
|
+
// never heard of. Note this is stricter than before — calls that carry
|
|
303
|
+
// their own `auth` (every user/account/reseller method takes
|
|
304
|
+
// `context.admin`) used to work with the master credentials unset, and
|
|
305
|
+
// now fail here. See the CHANGELOG entry for 0.2.0.
|
|
306
|
+
const missing = ['PROVISIONING_URL', 'E2E_ADMIN_USER', 'E2E_ADMIN_PW'].filter(key => !process.env[key])
|
|
307
|
+
if (missing.length) {
|
|
308
|
+
throw new TypeError(
|
|
309
|
+
`@open-xchange/soap-client: missing ${missing.join(', ')}. The module exports bind to the ` +
|
|
310
|
+
'environment; set these in .env, or use createProvisioningClient({ url, auth }) from ' +
|
|
311
|
+
"'@open-xchange/soap-client/client' to pass an endpoint and credentials explicitly."
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
defaultCreateClient = createClientFactory({
|
|
315
|
+
url: process.env.PROVISIONING_URL,
|
|
316
|
+
auth: { login: process.env.E2E_ADMIN_USER, password: process.env.E2E_ADMIN_PW }
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
return defaultCreateClient
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* This function creates a SOAP client for the specified service type, bound to
|
|
324
|
+
* the env-configured default endpoint (PROVISIONING_URL, E2E_ADMIN_USER/PW).
|
|
222
325
|
* @param {string} type The name of the service type.
|
|
223
326
|
* @returns {Promise<Object>} The SOAP client.
|
|
224
327
|
*/
|
|
225
328
|
async function createClientAsync (type) {
|
|
329
|
+
return getDefaultCreateClient()(type)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Create a SOAP client for the specified service type against a specific
|
|
334
|
+
* endpoint with specific credentials.
|
|
335
|
+
* @param {string} type The name of the service type.
|
|
336
|
+
* @param {string} baseUrl Base URL without trailing slash.
|
|
337
|
+
* @param {{auth: {login: string, password: string}}} defaultAuth Auth option injected per call.
|
|
338
|
+
* @returns {Promise<Object>} The SOAP client.
|
|
339
|
+
*/
|
|
340
|
+
async function createTypedClientAsync (type, baseUrl, defaultAuth) {
|
|
341
|
+
ensureDebugObserver()
|
|
226
342
|
const startMark = `${type}-start`
|
|
227
343
|
const endMark = `${type}-end`
|
|
228
344
|
performance.mark(startMark)
|
|
229
|
-
const endpoint = `${
|
|
345
|
+
const endpoint = `${baseUrl}/webservices/${type}`
|
|
230
346
|
const url = `${endpoint}/?wsdl`
|
|
231
347
|
// The WSDL fetch itself can hit transient network errors (ECONNRESET,
|
|
232
348
|
// ETIMEDOUT, ...). Without retry here, a single TLS hiccup during
|
|
@@ -241,10 +357,7 @@ async function createClientAsync (type) {
|
|
|
241
357
|
gzip: true
|
|
242
358
|
}), {
|
|
243
359
|
...RETRY_OPTIONS,
|
|
244
|
-
onFailedAttempt:
|
|
245
|
-
if (shouldAbortRetry(error)) throw new AbortError(error)
|
|
246
|
-
console.log(`Retrying WSDL fetch (${type}) in ${error.retriesLeft} attempts (${describeError(error)})`)
|
|
247
|
-
}
|
|
360
|
+
onFailedAttempt: context => handleFailedAttempt(context, `WSDL fetch (${type})`)
|
|
248
361
|
})
|
|
249
362
|
|
|
250
363
|
// https://stackoverflow.com/questions/30740415/namespace-for-array-field-in-node-soap-client-node-js
|
|
@@ -262,20 +375,25 @@ async function createClientAsync (type) {
|
|
|
262
375
|
const startMark = `${prop}-start`
|
|
263
376
|
const endMark = `${prop}-end`
|
|
264
377
|
performance.mark(startMark)
|
|
265
|
-
|
|
266
|
-
|
|
378
|
+
// Pull `auth` out before merging. Spreading `options` wholesale made a
|
|
379
|
+
// present-but-undefined `auth` (every user/account/reseller method
|
|
380
|
+
// sends `auth: context.admin`, and a context without a resolved admin
|
|
381
|
+
// makes that undefined) overwrite the endpoint credentials, so the
|
|
382
|
+
// call went out unauthenticated even though valid ones were
|
|
383
|
+
// configured. An absent or undefined `auth` now falls back to the
|
|
384
|
+
// endpoint default; a real one still wins.
|
|
385
|
+
const { auth, ...rest } = options ?? {}
|
|
386
|
+
const soapOptions = { ...defaultAuth, ...rest }
|
|
387
|
+
if (auth) {
|
|
267
388
|
// only send login and password instead of complete admin object.
|
|
268
389
|
// this can fail because of ambiguous namespacing
|
|
269
|
-
soapOptions.auth = { login:
|
|
390
|
+
soapOptions.auth = { login: auth.login, password: auth.password }
|
|
270
391
|
}
|
|
271
392
|
|
|
272
393
|
try {
|
|
273
394
|
const result = await pRetry(() => origMethod.apply(this, [soapOptions, { timeout: 30000, ...clientOptions }, ...args]), {
|
|
274
395
|
...RETRY_OPTIONS,
|
|
275
|
-
onFailedAttempt:
|
|
276
|
-
if (shouldAbortRetry(error)) throw new AbortError(error)
|
|
277
|
-
console.log(`Retrying ${String(prop)} in ${error.retriesLeft} attempts (${describeError(error)})`)
|
|
278
|
-
}
|
|
396
|
+
onFailedAttempt: context => handleFailedAttempt(context, String(prop))
|
|
279
397
|
})
|
|
280
398
|
|
|
281
399
|
performance.mark(endMark)
|
|
@@ -294,4 +412,4 @@ async function createClientAsync (type) {
|
|
|
294
412
|
})
|
|
295
413
|
}
|
|
296
414
|
|
|
297
|
-
export { createClientAsync, logSoapError, describeError, shouldAbortRetry, wrapSoapFault, unwrapError }
|
|
415
|
+
export { createClientAsync, createClientFactory, memoizeClient, installTimeoutRejectionFilter, logSoapError, describeError, shouldAbortRetry, wrapSoapFault, unwrapError, handleFailedAttempt }
|