Skip to content

Commit

Permalink
feat(api): Add support for storing and managing variables (#149)
Browse files Browse the repository at this point in the history
  • Loading branch information
rajdip-b authored Feb 20, 2024
1 parent 3960581 commit 963a8ae
Show file tree
Hide file tree
Showing 22 changed files with 1,892 additions and 25 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 @@ -16,6 +16,7 @@ import { WorkspaceModule } from '../workspace/workspace.module'
import { WorkspaceRoleModule } from '../workspace-role/workspace-role.module'
import { ApiKeyGuard } from '../auth/guard/api-key/api-key.guard'
import { EventModule } from '../event/event.module'
import { VariableModule } from '../variable/variable.module'

@Module({
controllers: [AppController],
Expand All @@ -34,7 +35,8 @@ import { EventModule } from '../event/event.module'
EnvironmentModule,
WorkspaceModule,
WorkspaceRoleModule,
EventModule
EventModule,
VariableModule
],
providers: [
{
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/common/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default async function cleanUp(prisma: PrismaClient) {
prisma.project.deleteMany(),
prisma.user.deleteMany(),
prisma.event.deleteMany(),
prisma.apiKey.deleteMany()
prisma.apiKey.deleteMany(),
prisma.variable.deleteMany()
])
}
18 changes: 16 additions & 2 deletions apps/api/src/common/create-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
Secret,
User,
Workspace,
WorkspaceRole
WorkspaceRole,
Variable
} from '@prisma/client'
import { JsonObject } from '@prisma/client/runtime/library'

Expand All @@ -19,7 +20,14 @@ export default async function createEvent(
triggerer?: EventTriggerer
severity?: EventSeverity
triggeredBy?: User
entity?: Workspace | Project | Environment | WorkspaceRole | ApiKey | Secret
entity?:
| Workspace
| Project
| Environment
| WorkspaceRole
| ApiKey
| Secret
| Variable
type: EventType
source: EventSource
title: string
Expand Down Expand Up @@ -85,6 +93,12 @@ export default async function createEvent(
}
break
}
case EventSource.VARIABLE: {
if (data.entity) {
baseData.sourceVariableId = data.entity.id
}
break
}
case EventSource.USER: {
break
}
Expand Down
13 changes: 13 additions & 0 deletions apps/api/src/common/get-default-project-environemnt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Environment, PrismaClient, Project } from '@prisma/client'

export default async function getDefaultEnvironmentOfProject(
projectId: Project['id'],
prisma: PrismaClient
): Promise<Environment | null> {
return await prisma.environment.findFirst({
where: {
projectId,
isDefault: true
}
})
}
57 changes: 57 additions & 0 deletions apps/api/src/common/get-variable-with-authority.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Authority, PrismaClient, User, Variable } from '@prisma/client'
import getCollectiveProjectAuthorities from './get-collective-project-authorities'
import { NotFoundException, UnauthorizedException } from '@nestjs/common'
import { VariableWithProjectAndVersion } from '../variable/variable.types'

