diff --git a/packages/twenty-server/src/engine/core-modules/application/application-variable/__tests__/application-variable.service.spec.ts b/packages/twenty-server/src/engine/core-modules/application/application-variable/__tests__/application-variable.service.spec.ts index 017ab70aaa..756942268f 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-variable/__tests__/application-variable.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-variable/__tests__/application-variable.service.spec.ts @@ -29,6 +29,7 @@ describe('ApplicationVariableEntityService', () => { { provide: getRepositoryToken(ApplicationVariableEntity), useValue: { + find: jest.fn(), findOne: jest.fn(), update: jest.fn(), save: jest.fn(), @@ -160,7 +161,7 @@ describe('ApplicationVariableEntityService', () => { describe('upsertManyApplicationVariableEntities', () => { it('should encrypt secret values when creating new variables', async () => { - repository.findOne.mockResolvedValue(null); + repository.find.mockResolvedValue([]); repository.save.mockResolvedValue({} as any); repository.delete.mockResolvedValue({ affected: 0 } as any); @@ -178,17 +179,19 @@ describe('ApplicationVariableEntityService', () => { }); expect(secretEncryptionService.encrypt).toHaveBeenCalledWith('my-secret'); - expect(repository.save).toHaveBeenCalledWith({ - key: 'SECRET_KEY', - value: 'encrypted_my-secret', - description: 'A secret key', - isSecret: true, - applicationId: mockApplicationId, - }); + expect(repository.save).toHaveBeenCalledWith([ + { + key: 'SECRET_KEY', + value: 'encrypted_my-secret', + description: 'A secret key', + isSecret: true, + applicationId: mockApplicationId, + }, + ]); }); it('should not encrypt non-secret values when creating new variables', async () => { - repository.findOne.mockResolvedValue(null); + repository.find.mockResolvedValue([]); repository.save.mockResolvedValue({} as any); repository.delete.mockResolvedValue({ affected: 0 } as any); @@ -206,17 +209,19 @@ describe('ApplicationVariableEntityService', () => { }); expect(secretEncryptionService.encrypt).not.toHaveBeenCalled(); - expect(repository.save).toHaveBeenCalledWith({ - key: 'PUBLIC_URL', - value: 'https://example.com', - description: 'Public URL', - isSecret: false, - applicationId: mockApplicationId, - }); + expect(repository.save).toHaveBeenCalledWith([ + { + key: 'PUBLIC_URL', + value: 'https://example.com', + description: 'Public URL', + isSecret: false, + applicationId: mockApplicationId, + }, + ]); }); it('should handle undefined isSecret as false', async () => { - repository.findOne.mockResolvedValue(null); + repository.find.mockResolvedValue([]); repository.save.mockResolvedValue({} as any); repository.delete.mockResolvedValue({ affected: 0 } as any); @@ -233,11 +238,11 @@ describe('ApplicationVariableEntityService', () => { }); expect(secretEncryptionService.encrypt).not.toHaveBeenCalled(); - expect(repository.save).toHaveBeenCalledWith( + expect(repository.save).toHaveBeenCalledWith([ expect.objectContaining({ isSecret: false, }), - ); + ]); }); it('should update existing variables without changing values', async () => { @@ -249,8 +254,8 @@ describe('ApplicationVariableEntityService', () => { applicationId: mockApplicationId, } as ApplicationVariableEntity; - repository.findOne.mockResolvedValue(existingVariable); - repository.update.mockResolvedValue({ affected: 1 } as any); + repository.find.mockResolvedValue([existingVariable]); + repository.save.mockResolvedValue({} as any); repository.delete.mockResolvedValue({ affected: 0 } as any); await service.upsertManyApplicationVariableEntities({ @@ -266,15 +271,88 @@ describe('ApplicationVariableEntityService', () => { workspaceId: mockWorkspaceId, }); - expect(repository.update).toHaveBeenCalledWith( - { key: 'EXISTING_VAR', applicationId: mockApplicationId }, + expect(repository.save).toHaveBeenCalledWith([ { + id: '1', description: 'Updated description', isSecret: true, - value: 'encrypted_new-value', }, - ); - expect(repository.save).not.toHaveBeenCalled(); + ]); + expect(repository.update).not.toHaveBeenCalled(); + }); + + it('should update existing value when shouldUpdateValue is true', async () => { + const existingVariable = { + id: '1', + key: 'EXISTING_VAR', + value: 'existing-encrypted-value', + isSecret: true, + applicationId: mockApplicationId, + } as ApplicationVariableEntity; + + repository.find.mockResolvedValue([existingVariable]); + repository.save.mockResolvedValue({} as any); + repository.delete.mockResolvedValue({ affected: 0 } as any); + + await service.upsertManyApplicationVariableEntities({ + applicationVariables: { + EXISTING_VAR: { + universalIdentifier: 'existing-var-123', + value: 'new-value', + description: 'Updated description', + isSecret: true, + }, + }, + applicationId: mockApplicationId, + workspaceId: mockWorkspaceId, + shouldUpdateValue: true, + }); + + expect(repository.save).toHaveBeenCalledWith([ + { + id: '1', + description: 'Updated description', + value: 'encrypted_new-value', + isSecret: true, + }, + ]); + }); + + it('should update existing value if isSecret changes', async () => { + const existingVariable = { + id: '1', + key: 'EXISTING_VAR', + value: 'existing-encrypted-value', + isSecret: true, + applicationId: mockApplicationId, + } as ApplicationVariableEntity; + + repository.find.mockResolvedValue([existingVariable]); + repository.save.mockResolvedValue({} as any); + repository.delete.mockResolvedValue({ affected: 0 } as any); + + await service.upsertManyApplicationVariableEntities({ + applicationVariables: { + EXISTING_VAR: { + universalIdentifier: 'existing-var-123', + value: 'new-value', + description: 'Updated description', + isSecret: false, + }, + }, + applicationId: mockApplicationId, + workspaceId: mockWorkspaceId, + }); + + expect(repository.save).toHaveBeenCalledWith([ + { + id: '1', + description: 'Updated description', + value: 'new-value', + isSecret: false, + }, + ]); + expect(repository.update).not.toHaveBeenCalled(); }); it('should handle undefined applicationVariables', async () => { @@ -284,7 +362,7 @@ describe('ApplicationVariableEntityService', () => { workspaceId: mockWorkspaceId, }); - expect(repository.findOne).not.toHaveBeenCalled(); + expect(repository.find).not.toHaveBeenCalled(); expect(repository.save).not.toHaveBeenCalled(); expect(repository.update).not.toHaveBeenCalled(); expect( diff --git a/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.service.ts index b938ab1354..23507e0fbe 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.service.ts @@ -84,45 +84,53 @@ export class ApplicationVariableEntityService { applicationVariables, applicationId, workspaceId, + shouldUpdateValue = false, }: { applicationVariables?: ApplicationVariables; applicationId: string; workspaceId: string; + shouldUpdateValue?: boolean; }) { if (!isDefined(applicationVariables)) { return; } + const keys = Object.keys(applicationVariables); + + const existingVariables = await this.applicationVariableRepository.find({ + where: { + applicationId, + key: In(keys), + }, + }); + + const existingVariablesByKey = new Map( + existingVariables.map((variable) => [variable.key, variable]), + ); + + const entitiesToSave: Partial[] = []; + for (const [key, { value, description, isSecret }] of Object.entries( applicationVariables, )) { + const existingVariable = existingVariablesByKey.get(key); const isSecretValue = isSecret ?? false; const encryptedValue = this.encryptSecretValue( value ?? '', isSecretValue, ); - if ( - await this.applicationVariableRepository.findOne({ - where: { - key, - applicationId, - }, - }) - ) { - await this.applicationVariableRepository.update( - { - key, - applicationId, - }, - { - value: encryptedValue, - description: description ?? '', - isSecret: isSecretValue, - }, - ); + if (isDefined(existingVariable)) { + entitiesToSave.push({ + id: existingVariable.id, + description: description ?? '', + isSecret: isSecretValue, + ...(shouldUpdateValue || existingVariable.isSecret !== isSecretValue + ? { value: encryptedValue } + : {}), + }); } else { - await this.applicationVariableRepository.save({ + entitiesToSave.push({ key, value: encryptedValue, description: description ?? '', @@ -132,9 +140,13 @@ export class ApplicationVariableEntityService { } } + if (entitiesToSave.length > 0) { + await this.applicationVariableRepository.save(entitiesToSave); + } + await this.applicationVariableRepository.delete({ applicationId, - key: Not(In(Object.keys(applicationVariables))), + key: Not(In(keys)), }); await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [