Skip to content

Commit

Permalink
feat(api): Add Integration support (#203)
Browse files Browse the repository at this point in the history
  • Loading branch information
rajdip-b authored May 6, 2024
1 parent 177dbbf commit f1ae87e
Show file tree
Hide file tree
Showing 28 changed files with 9,433 additions and 6,083 deletions.
4 changes: 3 additions & 1 deletion apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ApprovalModule } from '../approval/approval.module'
import { SocketModule } from '../socket/socket.module'
import { ProviderModule } from '../provider/provider.module'
import { ScheduleModule } from '@nestjs/schedule'
import { IntegrationModule } from '../integration/integration.module'

@Module({
controllers: [AppController],
Expand All @@ -44,7 +45,8 @@ import { ScheduleModule } from '@nestjs/schedule'
VariableModule,
ApprovalModule,
SocketModule,
ProviderModule
ProviderModule,
IntegrationModule
],
providers: [
{
Expand Down
34 changes: 18 additions & 16 deletions apps/api/src/common/alphanumeric-reason-pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
import { AlphanumericReasonValidationPipe } from './alphanumeric-reason-pipe';
import { BadRequestException } from '@nestjs/common';
import { AlphanumericReasonValidationPipe } from './alphanumeric-reason-pipe'
import { BadRequestException } from '@nestjs/common'

describe('AlphanumericReasonValidationPipe', () => {
let pipe: AlphanumericReasonValidationPipe;
let pipe: AlphanumericReasonValidationPipe

beforeEach(() => {
pipe = new AlphanumericReasonValidationPipe();
});
pipe = new AlphanumericReasonValidationPipe()
})

it('should allow alphanumeric string', () => {
const validInput = 'Test123';
expect(pipe.transform(validInput)).toBe(validInput);
});
const validInput = 'Test123'
expect(pipe.transform(validInput)).toBe(validInput)
})

it('should not allow strings with only spaces', () => {
expect(() => pipe.transform(' ')).toThrow(BadRequestException);
});
expect(() => pipe.transform(' ')).toThrow(BadRequestException)
})

it('should throw BadRequestException for non-alphanumeric string', () => {
const invalidInput = 'Test123$%^';
const invalidInput = 'Test123$%^'
try {
pipe.transform(invalidInput);
pipe.transform(invalidInput)
} catch (e) {
expect(e).toBeInstanceOf(BadRequestException);
expect(e.message).toBe('Reason must contain only alphanumeric characters and no leading or trailing spaces.');
expect(e).toBeInstanceOf(BadRequestException)
expect(e.message).toBe(
'Reason must contain only alphanumeric characters and no leading or trailing spaces.'
)
}
});
});
})
})
14 changes: 8 additions & 6 deletions apps/api/src/common/alphanumeric-reason-pipe.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common'

@Injectable()
export class AlphanumericReasonValidationPipe implements PipeTransform {
transform(value: string) {
if (/^[a-zA-Z0-9]+(?: [a-zA-Z0-9]+)*$/.test(value)) {
return value;
} else {
throw new BadRequestException('Reason must contain only alphanumeric characters and no leading or trailing spaces.');
if (/^[a-zA-Z0-9]+(?: [a-zA-Z0-9]+)*$/.test(value)) {
return value
} else {
throw new BadRequestException(
'Reason must contain only alphanumeric characters and no leading or trailing spaces.'
)
}
}
}
}
84 changes: 83 additions & 1 deletion apps/api/src/common/authority-checker.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PrismaClient, Authority, Workspace } from '@prisma/client'
import { PrismaClient, Authority, Workspace, Integration } from '@prisma/client'
import { VariableWithProjectAndVersion } from '../variable/variable.types'
import {
BadRequestException,
Expand Down Expand Up @@ -393,4 +393,86 @@ export class AuthorityCheckerService {

return secret
}

public async checkAuthorityOverIntegration(
input: AuthorityInput
): Promise<Integration> {
const { userId, entity, authority, prisma } = input

// Fetch the integration
let integration: Integration | null

try {
if (entity?.id) {
integration = await prisma.integration.findUnique({
where: {
id: entity.id
}
})
} else {
integration = await prisma.integration.findFirst({
where: {
name: entity?.name,
workspace: { members: { some: { userId: userId } } }
}
})
}
} catch (error) {
this.customLoggerService.error(error)
throw new Error(error)
}

if (!integration) {
throw new NotFoundException(`Integration with id ${entity.id} not found`)
}

// Check if the user has the required authorities
const permittedAuthorities = await getCollectiveWorkspaceAuthorities(
integration.workspaceId,
userId,
prisma
)

if (
!permittedAuthorities.has(authority) &&
!permittedAuthorities.has(Authority.WORKSPACE_ADMIN)
) {
throw new UnauthorizedException(
`User ${userId} does not have the required authorities`
)
}

// Additionally, we would also like to check the project authorities,
// if the integration is associated with a project
if (integration.projectId) {
const project = await prisma.project.findUnique({
where: {
id: integration.projectId
}
})

if (!project) {
throw new NotFoundException(
`Project with id ${integration.projectId} not found`
)
}

const projectAuthorities = await getCollectiveProjectAuthorities(
userId,
project,
prisma
)

if (
!projectAuthorities.has(authority) &&
!projectAuthorities.has(Authority.WORKSPACE_ADMIN)
) {
throw new UnauthorizedException(
`User ${userId} does not have the required authorities`
)
}
}

return integration
}
}
3 changes: 2 additions & 1 deletion apps/api/src/common/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default async function cleanUp(prisma: PrismaClient) {
prisma.user.deleteMany(),
prisma.event.deleteMany(),
prisma.apiKey.deleteMany(),
prisma.variable.deleteMany()
prisma.variable.deleteMany(),
prisma.integration.deleteMany()
])
}
100 changes: 98 additions & 2 deletions apps/api/src/common/create-event.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Logger } from '@nestjs/common'
import { InternalServerErrorException, Logger } from '@nestjs/common'
import {
Environment,
EventSeverity,
Expand All @@ -12,9 +12,11 @@ import {
Workspace,
WorkspaceRole,
Variable,
Approval
Approval,
Integration
} from '@prisma/client'
import { JsonObject } from '@prisma/client/runtime/library'
import IntegrationFactory from '../integration/plugins/factory/integration.factory'

const logger = new Logger('CreateEvent')

Expand All @@ -31,6 +33,7 @@ export default async function createEvent(
| Secret
| Variable
| Approval
| Integration
type: EventType
source: EventSource
title: string
Expand Down Expand Up @@ -59,5 +62,98 @@ export default async function createEvent(
}
})

if (data.entity) {
// We need to fetch all the integrations that will be triggered for this event.
// To do that, we will need to take the following steps:
// 1. Based on the entity, get the projectId and environmentId
// 2. Fetch the integration entities using the following query:
// a. Get all integrations that have the same projectId and environmentId and workspaceId
// b. Get all integrations that have the same projectId and workspaceId
// c. Get all integrations that have the same workspaceId
// 3. For each integration entity, check if the notifyOn array includes the event type
// 4. If it does, call the emitEvent function with the data and metadata

// Fetch the projectId and environmentId
let projectId: string | undefined, environmentId: string | undefined
switch (data.source) {
case EventSource.WORKSPACE:
break

case EventSource.PROJECT:
projectId = data.entity.id
break

case EventSource.ENVIRONMENT:
const environment = data.entity as Environment
projectId = environment.projectId
environmentId = environment.id
break

case EventSource.WORKSPACE_ROLE:
break

case EventSource.SECRET:
const secret = data.entity as Secret
projectId = secret.projectId
environmentId = secret?.environmentId
break

case EventSource.VARIABLE:
const variable = data.entity as Variable
projectId = variable.projectId
environmentId = variable?.environmentId
break

case EventSource.APPROVAL:
break

case EventSource.INTEGRATION:
break
default:
throw new InternalServerErrorException('Unsupported event source')
}

// Create a set to store the integrations
// const integrations = new Set<BaseIntegration>()
const integrationEntities = await prisma.integration.findMany({
where: {
OR: [
{
projectId,
environmentId,
workspaceId: data.workspaceId
},
{
projectId,
workspaceId: data.workspaceId
},
{
workspaceId: data.workspaceId
}
],
notifyOn: {
has: data.type
}
}
})

// Emit the event for each integration
for (const integration of integrationEntities) {
const integrationInstance = IntegrationFactory.createIntegration(
integration.type
)
integrationInstance.emitEvent(
{
entity: data.entity,
source: data.source,
eventType: data.type,
title: data.title,
description: data.description
},
integration.metadata
)
}
}

logger.log(`Event with id ${event.id} created`)
}
2 changes: 0 additions & 2 deletions apps/api/src/common/get-collective-project-authorities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,5 @@ export default async function getCollectiveProjectAuthorities(
})
})

// console.log('authorities: ', authorities)

return authorities
}
28 changes: 28 additions & 0 deletions apps/api/src/integration/controller/integration.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Test, TestingModule } from '@nestjs/testing'
import { IntegrationController } from './integration.controller'
import { PrismaService } from '../../prisma/prisma.service'
import { mockDeep } from 'jest-mock-extended'
import { AuthorityCheckerService } from '../../common/authority-checker.service'
import { IntegrationService } from '../service/integration.service'
import { CommonModule } from '../../common/common.module'

describe('IntegrationController', () => {
let controller: IntegrationController

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [CommonModule],
controllers: [IntegrationController],
providers: [PrismaService, AuthorityCheckerService, IntegrationService]
})
.overrideProvider(PrismaService)
.useValue(mockDeep<PrismaService>())
.compile()

controller = module.get<IntegrationController>(IntegrationController)
})

it('should be defined', () => {
expect(controller).toBeDefined()
})
})
Loading

0 comments on commit f1ae87e

Please sign in to comment.