export default async function getVariableWithAuthority(
userId: User['id'],
variableId: Variable['id'],
authority: Authority,
prisma: PrismaClient
): Promise<VariableWithProjectAndVersion> {
// Fetch the variable
let variable: VariableWithProjectAndVersion

try {
variable = await prisma.variable.findUnique({
where: {
id: variableId
},
include: {
versions: true,
project: true,
environment: {
select: {
id: true,
name: true
}
}
}
})
} catch (error) {
/* empty */
}

if (!variable) {
throw new NotFoundException(`Variable with id ${variableId} not found`)
}

// Check if the user has the project in their workspace role list
const permittedAuthorities = await getCollectiveProjectAuthorities(
userId,
variable.project,
prisma
)

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

return variable
}
2 changes: 2 additions & 0 deletions apps/api/src/event/controller/event.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class EventController {
@Query('projectId') projectId: string,
@Query('environmentId') environmentId: string,
@Query('secretId') secretId: string,
@Query('variableId') variableId: string,
@Query('apiKeyId') apiKeyId: string,
@Query('workspaceRoleId') workspaceRoleId: string
) {
Expand All @@ -32,6 +33,7 @@ export class EventController {
projectId,
environmentId,
secretId,
variableId,
apiKeyId,
workspaceRoleId
},
Expand Down
44 changes: 43 additions & 1 deletion apps/api/src/event/event.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { EnvironmentModule } from '../environment/environment.module'
import { ApiKeyModule } from '../api-key/api-key.module'
import createEvent from '../common/create-event'
import fetchEvents from '../common/fetch-events'
import { VariableService } from '../variable/service/variable.service'
import { VariableModule } from '../variable/variable.module'

describe('Event Controller Tests', () => {
let app: NestFastifyApplication
Expand All @@ -48,6 +50,7 @@ describe('Event Controller Tests', () => {
let workspaceRoleService: WorkspaceRoleService
let projectService: ProjectService
let secretService: SecretService
let variableService: VariableService

let user: User
let workspace: Workspace
Expand All @@ -67,7 +70,8 @@ describe('Event Controller Tests', () => {
SecretModule,
ProjectModule,
EnvironmentModule,
ApiKeyModule
ApiKeyModule,
VariableModule
]
})
.overrideProvider(MAIL_SERVICE)
Expand All @@ -85,6 +89,7 @@ describe('Event Controller Tests', () => {
workspaceRoleService = moduleRef.get(WorkspaceRoleService)
projectService = moduleRef.get(ProjectService)
secretService = moduleRef.get(SecretService)
variableService = moduleRef.get(VariableService)

await app.init()
await app.getHttpAdapter().getInstance().ready()
Expand Down Expand Up @@ -294,6 +299,43 @@ describe('Event Controller Tests', () => {
expect(response.json()).toEqual([event])
})

it('should be able to fetch a variable event', async () => {
const newVariable = await variableService.createVariable(
user,
{
name: 'My variable',
value: 'My value',
environmentId: environment.id
},
project.id
)

expect(newVariable).toBeDefined()

const response = await fetchEvents(
app,
user,
`variableId=${newVariable.id}`
)

const event = {
id: expect.any(String),
title: expect.any(String),
description: expect.any(String),
source: EventSource.VARIABLE,
triggerer: EventTriggerer.USER,
severity: EventSeverity.INFO,
type: EventType.VARIABLE_ADDED,
timestamp: expect.any(String),
metadata: expect.any(Object)
}

totalEvents.push(event)

expect(response.statusCode).toBe(200)
expect(response.json()).toEqual([event])
})

it('should be able to fetch a workspace role event', async () => {
const newWorkspaceRole = await workspaceRoleService.createWorkspaceRole(
user,
Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/event/service/event.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import getProjectWithAuthority from '../../common/get-project-with-authority'
import getEnvironmentWithAuthority from '../../common/get-environment-with-authority'
import getSecretWithAuthority from '../../common/get-secret-with-authority'
import { PrismaService } from '../../prisma/prisma.service'
import getVariableWithAuthority from '../../common/get-variable-with-authority'

@Injectable()
export class EventService {
Expand All @@ -17,6 +18,7 @@ export class EventService {
projectId?: string
environmentId?: string
secretId?: string
variableId?: string
apiKeyId?: string
workspaceRoleId?: string
},
Expand Down Expand Up @@ -69,6 +71,14 @@ export class EventService {
this.prisma
)
whereCondition['sourceSecretId'] = context.secretId
} else if (context.variableId) {
await getVariableWithAuthority(
user.id,
context.variableId,
Authority.READ_VARIABLE,
this.prisma
)
whereCondition['sourceVariableId'] = context.variableId
} else if (context.apiKeyId) {
whereCondition['sourceApiKeyId'] = context.apiKeyId
} else if (context.workspaceRoleId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.


ALTER TYPE "Authority" ADD VALUE 'CREATE_VARIABLE';
ALTER TYPE "Authority" ADD VALUE 'READ_VARIABLE';
ALTER TYPE "Authority" ADD VALUE 'UPDATE_VARIABLE';
ALTER TYPE "Authority" ADD VALUE 'DELETE_VARIABLE';

-- AlterEnum
ALTER TYPE "EventSource" ADD VALUE 'VARIABLE';

-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.


ALTER TYPE "EventType" ADD VALUE 'VARIABLE_UPDATED';
ALTER TYPE "EventType" ADD VALUE 'VARIABLE_DELETED';
ALTER TYPE "EventType" ADD VALUE 'VARIABLE_ADDED';

-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.


ALTER TYPE "NotificationType" ADD VALUE 'VARIABLE_UPDATED';
ALTER TYPE "NotificationType" ADD VALUE 'VARIABLE_DELETED';
ALTER TYPE "NotificationType" ADD VALUE 'VARIABLE_ADDED';

-- AlterTable
ALTER TABLE "Event" ADD COLUMN "sourceVariableId" TEXT;

-- CreateTable
CREATE TABLE "VariableVersion" (
"id" TEXT NOT NULL,
"value" TEXT NOT NULL,
"version" INTEGER NOT NULL DEFAULT 1,
"variableId" TEXT NOT NULL,
"createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdById" TEXT,

CONSTRAINT "VariableVersion_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "Variable" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"lastUpdatedById" TEXT,
"projectId" TEXT NOT NULL,
"environmentId" TEXT NOT NULL,

CONSTRAINT "Variable_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "VariableVersion_variableId_version_key" ON "VariableVersion"("variableId", "version");

-- CreateIndex
CREATE UNIQUE INDEX "Variable_projectId_environmentId_name_key" ON "Variable"("projectId", "environmentId", "name");

-- AddForeignKey
ALTER TABLE "Event" ADD CONSTRAINT "Event_sourceVariableId_fkey" FOREIGN KEY ("sourceVariableId") REFERENCES "Variable"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "VariableVersion" ADD CONSTRAINT "VariableVersion_variableId_fkey" FOREIGN KEY ("variableId") REFERENCES "Variable"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "VariableVersion" ADD CONSTRAINT "VariableVersion_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Variable" ADD CONSTRAINT "Variable_lastUpdatedById_fkey" FOREIGN KEY ("lastUpdatedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Variable" ADD CONSTRAINT "Variable_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Variable" ADD CONSTRAINT "Variable_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "Environment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Loading

0 comments on commit 963a8ae

Please sign in to comment.