5
5
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
6
*/
7
7
8
- import { retry , NotRetryableError , RetryError } from 'ts-retry-promise' ;
9
- import { PollingClient , StatusResult , Connection , Logger , Messages , Lifecycle , SfError } from '@salesforce/core' ;
10
- import { Duration , ensureArray } from '@salesforce/kit' ;
8
+ import { Connection , Logger , Messages , Lifecycle , SfError } from '@salesforce/core' ;
11
9
import { ensurePlainObject , ensureString , isPlainObject } from '@salesforce/ts-types' ;
12
10
import { RegistryAccess } from '../registry/registryAccess' ;
13
11
import { MetadataType } from '../registry/types' ;
@@ -30,7 +28,6 @@ export type ResolveConnectionResult = {
30
28
* Resolve MetadataComponents from an org connection
31
29
*/
32
30
export class ConnectionResolver {
33
- protected logger : Logger ;
34
31
private connection : Connection ;
35
32
private registry : RegistryAccess ;
36
33
@@ -41,7 +38,6 @@ export class ConnectionResolver {
41
38
public constructor ( connection : Connection , registry = new RegistryAccess ( ) , mdTypes ?: string [ ] ) {
42
39
this . connection = connection ;
43
40
this . registry = registry ;
44
- this . logger = Logger . childFromRoot ( this . constructor . name ) ;
45
41
this . mdTypeNames = mdTypes ?. length
46
42
? // ensure the types passed in are valid per the registry
47
43
mdTypes . filter ( ( t ) => this . registry . getTypeByName ( t ) )
@@ -57,21 +53,21 @@ export class ConnectionResolver {
57
53
const lifecycle = Lifecycle . getInstance ( ) ;
58
54
59
55
const componentFromDescribe = (
60
- await Promise . all ( this . mdTypeNames . map ( ( type ) => this . listMembers ( { type } ) ) )
56
+ await Promise . all ( this . mdTypeNames . map ( ( type ) => listMembers ( this . registry ) ( this . connection ) ( { type } ) ) )
61
57
) . flat ( ) ;
62
58
63
59
for ( const component of componentFromDescribe ) {
64
60
let componentType : MetadataType ;
65
- if ( typeof component . type === 'string' && component . type . length ) {
61
+ if ( isNonEmptyString ( component . type ) ) {
66
62
componentType = this . registry . getTypeByName ( component . type ) ;
67
- } else if ( typeof component . fileName === 'string' && component . fileName . length ) {
63
+ } else if ( isNonEmptyString ( component . fileName ) ) {
68
64
// fix { type: { "$": { "xsi:nil": "true" } } }
69
65
componentType = ensurePlainObject (
70
66
this . registry . getTypeBySuffix ( extName ( component . fileName ) ) ,
71
67
`No type found for ${ component . fileName } when matching by suffix. Check the file extension.`
72
68
) ;
73
69
component . type = componentType . name ;
74
- } else if ( component . type === undefined && component . fileName === undefined ) {
70
+ } else if ( ! isNonEmptyString ( component . type ) && ! isNonEmptyString ( component . fileName ) ) {
75
71
// has no type and has no filename! Warn and skip that component.
76
72
// eslint-disable-next-line no-await-in-loop
77
73
await Promise . all ( [
@@ -91,11 +87,10 @@ export class ConnectionResolver {
91
87
92
88
Aggregator . push ( component ) ;
93
89
componentTypes . add ( componentType ) ;
94
- const folderContentType = componentType . folderContentType ;
95
- if ( folderContentType ) {
90
+ if ( componentType . folderContentType ) {
96
91
childrenPromises . push (
97
- this . listMembers ( {
98
- type : this . registry . getTypeByName ( folderContentType ) . name ,
92
+ listMembers ( this . registry ) ( this . connection ) ( {
93
+ type : this . registry . getTypeByName ( componentType . folderContentType ) . name ,
99
94
folder : component . fullName ,
100
95
} )
101
96
) ;
@@ -106,7 +101,7 @@ export class ConnectionResolver {
106
101
const childTypes = componentType . children ?. types ;
107
102
if ( childTypes ) {
108
103
Object . values ( childTypes ) . map ( ( childType ) => {
109
- childrenPromises . push ( this . listMembers ( { type : childType . name } ) ) ;
104
+ childrenPromises . push ( listMembers ( this . registry ) ( this . connection ) ( { type : childType . name } ) ) ;
110
105
} ) ;
111
106
}
112
107
}
@@ -125,93 +120,60 @@ export class ConnectionResolver {
125
120
apiVersion : this . connection . getApiVersion ( ) ,
126
121
} ;
127
122
}
123
+ }
128
124
129
- private async listMembers ( query : ListMetadataQuery ) : Promise < RelevantFileProperties [ ] > {
130
- let members : RelevantFileProperties [ ] = [ ] ;
131
-
132
- const pollingOptions : PollingClient . Options = {
133
- frequency : Duration . milliseconds ( 1000 ) ,
134
- timeout : Duration . minutes ( 3 ) ,
135
- poll : async ( ) : Promise < StatusResult > => {
136
- const res = ensureArray ( await this . connection . metadata . list ( query ) ) ;
137
- return { completed : true , payload : res } ;
138
- } ,
139
- } ;
140
-
141
- const pollingClient = await PollingClient . create ( pollingOptions ) ;
142
-
143
- try {
144
- members = await pollingClient . subscribe ( ) ;
145
- } catch ( error ) {
146
- // throw error if PollingClient timed out.
147
- if ( error instanceof NotRetryableError ) {
148
- throw NotRetryableError ;
149
- }
150
- this . logger . debug ( ( error as Error ) . message ) ;
151
- members = [ ] ;
152
- }
153
-
154
- // if the Metadata Type doesn't return a correct fileName then help it out
155
- for ( const m of members ) {
156
- if ( typeof m . fileName == 'object' ) {
157
- const t = this . registry . getTypeByName ( query . type ) ;
158
- m . fileName = `${ t . directoryName } /${ m . fullName } .${ t . suffix } ` ;
159
- }
160
- }
125
+ const listMembers =
126
+ ( registry : RegistryAccess ) =>
127
+ ( connection : Connection ) =>
128
+ async ( query : ListMetadataQuery ) : Promise < RelevantFileProperties [ ] > => {
129
+ const mdType = registry . getTypeByName ( query . type ) ;
161
130
162
131
// Workaround because metadata.list({ type: 'StandardValueSet' }) returns []
163
- if ( query . type === this . registry . getRegistry ( ) . types . standardvalueset . name && members . length === 0 ) {
132
+ if ( mdType . name === registry . getRegistry ( ) . types . standardvalueset . name ) {
133
+ const members : RelevantFileProperties [ ] = [ ] ;
134
+
164
135
const standardValueSetPromises = standardValueSet . fullnames . map ( async ( standardValueSetFullName ) => {
165
136
try {
166
- // The 'singleRecordQuery' method was having connection errors, using `retry` resolves this
167
- // Note that this type of connection retry logic may someday be added to jsforce v2
168
- // Once that happens this logic could be reverted
169
- const standardValueSetRecord = await retry < StdValueSetRecord > ( async ( ) => {
170
- try {
171
- return await this . connection . singleRecordQuery (
172
- `SELECT Id, MasterLabel, Metadata FROM StandardValueSet WHERE MasterLabel = '${ standardValueSetFullName } '` ,
173
- { tooling : true }
174
- ) ;
175
- } catch ( err ) {
176
- // We exit the retry loop with `NotRetryableError` if we get an (expected) unsupported metadata type error
177
- const error = err as Error ;
178
- if ( error . message . includes ( 'either inaccessible or not supported in Metadata API' ) ) {
179
- this . logger . debug ( 'Expected error:' , error . message ) ;
180
- throw new NotRetryableError ( error . message ) ;
181
- }
182
-
183
- // Otherwise throw the err so we can retry again
184
- throw err ;
185
- }
186
- } ) ;
137
+ const standardValueSetRecord : StdValueSetRecord = await connection . singleRecordQuery (
138
+ `SELECT Id, MasterLabel, Metadata FROM StandardValueSet WHERE MasterLabel = '${ standardValueSetFullName } '` ,
139
+ { tooling : true }
140
+ ) ;
187
141
188
142
return (
189
143
standardValueSetRecord . Metadata . standardValue . length && {
190
144
fullName : standardValueSetRecord . MasterLabel ,
191
- fileName : `${ this . registry . getRegistry ( ) . types . standardvalueset . directoryName } /${
192
- standardValueSetRecord . MasterLabel
193
- } .${ this . registry . getRegistry ( ) . types . standardvalueset . suffix } `,
194
- type : this . registry . getRegistry ( ) . types . standardvalueset . name ,
145
+ fileName : `${ mdType . directoryName } /${ standardValueSetRecord . MasterLabel } .${ mdType . suffix } ` ,
146
+ type : mdType . name ,
195
147
}
196
148
) ;
197
149
} catch ( err ) {
198
- // error.message here will be overwritten by 'ts-retry-promise'
199
- // Example error.message from the library: "All retries failed" or "Met not retryable error"
200
- // 'ts-retry-promise' exposes the actual error on `error.lastError`
201
- const error = err as RetryError ;
202
-
203
- if ( error . lastError ?. message ) {
204
- this . logger . debug ( error . lastError . message ) ;
205
- }
150
+ const logger = Logger . childFromRoot ( 'ConnectionResolver.listMembers' ) ;
151
+ logger . debug ( err ) ;
206
152
}
207
153
} ) ;
208
154
for await ( const standardValueSetResult of standardValueSetPromises ) {
209
155
if ( standardValueSetResult ) {
210
156
members . push ( standardValueSetResult ) ;
211
157
}
212
158
}
159
+ return members ;
213
160
}
214
161
215
- return members ;
216
- }
217
- }
162
+ try {
163
+ return ( await connection . metadata . list ( query ) ) . map ( inferFilenamesFromType ( mdType ) ) ;
164
+ } catch ( error ) {
165
+ const logger = Logger . childFromRoot ( 'ConnectionResolver.listMembers' ) ;
166
+ logger . debug ( ( error as Error ) . message ) ;
167
+ return [ ] ;
168
+ }
169
+ } ;
170
+
171
+ /* if the Metadata Type doesn't return a correct fileName then help it out */
172
+ const inferFilenamesFromType =
173
+ ( metadataType : MetadataType ) =>
174
+ ( member : RelevantFileProperties ) : RelevantFileProperties =>
175
+ typeof member . fileName === 'object'
176
+ ? { ...member , fileName : `${ metadataType . directoryName } /${ member . fullName } .${ metadataType . suffix } ` }
177
+ : member ;
178
+
179
+ const isNonEmptyString = ( value : string | undefined ) : boolean => typeof value === 'string' && value . length > 0 ;
0 commit comments