diff --git a/apps/api/src/secret/secret.e2e.spec.ts b/apps/api/src/secret/secret.e2e.spec.ts index da0487d9..6d446980 100644 --- a/apps/api/src/secret/secret.e2e.spec.ts +++ b/apps/api/src/secret/secret.e2e.spec.ts @@ -619,14 +619,28 @@ describe('Secret Controller Tests', () => { expect(response.json().items.length).toBe(1) const { secret, values } = response.json().items[0] - expect(secret.id).toBeDefined() - expect(secret.name).toBeDefined() - expect(secret.note).toBeDefined() - expect(secret.projectId).toBeDefined() + expect(secret).toStrictEqual({ + id: secret1.id, + name: secret1.name, + slug: secret1.slug, + note: secret1.note, + projectId: project1.id, + lastUpdatedById: secret1.lastUpdatedById, + lastUpdatedBy: { + id: user1.id, + name: user1.name + }, + createdAt: secret1.createdAt.toISOString(), + updatedAt: secret1.updatedAt.toISOString(), + rotateAt: secret1.rotateAt.toISOString() + }) expect(values.length).toBe(1) const value = values[0] - expect(value.environment).toBeDefined() + expect(value.version).toBe(1) + expect(value.environment.id).toBe(environment1.id) + expect(value.environment.slug).toBe(environment1.slug) + expect(value.environment.name).toBe(environment1.name) expect(value.value).not.toEqual('Secret 1 value') //check metadata @@ -645,6 +659,69 @@ describe('Secret Controller Tests', () => { ) }) + it('should be able to fetch only new versions of secrets', async () => { + // Update secret1 + await secretService.updateSecret(user1, secret1.slug, { + entries: [ + { + value: 'Secret new 1 value', + environmentSlug: environment1.slug + } + ] + }) + + const response = await app.inject({ + method: 'GET', + url: `/secret/${project1.slug}?page=0&limit=10`, + headers: { + 'x-e2e-user-email': user1.email + } + }) + + expect(response.statusCode).toBe(200) + expect(response.json().items.length).toBe(1) + + const { secret, values } = response.json().items[0] + expect(secret).toStrictEqual({ + id: secret1.id, + name: secret1.name, + slug: secret1.slug, + note: secret1.note, + projectId: project1.id, + lastUpdatedById: secret1.lastUpdatedById, + lastUpdatedBy: { + id: user1.id, + name: user1.name + }, + createdAt: secret1.createdAt.toISOString(), + updatedAt: expect.any(String), + rotateAt: secret1.rotateAt.toISOString() + }) + expect(values.length).toBe(1) + + const value = values[0] + expect(value.version).toBe(2) + expect(value.environment.id).toBe(environment1.id) + expect(value.environment.slug).toBe(environment1.slug) + expect(value.environment.name).toBe(environment1.name) + expect(value.value).not.toEqual('Secret 1 new value') + + //check metadata + const metadata = response.json().metadata + expect(metadata.totalCount).toEqual(1) + expect(metadata.links.self).toEqual( + `/secret/${project1.slug}?decryptValue=false&page=0&limit=10&sort=name&order=asc&search=` + ) + expect(metadata.links.first).toEqual( + `/secret/${project1.slug}?decryptValue=false&page=0&limit=10&sort=name&order=asc&search=` + ) + expect(metadata.links.previous).toBeNull() + expect(metadata.links.next).toBeNull() + expect(metadata.links.last).toEqual( + `/secret/${project1.slug}?decryptValue=false&page=0&limit=10&sort=name&order=asc&search=` + ) + }) + it('should be able to fetch all secrets decrypted', async () => { const response = await app.inject({ method: 'GET', @@ -658,14 +735,28 @@ describe('Secret Controller Tests', () => { expect(response.json().items.length).toBe(1) const { secret, values } = response.json().items[0] - expect(secret.id).toBeDefined() - expect(secret.name).toBeDefined() - expect(secret.note).toBeDefined() - expect(secret.projectId).toBeDefined() + expect(secret).toStrictEqual({ + id: secret1.id, + name: secret1.name, + slug: secret1.slug, + note: secret1.note, + projectId: project1.id, + lastUpdatedById: secret1.lastUpdatedById, + lastUpdatedBy: { + id: user1.id, + name: user1.name + }, + createdAt: secret1.createdAt.toISOString(), + updatedAt: secret1.updatedAt.toISOString(), + rotateAt: secret1.rotateAt.toISOString() + }) expect(values.length).toBe(1) const value = values[0] - expect(value.environment).toBeDefined() + expect(value.version).toBe(1) + expect(value.environment.id).toBe(environment1.id) + expect(value.environment.slug).toBe(environment1.slug) + expect(value.environment.name).toBe(environment1.name) expect(value.value).toEqual('Secret 1 value') //check metadata diff --git a/apps/api/src/secret/service/secret.service.ts b/apps/api/src/secret/service/secret.service.ts index 517b8364..9a95a8b0 100644 --- a/apps/api/src/secret/service/secret.service.ts +++ b/apps/api/src/secret/service/secret.service.ts @@ -669,6 +669,8 @@ export class SecretService { }, versions: { select: { + value: true, + version: true, environment: { select: { name: true, @@ -687,93 +689,72 @@ export class SecretService { } }) - const secretsWithEnvironmentalValues = new Map< - Secret['id'], - { - secret: Secret - values: { - environment: { - name: Environment['name'] - id: Environment['id'] - } - value: SecretVersion['value'] - version: SecretVersion['version'] - }[] - } - >() - - // Find all the environments for this project - const environments = await this.prisma.environment.findMany({ - where: { - projectId - } - }) - const environmentIds = new Map( - environments.map((env) => [env.id, env.name]) - ) + const secretsWithEnvironmentalValues = new Set<{ + secret: Partial + values: { + environment: { + name: Environment['name'] + id: Environment['id'] + slug: Environment['slug'] + } + value: SecretVersion['value'] + version: SecretVersion['version'] + }[] + }>() for (const secret of secrets) { - // Make a copy of the environment IDs - const envIds = new Map(environmentIds) - let iterations = envIds.size - - // Find the latest version for each environment - while (iterations--) { - const latestVersion = await this.prisma.secretVersion.findFirst({ - where: { - secretId: secret.id, - environmentId: { - in: Array.from(envIds.keys()) - } - }, - orderBy: { - version: 'desc' - }, - include: { - environment: { - select: { - id: true, - slug: true - } - } + // Logic to update the map: + // 1. If the environment ID is not present in the key, insert the environment ID and the secret version + // 2. If the environment ID is already present, check if the existing secret version is lesser than the new secret version. + // If it is, update the secret version + const envIdToSecretVersionMap = new Map< + Environment['id'], + Partial & { + environment: { + id: Environment['id'] + slug: Environment['slug'] + name: Environment['name'] } - }) + } + >() - if (!latestVersion) continue + for (const secretVersion of secret.versions) { + const environmentId = secretVersion.environment.id + const existingSecretVersion = envIdToSecretVersionMap.get(environmentId) - if (secretsWithEnvironmentalValues.has(secret.id)) { - secretsWithEnvironmentalValues.get(secret.id).values.push({ - environment: { - id: latestVersion.environmentId, - name: envIds.get(latestVersion.environmentId) - }, - value: decryptValue - ? await decrypt(project.privateKey, latestVersion.value) - : latestVersion.value, - version: latestVersion.version - }) + if (!existingSecretVersion) { + envIdToSecretVersionMap.set(environmentId, secretVersion) } else { - secretsWithEnvironmentalValues.set(secret.id, { - secret, - values: [ - { - environment: { - id: latestVersion.environmentId, - name: envIds.get(latestVersion.environmentId) - }, - value: decryptValue - ? await decrypt(project.privateKey, latestVersion.value) - : latestVersion.value, - version: latestVersion.version - } - ] - }) + if (existingSecretVersion.version < secretVersion.version) { + envIdToSecretVersionMap.set(environmentId, secretVersion) + } } - - envIds.delete(latestVersion.environmentId) } + + delete secret.versions + + // Add the secret to the map + secretsWithEnvironmentalValues.add({ + secret, + values: await Promise.all( + Array.from(envIdToSecretVersionMap.values()).map( + async (secretVersion) => ({ + environment: { + id: secretVersion.environment.id, + name: secretVersion.environment.name, + slug: secretVersion.environment.slug + }, + value: decryptValue + ? await decrypt(project.privateKey, secretVersion.value) + : secretVersion.value, + version: secretVersion.version + }) + ) + ) + }) } + // console.log(secretsWithEnvironmentalValues) const items = Array.from(secretsWithEnvironmentalValues.values()) // Calculate pagination metadata diff --git a/apps/api/src/variable/service/variable.service.ts b/apps/api/src/variable/service/variable.service.ts index f1e6288b..16f21951 100644 --- a/apps/api/src/variable/service/variable.service.ts +++ b/apps/api/src/variable/service/variable.service.ts @@ -590,8 +590,11 @@ export class VariableService { }, versions: { select: { + value: true, + version: true, environment: { select: { + name: true, id: true, slug: true } @@ -606,85 +609,68 @@ export class VariableService { } }) - const variablesWithEnvironmentalValues = new Map< - Variable['id'], - { - variable: Variable - values: { + const variablesWithEnvironmentalValues = new Set<{ + variable: Partial + values: { + environment: { + name: Environment['name'] + id: Environment['id'] + slug: Environment['slug'] + } + value: VariableVersion['value'] + version: VariableVersion['version'] + }[] + }>() + + for (const variable of variables) { + // Logic to update the map: + // 1. If the environment ID is not present in the key, insert the environment ID and the variable version + // 2. If the environment ID is already present, check if the existing variable version is lesser than the new variable version. + // If it is, update the variable version + const envIdToVariableVersionMap = new Map< + Environment['id'], + Partial & { environment: { - name: Environment['name'] id: Environment['id'] slug: Environment['slug'] + name: Environment['name'] } - value: VariableVersion['value'] - version: VariableVersion['version'] - }[] - } - >() - - // Find all the environments for this project - const environments = await this.prisma.environment.findMany({ - where: { - projectId - } - }) - const environmentIds = new Map( - environments.map((env) => [env.id, env.name]) - ) - - for (const variable of variables) { - // Make a copy of the environment IDs - const envIds = new Map(environmentIds) - let iterations = envIds.size - - // Find the latest version for each environment - while (iterations--) { - const latestVersion = await this.prisma.variableVersion.findFirst({ - where: { - variableId: variable.id, - environmentId: { - in: Array.from(envIds.keys()) - } - }, - orderBy: { - version: 'desc' - }, - include: { - environment: true - } - }) + } + >() - if (!latestVersion) continue + for (const variableVersion of variable.versions) { + const environmentId = variableVersion.environment.id + const existingVariableVersion = + envIdToVariableVersionMap.get(environmentId) - if (variablesWithEnvironmentalValues.has(variable.id)) { - variablesWithEnvironmentalValues.get(variable.id).values.push({ - environment: { - id: latestVersion.environmentId, - name: envIds.get(latestVersion.environmentId), - slug: latestVersion.environment.slug - }, - value: latestVersion.value, - version: latestVersion.version - }) + if (!existingVariableVersion) { + envIdToVariableVersionMap.set(environmentId, variableVersion) } else { - variablesWithEnvironmentalValues.set(variable.id, { - variable, - values: [ - { - environment: { - id: latestVersion.environmentId, - name: envIds.get(latestVersion.environmentId), - slug: latestVersion.environment.slug - }, - value: latestVersion.value, - version: latestVersion.version - } - ] - }) + if (existingVariableVersion.version < variableVersion.version) { + envIdToVariableVersionMap.set(environmentId, variableVersion) + } } - - envIds.delete(latestVersion.environmentId) } + + delete variable.versions + + // Add the variable to the map + variablesWithEnvironmentalValues.add({ + variable, + values: await Promise.all( + Array.from(envIdToVariableVersionMap.values()).map( + async (variableVersion) => ({ + environment: { + id: variableVersion.environment.id, + name: variableVersion.environment.name, + slug: variableVersion.environment.slug + }, + value: variableVersion.value, + version: variableVersion.version + }) + ) + ) + }) } const items = Array.from(variablesWithEnvironmentalValues.values()) diff --git a/apps/api/src/variable/variable.e2e.spec.ts b/apps/api/src/variable/variable.e2e.spec.ts index b7adbd31..8a44268b 100644 --- a/apps/api/src/variable/variable.e2e.spec.ts +++ b/apps/api/src/variable/variable.e2e.spec.ts @@ -597,13 +597,92 @@ describe('Variable Controller Tests', () => { const { variable, values } = response.json().items[0] expect(variable).toBeDefined() + expect(variable.versions).toBeUndefined() expect(values).toBeDefined() expect(values.length).toBe(1) expect(values[0].value).toBe('Variable 1 value') + expect(values[0].version).toBe(1) expect(values[0].environment.id).toBe(environment1.id) expect(values[0].environment.slug).toBe(environment1.slug) - expect(variable.id).toBe(variable1.id) - expect(variable.name).toBe('Variable 1') + expect(values[0].environment.name).toBe(environment1.name) + expect(variable).toStrictEqual({ + id: variable1.id, + name: variable1.name, + slug: variable1.slug, + note: variable1.note, + projectId: project1.id, + lastUpdatedById: variable1.lastUpdatedById, + lastUpdatedBy: { + id: user1.id, + name: user1.name + }, + createdAt: variable1.createdAt.toISOString(), + updatedAt: variable1.updatedAt.toISOString() + }) + + //check metadata + const metadata = response.json().metadata + expect(metadata.totalCount).toEqual(1) + expect(metadata.links.self).toEqual( + `/variable/${project1.slug}?page=0&limit=10&sort=name&order=asc&search=` + ) + expect(metadata.links.first).toEqual( + `/variable/${project1.slug}?page=0&limit=10&sort=name&order=asc&search=` + ) + expect(metadata.links.previous).toBeNull() + expect(metadata.links.next).toBeNull() + expect(metadata.links.last).toEqual( + `/variable/${project1.slug}?page=0&limit=10&sort=name&order=asc&search=` + ) + }) + + it('should be able to fetch only new versions of variable', async () => { + // update variable1 + await variableService.updateVariable(user1, variable1.slug, { + entries: [ + { + environmentSlug: environment1.slug, + value: 'Variable 1 new value' + } + ] + }) + + const response = await app.inject({ + method: 'GET', + url: `/variable/${project1.slug}?page=0&limit=10`, + headers: { + 'x-e2e-user-email': user1.email + } + }) + + expect(response.statusCode).toBe(200) + expect(response.json().items.length).toBe(1) + + const { variable, values } = response.json().items[0] + expect(variable).toBeDefined() + expect(variable.versions).toBeUndefined() + expect(values).toBeDefined() + expect(values.length).toBe(1) + expect(values[0].value).toBe('Variable 1 new value') + expect(values[0].version).toBe(2) + expect(values[0].environment.id).toBe(environment1.id) + expect(values[0].environment.slug).toBe(environment1.slug) + expect(values[0].environment.name).toBe(environment1.name) + + expect(variable).toStrictEqual({ + id: variable1.id, + name: variable1.name, + slug: variable1.slug, + note: variable1.note, + projectId: project1.id, + lastUpdatedById: variable1.lastUpdatedById, + lastUpdatedBy: { + id: user1.id, + name: user1.name + }, + createdAt: variable1.createdAt.toISOString(), + updatedAt: expect.any(String) + }) //check metadata const metadata = response.json().metadata diff --git a/packages/schema/src/variable/index.ts b/packages/schema/src/variable/index.ts index ee2a2ee5..185ef31f 100644 --- a/packages/schema/src/variable/index.ts +++ b/packages/schema/src/variable/index.ts @@ -98,8 +98,8 @@ export const GetAllVariablesOfProjectRequestSchema = PageRequestSchema.extend({ }) export const GetAllVariablesOfProjectResponseSchema = PageResponseSchema( - VariableSchema.omit({ project: true, versions: true }).extend({ - variable: z.object({ + z.object({ + variable: VariableSchema.omit({ project: true, versions: true }).extend({ lastUpdatedBy: z.object({ id: z.string(), name: z.string() diff --git a/packages/schema/tests/secret.spec.ts b/packages/schema/tests/secret.spec.ts index 0a3dc096..823d471d 100644 --- a/packages/schema/tests/secret.spec.ts +++ b/packages/schema/tests/secret.spec.ts @@ -16,6 +16,7 @@ import { GetRevisionsOfSecretResponseSchema } from '@/secret' import { rotateAfterEnum } from '@/enums' +import { env, versions } from 'process' describe('Secret Schema Tests', () => { describe('SecretSchema Tests', () => { diff --git a/packages/schema/tests/variable.spec.ts b/packages/schema/tests/variable.spec.ts index 43d6e5f6..291990ac 100644 --- a/packages/schema/tests/variable.spec.ts +++ b/packages/schema/tests/variable.spec.ts @@ -389,15 +389,15 @@ describe('Variable Schema Tests', () => { const result = GetAllVariablesOfProjectResponseSchema.safeParse({ items: [ { - id: 'variable123', - name: 'Variable Name', - slug: 'variable-slug', - createdAt: '2024-10-01T00:00:00Z', - updatedAt: '2024-10-01T00:00:00Z', - note: 'This is a note', - lastUpdatedById: 'user123', - projectId: 'project123', variable: { + id: 'variable123', + name: 'Variable Name', + slug: 'variable-slug', + createdAt: '2024-10-01T00:00:00Z', + updatedAt: '2024-10-01T00:00:00Z', + note: 'This is a note', + lastUpdatedById: 'user123', + projectId: 'project123', lastUpdatedBy: { id: 'user123', name: 'John Doe'