diff --git a/CHANGELOG.md b/CHANGELOG.md index b744f218..0a50c116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +## [2.5.0](https://github.com/keyshade-xyz/keyshade/compare/v2.4.0...v2.5.0) (2024-09-16) + +### 🚀 Features + +* **api-client:** Added workspace controller ([#427](https://github.com/keyshade-xyz/keyshade/issues/427)) ([2f4edec](https://github.com/keyshade-xyz/keyshade/commit/2f4edecd0837f7658892a37902c62f05ab96c884)) +* **api-client:** Added workspace role controller ([#430](https://github.com/keyshade-xyz/keyshade/issues/430)) ([b03ce8e](https://github.com/keyshade-xyz/keyshade/commit/b03ce8ee346ee054b2460093ee5542551c175f60)) +* **api-client:** Synced with latest API ([27f4309](https://github.com/keyshade-xyz/keyshade/commit/27f4309817b7736d273e9c3532af00e3ed73d943)) +* **api:** Add slug in entities ([#415](https://github.com/keyshade-xyz/keyshade/issues/415)) ([89e2fcc](https://github.com/keyshade-xyz/keyshade/commit/89e2fccc390771c925ea9d1c4ede8270ec6e5a80)) +* **api:** Included default workspace details in getSelf function ([#414](https://github.com/keyshade-xyz/keyshade/issues/414)) ([e67bbd6](https://github.com/keyshade-xyz/keyshade/commit/e67bbd6d37f01732bbfe65e17b71b2ba8202200b)) +* **platform:** Add loading skeleton in the [secure]s page ([#423](https://github.com/keyshade-xyz/keyshade/issues/423)) ([a97681e](https://github.com/keyshade-xyz/keyshade/commit/a97681edab4ffb670b4479e3a8d2d2879c75f992)) +* **schema:** Added a schema package ([01ea232](https://github.com/keyshade-xyz/keyshade/commit/01ea232a9d1bc51a0b0cae3fd6edd378b88a5755)) +* **web:** Update about and careers page ([e167f53](https://github.com/keyshade-xyz/keyshade/commit/e167f537bca0218eb071b42e31d963e9852e2885)) + +### 🐛 Bug Fixes + +* **api:** Error messages fixed in api-key service ([#418](https://github.com/keyshade-xyz/keyshade/issues/418)) ([edfbce0](https://github.com/keyshade-xyz/keyshade/commit/edfbce068dc0c3d23a92ce3a9d69c06f8457b7d8)) + +### 📚 Documentation + +* Fixed minor typo in postman workspace link ([#411](https://github.com/keyshade-xyz/keyshade/issues/411)) ([ed23116](https://github.com/keyshade-xyz/keyshade/commit/ed231165e341be91f753b02200f8d4d11d42c3b3)) +* Updated Postman links ([444bfb1](https://github.com/keyshade-xyz/keyshade/commit/444bfb1a5d85656ce98011c442e5238d2b786a72)) + +### 🔧 Miscellaneous Chores + +* **api:** Suppressed version check test in [secure] ([4688e8c](https://github.com/keyshade-xyz/keyshade/commit/4688e8c429e63b3fb18ba0bb77d53fa875999792)) +* **api:** Update slug generation method ([#420](https://github.com/keyshade-xyz/keyshade/issues/420)) ([1f864df](https://github.com/keyshade-xyz/keyshade/commit/1f864df4180f65c8f42f8984b6a014d44c366901)) + +### 🔨 Code Refactoring + +* **API:** Refactor workspace-membership into a separate module ([#421](https://github.com/keyshade-xyz/keyshade/issues/421)) ([574170f](https://github.com/keyshade-xyz/keyshade/commit/574170f39d0a56d6087d8518ca02bbe6b9fd9740)) +* **platform:** added optional chaining due to strict null check ([#413](https://github.com/keyshade-xyz/keyshade/issues/413)) ([907e369](https://github.com/keyshade-xyz/keyshade/commit/907e3694cd23d01f2190e0ed80a0fffb97d0268d)) + ## [2.4.0](https://github.com/keyshade-xyz/keyshade/compare/v2.3.0...v2.4.0) (2024-09-05) ### 🚀 Features diff --git a/apps/api/src/common/slug-generator.spec.ts b/apps/api/src/common/slug-generator.spec.ts new file mode 100644 index 00000000..a2921839 --- /dev/null +++ b/apps/api/src/common/slug-generator.spec.ts @@ -0,0 +1,113 @@ +import { PrismaService } from '@/prisma/prisma.service' +import generateEntitySlug, { + generateSlugName, + incrementSlugSuffix +} from './slug-generator' +import { mockDeep } from 'jest-mock-extended' + +describe('generateEntitySlug', () => { + let prisma + + beforeEach(() => { + prisma = mockDeep() + }) + + describe('generateSlugName', () => { + it('should convert name to slug format', () => { + expect(generateSlugName('Hello World')).toBe('hello-world') + expect(generateSlugName('Entity with 123')).toBe('entity-with-123') + expect(generateSlugName('Special #Name!')).toBe('special-name') + }) + }) + + describe('incrementSlugSuffix', () => { + it('should return base slug with `-0` when no suffix is found', () => { + const result = incrementSlugSuffix('', 'my-slug') + expect(result).toBe('my-slug-0') + }) + + it('should increment suffix when found', () => { + const result = incrementSlugSuffix('my-slug-0', 'my-slug') + expect(result).toBe('my-slug-1') + }) + + it('should handle complex increment cases with carryover', () => { + const result = incrementSlugSuffix('my-slug-z', 'my-slug') + expect(result).toBe('my-slug-00') + }) + }) + + describe('generateEntitySlug for each entity type', () => { + it('should generate a unique slug for WORKSPACE_ROLE', async () => { + prisma.$queryRaw.mockResolvedValue([ + { + slug: 'workspace-role-0' + } + ]) + + const slug = await generateEntitySlug( + 'Workspace Role', + 'WORKSPACE_ROLE', + prisma + ) + expect(slug).toBe('workspace-role-1') + }) + + it('should generate a unique slug for WORKSPACE', async () => { + prisma.$queryRaw.mockResolvedValue([]) + + const slug = await generateEntitySlug('Workspace', 'WORKSPACE', prisma) + expect(slug).toBe('workspace-0') + }) + + it('should generate a unique slug for PROJECT', async () => { + prisma.$queryRaw.mockResolvedValue([{ slug: 'project-z' }]) + + const slug = await generateEntitySlug('Project', 'PROJECT', prisma) + expect(slug).toBe('project-00') + }) + + it('should generate a unique slug for VARIABLE', async () => { + prisma.$queryRaw.mockResolvedValue([{ slug: 'variable-az' }]) + + const slug = await generateEntitySlug('Variable', 'VARIABLE', prisma) + expect(slug).toBe('variable-b0') + }) + + it('should generate a unique slug for SECRET', async () => { + prisma.$queryRaw.mockResolvedValue([{ slug: 'secret-9' }]) + + const slug = await generateEntitySlug('Secret', 'SECRET', prisma) + expect(slug).toBe('secret-a') + }) + + it('should generate a unique slug for INTEGRATION', async () => { + prisma.$queryRaw.mockResolvedValue([{ slug: 'integration-b' }]) + + const slug = await generateEntitySlug( + 'Integration', + 'INTEGRATION', + prisma + ) + expect(slug).toBe('integration-c') + }) + + it('should generate a unique slug for ENVIRONMENT', async () => { + prisma.$queryRaw.mockResolvedValue([{ slug: 'environment-zz' }]) + + const slug = await generateEntitySlug( + 'Environment', + 'ENVIRONMENT', + prisma + ) + expect(slug).toBe('environment-000') + }) + + it('should generate a unique slug for API_KEY', async () => { + prisma.$queryRaw.mockResolvedValue([{ slug: 'api-key-09' }]) + + const slug = await generateEntitySlug('Api @Key', 'API_KEY', prisma) + expect(slug).toBe('api-key-0a') + }) + }) +}) diff --git a/apps/api/src/common/slug-generator.ts b/apps/api/src/common/slug-generator.ts index 2402c485..eca3f47d 100644 --- a/apps/api/src/common/slug-generator.ts +++ b/apps/api/src/common/slug-generator.ts @@ -1,14 +1,59 @@ import { PrismaService } from '@/prisma/prisma.service' import { Workspace } from '@prisma/client' +export const incrementSlugSuffix = ( + existingSlug: string, + baseSlug: string +): string => { + const charset = '0123456789abcdefghijklmnopqrstuvwxyz' + + let suffix = '' + + if (existingSlug) { + suffix = existingSlug.substring(baseSlug.length + 1) + } + + if (!suffix) { + return `${baseSlug}-0` + } + + let result = '' + let carry = true + + for (let i = suffix.length - 1; i >= 0; i--) { + if (carry) { + const currentChar = suffix[i] + const index = charset.indexOf(currentChar) + + if (index === -1) { + throw new Error(`Invalid character in slug suffix: ${currentChar}`) + } + const nextIndex = (index + 1) % charset.length + result = charset[nextIndex] + result + + // Carry over if we wrapped around to '0' + carry = nextIndex === 0 + } else { + // No carry, just append the remaining part of the suffix + result = suffix[i] + result + } + } + + if (carry) { + result = '0' + result + } + + return `${baseSlug}-${result}` +} + /** - * Generates a unique slug for the given name. It keeps generating slugs until it finds + * Generates a slug for the given name. It keeps generating slugs until it finds * one that does not exist in the database. * * @param name The name of the entity. - * @returns A unique slug for the given entity. + * @returns A alphanumeric slug for the given name. */ -const generateSlug = (name: string): string => { +export const generateSlugName = (name: string): string => { // Convert to lowercase const lowerCaseName = name.trim().toLowerCase() @@ -16,78 +61,140 @@ const generateSlug = (name: string): string => { const hyphenatedName = lowerCaseName.replace(/\s+/g, '-') // Replace all non-alphanumeric characters with hyphens - const alphanumericName = hyphenatedName.replace(/[^a-zA-Z0-9-]/g, '-') + const alphanumericName = hyphenatedName.replace(/[^a-zA-Z0-9-]/g, '') - // Append the name with 5 alphanumeric characters - const slug = - alphanumericName + '-' + Math.random().toString(36).substring(2, 7) - return slug + return alphanumericName } -const checkWorkspaceRoleSlugExists = async ( +const getWorkspaceRoleIfSlugExists = async ( slug: Workspace['slug'], prisma: PrismaService -): Promise => { - return (await prisma.workspaceRole.count({ where: { slug } })) > 0 +): Promise => { + const search = `${slug}-[a-z0-9]*` + const existingSlug: { slug: string }[] = await prisma.$queryRaw< + { slug: string }[] + >` + SELECT slug + FROM "WorkspaceRole" + WHERE slug ~ ${search} + ORDER BY slug DESC + LIMIT 1 + ` + return existingSlug.length > 0 ? existingSlug[0].slug : '' } -const checkWorkspaceSlugExists = async ( +const getWorkspaceSlugExists = async ( slug: Workspace['slug'], prisma: PrismaService -): Promise => { - return (await prisma.workspace.count({ where: { slug } })) > 0 +): Promise => { + const search = `${slug}-[a-z0-9]*` + const existingSlug = await prisma.$queryRaw<{ slug: string }[]>` + SELECT slug + FROM "Workspace" + WHERE slug ~ ${search} + ORDER BY slug DESC + LIMIT 1 + ` + return existingSlug.length > 0 ? existingSlug[0].slug : '' } -const checkProjectSlugExists = async ( +const getProjectSlugExists = async ( slug: Workspace['slug'], prisma: PrismaService -): Promise => { - return (await prisma.project.count({ where: { slug } })) > 0 +): Promise => { + const search = `${slug}-[a-z0-9]*` + const existingSlug = await prisma.$queryRaw<{ slug: string }[]>` + SELECT slug + FROM "Project" + WHERE slug ~ ${search} + ORDER BY slug DESC + LIMIT 1 + ` + return existingSlug.length > 0 ? existingSlug[0].slug : '' } -const checkVariableSlugExists = async ( +const getVariableSlugExists = async ( slug: Workspace['slug'], prisma: PrismaService -): Promise => { - return (await prisma.variable.count({ where: { slug } })) > 0 +): Promise => { + const search = `${slug}-[a-z0-9]*` + const existingSlug = await prisma.$queryRaw<{ slug: string }[]>` + SELECT slug + FROM "Variable" + WHERE slug ~ ${search} + ORDER BY slug DESC + LIMIT 1 + ` + return existingSlug.length > 0 ? existingSlug[0].slug : '' } -const checkSecretSlugExists = async ( +const getSecretSlugExists = async ( slug: Workspace['slug'], prisma: PrismaService -): Promise => { - return (await prisma.secret.count({ where: { slug } })) > 0 +): Promise => { + const search = `${slug}-[a-z0-9]*` + const existingSlug = await prisma.$queryRaw<{ slug: string }[]>` + SELECT slug + FROM "Secret" + WHERE slug ~ ${search} + ORDER BY slug DESC + LIMIT 1 + ` + return existingSlug.length > 0 ? existingSlug[0].slug : '' } -const checkIntegrationSlugExists = async ( +const getIntegrationSlugExists = async ( slug: Workspace['slug'], prisma: PrismaService -): Promise => { - return (await prisma.integration.count({ where: { slug } })) > 0 +): Promise => { + const search = `${slug}-[a-z0-9]*` + const existingSlug = await prisma.$queryRaw<{ slug: string }[]>` + SELECT slug + FROM "Integration" + WHERE slug ~ ${search} + ORDER BY slug DESC + LIMIT 1 + ` + return existingSlug.length > 0 ? existingSlug[0].slug : '' } -const checkEnvironmentSlugExists = async ( +const getEnvironmentSlugExists = async ( slug: Workspace['slug'], prisma: PrismaService -): Promise => { - return (await prisma.environment.count({ where: { slug } })) > 0 +): Promise => { + const search = `${slug}-[a-z0-9]*` + const existingSlug = await prisma.$queryRaw<{ slug: string }[]>` + SELECT slug + FROM "Environment" + WHERE slug ~ ${search} + ORDER BY slug DESC + LIMIT 1 + ` + return existingSlug.length > 0 ? existingSlug[0].slug : '' } -const checkApiKeySlugExists = async ( +const getApiKeySlugExists = async ( slug: Workspace['slug'], prisma: PrismaService -): Promise => { - return (await prisma.apiKey.count({ where: { slug } })) > 0 +): Promise => { + const search = `${slug}-[a-z0-9]*` + const existingSlug = await prisma.$queryRaw<{ slug: string }[]>` + SELECT slug + FROM "ApiKey" + WHERE slug ~ ${search} + ORDER BY slug DESC + LIMIT 1 + ` + return existingSlug.length > 0 ? existingSlug[0].slug : '' } /** - * Generates a unique slug for the given entity type and name. It keeps - * generating slugs until it finds one that does not exist in the database. + * Generates a slug for the given name and entity type. It keeps generating slugs until it finds + * one that does not exist in the database. * * @param name The name of the entity. * @param entityType The type of the entity. - * @param prisma The Prisma client to use to check the existence of the slug. - * @returns A unique slug for the given entity. + * @returns A alphanumeric slug for the given name. */ export default async function generateEntitySlug( name: string, @@ -102,49 +209,33 @@ export default async function generateEntitySlug( | 'API_KEY', prisma: PrismaService ): Promise { - while (true) { - const slug = generateSlug(name) - switch (entityType) { - case 'WORKSPACE_ROLE': - if (await checkWorkspaceRoleSlugExists(slug, prisma)) { - continue - } - return slug - case 'WORKSPACE': - if (await checkWorkspaceSlugExists(slug, prisma)) { - continue - } - return slug - case 'PROJECT': - if (await checkProjectSlugExists(slug, prisma)) { - continue - } - return slug - case 'VARIABLE': - if (await checkVariableSlugExists(slug, prisma)) { - continue - } - return slug - case 'SECRET': - if (await checkSecretSlugExists(slug, prisma)) { - continue - } - return slug - case 'INTEGRATION': - if (await checkIntegrationSlugExists(slug, prisma)) { - continue - } - return slug - case 'ENVIRONMENT': - if (await checkEnvironmentSlugExists(slug, prisma)) { - continue - } - return slug - case 'API_KEY': - if (await checkApiKeySlugExists(slug, prisma)) { - continue - } - return slug - } + const baseSlug = generateSlugName(name) + let existingSlug = '' + switch (entityType) { + case 'WORKSPACE_ROLE': + existingSlug = await getWorkspaceRoleIfSlugExists(baseSlug, prisma) + break + case 'WORKSPACE': + existingSlug = await getWorkspaceSlugExists(baseSlug, prisma) + break + case 'PROJECT': + existingSlug = await getProjectSlugExists(baseSlug, prisma) + break + case 'VARIABLE': + existingSlug = await getVariableSlugExists(baseSlug, prisma) + break + case 'SECRET': + existingSlug = await getSecretSlugExists(baseSlug, prisma) + break + case 'INTEGRATION': + existingSlug = await getIntegrationSlugExists(baseSlug, prisma) + break + case 'ENVIRONMENT': + existingSlug = await getEnvironmentSlugExists(baseSlug, prisma) + break + case 'API_KEY': + existingSlug = await getApiKeySlugExists(baseSlug, prisma) + break } + return incrementSlugSuffix(existingSlug, baseSlug) } diff --git a/apps/api/src/common/workspace.ts b/apps/api/src/common/workspace.ts index ae298a68..767eb443 100644 --- a/apps/api/src/common/workspace.ts +++ b/apps/api/src/common/workspace.ts @@ -34,7 +34,7 @@ export const createWorkspace = async ( id: workspaceId, slug: await generateEntitySlug(dto.name, 'WORKSPACE', prisma), name: dto.name, - description: dto.description, + icon: dto.icon, isFreeTier: true, ownerId: user.id, isDefault, diff --git a/apps/api/src/event/event.e2e.spec.ts b/apps/api/src/event/event.e2e.spec.ts index 2bd7e705..c2baa4b7 100644 --- a/apps/api/src/event/event.e2e.spec.ts +++ b/apps/api/src/event/event.e2e.spec.ts @@ -99,7 +99,7 @@ describe('Event Controller Tests', () => { it('should be able to fetch a workspace event', async () => { const workspace = await workspaceService.createWorkspace(user, { name: 'My workspace', - description: 'Some description' + icon: '🤓' }) expect(workspace).toBeDefined() @@ -144,7 +144,7 @@ describe('Event Controller Tests', () => { it('should be able to fetch a project event', async () => { const workspace = await workspaceService.createWorkspace(user, { name: 'My workspace', - description: 'Some description' + icon: '🤓' }) const project = (await projectService.createProject(user, workspace.slug, { @@ -198,7 +198,7 @@ describe('Event Controller Tests', () => { it('should be able to fetch an environment event', async () => { const workspace = await workspaceService.createWorkspace(user, { name: 'My workspace', - description: 'Some description' + icon: '🤓' }) const project = await projectService.createProject(user, workspace.slug, { @@ -261,7 +261,7 @@ describe('Event Controller Tests', () => { it('should be able to fetch a secret event', async () => { const workspace = await workspaceService.createWorkspace(user, { name: 'My workspace', - description: 'Some description' + icon: '🤓' }) const project = await projectService.createProject(user, workspace.slug, { @@ -340,7 +340,7 @@ describe('Event Controller Tests', () => { it('should be able to fetch a variable event', async () => { const workspace = await workspaceService.createWorkspace(user, { name: 'My workspace', - description: 'Some description' + icon: '🤓' }) const project = await projectService.createProject(user, workspace.slug, { @@ -419,7 +419,7 @@ describe('Event Controller Tests', () => { it('should be able to fetch a workspace role event', async () => { const workspace = await workspaceService.createWorkspace(user, { name: 'My workspace', - description: 'Some description' + icon: '🤓' }) const project = await projectService.createProject(user, workspace.slug, { @@ -485,7 +485,7 @@ describe('Event Controller Tests', () => { it('should be able to fetch all events', async () => { const workspace = await workspaceService.createWorkspace(user, { name: 'My workspace', - description: 'Some description' + icon: '🤓' }) const response = await app.inject({ @@ -518,7 +518,7 @@ describe('Event Controller Tests', () => { it('should throw an error with wrong severity value', async () => { const workspace = await workspaceService.createWorkspace(user, { name: 'My workspace', - description: 'Some description' + icon: '🤓' }) const response = await app.inject({ @@ -535,7 +535,7 @@ describe('Event Controller Tests', () => { it('should throw an error with wrong source value', async () => { const workspace = await workspaceService.createWorkspace(user, { name: 'My workspace', - description: 'Some description' + icon: '🤓' }) const response = await app.inject({ @@ -552,7 +552,7 @@ describe('Event Controller Tests', () => { it('should throw an error if user is not provided in event creation for user-triggered event', async () => { const workspace = await workspaceService.createWorkspace(user, { name: 'My workspace', - description: 'Some description' + icon: '🤓' }) try { @@ -577,7 +577,7 @@ describe('Event Controller Tests', () => { it('should throw an exception for invalid event source', async () => { const workspace = await workspaceService.createWorkspace(user, { name: 'My workspace', - description: 'Some description' + icon: '🤓' }) try { @@ -602,7 +602,7 @@ describe('Event Controller Tests', () => { it('should throw an exception for invalid event type', async () => { const workspace = await workspaceService.createWorkspace(user, { name: 'My workspace', - description: 'Some description' + icon: '🤓' }) try { diff --git a/apps/api/src/prisma/migrations/20240915122629_add_icon_remove_description_from_workspace/migration.sql b/apps/api/src/prisma/migrations/20240915122629_add_icon_remove_description_from_workspace/migration.sql new file mode 100644 index 00000000..dd48e54f --- /dev/null +++ b/apps/api/src/prisma/migrations/20240915122629_add_icon_remove_description_from_workspace/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `description` on the `Workspace` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Workspace" DROP COLUMN "description", +ADD COLUMN "icon" TEXT; diff --git a/apps/api/src/prisma/schema.prisma b/apps/api/src/prisma/schema.prisma index db9dc1c7..5aaa434c 100644 --- a/apps/api/src/prisma/schema.prisma +++ b/apps/api/src/prisma/schema.prisma @@ -449,12 +449,12 @@ model Workspace { id String @id @default(cuid()) name String slug String @unique - description String? isFreeTier Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt ownerId String isDefault Boolean @default(false) + icon String? lastUpdatedBy User? @relation(fields: [lastUpdatedById], references: [id], onUpdate: Cascade, onDelete: SetNull) lastUpdatedById String? diff --git a/apps/api/src/project/project.e2e.spec.ts b/apps/api/src/project/project.e2e.spec.ts index 88b81f49..a27ab523 100644 --- a/apps/api/src/project/project.e2e.spec.ts +++ b/apps/api/src/project/project.e2e.spec.ts @@ -57,7 +57,7 @@ describe('Project Controller Tests', () => { let user1: User, user2: User let workspace1: Workspace, workspace2: Workspace - let project1: Project, project2: Project, project3: Project + let project1: Project, project2: Project, project3: Project, project4: Project beforeAll(async () => { const moduleRef = await Test.createTestingModule({ @@ -142,6 +142,13 @@ describe('Project Controller Tests', () => { storePrivateKey: true, accessLevel: ProjectAccessLevel.GLOBAL })) as Project + + project4 = (await projectService.createProject(user2, workspace2.slug, { + name: 'Project4', + description: + 'Project for testing if all environments,secrets and keys are being fetched or not', + storePrivateKey: true + })) as Project }) afterEach(async () => { @@ -519,6 +526,97 @@ describe('Project Controller Tests', () => { expect(response.statusCode).toBe(401) }) + + it('should fetch correct counts of environments, variables, and secrets for projects in a workspace', async () => { + // Add an environment to the project + const environment = (await environmentService.createEnvironment( + user2, + { + name: 'Dev' + }, + project4.slug + )) as Environment + + // Add two secrets + ;(await secretService.createSecret( + user2, + { + name: 'API_KEY', + entries: [ + { + value: 'some_key', + environmentSlug: environment.slug + } + ] + }, + project4.slug + )) as Secret + ;(await secretService.createSecret( + user2, + { + name: 'DB_PASSWORD', + entries: [ + { + value: 'password', + environmentSlug: environment.slug + } + ] + }, + project4.slug + )) as Secret + + // Add two variables + ;(await variableService.createVariable( + user2, + { + name: 'PORT', + entries: [ + { + value: '8080', + environmentSlug: environment.slug + } + ] + }, + project4.slug + )) as Variable + ;(await variableService.createVariable( + user2, + { + name: 'EXPIRY', + entries: [ + { + value: '3600', + environmentSlug: environment.slug + } + ] + }, + project4.slug + )) as Variable + + const response = await app.inject({ + method: 'GET', + url: `/project/all/${workspace2.slug}?page=0&limit=10&search=Project4`, + headers: { + 'x-e2e-user-email': user2.email + } + }) + + expect(response.statusCode).toBe(200) + expect(response.json().items.length).toEqual(1) + + const project = response.json().items[0] + expect(project.totalEnvironmentsOfProject).toEqual(2) + expect(project.totalVariablesOfProject).toEqual(2) + expect(project.totalSecretsOfProject).toEqual(2) + // Verify project details + expect(project.name).toEqual('Project4') + expect(project.description).toEqual( + 'Project for testing if all environments,secrets and keys are being fetched or not' + ) + // Verify that sensitive data is not included + expect(project).not.toHaveProperty('privateKey') + expect(project).not.toHaveProperty('publicKey') + }) }) it('should create environments if provided', async () => { @@ -852,12 +950,16 @@ describe('Project Controller Tests', () => { ) // Add user to workspace as a member - await workspaceMembershipService.inviteUsersToWorkspace(user1, workspace1.slug, [ - { - email: johnny.email, - roleSlugs: [role.slug] - } - ]) + await workspaceMembershipService.inviteUsersToWorkspace( + user1, + workspace1.slug, + [ + { + email: johnny.email, + roleSlugs: [role.slug] + } + ] + ) // Accept the invitation on behalf of the user await workspaceMembershipService.acceptInvitation(johnny, workspace1.slug) diff --git a/apps/api/src/project/service/project.service.ts b/apps/api/src/project/service/project.service.ts index 421d2832..78acca95 100644 --- a/apps/api/src/project/service/project.service.ts +++ b/apps/api/src/project/service/project.service.ts @@ -777,7 +777,7 @@ export class ProjectService { const workspaceId = workspace.id //fetch projects with required properties - const items = ( + const projects = ( await this.prisma.project.findMany({ skip: page * limit, take: limitMaxItemsPerPage(limit), @@ -801,7 +801,19 @@ export class ProjectService { workspace: { members: { some: { - userId: user.id + userId: user.id, + roles: { + some: { + role: { + authorities: { + hasSome: [ + Authority.WORKSPACE_ADMIN, + Authority.READ_PROJECT + ] + } + } + } + } } } } @@ -809,6 +821,76 @@ export class ProjectService { }) ).map((project) => excludeFields(project, 'privateKey', 'publicKey')) + const items = await Promise.all( + projects.map(async (project) => { + let totalEnvironmentsOfProject = 0 + let totalVariablesOfProject = 0 + let totalSecretsOfProject = 0 + // When we later implement RBAC for environments, we would need to updated + // this code to only include environments like we do while fetching projects. + + // What would be even better is, we should fetch environments directly. And then, + // accumulate the projects into a set of projects. And then, return that set along + // with the required data. + const allEnvs = await this.prisma.environment.findMany({ + where: { projectId: project.id } + }) + + // This entire block will become invalid after RBAC for environments are implemented + const envPromises = allEnvs.map(async (env) => { + const hasRequiredPermission = + await this.authorityCheckerService.checkAuthorityOverEnvironment({ + userId: user.id, + entity: { slug: env.slug }, + authorities: [ + Authority.READ_ENVIRONMENT, + Authority.READ_SECRET, + Authority.READ_VARIABLE + ], + prisma: this.prisma + }) + if (hasRequiredPermission) { + totalEnvironmentsOfProject += 1 + + const fetchSecretCount = this.prisma.secret.count({ + where: { + projectId: project.id, + versions: { some: { environmentId: env.id } } + } + }) + + const fetchVariableCount = this.prisma.variable.count({ + where: { + projectId: project.id, + versions: { some: { environmentId: env.id } } + } + }) + + return this.prisma.$transaction([ + fetchSecretCount, + fetchVariableCount + ]) + } + return [0, 0] + }) + const counts = await Promise.all(envPromises) + totalSecretsOfProject = counts.reduce( + (sum, [secretCount]) => sum + secretCount, + 0 + ) + totalVariablesOfProject = counts.reduce( + (sum, [, variableCount]) => sum + variableCount, + 0 + ) + return { + ...project, + totalEnvironmentsOfProject, + totalVariablesOfProject, + totalSecretsOfProject + } + }) + ) + //calculate metadata const totalCount = await this.prisma.project.count({ where: { diff --git a/apps/api/src/workspace-membership/workspace-membership.e2e.spec.ts b/apps/api/src/workspace-membership/workspace-membership.e2e.spec.ts index 9a28da49..01954cff 100644 --- a/apps/api/src/workspace-membership/workspace-membership.e2e.spec.ts +++ b/apps/api/src/workspace-membership/workspace-membership.e2e.spec.ts @@ -220,7 +220,7 @@ describe('Workspace Membership Controller Tests', () => { it('should not be able to transfer ownership to a non member', async () => { const newWorkspace = await workspaceService.createWorkspace(user1, { name: 'Workspace 2', - description: 'Workspace 2 description' + icon: '🤓' }) const response = await app.inject({ @@ -241,7 +241,7 @@ describe('Workspace Membership Controller Tests', () => { it('should not be able to transfer ownership to a member who did not accept the invitation', async () => { const newWorkspace = await workspaceService.createWorkspace(user1, { name: 'Workspace 2', - description: 'Workspace 2 description' + icon: '🤓' }) // Create membership @@ -265,7 +265,7 @@ describe('Workspace Membership Controller Tests', () => { it('should be able to transfer the ownership of the workspace', async () => { const newWorkspace = await workspaceService.createWorkspace(user1, { name: 'Workspace 2', - description: 'Workspace 2 description' + icon: '🤓' }) // Create membership @@ -306,7 +306,7 @@ describe('Workspace Membership Controller Tests', () => { it('should not be able to transfer ownership if is not admin', async () => { const newWorkspace = await workspaceService.createWorkspace(user1, { name: 'Workspace 2', - description: 'Workspace 2 description' + icon: '🤓' }) // Create membership diff --git a/apps/api/src/workspace/dto/create.workspace/create.workspace.ts b/apps/api/src/workspace/dto/create.workspace/create.workspace.ts index 71f81dba..5be165a3 100644 --- a/apps/api/src/workspace/dto/create.workspace/create.workspace.ts +++ b/apps/api/src/workspace/dto/create.workspace/create.workspace.ts @@ -7,5 +7,5 @@ export class CreateWorkspace { @IsString() @IsOptional() - description?: string + icon?: string } diff --git a/apps/api/src/workspace/service/workspace.service.ts b/apps/api/src/workspace/service/workspace.service.ts index 4daea549..a13c2538 100644 --- a/apps/api/src/workspace/service/workspace.service.ts +++ b/apps/api/src/workspace/service/workspace.service.ts @@ -96,7 +96,7 @@ export class WorkspaceService { slug: dto.name ? await generateEntitySlug(dto.name, 'WORKSPACE', this.prisma) : undefined, - description: dto.description, + icon: dto.icon, lastUpdatedBy: { connect: { id: user.id @@ -210,18 +210,10 @@ export class WorkspaceService { userId: user.id } }, - OR: [ - { - name: { - contains: search - } - }, - { - description: { - contains: search - } - } - ] + + name: { + contains: search + } } }) @@ -233,18 +225,10 @@ export class WorkspaceService { userId: user.id } }, - OR: [ - { - name: { - contains: search - } - }, - { - description: { - contains: search - } - } - ] + + name: { + contains: search + } } }) @@ -281,7 +265,7 @@ export class WorkspaceService { const data: any = {} data.name = workspace.name - data.description = workspace.description + data.icon = workspace.icon // Get all the roles of the workspace data.workspaceRoles = await this.prisma.workspaceRole.findMany({ diff --git a/apps/api/src/workspace/workspace.e2e.spec.ts b/apps/api/src/workspace/workspace.e2e.spec.ts index 098452cb..ce019d1b 100644 --- a/apps/api/src/workspace/workspace.e2e.spec.ts +++ b/apps/api/src/workspace/workspace.e2e.spec.ts @@ -192,7 +192,7 @@ describe('Workspace Controller Tests', () => { url: '/workspace', payload: { name: 'Workspace 1', - description: 'Workspace 1 description' + icon: '🤓' } }) @@ -201,7 +201,7 @@ describe('Workspace Controller Tests', () => { expect(body.name).toBe('Workspace 1') expect(body.slug).toBeDefined() - expect(body.description).toBe('Workspace 1 description') + expect(body.icon).toBe('🤓') expect(body.ownerId).toBe(user1.id) expect(body.isFreeTier).toBe(true) expect(body.isDefault).toBe(false) @@ -216,7 +216,7 @@ describe('Workspace Controller Tests', () => { url: '/workspace', payload: { name: 'My Workspace', - description: 'My Workspace description' + icon: '🤓' } }) @@ -231,7 +231,7 @@ describe('Workspace Controller Tests', () => { it('should let other user to create workspace with same name', async () => { await workspaceService.createWorkspace(user1, { name: 'Workspace 1', - description: 'Workspace 1 description' + icon: '🤓' }) const response = await app.inject({ @@ -242,7 +242,7 @@ describe('Workspace Controller Tests', () => { url: '/workspace', payload: { name: 'Workspace 1', - description: 'Workspace 1 description' + icon: '🤓' } }) @@ -250,7 +250,7 @@ describe('Workspace Controller Tests', () => { workspace2 = response.json() expect(workspace2.name).toBe('Workspace 1') - expect(workspace2.description).toBe('Workspace 1 description') + expect(workspace2.icon).toBe('🤓') expect(workspace2.ownerId).toBe(user2.id) expect(workspace2.isFreeTier).toBe(true) expect(workspace2.isDefault).toBe(false) @@ -321,7 +321,7 @@ describe('Workspace Controller Tests', () => { url: `/workspace/${workspace1.slug}`, payload: { name: 'Workspace 1 Updated', - description: 'Workspace 1 updated description' + icon: '🔥' } }) @@ -330,7 +330,7 @@ describe('Workspace Controller Tests', () => { expect(body.name).toBe('Workspace 1 Updated') expect(body.slug).not.toBe(workspace1.slug) - expect(body.description).toBe('Workspace 1 updated description') + expect(body.icon).toBe('🔥') }) it('should not be able to change the name to an existing workspace or same name', async () => { @@ -362,7 +362,7 @@ describe('Workspace Controller Tests', () => { url: `/workspace/${workspace1.slug}`, payload: { name: 'Workspace 1 Updated', - description: 'Workspace 1 updated description' + icon: '🤓' } }) @@ -371,7 +371,7 @@ describe('Workspace Controller Tests', () => { it('should have created a WORKSPACE_UPDATED event', async () => { await workspaceService.updateWorkspace(user1, workspace1.slug, { - description: 'Workspace 1 Description' + icon: '🤓' }) const response = await fetchEvents( @@ -526,7 +526,7 @@ describe('Workspace Controller Tests', () => { const body = response.json() expect(body.name).toEqual(workspace1.name) - expect(body.description).toEqual(workspace1.description) + expect(body.icon).toEqual(workspace1.icon) expect(body.workspaceRoles).toBeInstanceOf(Array) expect(body.projects).toBeInstanceOf(Array) }) @@ -536,7 +536,7 @@ describe('Workspace Controller Tests', () => { it('should be able to delete the workspace', async () => { const newWorkspace = await workspaceService.createWorkspace(user1, { name: 'Workspace 2', - description: 'Workspace 2 description' + icon: '🤓' }) const response = await app.inject({ diff --git a/apps/web/public/about/team/aritra.jpeg b/apps/web/public/about/team/aritra.jpeg index 0844e879..3b2f286d 100644 Binary files a/apps/web/public/about/team/aritra.jpeg and b/apps/web/public/about/team/aritra.jpeg differ diff --git a/apps/web/src/app/(main)/career/page.tsx b/apps/web/src/app/(main)/career/page.tsx index ad444071..24d0b8c2 100644 --- a/apps/web/src/app/(main)/career/page.tsx +++ b/apps/web/src/app/(main)/career/page.tsx @@ -1,4 +1,5 @@ import { ColorBGSVG } from '@public/hero' +import Link from 'next/link' import EncryptButton from '@/components/ui/encrypt-btn' function Career(): React.JSX.Element { @@ -8,8 +9,14 @@ function Career(): React.JSX.Element {

