feat: teamEnvironment module added
This commit is contained in:
@@ -9,6 +9,7 @@ import { UserEnvironmentsModule } from './user-environment/user-environments.mod
|
||||
import { UserHistoryModule } from './user-history/user-history.module';
|
||||
import { subscriptionContextCookieParser } from './auth/helper';
|
||||
import { TeamModule } from './team/team.module';
|
||||
import { TeamEnvironmentsModule } from './team-environments/team-environments.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -47,6 +48,7 @@ import { TeamModule } from './team/team.module';
|
||||
UserEnvironmentsModule,
|
||||
UserHistoryModule,
|
||||
TeamModule,
|
||||
TeamEnvironmentsModule,
|
||||
],
|
||||
providers: [GQLComplexityPlugin],
|
||||
})
|
||||
|
||||
@@ -163,7 +163,7 @@ export const SHORTCODE_INVALID_JSON = 'shortcode/invalid_json' as const;
|
||||
* Invalid or non-existent TEAM ENVIRONMMENT ID
|
||||
* (TeamEnvironmentsService)
|
||||
*/
|
||||
export const TEAM_ENVIRONMMENT_NOT_FOUND =
|
||||
export const TEAM_ENVIRONMENT_NOT_FOUND =
|
||||
'team_environment/not_found' as const;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@ import { UserSettings } from 'src/user-settings/user-settings.model';
|
||||
import { UserEnvironment } from '../user-environment/user-environments.model';
|
||||
import { UserHistory } from '../user-history/user-history.model';
|
||||
import { TeamMember } from 'src/team/team.model';
|
||||
import { TeamEnvironment } from 'src/team-environments/team-environments.model';
|
||||
|
||||
// A custom message type that defines the topic and the corresponding payload.
|
||||
// For every module that publishes a subscription add its type def and the possible subscription type.
|
||||
@@ -19,5 +20,8 @@ export type TopicDef = {
|
||||
[topic: `team/${string}/member_removed`]: string;
|
||||
[topic: `team/${string}/member_added`]: TeamMember;
|
||||
[topic: `team/${string}/member_updated`]: TeamMember;
|
||||
[topic: `team_environment/${string}/created`]: TeamEnvironment;
|
||||
[topic: `team_environment/${string}/updated`]: TeamEnvironment;
|
||||
[topic: `team_environment/${string}/deleted`]: TeamEnvironment;
|
||||
[topic: `user_history/${string}/deleted_many`]: number;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import * as S from 'fp-ts/string';
|
||||
import { pipe } from 'fp-ts/function';
|
||||
import {
|
||||
getAnnotatedRequiredRoles,
|
||||
getGqlArg,
|
||||
getUserFromGQLContext,
|
||||
namedTrace,
|
||||
throwErr,
|
||||
} from 'src/utils';
|
||||
import { TeamEnvironmentsService } from './team-environments.service';
|
||||
import {
|
||||
BUG_AUTH_NO_USER_CTX,
|
||||
BUG_TEAM_ENV_GUARD_NO_ENV_ID,
|
||||
BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES,
|
||||
TEAM_ENVIRONMENT_NOT_TEAM_MEMBER,
|
||||
TEAM_ENVIRONMENT_NOT_FOUND,
|
||||
} from 'src/errors';
|
||||
import { TeamService } from 'src/team/team.service';
|
||||
|
||||
/**
|
||||
* A guard which checks whether the caller of a GQL Operation
|
||||
* is in the team which owns the environment.
|
||||
* This guard also requires the RequireRole decorator for access control
|
||||
*/
|
||||
@Injectable()
|
||||
export class GqlTeamEnvTeamGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
private readonly teamEnvironmentService: TeamEnvironmentsService,
|
||||
private readonly teamService: TeamService,
|
||||
) {}
|
||||
|
||||
canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
return pipe(
|
||||
TE.Do,
|
||||
|
||||
TE.bindW('requiredRoles', () =>
|
||||
pipe(
|
||||
getAnnotatedRequiredRoles(this.reflector, context),
|
||||
TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES),
|
||||
),
|
||||
),
|
||||
|
||||
TE.bindW('user', () =>
|
||||
pipe(
|
||||
getUserFromGQLContext(context),
|
||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
||||
),
|
||||
),
|
||||
|
||||
TE.bindW('envID', () =>
|
||||
pipe(
|
||||
getGqlArg('id', context),
|
||||
O.fromPredicate(S.isString),
|
||||
TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_ENV_ID),
|
||||
),
|
||||
),
|
||||
|
||||
TE.bindW('membership', ({ envID, user }) =>
|
||||
pipe(
|
||||
this.teamEnvironmentService.getTeamEnvironment(envID),
|
||||
TE.fromTaskOption(() => TEAM_ENVIRONMENT_NOT_FOUND),
|
||||
TE.chainW((env) =>
|
||||
pipe(
|
||||
this.teamService.getTeamMemberTE(env.teamID, user.uid),
|
||||
TE.mapLeft(() => TEAM_ENVIRONMENT_NOT_TEAM_MEMBER),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
TE.map(({ membership, requiredRoles }) =>
|
||||
requiredRoles.includes(membership.role),
|
||||
),
|
||||
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class TeamEnvironment {
|
||||
@Field(() => ID, {
|
||||
description: 'ID of the Team Environment',
|
||||
})
|
||||
id: string;
|
||||
|
||||
@Field(() => ID, {
|
||||
description: 'ID of the team this environment belongs to',
|
||||
})
|
||||
teamID: string;
|
||||
|
||||
@Field({
|
||||
description: 'Name of the environment',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@Field({
|
||||
description: 'All variables present in the environment',
|
||||
})
|
||||
variables: string; // JSON string of the variables object (format:[{ key: "bla", value: "bla_val" }, ...] ) which will be received from the client
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TeamEnvironmentsService } from './team-environments.service';
|
||||
import { TeamEnvironmentsResolver } from './team-environments.resolver';
|
||||
import { UserModule } from 'src/user/user.module';
|
||||
import { PubSubModule } from 'src/pubsub/pubsub.module';
|
||||
import { TeamModule } from 'src/team/team.module';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { GqlTeamEnvTeamGuard } from './gql-team-env-team.guard';
|
||||
import { TeamEnvsTeamResolver } from './team.resolver';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, PubSubModule, UserModule, TeamModule],
|
||||
providers: [
|
||||
TeamEnvironmentsResolver,
|
||||
TeamEnvironmentsService,
|
||||
GqlTeamEnvTeamGuard,
|
||||
TeamEnvsTeamResolver,
|
||||
],
|
||||
exports: [TeamEnvironmentsService, GqlTeamEnvTeamGuard],
|
||||
})
|
||||
export class TeamEnvironmentsModule {}
|
||||
@@ -0,0 +1,205 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Resolver, Mutation, Args, Subscription, ID } from '@nestjs/graphql';
|
||||
import { pipe } from 'fp-ts/function';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { RequiresTeamRole } from 'src/team/decorators/requires-team-role.decorator';
|
||||
import { GqlTeamMemberGuard } from 'src/team/guards/gql-team-member.guard';
|
||||
import { TeamMemberRole } from 'src/team/team.model';
|
||||
import { throwErr } from 'src/utils';
|
||||
import { GqlTeamEnvTeamGuard } from './gql-team-env-team.guard';
|
||||
import { TeamEnvironment } from './team-environments.model';
|
||||
import { TeamEnvironmentsService } from './team-environments.service';
|
||||
|
||||
@Resolver(() => 'TeamEnvironment')
|
||||
export class TeamEnvironmentsResolver {
|
||||
constructor(
|
||||
private readonly teamEnvironmentsService: TeamEnvironmentsService,
|
||||
private readonly pubsub: PubSubService,
|
||||
) {}
|
||||
|
||||
/* Mutations */
|
||||
|
||||
@Mutation(() => TeamEnvironment, {
|
||||
description: 'Create a new Team Environment for given Team ID',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||
createTeamEnvironment(
|
||||
@Args({
|
||||
name: 'name',
|
||||
description: 'Name of the Team Environment',
|
||||
})
|
||||
name: string,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'ID of the Team',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
@Args({
|
||||
name: 'variables',
|
||||
description: 'JSON string of the variables object',
|
||||
})
|
||||
variables: string,
|
||||
): Promise<TeamEnvironment> {
|
||||
return this.teamEnvironmentsService.createTeamEnvironment(
|
||||
name,
|
||||
teamID,
|
||||
variables,
|
||||
)();
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Delete a Team Environment for given Team ID',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||
deleteTeamEnvironment(
|
||||
@Args({
|
||||
name: 'id',
|
||||
description: 'ID of the Team Environment',
|
||||
type: () => ID,
|
||||
})
|
||||
id: string,
|
||||
): Promise<boolean> {
|
||||
return pipe(
|
||||
this.teamEnvironmentsService.deleteTeamEnvironment(id),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
}
|
||||
|
||||
@Mutation(() => TeamEnvironment, {
|
||||
description:
|
||||
'Add/Edit a single environment variable or variables to a Team Environment',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||
updateTeamEnvironment(
|
||||
@Args({
|
||||
name: 'id',
|
||||
description: 'ID of the Team Environment',
|
||||
type: () => ID,
|
||||
})
|
||||
id: string,
|
||||
@Args({
|
||||
name: 'name',
|
||||
description: 'Name of the Team Environment',
|
||||
})
|
||||
name: string,
|
||||
@Args({
|
||||
name: 'variables',
|
||||
description: 'JSON string of the variables object',
|
||||
})
|
||||
variables: string,
|
||||
): Promise<TeamEnvironment> {
|
||||
return pipe(
|
||||
this.teamEnvironmentsService.updateTeamEnvironment(id, name, variables),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
}
|
||||
|
||||
@Mutation(() => TeamEnvironment, {
|
||||
description: 'Delete all variables from a Team Environment',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||
deleteAllVariablesFromTeamEnvironment(
|
||||
@Args({
|
||||
name: 'id',
|
||||
description: 'ID of the Team Environment',
|
||||
type: () => ID,
|
||||
})
|
||||
id: string,
|
||||
): Promise<TeamEnvironment> {
|
||||
return pipe(
|
||||
this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(id),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
}
|
||||
|
||||
@Mutation(() => TeamEnvironment, {
|
||||
description: 'Create a duplicate of an existing environment',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||
createDuplicateEnvironment(
|
||||
@Args({
|
||||
name: 'id',
|
||||
description: 'ID of the Team Environment',
|
||||
type: () => ID,
|
||||
})
|
||||
id: string,
|
||||
): Promise<TeamEnvironment> {
|
||||
return pipe(
|
||||
this.teamEnvironmentsService.createDuplicateEnvironment(id),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
}
|
||||
|
||||
/* Subscriptions */
|
||||
|
||||
@Subscription(() => TeamEnvironment, {
|
||||
description: 'Listen for Team Environment Updates',
|
||||
resolve: (value) => value,
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
@RequiresTeamRole(
|
||||
TeamMemberRole.OWNER,
|
||||
TeamMemberRole.EDITOR,
|
||||
TeamMemberRole.VIEWER,
|
||||
)
|
||||
teamEnvironmentUpdated(
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'ID of the Team',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
) {
|
||||
return this.pubsub.asyncIterator(`team_environment/${teamID}/updated`);
|
||||
}
|
||||
|
||||
@Subscription(() => TeamEnvironment, {
|
||||
description: 'Listen for Team Environment Creation Messages',
|
||||
resolve: (value) => value,
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
@RequiresTeamRole(
|
||||
TeamMemberRole.OWNER,
|
||||
TeamMemberRole.EDITOR,
|
||||
TeamMemberRole.VIEWER,
|
||||
)
|
||||
teamEnvironmentCreated(
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'ID of the Team',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
) {
|
||||
return this.pubsub.asyncIterator(`team_environment/${teamID}/created`);
|
||||
}
|
||||
|
||||
@Subscription(() => TeamEnvironment, {
|
||||
description: 'Listen for Team Environment Deletion Messages',
|
||||
resolve: (value) => value,
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
@RequiresTeamRole(
|
||||
TeamMemberRole.OWNER,
|
||||
TeamMemberRole.EDITOR,
|
||||
TeamMemberRole.VIEWER,
|
||||
)
|
||||
teamEnvironmentDeleted(
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'ID of the Team',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
) {
|
||||
return this.pubsub.asyncIterator(`team_environment/${teamID}/deleted`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { TeamEnvironment } from './team-environments.model';
|
||||
import { TeamEnvironmentsService } from './team-environments.service';
|
||||
import { TEAM_ENVIRONMENT_NOT_FOUND, TEAM_MEMBER_NOT_FOUND } from 'src/errors';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
|
||||
const mockPubSub = {
|
||||
publish: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const teamEnvironmentsService = new TeamEnvironmentsService(
|
||||
mockPrisma,
|
||||
mockPubSub as any,
|
||||
);
|
||||
|
||||
const teamEnvironment = {
|
||||
id: '123',
|
||||
name: 'test',
|
||||
teamID: 'abc123',
|
||||
variables: [{}],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockReset(mockPrisma);
|
||||
mockPubSub.publish.mockClear();
|
||||
});
|
||||
|
||||
describe('TeamEnvironmentsService', () => {
|
||||
describe('getTeamEnvironment', () => {
|
||||
test('queries the db with the id', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
||||
|
||||
await teamEnvironmentsService.getTeamEnvironment('123')();
|
||||
|
||||
expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: {
|
||||
id: '123',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('requests prisma to reject the query promise if not found', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
||||
|
||||
await teamEnvironmentsService.getTeamEnvironment('123')();
|
||||
|
||||
expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rejectOnNotFound: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should return a Some of the correct environment if exists', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
||||
|
||||
const result = await teamEnvironmentsService.getTeamEnvironment('123')();
|
||||
|
||||
expect(result).toEqualSome(teamEnvironment);
|
||||
});
|
||||
|
||||
test('should return a None if the environment does not exist', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
|
||||
|
||||
const result = await teamEnvironmentsService.getTeamEnvironment('123')();
|
||||
|
||||
expect(result).toBeNone();
|
||||
});
|
||||
});
|
||||
describe('createTeamEnvironment', () => {
|
||||
test('should create and return a new team environment given a valid name,variable and team ID', async () => {
|
||||
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
|
||||
|
||||
const result = await teamEnvironmentsService.createTeamEnvironment(
|
||||
teamEnvironment.name,
|
||||
teamEnvironment.teamID,
|
||||
JSON.stringify(teamEnvironment.variables),
|
||||
)();
|
||||
|
||||
expect(result).toEqual(<TeamEnvironment>{
|
||||
id: teamEnvironment.id,
|
||||
name: teamEnvironment.name,
|
||||
teamID: teamEnvironment.teamID,
|
||||
variables: JSON.stringify(teamEnvironment.variables),
|
||||
});
|
||||
});
|
||||
|
||||
test('should reject if given team ID is invalid', async () => {
|
||||
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
|
||||
|
||||
await expect(
|
||||
teamEnvironmentsService.createTeamEnvironment(
|
||||
teamEnvironment.name,
|
||||
'invalidteamid',
|
||||
JSON.stringify(teamEnvironment.variables),
|
||||
),
|
||||
).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
test('should reject if provided team environment name is not a string', async () => {
|
||||
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
|
||||
|
||||
await expect(
|
||||
teamEnvironmentsService.createTeamEnvironment(
|
||||
null as any,
|
||||
teamEnvironment.teamID,
|
||||
JSON.stringify(teamEnvironment.variables),
|
||||
),
|
||||
).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
test('should reject if provided variable is not a string', async () => {
|
||||
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
|
||||
|
||||
await expect(
|
||||
teamEnvironmentsService.createTeamEnvironment(
|
||||
teamEnvironment.name,
|
||||
teamEnvironment.teamID,
|
||||
null as any,
|
||||
),
|
||||
).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is created successfully', async () => {
|
||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce(teamEnvironment);
|
||||
|
||||
const result = await teamEnvironmentsService.createTeamEnvironment(
|
||||
teamEnvironment.name,
|
||||
teamEnvironment.teamID,
|
||||
JSON.stringify(teamEnvironment.variables),
|
||||
)();
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_environment/${teamEnvironment.teamID}/created`,
|
||||
result,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteTeamEnvironment', () => {
|
||||
test('should resolve to true given a valid team environment ID', async () => {
|
||||
mockPrisma.teamEnvironment.delete.mockResolvedValueOnce(teamEnvironment);
|
||||
|
||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
|
||||
expect(result).toEqualRight(true);
|
||||
});
|
||||
|
||||
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if given id is invalid', async () => {
|
||||
mockPrisma.teamEnvironment.delete.mockRejectedValue('RecordNotFound');
|
||||
|
||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||
'invalidid',
|
||||
)();
|
||||
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
});
|
||||
|
||||
test('should send pubsub message to "team_environment/<teamID>/deleted" if team environment is deleted successfully', async () => {
|
||||
mockPrisma.teamEnvironment.delete.mockResolvedValueOnce(teamEnvironment);
|
||||
|
||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_environment/${teamEnvironment.teamID}/deleted`,
|
||||
{
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify(teamEnvironment.variables),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateVariablesInTeamEnvironment', () => {
|
||||
test('should add new variable to a team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||
...teamEnvironment,
|
||||
variables: [{ key: 'value' }],
|
||||
});
|
||||
|
||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{ key: 'value' }]),
|
||||
)();
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify([{ key: 'value' }]),
|
||||
});
|
||||
});
|
||||
|
||||
test('should add new variable to already existing list of variables in a team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||
...teamEnvironment,
|
||||
variables: [{ key: 'value' }, { key_2: 'value_2' }],
|
||||
});
|
||||
|
||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{ key: 'value' }, { key_2: 'value_2' }]),
|
||||
)();
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify([{ key: 'value' }, { key_2: 'value_2' }]),
|
||||
});
|
||||
});
|
||||
|
||||
test('should edit existing variables in a team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||
...teamEnvironment,
|
||||
variables: [{ key: '1234' }],
|
||||
});
|
||||
|
||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{ key: '1234' }]),
|
||||
)();
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify([{ key: '1234' }]),
|
||||
});
|
||||
});
|
||||
|
||||
test('should delete existing variable in a team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
|
||||
|
||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{}]),
|
||||
)();
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify([{}]),
|
||||
});
|
||||
});
|
||||
|
||||
test('should edit name of an existing team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||
...teamEnvironment,
|
||||
variables: [{ key: '123' }],
|
||||
});
|
||||
|
||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{ key: '123' }]),
|
||||
)();
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify([{ key: '123' }]),
|
||||
});
|
||||
});
|
||||
|
||||
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
||||
|
||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||
'invalidid',
|
||||
teamEnvironment.name,
|
||||
JSON.stringify(teamEnvironment.variables),
|
||||
)();
|
||||
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
});
|
||||
|
||||
test('should send pubsub message to "team_environment/<teamID>/updated" if team environment is updated successfully', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
|
||||
|
||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{ key: 'value' }]),
|
||||
)();
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_environment/${teamEnvironment.teamID}/updated`,
|
||||
{
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify(teamEnvironment.variables),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteAllVariablesFromTeamEnvironment', () => {
|
||||
test('should delete all variables in a team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
|
||||
|
||||
const result =
|
||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify([{}]),
|
||||
});
|
||||
});
|
||||
|
||||
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
||||
|
||||
const result =
|
||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||
'invalidid',
|
||||
)();
|
||||
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
});
|
||||
|
||||
test('should send pubsub message to "team_environment/<teamID>/updated" if team environment is updated successfully', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
|
||||
|
||||
const result =
|
||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_environment/${teamEnvironment.teamID}/updated`,
|
||||
{
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify([{}]),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createDuplicateEnvironment', () => {
|
||||
test('should duplicate an existing team environment', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
|
||||
teamEnvironment,
|
||||
);
|
||||
|
||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
||||
...teamEnvironment,
|
||||
id: 'newid',
|
||||
});
|
||||
|
||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
id: 'newid',
|
||||
variables: JSON.stringify(teamEnvironment.variables),
|
||||
});
|
||||
});
|
||||
|
||||
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
|
||||
|
||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
});
|
||||
|
||||
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is updated successfully', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
|
||||
teamEnvironment,
|
||||
);
|
||||
|
||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
||||
...teamEnvironment,
|
||||
id: 'newid',
|
||||
});
|
||||
|
||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_environment/${teamEnvironment.teamID}/created`,
|
||||
{
|
||||
...teamEnvironment,
|
||||
id: 'newid',
|
||||
variables: JSON.stringify([{}]),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,234 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { pipe } from 'fp-ts/function';
|
||||
import * as T from 'fp-ts/Task';
|
||||
import * as TO from 'fp-ts/TaskOption';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import * as A from 'fp-ts/Array';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { TeamEnvironment } from './team-environments.model';
|
||||
import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors';
|
||||
|
||||
@Injectable()
|
||||
export class TeamEnvironmentsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly pubsub: PubSubService,
|
||||
) {}
|
||||
|
||||
getTeamEnvironment(id: string) {
|
||||
return TO.tryCatch(() =>
|
||||
this.prisma.teamEnvironment.findFirst({
|
||||
where: { id },
|
||||
rejectOnNotFound: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
createTeamEnvironment(name: string, teamID: string, variables: string) {
|
||||
return pipe(
|
||||
() =>
|
||||
this.prisma.teamEnvironment.create({
|
||||
data: {
|
||||
name: name,
|
||||
teamID: teamID,
|
||||
variables: JSON.parse(variables),
|
||||
},
|
||||
}),
|
||||
T.chainFirst(
|
||||
(environment) => () =>
|
||||
this.pubsub.publish(
|
||||
`team_environment/${environment.teamID}/created`,
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
),
|
||||
T.map((data) => {
|
||||
return <TeamEnvironment>{
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
teamID: data.teamID,
|
||||
variables: JSON.stringify(data.variables),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
deleteTeamEnvironment(id: string) {
|
||||
return pipe(
|
||||
TE.tryCatch(
|
||||
() =>
|
||||
this.prisma.teamEnvironment.delete({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
}),
|
||||
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
||||
),
|
||||
TE.chainFirst((environment) =>
|
||||
TE.fromTask(() =>
|
||||
this.pubsub.publish(
|
||||
`team_environment/${environment.teamID}/deleted`,
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
TE.map((data) => true),
|
||||
);
|
||||
}
|
||||
|
||||
updateTeamEnvironment(id: string, name: string, variables: string) {
|
||||
return pipe(
|
||||
TE.tryCatch(
|
||||
() =>
|
||||
this.prisma.teamEnvironment.update({
|
||||
where: { id: id },
|
||||
data: {
|
||||
name,
|
||||
variables: JSON.parse(variables),
|
||||
},
|
||||
}),
|
||||
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
||||
),
|
||||
TE.chainFirst((environment) =>
|
||||
TE.fromTask(() =>
|
||||
this.pubsub.publish(
|
||||
`team_environment/${environment.teamID}/updated`,
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
TE.map(
|
||||
(environment) =>
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
deleteAllVariablesFromTeamEnvironment(id: string) {
|
||||
return pipe(
|
||||
TE.tryCatch(
|
||||
() =>
|
||||
this.prisma.teamEnvironment.update({
|
||||
where: { id: id },
|
||||
data: {
|
||||
variables: [],
|
||||
},
|
||||
}),
|
||||
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
||||
),
|
||||
TE.chainFirst((environment) =>
|
||||
TE.fromTask(() =>
|
||||
this.pubsub.publish(
|
||||
`team_environment/${environment.teamID}/updated`,
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
TE.map(
|
||||
(environment) =>
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
createDuplicateEnvironment(id: string) {
|
||||
return pipe(
|
||||
TE.tryCatch(
|
||||
() =>
|
||||
this.prisma.teamEnvironment.findFirst({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
rejectOnNotFound: true,
|
||||
}),
|
||||
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
||||
),
|
||||
TE.chain((environment) =>
|
||||
TE.fromTask(() =>
|
||||
this.prisma.teamEnvironment.create({
|
||||
data: {
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: environment.variables as Prisma.JsonArray,
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
TE.chainFirst((environment) =>
|
||||
TE.fromTask(() =>
|
||||
this.pubsub.publish(
|
||||
`team_environment/${environment.teamID}/created`,
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
TE.map(
|
||||
(environment) =>
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
fetchAllTeamEnvironments(teamID: string) {
|
||||
return pipe(
|
||||
() =>
|
||||
this.prisma.teamEnvironment.findMany({
|
||||
where: {
|
||||
teamID: teamID,
|
||||
},
|
||||
}),
|
||||
T.map(
|
||||
A.map(
|
||||
(environment) =>
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
import { Team } from 'src/team/team.model';
|
||||
import { TeamEnvironment } from './team-environments.model';
|
||||
import { TeamEnvironmentsService } from './team-environments.service';
|
||||
|
||||
@Resolver(() => Team)
|
||||
export class TeamEnvsTeamResolver {
|
||||
constructor(private teamEnvironmentService: TeamEnvironmentsService) {}
|
||||
|
||||
@ResolveField(() => [TeamEnvironment], {
|
||||
description: 'Returns all Team Environments for the given Team',
|
||||
})
|
||||
teamEnvironments(@Parent() team: Team): Promise<TeamEnvironment[]> {
|
||||
return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id)();
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
BUG_AUTH_NO_USER_CTX,
|
||||
BUG_TEAM_NO_REQUIRE_TEAM_ROLE,
|
||||
BUG_TEAM_NO_TEAM_ID,
|
||||
TEAM_MEMBER_NOT_FOUND,
|
||||
} from 'src/errors';
|
||||
|
||||
@Injectable()
|
||||
@@ -23,24 +24,20 @@ export class GqlTeamMemberGuard implements CanActivate {
|
||||
'requiresTeamRole',
|
||||
context.getHandler(),
|
||||
);
|
||||
|
||||
if (!requireRoles) throw new Error(BUG_TEAM_NO_REQUIRE_TEAM_ROLE);
|
||||
|
||||
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||
|
||||
const { user } = gqlExecCtx.getContext().req;
|
||||
|
||||
if (user == undefined) throw new Error(BUG_AUTH_NO_USER_CTX);
|
||||
|
||||
const { teamID } = gqlExecCtx.getArgs<{ teamID: string }>();
|
||||
|
||||
if (!teamID) throw new Error(BUG_TEAM_NO_TEAM_ID);
|
||||
|
||||
const status = await this.teamService.getTeamMember(teamID, user.uid);
|
||||
const teamMember = await this.teamService.getTeamMember(teamID, user.uid);
|
||||
if (!teamMember) throw new Error(TEAM_MEMBER_NOT_FOUND);
|
||||
|
||||
if (!status) throw new Error('team/member_not_found');
|
||||
|
||||
if (requireRoles.includes(status.role)) return true;
|
||||
if (requireRoles.includes(teamMember.role)) return true;
|
||||
|
||||
throw new Error(TEAM_NOT_REQUIRED_ROLE);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export class Team {
|
||||
@ObjectType()
|
||||
export class TeamMember {
|
||||
@Field(() => ID, {
|
||||
description: 'Membership ID of the Team Member'
|
||||
description: 'Membership ID of the Team Member',
|
||||
})
|
||||
membershipID: string;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { PubSubModule } from '../pubsub/pubsub.module';
|
||||
|
||||
@Module({
|
||||
imports: [UserModule ,PubSubModule , PrismaModule ],
|
||||
imports: [UserModule, PubSubModule, PrismaModule],
|
||||
providers: [
|
||||
TeamService,
|
||||
TeamResolver,
|
||||
|
||||
@@ -66,7 +66,7 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
role: TeamMemberRole,
|
||||
): Promise<E.Left<string> | E.Right<TeamMember>> {
|
||||
const user = await this.userService.findUserByEmail(email);
|
||||
if(O.isNone(user)) return E.left(USER_NOT_FOUND);
|
||||
if (O.isNone(user)) return E.left(USER_NOT_FOUND);
|
||||
|
||||
const teamMember = await this.addMemberToTeam(teamID, user.value.uid, role);
|
||||
return E.right(teamMember);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
import { pipe } from 'fp-ts/lib/function';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import * as T from 'fp-ts/Task';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { User } from './user/user.model';
|
||||
import * as A from 'fp-ts/Array';
|
||||
import { TeamMemberRole } from './team/team.model';
|
||||
import { User } from './user/user.model';
|
||||
import { JSON_INVALID } from './errors';
|
||||
|
||||
/**
|
||||
@@ -51,14 +53,14 @@ export const namedTrace =
|
||||
* @param context NestJS Execution Context
|
||||
* @returns An Option which contains the defined roles
|
||||
*/
|
||||
// export const getAnnotatedRequiredRoles = (
|
||||
// reflector: Reflector,
|
||||
// context: ExecutionContext,
|
||||
// ) =>
|
||||
// pipe(
|
||||
// reflector.get<TeamMemberRole[]>('requiresTeamRole', context.getHandler()),
|
||||
// O.fromNullable,
|
||||
// );
|
||||
export const getAnnotatedRequiredRoles = (
|
||||
reflector: Reflector,
|
||||
context: ExecutionContext,
|
||||
) =>
|
||||
pipe(
|
||||
reflector.get<TeamMemberRole[]>('requiresTeamRole', context.getHandler()),
|
||||
O.fromNullable,
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the user from the NestJS GQL Execution Context.
|
||||
@@ -70,7 +72,7 @@ export const getUserFromGQLContext = (ctx: ExecutionContext) =>
|
||||
pipe(
|
||||
ctx,
|
||||
GqlExecutionContext.create,
|
||||
(ctx) => ctx.getContext<{ user?: User }>(),
|
||||
(ctx) => ctx.getContext().req,
|
||||
({ user }) => user,
|
||||
O.fromNullable,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user