11import { IEncryptionService } from "@/features/encrypt/EncryptionService"
22import {
33 Project ,
4- Version ,
5- IProjectConfig ,
6- IProjectDataSource ,
7- ProjectConfigParser ,
8- ProjectConfigRemoteVersion ,
9- ProjectConfigRemoteSpecification
4+ IProjectDataSource
105} from "../domain"
116import IAzureDevOpsRepositoryDataSource , {
12- AzureDevOpsRepositoryWithRefs ,
13- AzureDevOpsRepositoryRef
7+ AzureDevOpsRepositoryWithRefs
148} from "../domain/IAzureDevOpsRepositoryDataSource"
15- import RemoteConfig from "../domain/RemoteConfig "
9+ import ProjectMapper , { type URLBuilders , type RepositoryRef } from "../domain/ProjectMapper "
1610import { IRemoteConfigEncoder } from "../domain/RemoteConfigEncoder"
1711
12+ const azureDevOpsURLBuilders : URLBuilders < AzureDevOpsRepositoryWithRefs > = {
13+ getImageRef ( repository : AzureDevOpsRepositoryWithRefs ) : string {
14+ return repository . defaultBranchRef . name
15+ } ,
16+ getBlobRef ( ref : RepositoryRef ) : string {
17+ return ref . name
18+ } ,
19+ getOwnerUrl ( owner : string ) : string {
20+ return `https://dev.azure.com/${ owner } `
21+ } ,
22+ getProjectUrl ( repository : AzureDevOpsRepositoryWithRefs ) : string {
23+ return repository . webUrl
24+ } ,
25+ getVersionUrl ( repository : AzureDevOpsRepositoryWithRefs , ref : RepositoryRef ) : string {
26+ return `${ repository . webUrl } ?version=GB${ ref . name } `
27+ } ,
28+ getSpecEditUrl ( repository : AzureDevOpsRepositoryWithRefs , ref : RepositoryRef , fileName : string ) : string {
29+ return `${ repository . webUrl } ?path=/${ fileName } &version=GB${ ref . name } &_a=contents`
30+ }
31+ // No getDiffUrl or getPrUrl - diff calculation is not supported for Azure DevOps
32+ }
33+
1834export default class AzureDevOpsProjectDataSource implements IProjectDataSource {
1935 private readonly repositoryDataSource : IAzureDevOpsRepositoryDataSource
20- private readonly repositoryNameSuffix : string
21- private readonly encryptionService : IEncryptionService
22- private readonly remoteConfigEncoder : IRemoteConfigEncoder
36+ private readonly projectMapper : ProjectMapper < AzureDevOpsRepositoryWithRefs >
2337
2438 constructor ( config : {
2539 repositoryDataSource : IAzureDevOpsRepositoryDataSource
@@ -28,243 +42,16 @@ export default class AzureDevOpsProjectDataSource implements IProjectDataSource
2842 remoteConfigEncoder : IRemoteConfigEncoder
2943 } ) {
3044 this . repositoryDataSource = config . repositoryDataSource
31- this . repositoryNameSuffix = config . repositoryNameSuffix
32- this . encryptionService = config . encryptionService
33- this . remoteConfigEncoder = config . remoteConfigEncoder
45+ this . projectMapper = new ProjectMapper ( {
46+ repositoryNameSuffix : config . repositoryNameSuffix ,
47+ urlBuilders : azureDevOpsURLBuilders ,
48+ encryptionService : config . encryptionService ,
49+ remoteConfigEncoder : config . remoteConfigEncoder
50+ } )
3451 }
3552
3653 async getProjects ( ) : Promise < Project [ ] > {
3754 const repositories = await this . repositoryDataSource . getRepositories ( )
38- return repositories . map ( repository => {
39- return this . mapProject ( repository )
40- } )
41- . filter ( ( project : Project ) => {
42- return project . versions . length > 0
43- } )
44- . sort ( ( a : Project , b : Project ) => {
45- return a . name . localeCompare ( b . name )
46- } )
47- }
48-
49- private mapProject ( repository : AzureDevOpsRepositoryWithRefs ) : Project {
50- const config = this . getConfig ( repository )
51- let imageURL : string | undefined
52- if ( config && config . image ) {
53- imageURL = this . getAzureDevOpsBlobURL ( {
54- organization : repository . owner ,
55- repositoryName : repository . name ,
56- path : config . image ,
57- ref : repository . defaultBranchRef . name
58- } )
59- }
60- const versions = this . sortVersions (
61- this . addRemoteVersions (
62- this . getVersions ( repository ) ,
63- config ?. remoteVersions || [ ]
64- ) ,
65- repository . defaultBranchRef . name
66- ) . filter ( version => {
67- return version . specifications . length > 0
68- } )
69- . map ( version => this . setDefaultSpecification ( version , config ?. defaultSpecificationName ) )
70-
71- const defaultName = repository . name . replace ( new RegExp ( this . repositoryNameSuffix + "$" ) , "" )
72- return {
73- id : `${ repository . owner } -${ defaultName } ` ,
74- owner : repository . owner ,
75- name : defaultName ,
76- displayName : config ?. name || defaultName ,
77- versions,
78- imageURL : imageURL ,
79- ownerUrl : `https://dev.azure.com/${ repository . owner } ` ,
80- url : repository . webUrl
81- }
82- }
83-
84- private getConfig ( repository : AzureDevOpsRepositoryWithRefs ) : IProjectConfig | null {
85- const yml = repository . configYml || repository . configYaml
86- if ( ! yml || ! yml . text || yml . text . length == 0 ) {
87- return null
88- }
89- const parser = new ProjectConfigParser ( )
90- return parser . parse ( yml . text )
91- }
92-
93- private getVersions ( repository : AzureDevOpsRepositoryWithRefs ) : Version [ ] {
94- const branchVersions = repository . branches . map ( branch => {
95- const isDefaultRef = branch . name == repository . defaultBranchRef . name
96- return this . mapVersionFromRef ( {
97- organization : repository . owner ,
98- repositoryName : repository . name ,
99- webUrl : repository . webUrl ,
100- ref : branch ,
101- isDefaultRef
102- } )
103- } )
104- const tagVersions = repository . tags . map ( tag => {
105- return this . mapVersionFromRef ( {
106- organization : repository . owner ,
107- repositoryName : repository . name ,
108- webUrl : repository . webUrl ,
109- ref : tag
110- } )
111- } )
112- return branchVersions . concat ( tagVersions )
113- }
114-
115- private mapVersionFromRef ( {
116- organization,
117- repositoryName,
118- webUrl,
119- ref,
120- isDefaultRef
121- } : {
122- organization : string
123- repositoryName : string
124- webUrl : string
125- ref : AzureDevOpsRepositoryRef
126- isDefaultRef ?: boolean
127- } ) : Version {
128- const specifications = ref . files . filter ( file => {
129- return this . isOpenAPISpecification ( file . name )
130- } ) . map ( file => {
131- return {
132- id : file . name ,
133- name : file . name ,
134- url : this . getAzureDevOpsBlobURL ( {
135- organization,
136- repositoryName,
137- path : file . name ,
138- ref : ref . name
139- } ) ,
140- // Azure DevOps edit URL format
141- editURL : `${ webUrl } ?path=/${ file . name } &version=GB${ ref . name } &_a=contents` ,
142- isDefault : false // initial value
143- }
144- } ) . sort ( ( a , b ) => a . name . localeCompare ( b . name ) )
145-
146- return {
147- id : ref . name ,
148- name : ref . name ,
149- specifications : specifications ,
150- url : `${ webUrl } ?version=GB${ ref . name } ` ,
151- isDefault : isDefaultRef || false
152- }
153- }
154-
155- private isOpenAPISpecification ( filename : string ) {
156- return ! filename . startsWith ( "." ) && (
157- filename . endsWith ( ".yml" ) || filename . endsWith ( ".yaml" )
158- )
159- }
160-
161- private getAzureDevOpsBlobURL ( {
162- organization,
163- repositoryName,
164- path,
165- ref
166- } : {
167- organization : string
168- repositoryName : string
169- path : string
170- ref : string
171- } ) : string {
172- // Use internal API route for fetching blob content
173- return `/api/blob/${ organization } /${ repositoryName } /${ path } ?ref=${ ref } `
174- }
175-
176- private addRemoteVersions (
177- existingVersions : Version [ ] ,
178- remoteVersions : ProjectConfigRemoteVersion [ ]
179- ) : Version [ ] {
180- const versions = [ ...existingVersions ]
181- const versionIds = versions . map ( e => e . id )
182- for ( const remoteVersion of remoteVersions ) {
183- const baseVersionId = this . makeURLSafeID (
184- ( remoteVersion . id || remoteVersion . name ) . toLowerCase ( )
185- )
186- // If the version ID exists then we suffix it with a number to ensure unique versions.
187- const existingVersionIdCount = versionIds . filter ( e => e == baseVersionId ) . length
188- const versionId = baseVersionId + ( existingVersionIdCount > 0 ? existingVersionIdCount : "" )
189- const specifications = remoteVersion . specifications . map ( e => {
190- const remoteConfig : RemoteConfig = {
191- url : e . url ,
192- auth : this . tryDecryptAuth ( e )
193- }
194-
195- const encodedRemoteConfig = this . remoteConfigEncoder . encode ( remoteConfig )
196-
197- return {
198- id : this . makeURLSafeID ( ( e . id || e . name ) . toLowerCase ( ) ) ,
199- name : e . name ,
200- url : `/api/remotes/${ encodedRemoteConfig } ` ,
201- isDefault : false // initial value
202- }
203- } )
204- versions . push ( {
205- id : versionId ,
206- name : remoteVersion . name ,
207- specifications,
208- isDefault : false
209- } )
210- versionIds . push ( baseVersionId )
211- }
212- return versions
213- }
214-
215- private sortVersions ( versions : Version [ ] , defaultBranchName : string ) : Version [ ] {
216- const candidateDefaultBranches = [
217- defaultBranchName , "main" , "master" , "develop" , "development" , "trunk"
218- ]
219- // Reverse them so the top-priority branches end up at the top of the list.
220- . reverse ( )
221- const copiedVersions = [ ...versions ] . sort ( ( a , b ) => {
222- return a . name . localeCompare ( b . name )
223- } )
224- // Move the top-priority branches to the top of the list.
225- for ( const candidateDefaultBranch of candidateDefaultBranches ) {
226- const defaultBranchIndex = copiedVersions . findIndex ( version => {
227- return version . name === candidateDefaultBranch
228- } )
229- if ( defaultBranchIndex !== - 1 ) {
230- const branchVersion = copiedVersions [ defaultBranchIndex ]
231- copiedVersions . splice ( defaultBranchIndex , 1 )
232- copiedVersions . splice ( 0 , 0 , branchVersion )
233- }
234- }
235- return copiedVersions
236- }
237-
238- private makeURLSafeID ( str : string ) : string {
239- return str
240- . replace ( / / g, "-" )
241- . replace ( / [ ^ A - Z a - z 0 - 9 - ] / g, "" )
242- }
243-
244- private tryDecryptAuth ( projectConfigRemoteSpec : ProjectConfigRemoteSpecification ) : { type : string , username : string , password : string } | undefined {
245- if ( ! projectConfigRemoteSpec . auth ) {
246- return undefined
247- }
248-
249- try {
250- return {
251- type : projectConfigRemoteSpec . auth . type ,
252- username : this . encryptionService . decrypt ( projectConfigRemoteSpec . auth . encryptedUsername ) ,
253- password : this . encryptionService . decrypt ( projectConfigRemoteSpec . auth . encryptedPassword )
254- }
255- } catch ( error ) {
256- console . info ( `Failed to decrypt remote specification auth for ${ projectConfigRemoteSpec . name } (${ projectConfigRemoteSpec . url } ). Perhaps a different public key was used?:` , error )
257- return undefined
258- }
259- }
260-
261- private setDefaultSpecification ( version : Version , defaultSpecificationName ?: string ) : Version {
262- return {
263- ...version ,
264- specifications : version . specifications . map ( spec => ( {
265- ...spec ,
266- isDefault : spec . name === defaultSpecificationName
267- } ) )
268- }
55+ return this . projectMapper . mapRepositories ( repositories )
26956 }
27057}
0 commit comments