Skip to content

Commit

Permalink
Merge pull request #495 from kubeshop/f1ames/feat/get-project-info
Browse files Browse the repository at this point in the history
feat(synchronizer): introduce 'getProjectInfo()' method
  • Loading branch information
f1ames authored Aug 25, 2023
2 parents 4b2f245 + d792c1e commit 8040f9a
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .changeset/tall-paws-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@monokle/synchronizer": minor
---

Introduced synchronizer `getProjectInfo()` method
122 changes: 122 additions & 0 deletions packages/synchronizer/src/__tests__/synchronizer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '[email protected]',
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: '[email protected]',
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 = '') {
Expand Down
96 changes: 74 additions & 22 deletions packages/synchronizer/src/utils/synchronizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<PolicyData> | undefined;
private _projectDataCache: Record<string, ApiUserProject> = {};

constructor(
private _storageHandler: StorageHandlerPolicy,
Expand All @@ -29,6 +37,35 @@ export class Synchronizer extends EventEmitter {
super();
}

async getProjectInfo(rootPath: string, accessToken: string, forceRefetch?: boolean): Promise<ProjectInfo | null>;
async getProjectInfo(repoData: RepoRemoteData, accessToken: string, forceRefetch?: boolean): Promise<ProjectInfo | null>;
async getProjectInfo(
repoDataOrRootPath: RepoRemoteData | string,
accessToken: string,
forceRefetch = false,
): Promise<ProjectInfo | null> {
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<PolicyData>;
async getPolicy(repoData: RepoRemoteData, forceRefetch?: boolean, accessToken?: string): Promise<PolicyData>;
async getPolicy(
Expand Down Expand Up @@ -95,38 +132,19 @@ 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(
`The '${repoId}' repository does not belong to any project in Monokle Cloud. Configure it on ${projectUrl}.`
);
}

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}.`
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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}`;
}
}

0 comments on commit 8040f9a

Please sign in to comment.