Careers at KeyShade

- We are booting up, keep an eye out for open positions. Meanwhile, you - can contribute to our project. + We are booting up, keep an eye out for open positions on our{' '} + + Notion board + + . Meanwhile, you can contribute to our project.

(response) } + + async getRevisionsOfSecret( + request: GetRevisionsOfSecretRequest, + headers?: Record + ): Promise> { + const url = parsePaginationUrl( + `/api/secret/${request.secretSlug}/revisions/${request.environmentSlug}`, + request + ) + const response = await this.apiClient.get(url, headers) + + return await parseResponse(response) + } } diff --git a/packages/api-client/src/controllers/variable.ts b/packages/api-client/src/controllers/variable.ts index 83a058fb..25256b10 100644 --- a/packages/api-client/src/controllers/variable.ts +++ b/packages/api-client/src/controllers/variable.ts @@ -11,6 +11,8 @@ import { GetAllVariablesOfEnvironmentResponse, GetAllVariablesOfProjectRequest, GetAllVariablesOfProjectResponse, + GetRevisionsOfVariableRequest, + GetRevisionsOfVariableResponse, RollBackVariableRequest, RollBackVariableResponse, UpdateVariableRequest, @@ -96,4 +98,17 @@ export default class VariableController { return await parseResponse(response) } + + async getRevisionsOfVariable( + request: GetRevisionsOfVariableRequest, + headers: Record + ): Promise> { + const url = parsePaginationUrl( + `/api/variable/${request.variableSlug}/revisions/${request.environmentSlug}`, + request + ) + const response = await this.apiClient.get(url, headers) + + return await parseResponse(response) + } } diff --git a/packages/api-client/src/core/pagination-parser.ts b/packages/api-client/src/core/pagination-parser.ts index c9c4307b..b25a1dd8 100644 --- a/packages/api-client/src/core/pagination-parser.ts +++ b/packages/api-client/src/core/pagination-parser.ts @@ -10,7 +10,7 @@ import { PageRequest } from '@api-client/types/index.types' */ export function parsePaginationUrl( baseUrl: string, - request: PageRequest + request: Partial ): string { let url = `${baseUrl}?` request.page && (url += `page=${request.page}&`) diff --git a/packages/api-client/src/types/environment.types.d.ts b/packages/api-client/src/types/environment.types.d.ts index d8d8a545..076ddc32 100644 --- a/packages/api-client/src/types/environment.types.d.ts +++ b/packages/api-client/src/types/environment.types.d.ts @@ -1,12 +1,6 @@ import { PageRequest, PageResponse } from './index.types' -export interface CreateEnvironmentRequest { - name: string - description?: string - projectId: string -} - -export interface CreateEnvironmentResponse { +interface Environment { id: string name: string slug: string @@ -17,57 +11,42 @@ export interface CreateEnvironmentResponse { projectId: string } -export interface UpdateEnvironmentRequest { - slug: string - name?: string +export interface CreateEnvironmentRequest { + name: string description?: string + projectId: string } -export interface UpdateEnvironmentResponse { - id: string - name: string +export interface CreateEnvironmentResponse extends Environment {} + +export interface UpdateEnvironmentRequest + extends Partial> { slug: string - description: string | null - createdAt: string - updatedAt: string - lastUpdatedById: string - projectId: string } +export interface UpdateEnvironmentResponse extends Environment {} + export interface GetEnvironmentRequest { slug: string } -export interface GetEnvironmentResponse { - id: string - name: string - slug: string - description: string | null - createdAt: string - updatedAt: string - lastUpdatedById: string - projectId: string -} +export interface GetEnvironmentResponse extends Environment {} export interface GetAllEnvironmentsOfProjectRequest extends PageRequest { projectSlug: string } export interface GetAllEnvironmentsOfProjectResponse - extends PageResponse<{ - id: string - slug: string - name: string - description: string | null - createdAt: string - updatedAt: string - lastUpdatedBy: { - id: string - name: string - email: string - profilePictureUrl: string | null + extends PageResponse< + Environment & { + lastUpdatedBy: { + id: string + name: string + email: string + profilePictureUrl: string | null + } } - }> {} + > {} export interface DeleteEnvironmentRequest { slug: string diff --git a/packages/api-client/src/types/integration.types.d.ts b/packages/api-client/src/types/integration.types.d.ts index 75b15806..035f5658 100644 --- a/packages/api-client/src/types/integration.types.d.ts +++ b/packages/api-client/src/types/integration.types.d.ts @@ -36,17 +36,8 @@ export enum EventType { INTEGRATION_UPDATED, INTEGRATION_DELETED } -export interface CreateIntegrationRequest { - workspaceSlug?: string - projectSlug?: string - name: string - type: string - notifyOn: [string] - metadata: Record - environmentSlug: string -} -export interface CreateIntegrationResponse { +interface Integration { id: string name: string slug: string @@ -60,31 +51,25 @@ export interface CreateIntegrationResponse { environmentId: string } -export interface UpdateIntegrationRequest { - integrationSlug: string +export interface CreateIntegrationRequest { workspaceSlug?: string projectSlug?: string - name?: string - type?: IntegrationType - notifyOn?: EventType[] - metadata?: Record - environmentId?: string -} - -export interface UpdateIntegrationResponse { - id: string name: string - slug: string + type: string + notifyOn: [string] metadata: Record - createdAt: string - updatedAt: string - type: IntegrationType - notifyOn: EventType[] - workspaceId: string - projectId: string - environmentId: string + environmentSlug: string } +export interface CreateIntegrationResponse extends Integration {} + +export interface UpdateIntegrationRequest + extends Partial> { + integrationSlug: string +} + +export interface UpdateIntegrationResponse extends Integration {} + export interface DeleteIntegrationResponse {} export interface DeleteIntegrationRequest { @@ -95,35 +80,10 @@ export interface GetIntegrationRequest { integrationSlug: string } -export interface GetIntegrationResponse { - id: string - name: string - slug: string - metadata: Record - createdAt: string - updatedAt: string - type: IntegrationType - notifyOn: EventType[] - workspaceId: string - projectId: string - environmentId: string -} +export interface GetIntegrationResponse extends Integration {} export interface GetAllIntegrationRequest extends PageRequest { workspaceSlug: string } -export interface GetAllIntegrationResponse - extends PageResponse<{ - id: string - name: string - slug: string - metadata: Record - createdAt: string - updatedAt: string - type: IntegrationType - notifyOn: EventType[] - workspaceId: string - projectId: string - environmentId: string - }> {} +export interface GetAllIntegrationResponse extends PageResponse {} diff --git a/packages/api-client/src/types/project.types.d.ts b/packages/api-client/src/types/project.types.d.ts index a3569205..55afb05b 100644 --- a/packages/api-client/src/types/project.types.d.ts +++ b/packages/api-client/src/types/project.types.d.ts @@ -1,15 +1,6 @@ import { PageRequest, PageResponse } from './index.types' -export interface CreateProjectRequest { - name: string - workspaceSlug: string - description?: string - storePrivateKey?: boolean - environments?: CreateEnvironment[] - accessLevel: string -} - -export interface CreateProjectResponse { +interface Project { id: string name: string slug: string @@ -28,30 +19,26 @@ export interface CreateProjectResponse { forkedFromId: string } -export interface UpdateProjectRequest { - projectSlug: string - name?: string -} - -export interface UpdateProjectResponse { - id: string +export interface CreateProjectRequest { name: string - slug: string - description: string - createdAt: string - updatedAt: string - publicKey: string - privateKey: string - storePrivateKey: boolean - isDisabled: boolean + workspaceSlug: string + description?: string + storePrivateKey?: boolean + environments?: CreateEnvironment[] accessLevel: string - pendingCreation: boolean - isForked: boolean - lastUpdatedById: string - workspaceId: string - forkedFromId: string } +export interface CreateProjectResponse extends Project {} + +export interface UpdateProjectRequest + extends Partial> { + projectSlug: string + regenerateKeyPair?: boolean + privateKey?: string +} + +export interface UpdateProjectResponse extends Project {} + export interface DeleteProjectRequest { projectSlug: string } @@ -62,24 +49,7 @@ export interface GetProjectRequest { projectSlug: string } -export interface GetProjectResponse { - id: string - name: string - slug: string - description: string - createdAt: string - updatedAt: string - publicKey: string - privateKey: string - storePrivateKey: boolean - isDisabled: boolean - accessLevel: string - pendingCreation: boolean - isForked: boolean - lastUpdatedById: string - workspaceId: string - forkedFromId: string -} +export interface GetProjectResponse extends Project {} export interface ForkProjectRequest { projectSlug: string @@ -88,24 +58,7 @@ export interface ForkProjectRequest { storePrivateKey?: boolean } -export interface ForkProjectResponse { - id: string - name: string - slug: string - description: string - createdAt: string - updatedAt: string - publicKey: string - privateKey: string - storePrivateKey: boolean - isDisabled: boolean - accessLevel: string - pendingCreation: boolean - isForked: boolean - lastUpdatedById: string - workspaceId: string - forkedFromId: string -} +export interface ForkProjectResponse extends Project {} export interface SyncProjectRequest { projectSlug: string @@ -125,44 +78,10 @@ export interface GetForkRequest extends PageRequest { workspaceSlug: string } -export interface GetForkResponse - extends PageResponse<{ - id: string - name: string - slug: string - description: string - createdAt: string - updatedAt: string - publicKey: string - privateKey: string - storePrivateKey: boolean - isDisabled: boolean - accessLevel: string - pendingCreation: boolean - isForked: boolean - lastUpdatedById: string - workspaceId: string - forkedFromId: string - }> {} +export interface GetForkResponse extends PageResponse {} export interface GetAllProjectsRequest extends PageRequest { workspaceSlug: string } -export interface GetAllProjectsResponse - extends PageResponse<{ - id: string - name: string - slug: string - description: string - createdAt: string - updatedAt: string - storePrivateKey: boolean - isDisabled: boolean - accessLevel: string - pendingCreation: boolean - isForked: boolean - lastUpdatedById: string - workspaceId: string - forkedFromId: string - }> {} +export interface GetAllProjectsResponse extends PageResponse {} diff --git a/packages/api-client/src/types/secret.types.d.ts b/packages/api-client/src/types/secret.types.d.ts index 21511a20..4d07a84d 100644 --- a/packages/api-client/src/types/secret.types.d.ts +++ b/packages/api-client/src/types/secret.types.d.ts @@ -1,19 +1,6 @@ import { PageRequest, PageResponse } from './index.types' -export interface CreateSecretRequest { - projectSlug: string - name: string - note?: string - rotateAfter?: '24' | '168' | '720' | '8760' | 'never' - entries?: [ - { - value: string - environmentSlug: string - } - ] -} - -export interface CreateSecretResponse { +interface Secret { id: string name: string slug: string @@ -28,15 +15,16 @@ export interface CreateSecretResponse { } versions: [ { - value: string + id?: string environmentId: string + value: string } ] } -export interface UpdateSecretRequest { - secretSlug: string - name?: string +export interface CreateSecretRequest { + projectSlug: string + name: string note?: string rotateAfter?: '24' | '168' | '720' | '8760' | 'never' entries?: [ @@ -47,13 +35,15 @@ export interface UpdateSecretRequest { ] } +export interface CreateSecretResponse extends Secret {} + +export interface UpdateSecretRequest + extends Partial> { + secretSlug: string +} + export interface UpdateSecretResponse { - secret: { - id: string - name: string - note: string - slug: string - } + secret: Pick updatedVersions: [ { id?: string @@ -83,16 +73,7 @@ export interface GetAllSecretsOfProjectRequest extends PageRequest { } export interface GetAllSecretsOfProjectResponse extends PageResponse<{ - secret: { - id: string - slug: string - name: string - createdAt: string - updatedAt: string - rotateAt: string - note: string | null - lastUpdatedById: string - projectId: string + secret: Omit & { lastUpdatedBy: { id: string name: string @@ -117,3 +98,18 @@ export type GetAllSecretsOfEnvironmentResponse = { value: string isPlaintext: boolean }[] + +export interface GetRevisionsOfSecretRequest extends Partial { + secretSlug: string + environmentSlug: string +} + +export interface GetRevisionsOfSecretResponse + extends PageResponse<{ + id: string + value: string + version: number + createdOn: string + createdById: string + environmentId: string + }> {} diff --git a/packages/api-client/src/types/variable.types.d.ts b/packages/api-client/src/types/variable.types.d.ts index 3c38a59f..7240b3aa 100644 --- a/packages/api-client/src/types/variable.types.d.ts +++ b/packages/api-client/src/types/variable.types.d.ts @@ -1,18 +1,6 @@ import { PageRequest, PageResponse } from './index.types' -export interface CreateVariableRequest { - projectSlug: string - name: string - note?: string - entries?: [ - { - value: string - environmentSlug: string - } - ] -} - -export interface CreateVariableResponse { +interface Variable { id: string name: string slug: string @@ -31,6 +19,21 @@ export interface CreateVariableResponse { } ] } + +export interface CreateVariableRequest { + projectSlug: string + name: string + note?: string + entries?: [ + { + value: string + environmentSlug: string + } + ] +} + +export interface CreateVariableResponse extends Variable {} + export interface UpdateVariableRequest { variableSlug: string name?: string @@ -42,12 +45,7 @@ export interface UpdateVariableRequest { ] } export interface UpdateVariableResponse { - variable: { - id: string - name: string - note: string - slug: string - } + variable: Pick updatedVersions: [ { value: string @@ -77,32 +75,26 @@ export interface GetAllVariablesOfProjectRequest extends PageRequest { } export interface GetAllVariablesOfProjectResponse - extends PageResponse<{ - variable: { - id: string - name: string - slug: string - createdAt: string - updatedAt: string - note: string | null - lastUpdatedById: string - projectId: string - lastUpdatedBy: { - id: string - name: string - } - } - values: { - environment: { - id: string - name: string + extends PageResponse< + Omit & { + variable: { + lastUpdatedBy: { + id: string + name: string + } } - value: string - version: number + values: { + environment: { + id: string + name: string + } + value: string + version: number + }[] } - }> {} + > {} -export interface GetAllVariablesOfEnvironmentRequest extends PageRequest { +export interface GetAllVariablesOfEnvironmentRequest { projectSlug: string environmentSlug: string } @@ -112,3 +104,19 @@ export type GetAllVariablesOfEnvironmentResponse = { value: string isPlaintext: boolean }[] + +export interface GetRevisionsOfVariableRequest extends Partial { + variableSlug: string + environmentSlug: string +} + +export interface GetRevisionsOfVariableResponse + extends PageResponse<{ + id: string + value: string + version: number + variableId: string + createdOn: string + createdById: string + environmentId: string + }> {} diff --git a/packages/api-client/src/types/workspace.types.d.ts b/packages/api-client/src/types/workspace.types.d.ts index 9def521d..b9c190a1 100644 --- a/packages/api-client/src/types/workspace.types.d.ts +++ b/packages/api-client/src/types/workspace.types.d.ts @@ -4,7 +4,7 @@ interface Workspace { id: string name: string slug: string - description: string + icon: string isFreeTier: boolean createdAt: string updatedAt: string @@ -15,7 +15,7 @@ interface Workspace { export interface CreateWorkspaceRequest { name: string - description?: string + icon?: string } export interface CreateWorkspaceResponse extends Workspace {} @@ -50,7 +50,7 @@ export interface ExportDataRequest { export interface ExportDataResponse { name: string - description: string + icon: string workspaceRoles: { name: string description: string diff --git a/packages/api-client/tests/secret.spec.ts b/packages/api-client/tests/secret.spec.ts index a9b9b8ed..38cd6966 100644 --- a/packages/api-client/tests/secret.spec.ts +++ b/packages/api-client/tests/secret.spec.ts @@ -84,18 +84,7 @@ describe('Secret Controller Tests', () => { }, { 'x-e2e-user-email': email } ) - - expect(createSecretResponse.data.slug).toBeDefined() - secretSlug = createSecretResponse.data.slug - - // Fetch all secrets - const secrets = await secretController.getAllSecretsOfProject( - { projectSlug }, - { 'x-e2e-user-email': email } - ) - - expect(secrets.data.items.length).toBe(1) }) afterEach(async () => { @@ -252,4 +241,12 @@ describe('Secret Controller Tests', () => { ) expect(secrets.data.items.length).toBe(0) }) + + it('should be able to fetch revisions of a secret', async () => { + const revisions = await secretController.getRevisionsOfSecret( + { secretSlug, environmentSlug }, + { 'x-e2e-user-email': email } + ) + expect(revisions.data.items.length).toBe(1) + }) }) diff --git a/packages/api-client/tests/variable.spec.ts b/packages/api-client/tests/variable.spec.ts index b7295a4e..9d25af96 100644 --- a/packages/api-client/tests/variable.spec.ts +++ b/packages/api-client/tests/variable.spec.ts @@ -9,7 +9,7 @@ describe('Get Variable Tests', () => { const email = 'johndoe@example.com' let workspaceSlug: string | null let projectSlug: string | null - let environment: any + let environment let variableSlug: string | null beforeAll(async () => { @@ -64,8 +64,7 @@ describe('Get Variable Tests', () => { }) }) - // Create a variable - it('should create a variable', async () => { + beforeEach(async () => { const variable = await variableController.createVariable( { projectSlug, @@ -78,11 +77,50 @@ describe('Get Variable Tests', () => { 'x-e2e-user-email': email } ) - expect(variable.data.name).toBe('Variable 1') + + variableSlug = variable.data.slug + }) + + afterEach(async () => { + await variableController.deleteVariable( + { + variableSlug + }, + { + 'x-e2e-user-email': email + } + ) + }) + + // Create a variable + it('should create a variable', async () => { + const variable = await variableController.createVariable( + { + projectSlug, + name: 'Variable 2', + entries: [ + { value: 'Variable 2 value', environmentSlug: environment.slug } + ] + }, + { + 'x-e2e-user-email': email + } + ) + expect(variable.data.name).toBe('Variable 2') expect(variable.data.versions.length).toBe(1) - expect(variable.data.versions[0].value).toBe('Variable 1 value') + expect(variable.data.versions[0].value).toBe('Variable 2 value') expect(variable.data.versions[0].environmentId).toBe(environment.id) - variableSlug = variable.data.slug + + // Delete the variable + const deleteVariable = await variableController.deleteVariable( + { + variableSlug: variable.data.slug + }, + { + 'x-e2e-user-email': email + } + ) + expect(deleteVariable.success).toBe(true) }) // Update Name of the Variable @@ -98,7 +136,16 @@ describe('Get Variable Tests', () => { ) expect(updatedVariable.data.variable.name).toBe('UpdatedVariable 1') - variableSlug = updatedVariable.data.variable.slug + // Delete the variable + const deleteVariable = await variableController.deleteVariable( + { + variableSlug: updatedVariable.data.variable.slug + }, + { + 'x-e2e-user-email': email + } + ) + expect(deleteVariable.success).toBe(true) }) // Create a new version of Variable @@ -124,7 +171,21 @@ describe('Get Variable Tests', () => { // Roll back a variable it('should rollback a variable', async () => { - const rolledBackVariable: any = await variableController.rollbackVariable( + // Add a new version + await variableController.updateVariable( + { + entries: [ + { + value: '1234', + environmentSlug: environment.slug + } + ], + variableSlug + }, + { 'x-e2e-user-email': email } + ) + + const rolledBackVariable = await variableController.rollbackVariable( { variableSlug, version: 1, @@ -137,65 +198,44 @@ describe('Get Variable Tests', () => { // Get all the variables of project it('should get all variables of project', async () => { - const response: any = await variableController.getAllVariablesOfProject( + const response = await variableController.getAllVariablesOfProject( { projectSlug }, { 'x-e2e-user-email': email } ) + expect(response.data.items.length).toBe(1) const variable1 = response.data.items[0] const variable = variable1.variable const values = variable1.values expect(variable).toHaveProperty('slug') - expect(typeof variable.slug).toBe('string') expect(variable).toHaveProperty('name') - expect(typeof variable.name).toBe('string') - expect(variable).toHaveProperty('createdAt') - expect(typeof variable.createdAt).toBe('string') - expect(variable).toHaveProperty('updatedAt') - expect(typeof variable.updatedAt).toBe('string') - expect(variable).toHaveProperty('note') - expect(typeof variable.note === 'string' || variable.note === null).toBe( - true - ) - expect(variable).toHaveProperty('lastUpdatedById') - expect(typeof variable.lastUpdatedById).toBe('string') expect(variable.lastUpdatedBy).toHaveProperty('id') - expect(typeof variable.lastUpdatedBy.id).toBe('string') - expect(variable.lastUpdatedBy).toHaveProperty('name') - expect(typeof variable.lastUpdatedBy.name).toBe('string') values.forEach((value) => { expect(value).toHaveProperty('environment') expect(value.environment).toHaveProperty('id') - expect(typeof value.environment.id).toBe('string') expect(value.environment).toHaveProperty('name') - expect(typeof value.environment.name).toBe('string') - expect(value).toHaveProperty('value') - expect(typeof value.value).toBe('string') - expect(value).toHaveProperty('version') - expect(typeof value.version).toBe('number') }) }) // Get all variables for an environment it('should get all variables for an environment', async () => { - const variables: any = - await variableController.getAllVariablesOfEnvironment( - { - environmentSlug: environment.slug, - projectSlug - }, - { 'x-e2e-user-email': email } - ) + const variables = await variableController.getAllVariablesOfEnvironment( + { + environmentSlug: environment.slug, + projectSlug + }, + { 'x-e2e-user-email': email } + ) expect(variables.data.length).toBe(1) variables.data.forEach((variable) => { @@ -209,7 +249,7 @@ describe('Get Variable Tests', () => { expect(typeof variable.isPlaintext).toBe('boolean') }) const variable1 = variables.data[0] - expect(variable1.name).toBe('UpdatedVariable 1') + expect(variable1.name).toBe('Variable 1') expect(variable1.value).toBe('Variable 1 value') expect(variable1.isPlaintext).toBe(true) }) @@ -220,10 +260,18 @@ describe('Get Variable Tests', () => { { variableSlug }, { 'x-e2e-user-email': email } ) - const variables: any = await variableController.getAllVariablesOfProject( + const variables = await variableController.getAllVariablesOfProject( { projectSlug }, { 'x-e2e-user-email': email } ) expect(variables.data.items.length).toBe(0) }) + + it('should be able to fetch revisions of a secret', async () => { + const revisions = await variableController.getRevisionsOfVariable( + { variableSlug, environmentSlug: environment.slug }, + { 'x-e2e-user-email': email } + ) + expect(revisions.data.items.length).toBe(1) + }) }) diff --git a/packages/api-client/tests/workspace.spec.ts b/packages/api-client/tests/workspace.spec.ts index a961ab77..3608fc24 100644 --- a/packages/api-client/tests/workspace.spec.ts +++ b/packages/api-client/tests/workspace.spec.ts @@ -78,7 +78,7 @@ describe('Workspaces Controller Tests', () => { await workspaceController.createWorkspace( { name: 'New Workspace', - description: 'This is a new workspace' + icon: '🤓' }, { 'x-e2e-user-email': email diff --git a/packages/schema/src/workspace.ts b/packages/schema/src/workspace.ts index 88d2e6c5..7164eee5 100644 --- a/packages/schema/src/workspace.ts +++ b/packages/schema/src/workspace.ts @@ -2,7 +2,7 @@ import { z } from 'zod' export const CreateWorkspaceSchema = z.object({ name: z.string(), - description: z.string().optional(), + icon: z.string().optional(), isDefault: z.boolean().optional() })