From d792c1ef9ae45529a7933b2d5fda599e3837941c Mon Sep 17 00:00:00 2001 From: f1ames Date: Fri, 25 Aug 2023 10:36:32 +0200 Subject: [PATCH] feat(synchronizer): introduce 'getProjectInfo()' method --- .changeset/tall-paws-shout.md | 5 + .../src/__tests__/synchronizer.spec.ts | 122 ++++++++++++++++++ .../synchronizer/src/utils/synchronizer.ts | 96 ++++++++++---- 3 files changed, 201 insertions(+), 22 deletions(-) create mode 100644 .changeset/tall-paws-shout.md diff --git a/.changeset/tall-paws-shout.md b/.changeset/tall-paws-shout.md new file mode 100644 index 000000000..3983f8e0a --- /dev/null +++ b/.changeset/tall-paws-shout.md @@ -0,0 +1,5 @@ +--- +"@monokle/synchronizer": minor +--- + +Introduced synchronizer `getProjectInfo()` method diff --git a/packages/synchronizer/src/__tests__/synchronizer.spec.ts b/packages/synchronizer/src/__tests__/synchronizer.spec.ts index 0bd36c8d6..3366196bc 100644 --- a/packages/synchronizer/src/__tests__/synchronizer.spec.ts +++ b/packages/synchronizer/src/__tests__/synchronizer.spec.ts @@ -300,6 +300,128 @@ describe('Synchronizer Tests', () => { return result; }); }); + + describe('getProjectInfo', () => { + it('returns valid project info', async () => { + const storagePath = await createTmpConfigDir(); + const synchronizer = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath)); + + const queryApiStub = sinon.stub((synchronizer as any)._apiHandler, 'queryApi').callsFake(async (...args) => { + const query = args[0] as string; + + if (query.includes('query getUser')) { + return { + data: { + me: { + id: 6, + email: 'user6@kubeshop.io', + projects: [ + { + project: { + id: 6000, + slug: 'user6-proj', + name: 'User6 Project', + repositories: [ + { + id: 'user6-proj-policy-id', + projectId: 6000, + provider: 'GITHUB', + owner: 'kubeshop', + name: 'monokle-core', + prChecks: false, + canEnablePrChecks: true, + }, + ], + }, + }, + ], + }, + }, + }; + } + + return {}; + }); + stubs.push(queryApiStub); + + const repoData = { + provider: 'github.com', + remote: 'origin', + owner: 'kubeshop', + name: 'monokle-core', + }; + + const projectInfo = await synchronizer.getProjectInfo(repoData, 'SAMPLE_ACCESS_TOKEN'); + + assert.isObject(projectInfo); + assert.equal(projectInfo!.id, 6000); + assert.equal(projectInfo!.slug, 'user6-proj'); + assert.equal(projectInfo!.name, 'User6 Project'); + assert.equal(queryApiStub.callCount, 1); + }); + + it('utilizes cache correctly', async () => { + const storagePath = await createTmpConfigDir(); + const synchronizer = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath)); + + const queryApiStub = sinon.stub((synchronizer as any)._apiHandler, 'queryApi').callsFake(async (...args) => { + const query = args[0] as string; + + if (query.includes('query getUser')) { + return { + data: { + me: { + id: 6, + email: 'user6@kubeshop.io', + projects: [ + { + project: { + id: 6000, + slug: 'user6-proj', + name: 'User6 Project', + repositories: [ + { + id: 'user6-proj-policy-id', + projectId: 6000, + provider: 'GITHUB', + owner: 'kubeshop', + name: 'monokle-core', + prChecks: false, + canEnablePrChecks: true, + }, + ], + }, + }, + ], + }, + }, + }; + } + + return {}; + }); + stubs.push(queryApiStub); + + const repoData = { + provider: 'github.com', + remote: 'origin', + owner: 'kubeshop', + name: 'monokle-core', + }; + + const projectInfo = await synchronizer.getProjectInfo(repoData, 'SAMPLE_ACCESS_TOKEN'); + assert.equal(projectInfo!.id, 6000); + assert.equal(queryApiStub.callCount, 1); + + const projectInfoRetry = await synchronizer.getProjectInfo(repoData, 'SAMPLE_ACCESS_TOKEN'); + assert.equal(projectInfoRetry!.id, 6000); + assert.equal(queryApiStub.callCount, 1); + + const projectInfoRetryForce = await synchronizer.getProjectInfo(repoData, 'SAMPLE_ACCESS_TOKEN', true); + assert.equal(projectInfoRetryForce!.id, 6000); + assert.equal(queryApiStub.callCount, 2); + }); + }) }); async function createTmpConfigDir(copyPolicyFixture = '') { diff --git a/packages/synchronizer/src/utils/synchronizer.ts b/packages/synchronizer/src/utils/synchronizer.ts index 8e99d758a..2eac5cc29 100644 --- a/packages/synchronizer/src/utils/synchronizer.ts +++ b/packages/synchronizer/src/utils/synchronizer.ts @@ -5,6 +5,7 @@ import {ApiHandler} from '../handlers/apiHandler.js'; import {GitHandler} from '../handlers/gitHandler.js'; import type {StoragePolicyFormat} from '../handlers/storageHandlerPolicy.js'; import type {RepoRemoteData} from '../handlers/gitHandler.js'; +import type {ApiUserProject} from '../handlers/apiHandler.js'; export type RepoRemoteInputData = { provider: string; @@ -18,8 +19,15 @@ export type PolicyData = { policy: StoragePolicyFormat; }; +export type ProjectInfo = { + id: number; + name: string; + slug: string; +}; + export class Synchronizer extends EventEmitter { private _pullPromise: Promise | undefined; + private _projectDataCache: Record = {}; constructor( private _storageHandler: StorageHandlerPolicy, @@ -29,6 +37,35 @@ export class Synchronizer extends EventEmitter { super(); } + async getProjectInfo(rootPath: string, accessToken: string, forceRefetch?: boolean): Promise; + async getProjectInfo(repoData: RepoRemoteData, accessToken: string, forceRefetch?: boolean): Promise; + async getProjectInfo( + repoDataOrRootPath: RepoRemoteData | string, + accessToken: string, + forceRefetch = false, + ): Promise { + const repoData = + typeof repoDataOrRootPath === 'string' ? await this.getRootGitData(repoDataOrRootPath) : repoDataOrRootPath; + + const cacheId = this.getRepoCacheId(repoData, accessToken); + const cachedProjectInfo = this._projectDataCache[cacheId]; + if (cachedProjectInfo && !forceRefetch) { + return { + id: cachedProjectInfo.id, + name: cachedProjectInfo.name, + slug: cachedProjectInfo.slug, + }; + } + + const freshProjectInfo = await this.getMatchingProject(repoData, accessToken); + + return !freshProjectInfo ? null : { + id: freshProjectInfo.id, + name: freshProjectInfo.name, + slug: freshProjectInfo.slug, + }; + } + async getPolicy(rootPath: string, forceRefetch?: boolean, accessToken?: string): Promise; async getPolicy(repoData: RepoRemoteData, forceRefetch?: boolean, accessToken?: string): Promise; async getPolicy( @@ -95,28 +132,9 @@ export class Synchronizer extends EventEmitter { }; try { - const userData = await this._apiHandler.getUser(accessToken); - if (!userData?.data?.me) { - throw new Error('Cannot fetch user data, make sure you are authenticated and have internet access.'); - } - - if (!repoData?.provider || !repoData?.owner || !repoData?.name) { - throw new Error(`Provided invalid git repository data: '${JSON.stringify(repoData)}'.`); - } - + const repoProject = await this.getMatchingProject(repoData, accessToken); const repoId = `${repoData.provider}:${repoData.owner}/${repoData.name}`; - const repoMainProject = userData.data.me.projects.find(project => { - return project.project.repositories.find( - repo => repo.owner === repoData.owner && repo.name === repoData.name && repo.prChecks - ); - }); - - const repoFirstProject = userData.data.me.projects.find(project => { - return project.project.repositories.find(repo => repo.owner === repoData.owner && repo.name === repoData.name); - }); - - const repoProject = repoMainProject ?? repoFirstProject; if (!repoProject) { const projectUrl = this.generateDeepLinkProjectList(); throw new Error( @@ -124,9 +142,9 @@ export class Synchronizer extends EventEmitter { ); } - const repoPolicy = await this._apiHandler.getPolicy(repoProject.project.slug, accessToken); + const repoPolicy = await this._apiHandler.getPolicy(repoProject.slug, accessToken); - const policyUrl = this.generateDeepLinkProjectPolicy(repoProject.project.slug); + const policyUrl = this.generateDeepLinkProjectPolicy(repoProject.slug); if (!repoPolicy?.data?.getProject?.policy) { throw new Error( `The '${repoId}' repository project does not have policy defined. Configure it on ${policyUrl}.` @@ -158,6 +176,36 @@ export class Synchronizer extends EventEmitter { } } + private async getMatchingProject(repoData: RepoRemoteData, accessToken: string) { + const userData = await this._apiHandler.getUser(accessToken); + if (!userData?.data?.me) { + throw new Error('Cannot fetch user data, make sure you are authenticated and have internet access.'); + } + + if (!repoData?.provider || !repoData?.owner || !repoData?.name) { + throw new Error(`Provided invalid git repository data: '${JSON.stringify(repoData)}'.`); + } + + const repoMainProject = userData.data.me.projects.find(project => { + return project.project.repositories.find( + repo => repo.owner === repoData.owner && repo.name === repoData.name && repo.prChecks + ); + }); + + const repoFirstProject = userData.data.me.projects.find(project => { + return project.project.repositories.find(repo => repo.owner === repoData.owner && repo.name === repoData.name); + }); + + const matchingProject = repoMainProject ?? repoFirstProject; + + if (matchingProject?.project) { + const cacheId = this.getRepoCacheId(repoData, accessToken); + this._projectDataCache[cacheId] = matchingProject.project; + } + + return matchingProject?.project ?? null; + } + private async getRootGitData(rootPath: string) { const repoData = await this._gitHandler.getRepoRemoteData(rootPath); if (!repoData) { @@ -190,4 +238,8 @@ export class Synchronizer extends EventEmitter { return `${provider}-${repoData.owner}-${repoData.name}.policy.yaml`; } + + private getRepoCacheId(repoData: RepoRemoteData, prefix: string) { + return `${prefix}-${repoData.provider}-${repoData.owner}-${repoData.name}`; + } }