feat: teamEnvironment module added

This commit is contained in:
Mir Arif Hasan
2023-02-08 15:18:46 +06:00
parent 9bee62ada9
commit c5d8a446ae
15 changed files with 1012 additions and 21 deletions

View File

@@ -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],
})

View File

@@ -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;
/**

View File

@@ -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;
};

View File

@@ -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),
)();
}
}

View File

@@ -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
}

View File

@@ -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 {}

View File

@@ -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`);
}
}

View File

@@ -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([{}]),
},
);
});
});
});

View File

@@ -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),
},
),
),
);
}
}

View File

@@ -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)();
}
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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);

View File

@@ -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,
);