Environment variables in admin panel (read only) - backend (#9943)
Backend for https://github.com/twentyhq/core-team-issues/issues/293 POC - https://github.com/twentyhq/twenty/pull/9903 --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@ -7,6 +7,8 @@ import {
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { EnvironmentVariablesGroup } from 'src/engine/core-modules/environment/enums/environment-variables-group.enum';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
@ -17,6 +19,7 @@ const WorkspaceFindOneMock = jest.fn();
|
||||
const FeatureFlagUpdateMock = jest.fn();
|
||||
const FeatureFlagSaveMock = jest.fn();
|
||||
const LoginTokenServiceGenerateLoginTokenMock = jest.fn();
|
||||
const EnvironmentServiceGetAllMock = jest.fn();
|
||||
|
||||
jest.mock(
|
||||
'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum',
|
||||
@ -29,6 +32,13 @@ jest.mock(
|
||||
},
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'src/engine/core-modules/environment/constants/environment-variables-hidden-groups',
|
||||
() => ({
|
||||
ENVIRONMENT_VARIABLES_HIDDEN_GROUPS: new Set(['HIDDEN_GROUP']),
|
||||
}),
|
||||
);
|
||||
|
||||
describe('AdminPanelService', () => {
|
||||
let service: AdminPanelService;
|
||||
|
||||
@ -61,6 +71,12 @@ describe('AdminPanelService', () => {
|
||||
generateLoginToken: LoginTokenServiceGenerateLoginTokenMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: EnvironmentService,
|
||||
useValue: {
|
||||
getAll: EnvironmentServiceGetAllMock,
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@ -214,4 +230,144 @@ describe('AdminPanelService', () => {
|
||||
|
||||
expect(UserFindOneMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('getEnvironmentVariablesGrouped', () => {
|
||||
it('should correctly group environment variables', () => {
|
||||
EnvironmentServiceGetAllMock.mockReturnValue({
|
||||
VAR_1: {
|
||||
value: 'value1',
|
||||
metadata: {
|
||||
group: 'GROUP_1',
|
||||
description: 'Description 1',
|
||||
},
|
||||
},
|
||||
VAR_2: {
|
||||
value: 'value2',
|
||||
metadata: {
|
||||
group: 'GROUP_1',
|
||||
subGroup: 'SUBGROUP_1',
|
||||
description: 'Description 2',
|
||||
sensitive: true,
|
||||
},
|
||||
},
|
||||
VAR_3: {
|
||||
value: 'value3',
|
||||
metadata: {
|
||||
group: 'GROUP_2',
|
||||
description: 'Description 3',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = service.getEnvironmentVariablesGrouped();
|
||||
|
||||
expect(result).toEqual({
|
||||
groups: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
groupName: 'GROUP_1',
|
||||
variables: [
|
||||
{
|
||||
name: 'VAR_1',
|
||||
value: 'value1',
|
||||
description: 'Description 1',
|
||||
sensitive: false,
|
||||
},
|
||||
],
|
||||
subgroups: [
|
||||
{
|
||||
subgroupName: 'SUBGROUP_1',
|
||||
variables: [
|
||||
{
|
||||
name: 'VAR_2',
|
||||
value: 'value2',
|
||||
description: 'Description 2',
|
||||
sensitive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
groupName: 'GROUP_2',
|
||||
variables: [
|
||||
{
|
||||
name: 'VAR_3',
|
||||
value: 'value3',
|
||||
description: 'Description 3',
|
||||
sensitive: false,
|
||||
},
|
||||
],
|
||||
subgroups: [],
|
||||
}),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should sort groups by position and variables alphabetically', () => {
|
||||
EnvironmentServiceGetAllMock.mockReturnValue({
|
||||
Z_VAR: {
|
||||
value: 'valueZ',
|
||||
metadata: {
|
||||
group: 'GROUP_1',
|
||||
description: 'Description Z',
|
||||
},
|
||||
},
|
||||
A_VAR: {
|
||||
value: 'valueA',
|
||||
metadata: {
|
||||
group: 'GROUP_1',
|
||||
description: 'Description A',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = service.getEnvironmentVariablesGrouped();
|
||||
|
||||
const group = result.groups.find(
|
||||
(g) => g.groupName === ('GROUP_1' as EnvironmentVariablesGroup),
|
||||
);
|
||||
|
||||
expect(group?.variables[0].name).toBe('A_VAR');
|
||||
expect(group?.variables[1].name).toBe('Z_VAR');
|
||||
});
|
||||
|
||||
it('should handle empty environment variables', () => {
|
||||
EnvironmentServiceGetAllMock.mockReturnValue({});
|
||||
|
||||
const result = service.getEnvironmentVariablesGrouped();
|
||||
|
||||
expect(result).toEqual({
|
||||
groups: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should exclude hidden groups from the output', () => {
|
||||
EnvironmentServiceGetAllMock.mockReturnValue({
|
||||
VAR_1: {
|
||||
value: 'value1',
|
||||
metadata: {
|
||||
group: 'HIDDEN_GROUP',
|
||||
description: 'Description 1',
|
||||
},
|
||||
},
|
||||
VAR_2: {
|
||||
value: 'value2',
|
||||
metadata: {
|
||||
group: 'VISIBLE_GROUP',
|
||||
description: 'Description 2',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = service.getEnvironmentVariablesGrouped();
|
||||
|
||||
expect(result.groups).toHaveLength(1);
|
||||
expect(result.groups[0].groupName).toBe('VISIBLE_GROUP');
|
||||
expect(result.groups).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
groupName: 'HIDDEN_GROUP',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,7 +1,8 @@
|
||||
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
|
||||
import { EnvironmentVariablesOutput } from 'src/engine/core-modules/admin-panel/dtos/environment-variables.output';
|
||||
import { ImpersonateInput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.input';
|
||||
import { ImpersonateOutput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.output';
|
||||
import { UpdateWorkspaceFeatureFlagInput } from 'src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input';
|
||||
@ -46,4 +47,10 @@ export class AdminPanelResolver {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard)
|
||||
@Query(() => EnvironmentVariablesOutput)
|
||||
async getEnvironmentVariablesGrouped(): Promise<EnvironmentVariablesOutput> {
|
||||
return this.adminService.getEnvironmentVariablesGrouped();
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,12 +3,20 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { EnvironmentVariable } from 'src/engine/core-modules/admin-panel/dtos/environment-variable.dto';
|
||||
import { EnvironmentVariablesGroupData } from 'src/engine/core-modules/admin-panel/dtos/environment-variables-group.dto';
|
||||
import { EnvironmentVariablesOutput } from 'src/engine/core-modules/admin-panel/dtos/environment-variables.output';
|
||||
import { UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { ENVIRONMENT_VARIABLES_GROUP_POSITION } from 'src/engine/core-modules/environment/constants/environment-variables-group-position';
|
||||
import { ENVIRONMENT_VARIABLES_HIDDEN_GROUPS } from 'src/engine/core-modules/environment/constants/environment-variables-hidden-groups';
|
||||
import { EnvironmentVariablesGroup } from 'src/engine/core-modules/environment/enums/environment-variables-group.enum';
|
||||
import { EnvironmentVariablesSubGroup } from 'src/engine/core-modules/environment/enums/environment-variables-sub-group.enum';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import {
|
||||
@ -25,6 +33,7 @@ import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.
|
||||
export class AdminPanelService {
|
||||
constructor(
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
@ -157,4 +166,74 @@ export class AdminPanelService {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getEnvironmentVariablesGrouped(): EnvironmentVariablesOutput {
|
||||
const rawEnvVars = this.environmentService.getAll();
|
||||
const groupedData = new Map<
|
||||
EnvironmentVariablesGroup,
|
||||
{
|
||||
variables: EnvironmentVariable[];
|
||||
subgroups: Map<EnvironmentVariablesSubGroup, EnvironmentVariable[]>;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const [varName, { value, metadata }] of Object.entries(rawEnvVars)) {
|
||||
const { group, subGroup, description } = metadata;
|
||||
|
||||
if (ENVIRONMENT_VARIABLES_HIDDEN_GROUPS.has(group)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const envVar: EnvironmentVariable = {
|
||||
name: varName,
|
||||
description,
|
||||
value: String(value),
|
||||
sensitive: metadata.sensitive ?? false,
|
||||
};
|
||||
|
||||
let currentGroup = groupedData.get(group);
|
||||
|
||||
if (!currentGroup) {
|
||||
currentGroup = {
|
||||
variables: [],
|
||||
subgroups: new Map(),
|
||||
};
|
||||
groupedData.set(group, currentGroup);
|
||||
}
|
||||
|
||||
if (subGroup) {
|
||||
let subgroupVars = currentGroup.subgroups.get(subGroup);
|
||||
|
||||
if (!subgroupVars) {
|
||||
subgroupVars = [];
|
||||
currentGroup.subgroups.set(subGroup, subgroupVars);
|
||||
}
|
||||
subgroupVars.push(envVar);
|
||||
} else {
|
||||
currentGroup.variables.push(envVar);
|
||||
}
|
||||
}
|
||||
|
||||
const groups: EnvironmentVariablesGroupData[] = Array.from(
|
||||
groupedData.entries(),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const positionA = ENVIRONMENT_VARIABLES_GROUP_POSITION[a[0]];
|
||||
const positionB = ENVIRONMENT_VARIABLES_GROUP_POSITION[b[0]];
|
||||
|
||||
return positionA - positionB;
|
||||
})
|
||||
.map(([groupName, data]) => ({
|
||||
groupName,
|
||||
variables: data.variables.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
subgroups: Array.from(data.subgroups.entries())
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([subgroupName, variables]) => ({
|
||||
subgroupName,
|
||||
variables,
|
||||
})),
|
||||
}));
|
||||
|
||||
return { groups };
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class EnvironmentVariable {
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
@Field()
|
||||
description: string;
|
||||
|
||||
@Field()
|
||||
value: string;
|
||||
|
||||
@Field()
|
||||
sensitive: boolean;
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { EnvironmentVariablesGroup } from 'src/engine/core-modules/environment/enums/environment-variables-group.enum';
|
||||
|
||||
import { EnvironmentVariable } from './environment-variable.dto';
|
||||
import { EnvironmentVariablesSubgroupData } from './environment-variables-subgroup.dto';
|
||||
|
||||
registerEnumType(EnvironmentVariablesGroup, {
|
||||
name: 'EnvironmentVariablesGroup',
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
export class EnvironmentVariablesGroupData {
|
||||
@Field(() => [EnvironmentVariable])
|
||||
variables: EnvironmentVariable[];
|
||||
|
||||
@Field(() => [EnvironmentVariablesSubgroupData])
|
||||
subgroups: EnvironmentVariablesSubgroupData[];
|
||||
|
||||
@Field(() => EnvironmentVariablesGroup)
|
||||
groupName: EnvironmentVariablesGroup;
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { EnvironmentVariablesSubGroup } from 'src/engine/core-modules/environment/enums/environment-variables-sub-group.enum';
|
||||
|
||||
import { EnvironmentVariable } from './environment-variable.dto';
|
||||
|
||||
registerEnumType(EnvironmentVariablesSubGroup, {
|
||||
name: 'EnvironmentVariablesSubGroup',
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
export class EnvironmentVariablesSubgroupData {
|
||||
@Field(() => [EnvironmentVariable])
|
||||
variables: EnvironmentVariable[];
|
||||
|
||||
@Field(() => EnvironmentVariablesSubGroup)
|
||||
subgroupName: EnvironmentVariablesSubGroup;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { EnvironmentVariablesGroupData } from './environment-variables-group.dto';
|
||||
|
||||
@ObjectType()
|
||||
export class EnvironmentVariablesOutput {
|
||||
@Field(() => [EnvironmentVariablesGroupData])
|
||||
groups: EnvironmentVariablesGroupData[];
|
||||
}
|
||||
Reference in New Issue
Block a user