Skip to content

Commit 3319461

Browse files
committed
Extract ProjectMapper class from project data sources
Move repository-to-project mapping logic into a dedicated ProjectMapper class. The mapper handles filtering, sorting, and all mapping transformations with generic type support for provider-specific repository types.
1 parent 5ddd1c2 commit 3319461

File tree

3 files changed

+340
-517
lines changed

3 files changed

+340
-517
lines changed
Lines changed: 31 additions & 244 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,39 @@
11
import { IEncryptionService } from "@/features/encrypt/EncryptionService"
22
import {
33
Project,
4-
Version,
5-
IProjectConfig,
6-
IProjectDataSource,
7-
ProjectConfigParser,
8-
ProjectConfigRemoteVersion,
9-
ProjectConfigRemoteSpecification
4+
IProjectDataSource
105
} from "../domain"
116
import IAzureDevOpsRepositoryDataSource, {
127
AzureDevOpsRepositoryWithRefs,
138
AzureDevOpsRepositoryRef
149
} from "../domain/IAzureDevOpsRepositoryDataSource"
15-
import RemoteConfig from "../domain/RemoteConfig"
10+
import ProjectMapper, { type URLBuilders } from "../domain/ProjectMapper"
1611
import { IRemoteConfigEncoder } from "../domain/RemoteConfigEncoder"
1712

13+
const azureDevOpsURLBuilders: URLBuilders<AzureDevOpsRepositoryWithRefs> = {
14+
getImageRef(repository: AzureDevOpsRepositoryWithRefs): string {
15+
return repository.defaultBranchRef.name
16+
},
17+
getBlobRef(ref: AzureDevOpsRepositoryRef): string {
18+
return ref.name
19+
},
20+
getOwnerUrl(owner: string): string {
21+
return `https://dev.azure.com/${owner}`
22+
},
23+
getProjectUrl(repository: AzureDevOpsRepositoryWithRefs): string {
24+
return repository.webUrl
25+
},
26+
getVersionUrl(repository: AzureDevOpsRepositoryWithRefs, ref: AzureDevOpsRepositoryRef): string {
27+
return `${repository.webUrl}?version=GB${ref.name}`
28+
},
29+
getSpecEditUrl(repository: AzureDevOpsRepositoryWithRefs, ref: AzureDevOpsRepositoryRef, fileName: string): string {
30+
return `${repository.webUrl}?path=/${fileName}&version=GB${ref.name}&_a=contents`
31+
}
32+
}
33+
1834
export 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-Za-z0-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

Comments
 (0)