Update enums to be all caps (#12372)

- Make custom domain public (remove from lab)
- Use ALL_CAPS definition for enums
This commit is contained in:
Félix Malfait
2025-05-29 14:08:36 +02:00
committed by GitHub
parent 76d0be7f81
commit 4485e8e3db
165 changed files with 845 additions and 847 deletions

View File

@ -44,7 +44,7 @@ export class GraphQLConfigService
createGqlOptions(): YogaDriverConfig {
const isDebugMode =
this.twentyConfigService.get('NODE_ENV') === NodeEnvironment.development;
this.twentyConfigService.get('NODE_ENV') === NodeEnvironment.DEVELOPMENT;
const plugins = [
useThrottler({
ttl: this.twentyConfigService.get('API_RATE_LIMITING_TTL'),

View File

@ -1,14 +1,14 @@
export enum AGGREGATE_OPERATIONS {
min = 'MIN',
max = 'MAX',
avg = 'AVG',
sum = 'SUM',
count = 'COUNT',
countUniqueValues = 'COUNT_UNIQUE_VALUES',
countEmpty = 'COUNT_EMPTY',
countNotEmpty = 'COUNT_NOT_EMPTY',
countTrue = 'COUNT_TRUE',
countFalse = 'COUNT_FALSE',
percentageEmpty = 'PERCENTAGE_EMPTY',
percentageNotEmpty = 'PERCENTAGE_NOT_EMPTY',
export enum AggregateOperations {
MIN = 'MIN',
MAX = 'MAX',
AVG = 'AVG',
SUM = 'SUM',
COUNT = 'COUNT',
COUNT_UNIQUE_VALUES = 'COUNT_UNIQUE_VALUES',
COUNT_EMPTY = 'COUNT_EMPTY',
COUNT_NOT_EMPTY = 'COUNT_NOT_EMPTY',
COUNT_TRUE = 'COUNT_TRUE',
COUNT_FALSE = 'COUNT_FALSE',
PERCENTAGE_EMPTY = 'PERCENTAGE_EMPTY',
PERCENTAGE_NOT_EMPTY = 'PERCENTAGE_NOT_EMPTY',
}

View File

@ -1,7 +1,7 @@
export enum DatabaseEventAction {
CREATED = 'created',
UPDATED = 'updated',
DELETED = 'deleted',
DESTROYED = 'destroyed',
RESTORED = 'restored',
CREATED = 'CREATED',
UPDATED = 'UPDATED',
DELETED = 'DELETED',
DESTROYED = 'DESTROYED',
RESTORED = 'RESTORED',
}

View File

@ -1,9 +1,9 @@
import { Injectable } from '@nestjs/common';
import { SelectQueryBuilder } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { SelectQueryBuilder } from 'typeorm';
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
import { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util';
@ -44,7 +44,7 @@ export class ProcessAggregateHelper {
: columnNames[0];
if (
!Object.values(AGGREGATE_OPERATIONS).includes(
!Object.values(AggregateOperations).includes(
aggregatedField.aggregateOperation,
)
) {
@ -58,44 +58,44 @@ export class ProcessAggregateHelper {
const columnExpression = `NULLIF(CONCAT(${concatenatedColumns}), '')`;
switch (aggregatedField.aggregateOperation) {
case AGGREGATE_OPERATIONS.countEmpty:
case AggregateOperations.COUNT_EMPTY:
queryBuilder.addSelect(
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(*) - COUNT(${columnExpression}) END`,
`${aggregatedFieldName}`,
);
break;
case AGGREGATE_OPERATIONS.countNotEmpty:
case AggregateOperations.COUNT_NOT_EMPTY:
queryBuilder.addSelect(
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(${columnExpression}) END`,
`${aggregatedFieldName}`,
);
break;
case AGGREGATE_OPERATIONS.countUniqueValues:
case AggregateOperations.COUNT_UNIQUE_VALUES:
queryBuilder.addSelect(
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(DISTINCT ${columnExpression}) END`,
`${aggregatedFieldName}`,
);
break;
case AGGREGATE_OPERATIONS.percentageEmpty:
case AggregateOperations.PERCENTAGE_EMPTY:
queryBuilder.addSelect(
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE CAST(((COUNT(*) - COUNT(${columnExpression})::decimal) / COUNT(*)) AS DECIMAL) END`,
`${aggregatedFieldName}`,
);
break;
case AGGREGATE_OPERATIONS.percentageNotEmpty:
case AggregateOperations.PERCENTAGE_NOT_EMPTY:
queryBuilder.addSelect(
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE CAST((COUNT(${columnExpression})::decimal / COUNT(*)) AS DECIMAL) END`,
`${aggregatedFieldName}`,
);
break;
case AGGREGATE_OPERATIONS.countTrue:
case AggregateOperations.COUNT_TRUE:
queryBuilder.addSelect(
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(CASE WHEN ${columnExpression}::boolean = TRUE THEN 1 ELSE NULL END) END`,
`${aggregatedFieldName}`,
);
break;
case AGGREGATE_OPERATIONS.countFalse:
case AggregateOperations.COUNT_FALSE:
queryBuilder.addSelect(
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(CASE WHEN ${columnExpression}::boolean = FALSE THEN 1 ELSE NULL END) END`,
`${aggregatedFieldName}`,

View File

@ -94,7 +94,7 @@ export abstract class GraphqlQueryBaseResolverService<
const featureFlagsMap = workspaceDataSource.featureFlagMap;
const isPermissionsV2Enabled =
featureFlagsMap[FeatureFlagKey.IsPermissionsV2Enabled];
featureFlagsMap[FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED];
if (objectMetadataItemWithFieldMaps.isSystem === true) {
await this.validateSystemObjectPermissionsOrThrow(options);

View File

@ -49,7 +49,7 @@ export const metadataModuleFactory = async (
}),
};
if (twentyConfigService.get('NODE_ENV') === NodeEnvironment.development) {
if (twentyConfigService.get('NODE_ENV') === NodeEnvironment.DEVELOPMENT) {
config.renderGraphiQL = () => {
return renderApolloPlayground({ path: 'metadata' });
};

View File

@ -29,7 +29,7 @@ export function WorkspaceQueryHook(
// Default to PreHook
if (!options.type) {
options.type = WorkspaceQueryHookType.PreHook;
options.type = WorkspaceQueryHookType.PRE_HOOK;
}
// eslint-disable-next-line @typescript-eslint/ban-types

View File

@ -14,8 +14,8 @@ import {
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
export enum WorkspaceQueryHookType {
PreHook = 'PreHook',
PostHook = 'PostHook',
PRE_HOOK = 'PRE_HOOK',
POST_HOOK = 'POST_HOOK',
}
export type WorkspacePreQueryHookPayload<T> = T extends 'createMany'

View File

@ -201,7 +201,7 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
isRequestScoped: boolean,
) {
switch (type) {
case WorkspaceQueryHookType.PreHook:
case WorkspaceQueryHookType.PRE_HOOK:
this.workspaceQueryHookStorage.registerWorkspaceQueryPreHookInstance(
key,
{
@ -211,7 +211,7 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
},
);
break;
case WorkspaceQueryHookType.PostHook:
case WorkspaceQueryHookType.POST_HOOK:
this.workspaceQueryHookStorage.registerWorkspacePostQueryHookInstance(
key,
{

View File

@ -1,13 +1,13 @@
import { GraphQLISODateTime } from '@nestjs/graphql';
import { GraphQLFloat, GraphQLInt, GraphQLScalarType } from 'graphql';
import { capitalize, isFieldMetadataDateKind } from 'twenty-shared/utils';
import { FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION } from 'twenty-shared/constants';
import { FieldMetadataType } from 'twenty-shared/types';
import { capitalize, isFieldMetadataDateKind } from 'twenty-shared/utils';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { getSubfieldsForAggregateOperation } from 'src/engine/twenty-orm/utils/get-subfields-for-aggregate-operation.util';
export type AggregationField = {
@ -17,7 +17,7 @@ export type AggregationField = {
fromFieldType: FieldMetadataType;
fromSubFields?: string[];
subFieldForNumericOperation?: string;
aggregateOperation: AGGREGATE_OPERATIONS;
aggregateOperation: AggregateOperations;
};
export const getAvailableAggregationsFromObjectFields = (
@ -37,7 +37,7 @@ export const getAvailableAggregationsFromObjectFields = (
fromField: field.name,
fromFieldType: field.type,
fromSubFields,
aggregateOperation: AGGREGATE_OPERATIONS.countUniqueValues,
aggregateOperation: AggregateOperations.COUNT_UNIQUE_VALUES,
};
acc[`countEmpty${capitalize(field.name)}`] = {
@ -46,7 +46,7 @@ export const getAvailableAggregationsFromObjectFields = (
fromField: field.name,
fromFieldType: field.type,
fromSubFields,
aggregateOperation: AGGREGATE_OPERATIONS.countEmpty,
aggregateOperation: AggregateOperations.COUNT_EMPTY,
};
acc[`countNotEmpty${capitalize(field.name)}`] = {
@ -55,7 +55,7 @@ export const getAvailableAggregationsFromObjectFields = (
fromField: field.name,
fromFieldType: field.type,
fromSubFields,
aggregateOperation: AGGREGATE_OPERATIONS.countNotEmpty,
aggregateOperation: AggregateOperations.COUNT_NOT_EMPTY,
};
acc[`percentageEmpty${capitalize(field.name)}`] = {
@ -64,7 +64,7 @@ export const getAvailableAggregationsFromObjectFields = (
fromField: field.name,
fromFieldType: field.type,
fromSubFields,
aggregateOperation: AGGREGATE_OPERATIONS.percentageEmpty,
aggregateOperation: AggregateOperations.PERCENTAGE_EMPTY,
};
acc[`percentageNotEmpty${capitalize(field.name)}`] = {
@ -73,7 +73,7 @@ export const getAvailableAggregationsFromObjectFields = (
fromField: field.name,
fromFieldType: field.type,
fromSubFields,
aggregateOperation: AGGREGATE_OPERATIONS.percentageNotEmpty,
aggregateOperation: AggregateOperations.PERCENTAGE_NOT_EMPTY,
};
if (isFieldMetadataDateKind(field.type)) {
@ -82,7 +82,7 @@ export const getAvailableAggregationsFromObjectFields = (
description: `Earliest date contained in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.min,
aggregateOperation: AggregateOperations.MIN,
};
acc[`max${capitalize(field.name)}`] = {
@ -90,7 +90,7 @@ export const getAvailableAggregationsFromObjectFields = (
description: `Latest date contained in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.max,
aggregateOperation: AggregateOperations.MAX,
};
}
@ -101,7 +101,7 @@ export const getAvailableAggregationsFromObjectFields = (
description: `Count of true values in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.countTrue,
aggregateOperation: AggregateOperations.COUNT_TRUE,
};
acc[`countFalse${capitalize(field.name)}`] = {
@ -109,7 +109,7 @@ export const getAvailableAggregationsFromObjectFields = (
description: `Count of false values in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.countFalse,
aggregateOperation: AggregateOperations.COUNT_FALSE,
};
break;
@ -119,7 +119,7 @@ export const getAvailableAggregationsFromObjectFields = (
description: `Minimum amount contained in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.min,
aggregateOperation: AggregateOperations.MIN,
};
acc[`max${capitalize(field.name)}`] = {
@ -127,7 +127,7 @@ export const getAvailableAggregationsFromObjectFields = (
description: `Maximum amount contained in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.max,
aggregateOperation: AggregateOperations.MAX,
};
acc[`avg${capitalize(field.name)}`] = {
@ -135,7 +135,7 @@ export const getAvailableAggregationsFromObjectFields = (
description: `Average amount contained in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.avg,
aggregateOperation: AggregateOperations.AVG,
};
acc[`sum${capitalize(field.name)}`] = {
@ -143,7 +143,7 @@ export const getAvailableAggregationsFromObjectFields = (
description: `Sum of amounts contained in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.sum,
aggregateOperation: AggregateOperations.SUM,
};
break;
case FieldMetadataType.CURRENCY:
@ -154,7 +154,7 @@ export const getAvailableAggregationsFromObjectFields = (
fromSubFields: getSubfieldsForAggregateOperation(field.type),
subFieldForNumericOperation: 'amountMicros',
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.min,
aggregateOperation: AggregateOperations.MIN,
};
acc[`max${capitalize(field.name)}AmountMicros`] = {
@ -163,7 +163,7 @@ export const getAvailableAggregationsFromObjectFields = (
fromField: field.name,
fromSubFields: getSubfieldsForAggregateOperation(field.type),
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.max,
aggregateOperation: AggregateOperations.MAX,
};
acc[`sum${capitalize(field.name)}AmountMicros`] = {
@ -172,7 +172,7 @@ export const getAvailableAggregationsFromObjectFields = (
fromField: field.name,
fromSubFields: getSubfieldsForAggregateOperation(field.type),
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.sum,
aggregateOperation: AggregateOperations.SUM,
};
acc[`avg${capitalize(field.name)}AmountMicros`] = {
@ -181,7 +181,7 @@ export const getAvailableAggregationsFromObjectFields = (
fromField: field.name,
fromSubFields: getSubfieldsForAggregateOperation(field.type),
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.avg,
aggregateOperation: AggregateOperations.AVG,
};
break;
}
@ -194,7 +194,7 @@ export const getAvailableAggregationsFromObjectFields = (
description: `Total number of records in the connection`,
fromField: FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION,
fromFieldType: FieldMetadataType.UUID,
aggregateOperation: AGGREGATE_OPERATIONS.count,
aggregateOperation: AggregateOperations.COUNT,
},
},
);

View File

@ -324,7 +324,7 @@ export class AuthService {
name: 'Chrome Extension',
redirectUrl:
this.twentyConfigService.get('NODE_ENV') ===
NodeEnvironment.development
NodeEnvironment.DEVELOPMENT
? authorizeAppInput.redirectUrl
: `https://${this.twentyConfigService.get(
'CHROME_EXTENSION_ID',

View File

@ -1,7 +1,7 @@
import { DynamicModule, Global } from '@nestjs/common';
import { CAPTCHA_DRIVER } from 'src/engine/core-modules/captcha/constants/captcha-driver.constants';
import { CaptchaService } from 'src/engine/core-modules/captcha/captcha.service';
import { CAPTCHA_DRIVER } from 'src/engine/core-modules/captcha/constants/captcha-driver.constants';
import { GoogleRecaptchaDriver } from 'src/engine/core-modules/captcha/drivers/google-recaptcha.driver';
import { TurnstileDriver } from 'src/engine/core-modules/captcha/drivers/turnstile.driver';
import {
@ -23,9 +23,9 @@ export class CaptchaModule {
}
switch (config.type) {
case CaptchaDriverType.GoogleRecaptcha:
case CaptchaDriverType.GOOGLE_RECAPTCHA:
return new GoogleRecaptchaDriver(config.options);
case CaptchaDriverType.Turnstile:
case CaptchaDriverType.TURNSTILE:
return new TurnstileDriver(config.options);
default:
return;

View File

@ -2,8 +2,8 @@ import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
import { registerEnumType } from '@nestjs/graphql';
export enum CaptchaDriverType {
GoogleRecaptcha = 'google-recaptcha',
Turnstile = 'turnstile',
GOOGLE_RECAPTCHA = 'GOOGLE_RECAPTCHA',
TURNSTILE = 'TURNSTILE',
}
registerEnumType(CaptchaDriverType, {
@ -16,12 +16,12 @@ export type CaptchaDriverOptions = {
};
export interface GoogleRecaptchaDriverFactoryOptions {
type: CaptchaDriverType.GoogleRecaptcha;
type: CaptchaDriverType.GOOGLE_RECAPTCHA;
options: CaptchaDriverOptions;
}
export interface TurnstileDriverFactoryOptions {
type: CaptchaDriverType.Turnstile;
type: CaptchaDriverType.TURNSTILE;
options: CaptchaDriverOptions;
}

View File

@ -61,13 +61,13 @@ describe('ClientConfigService', () => {
IS_MULTIWORKSPACE_ENABLED: true,
IS_EMAIL_VERIFICATION_REQUIRED: true,
DEFAULT_SUBDOMAIN: 'app',
NODE_ENV: NodeEnvironment.development,
SUPPORT_DRIVER: SupportDriver.Front,
NODE_ENV: NodeEnvironment.DEVELOPMENT,
SUPPORT_DRIVER: SupportDriver.FRONT,
SUPPORT_FRONT_CHAT_ID: 'chat-123',
SENTRY_ENVIRONMENT: 'development',
APP_VERSION: '1.0.0',
SENTRY_FRONT_DSN: 'https://sentry.example.com',
CAPTCHA_DRIVER: CaptchaDriverType.GoogleRecaptcha,
CAPTCHA_DRIVER: CaptchaDriverType.GOOGLE_RECAPTCHA,
CAPTCHA_SITE_KEY: 'site-key-123',
CHROME_EXTENSION_ID: 'extension-123',
MUTATION_MAXIMUM_AFFECTED_RECORDS: 1000,
@ -120,7 +120,7 @@ describe('ClientConfigService', () => {
frontDomain: 'app.twenty.com',
debugMode: true,
support: {
supportDriver: 'Front',
supportDriver: 'FRONT',
supportFrontChatId: 'chat-123',
},
sentry: {
@ -129,7 +129,7 @@ describe('ClientConfigService', () => {
dsn: 'https://sentry.example.com',
},
captcha: {
provider: 'GoogleRecaptcha',
provider: 'GOOGLE_RECAPTCHA',
siteKey: 'site-key-123',
},
chromeExtensionId: 'extension-123',
@ -152,7 +152,7 @@ describe('ClientConfigService', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
if (key === 'NODE_ENV') return NodeEnvironment.production;
if (key === 'NODE_ENV') return NodeEnvironment.PRODUCTION;
if (key === 'IS_BILLING_ENABLED') return false;
return undefined;
@ -191,14 +191,14 @@ describe('ClientConfigService', () => {
const result = await service.getClientConfig();
expect(result.support.supportDriver).toBe(SupportDriver.None);
expect(result.support.supportDriver).toBe(SupportDriver.NONE);
});
it('should handle billing enabled with feature flags', async () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
if (key === 'NODE_ENV') return NodeEnvironment.production;
if (key === 'NODE_ENV') return NodeEnvironment.PRODUCTION;
if (key === 'IS_BILLING_ENABLED') return true;
return undefined;
@ -209,44 +209,4 @@ describe('ClientConfigService', () => {
expect(result.canManageFeatureFlags).toBe(true);
});
});
describe('transformEnum', () => {
it('should transform enum by direct key match', () => {
const result = (service as any).transformEnum(
'GoogleRecaptcha',
CaptchaDriverType,
);
expect(result).toBe(CaptchaDriverType.GoogleRecaptcha);
});
it('should transform enum by value match', () => {
const result = (service as any).transformEnum(
'google-recaptcha',
CaptchaDriverType,
);
expect(result).toBe('GoogleRecaptcha');
});
it('should transform SupportDriver enum correctly', () => {
const result = (service as any).transformEnum('front', SupportDriver);
expect(result).toBe('Front');
});
it('should throw error for unknown enum value', () => {
expect(() => {
(service as any).transformEnum('unknown-value', CaptchaDriverType);
}).toThrow(
'Unknown enum value: unknown-value. Available keys: GoogleRecaptcha, Turnstile. Available values: google-recaptcha, turnstile',
);
});
it('should handle direct key match for SupportDriver', () => {
const result = (service as any).transformEnum('Front', SupportDriver);
expect(result).toBe(SupportDriver.Front);
});
});
});

View File

@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common';
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
import { ClientConfig } from 'src/engine/core-modules/client-config/client-config.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const';
@ -57,11 +56,9 @@ export class ClientConfigService {
frontDomain: this.domainManagerService.getFrontUrl().hostname,
debugMode:
this.twentyConfigService.get('NODE_ENV') ===
NodeEnvironment.development,
NodeEnvironment.DEVELOPMENT,
support: {
supportDriver: supportDriver
? this.transformEnum(supportDriver, SupportDriver)
: SupportDriver.None,
supportDriver: supportDriver ? supportDriver : SupportDriver.NONE,
supportFrontChatId: this.twentyConfigService.get(
'SUPPORT_FRONT_CHAT_ID',
),
@ -72,9 +69,7 @@ export class ClientConfigService {
dsn: this.twentyConfigService.get('SENTRY_FRONT_DSN'),
},
captcha: {
provider: captchaProvider
? this.transformEnum(captchaProvider, CaptchaDriverType)
: undefined,
provider: captchaProvider ? captchaProvider : undefined,
siteKey: this.twentyConfigService.get('CAPTCHA_SITE_KEY'),
},
chromeExtensionId: this.twentyConfigService.get('CHROME_EXTENSION_ID'),
@ -89,7 +84,7 @@ export class ClientConfigService {
analyticsEnabled: this.twentyConfigService.get('ANALYTICS_ENABLED'),
canManageFeatureFlags:
this.twentyConfigService.get('NODE_ENV') ===
NodeEnvironment.development ||
NodeEnvironment.DEVELOPMENT ||
this.twentyConfigService.get('IS_BILLING_ENABLED'),
publicFeatureFlags: PUBLIC_FEATURE_FLAGS,
isMicrosoftMessagingEnabled: this.twentyConfigService.get(
@ -111,34 +106,4 @@ export class ClientConfigService {
return clientConfig;
}
// GraphQL enum values are in PascalCase, but the config values are in kebab-case
// This function transforms the config values, the same way GraphQL does
private transformEnum<T extends Record<string, string>>(
value: string,
enumObject: T,
): T[keyof T] {
const directMatch = Object.keys(enumObject).find(
(key) => key === value,
) as keyof T;
if (directMatch) {
return enumObject[directMatch];
}
const valueMatch = Object.entries(enumObject).find(
([, enumValue]) => enumValue === value,
);
if (valueMatch) {
return valueMatch[0] as T[keyof T];
}
const availableKeys = Object.keys(enumObject);
const availableValues = Object.values(enumObject);
throw new Error(
`Unknown enum value: ${value}. Available keys: ${availableKeys.join(', ')}. Available values: ${availableValues.join(', ')}`,
);
}
}

View File

@ -33,7 +33,7 @@ describe('EmailDriverFactory', () => {
it('should return "logger" for logger driver', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockReturnValue(EmailDriver.Logger);
.mockReturnValue(EmailDriver.LOGGER);
const result = factory['buildConfigKey']();
@ -42,7 +42,7 @@ describe('EmailDriverFactory', () => {
});
it('should return smtp config key for smtp driver', () => {
jest.spyOn(twentyConfigService, 'get').mockReturnValue(EmailDriver.Smtp);
jest.spyOn(twentyConfigService, 'get').mockReturnValue(EmailDriver.SMTP);
jest
.spyOn(factory as any, 'getConfigGroupHash')
.mockReturnValue('smtp-hash-123');
@ -66,7 +66,7 @@ describe('EmailDriverFactory', () => {
it('should create logger driver', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockReturnValue(EmailDriver.Logger);
.mockReturnValue(EmailDriver.LOGGER);
const driver = factory['createDriver']();
@ -80,7 +80,7 @@ describe('EmailDriverFactory', () => {
.mockImplementation((key: string) => {
switch (key) {
case 'EMAIL_DRIVER':
return EmailDriver.Smtp;
return EmailDriver.SMTP;
case 'EMAIL_SMTP_HOST':
return 'smtp.example.com';
case 'EMAIL_SMTP_PORT':
@ -108,7 +108,7 @@ describe('EmailDriverFactory', () => {
.mockImplementation((key: string) => {
switch (key) {
case 'EMAIL_DRIVER':
return EmailDriver.Smtp;
return EmailDriver.SMTP;
case 'EMAIL_SMTP_HOST':
return undefined;
case 'EMAIL_SMTP_PORT':
@ -136,7 +136,7 @@ describe('EmailDriverFactory', () => {
it('should return current driver for logger', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockReturnValue(EmailDriver.Logger);
.mockReturnValue(EmailDriver.LOGGER);
const driver = factory.getCurrentDriver();
@ -147,7 +147,7 @@ describe('EmailDriverFactory', () => {
it('should reuse driver when config key unchanged', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockReturnValue(EmailDriver.Logger);
.mockReturnValue(EmailDriver.LOGGER);
const driver1 = factory.getCurrentDriver();
const driver2 = factory.getCurrentDriver();
@ -159,7 +159,7 @@ describe('EmailDriverFactory', () => {
// First call with logger
jest
.spyOn(twentyConfigService, 'get')
.mockReturnValue(EmailDriver.Logger);
.mockReturnValue(EmailDriver.LOGGER);
const driver1 = factory.getCurrentDriver();
@ -169,7 +169,7 @@ describe('EmailDriverFactory', () => {
.mockImplementation((key: string) => {
switch (key) {
case 'EMAIL_DRIVER':
return EmailDriver.Smtp;
return EmailDriver.SMTP;
case 'EMAIL_SMTP_HOST':
return 'smtp.example.com';
case 'EMAIL_SMTP_PORT':
@ -203,7 +203,7 @@ describe('EmailDriverFactory', () => {
.mockImplementation((key: string) => {
switch (key) {
case 'EMAIL_DRIVER':
return EmailDriver.Smtp;
return EmailDriver.SMTP;
case 'EMAIL_SMTP_HOST':
return 'smtp.example.com';
case 'EMAIL_SMTP_PORT':

View File

@ -18,11 +18,11 @@ export class EmailDriverFactory extends DriverFactoryBase<EmailDriverInterface>
protected buildConfigKey(): string {
const driver = this.twentyConfigService.get('EMAIL_DRIVER');
if (driver === EmailDriver.Logger) {
if (driver === EmailDriver.LOGGER) {
return 'logger';
}
if (driver === EmailDriver.Smtp) {
if (driver === EmailDriver.SMTP) {
const emailConfigHash = this.getConfigGroupHash(
ConfigVariablesGroup.EmailSettings,
);
@ -37,10 +37,10 @@ export class EmailDriverFactory extends DriverFactoryBase<EmailDriverInterface>
const driver = this.twentyConfigService.get('EMAIL_DRIVER');
switch (driver) {
case EmailDriver.Logger:
case EmailDriver.LOGGER:
return new LoggerDriver();
case EmailDriver.Smtp: {
case EmailDriver.SMTP: {
const host = this.twentyConfigService.get('EMAIL_SMTP_HOST');
const port = this.twentyConfigService.get('EMAIL_SMTP_PORT');
const user = this.twentyConfigService.get('EMAIL_SMTP_USER');

View File

@ -1,4 +1,4 @@
export enum EmailDriver {
Logger = 'logger',
Smtp = 'smtp',
LOGGER = 'LOGGER',
SMTP = 'SMTP',
}

View File

@ -19,21 +19,21 @@ export const exceptionHandlerModuleFactory = async (
const driverType = twentyConfigService.get('EXCEPTION_HANDLER_DRIVER');
switch (driverType) {
case ExceptionHandlerDriver.Console: {
case ExceptionHandlerDriver.CONSOLE: {
return {
type: ExceptionHandlerDriver.Console,
type: ExceptionHandlerDriver.CONSOLE,
};
}
case ExceptionHandlerDriver.Sentry: {
case ExceptionHandlerDriver.SENTRY: {
return {
type: ExceptionHandlerDriver.Sentry,
type: ExceptionHandlerDriver.SENTRY,
options: {
environment: twentyConfigService.get('SENTRY_ENVIRONMENT'),
release: twentyConfigService.get('APP_VERSION'),
dsn: twentyConfigService.get('SENTRY_DSN') ?? '',
serverInstance: adapterHost.httpAdapter?.getInstance(),
debug:
twentyConfigService.get('NODE_ENV') === NodeEnvironment.development,
twentyConfigService.get('NODE_ENV') === NodeEnvironment.DEVELOPMENT,
},
};
}

View File

@ -22,7 +22,7 @@ export class ExceptionHandlerModule extends ConfigurableModuleClass {
const provider = {
provide: EXCEPTION_HANDLER_DRIVER,
useValue:
options.type === ExceptionHandlerDriver.Console
options.type === ExceptionHandlerDriver.CONSOLE
? new ExceptionHandlerConsoleDriver()
: new ExceptionHandlerSentryDriver(),
};
@ -45,7 +45,7 @@ export class ExceptionHandlerModule extends ConfigurableModuleClass {
return null;
}
return config.type === ExceptionHandlerDriver.Console
return config.type === ExceptionHandlerDriver.CONSOLE
? new ExceptionHandlerConsoleDriver()
: new ExceptionHandlerSentryDriver();
},

View File

@ -1,12 +1,12 @@
import { Router } from 'express';
export enum ExceptionHandlerDriver {
Sentry = 'sentry',
Console = 'console',
SENTRY = 'SENTRY',
CONSOLE = 'CONSOLE',
}
export interface ExceptionHandlerSentryDriverFactoryOptions {
type: ExceptionHandlerDriver.Sentry;
type: ExceptionHandlerDriver.SENTRY;
options: {
environment?: string;
release?: string;
@ -17,7 +17,7 @@ export interface ExceptionHandlerSentryDriverFactoryOptions {
}
export interface ExceptionHandlerDriverFactoryOptions {
type: ExceptionHandlerDriver.Console;
type: ExceptionHandlerDriver.CONSOLE;
}
export type ExceptionHandlerModuleOptions =

View File

@ -7,16 +7,13 @@ type FeatureFlagMetadata = {
};
export type PublicFeatureFlag = {
key: Extract<
FeatureFlagKey,
FeatureFlagKey.IsWorkflowEnabled | FeatureFlagKey.IsCustomDomainEnabled
>;
key: Extract<FeatureFlagKey, FeatureFlagKey.IS_WORKFLOW_ENABLED>;
metadata: FeatureFlagMetadata;
};
export const PUBLIC_FEATURE_FLAGS: PublicFeatureFlag[] = [
{
key: FeatureFlagKey.IsWorkflowEnabled,
key: FeatureFlagKey.IS_WORKFLOW_ENABLED,
metadata: {
label: 'Workflows',
description: 'Create custom workflows to automate your work.',
@ -25,15 +22,9 @@ export const PUBLIC_FEATURE_FLAGS: PublicFeatureFlag[] = [
},
...(process.env.CLOUDFLARE_API_KEY
? [
{
key: FeatureFlagKey.IsCustomDomainEnabled as PublicFeatureFlag['key'],
metadata: {
label: 'Custom Domain',
description: 'Customize your workspace URL with your own domain.',
imagePath:
'https://twenty.com/images/lab/is-custom-domain-enabled.png',
},
},
// {
// Here you can add cloud only feature flags
// },
]
: []),
];

View File

@ -1,11 +1,10 @@
export enum FeatureFlagKey {
IsAirtableIntegrationEnabled = 'IS_AIRTABLE_INTEGRATION_ENABLED',
IsPostgreSQLIntegrationEnabled = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IsStripeIntegrationEnabled = 'IS_STRIPE_INTEGRATION_ENABLED',
IsCopilotEnabled = 'IS_COPILOT_ENABLED',
IsWorkflowEnabled = 'IS_WORKFLOW_ENABLED',
IsUniqueIndexesEnabled = 'IS_UNIQUE_INDEXES_ENABLED',
IsJsonFilterEnabled = 'IS_JSON_FILTER_ENABLED',
IsCustomDomainEnabled = 'IS_CUSTOM_DOMAIN_ENABLED',
IsPermissionsV2Enabled = 'IS_PERMISSIONS_V2_ENABLED',
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
IS_COPILOT_ENABLED = 'IS_COPILOT_ENABLED',
IS_WORKFLOW_ENABLED = 'IS_WORKFLOW_ENABLED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_PERMISSIONS_V2_ENABLED = 'IS_PERMISSIONS_V2_ENABLED',
}

View File

@ -36,7 +36,7 @@ describe('FeatureFlagService', () => {
};
const workspaceId = 'workspace-id';
const featureFlag = FeatureFlagKey.IsWorkflowEnabled;
const featureFlag = FeatureFlagKey.IS_WORKFLOW_ENABLED;
beforeEach(async () => {
jest.clearAllMocks();
@ -121,13 +121,13 @@ describe('FeatureFlagService', () => {
// Prepare
mockWorkspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap.mockResolvedValue(
{
[FeatureFlagKey.IsWorkflowEnabled]: true,
[FeatureFlagKey.IsCopilotEnabled]: false,
[FeatureFlagKey.IS_WORKFLOW_ENABLED]: true,
[FeatureFlagKey.IS_COPILOT_ENABLED]: false,
},
);
const mockFeatureFlags = [
{ key: FeatureFlagKey.IsWorkflowEnabled, value: true },
{ key: FeatureFlagKey.IsCopilotEnabled, value: false },
{ key: FeatureFlagKey.IS_WORKFLOW_ENABLED, value: true },
{ key: FeatureFlagKey.IS_COPILOT_ENABLED, value: false },
];
// Act
@ -145,8 +145,8 @@ describe('FeatureFlagService', () => {
it('should return a map of feature flags for a workspace', async () => {
// Prepare
const mockFeatureFlags = [
{ key: FeatureFlagKey.IsWorkflowEnabled, value: true, workspaceId },
{ key: FeatureFlagKey.IsCopilotEnabled, value: false, workspaceId },
{ key: FeatureFlagKey.IS_WORKFLOW_ENABLED, value: true, workspaceId },
{ key: FeatureFlagKey.IS_COPILOT_ENABLED, value: false, workspaceId },
];
mockFeatureFlagRepository.find.mockResolvedValue(mockFeatureFlags);
@ -156,8 +156,8 @@ describe('FeatureFlagService', () => {
// Assert
expect(result).toEqual({
[FeatureFlagKey.IsWorkflowEnabled]: true,
[FeatureFlagKey.IsCopilotEnabled]: false,
[FeatureFlagKey.IS_WORKFLOW_ENABLED]: true,
[FeatureFlagKey.IS_COPILOT_ENABLED]: false,
});
});
});
@ -166,8 +166,8 @@ describe('FeatureFlagService', () => {
it('should enable multiple feature flags for a workspace', async () => {
// Prepare
const keys = [
FeatureFlagKey.IsWorkflowEnabled,
FeatureFlagKey.IsCopilotEnabled,
FeatureFlagKey.IS_WORKFLOW_ENABLED,
FeatureFlagKey.IS_COPILOT_ENABLED,
];
mockFeatureFlagRepository.upsert.mockResolvedValue({});
@ -212,7 +212,6 @@ describe('FeatureFlagService', () => {
// Assert
expect(result).toEqual(mockFeatureFlag);
expect(mockFeatureFlagRepository.save).toHaveBeenCalledWith({
// @ts-expect-error legacy noImplicitAny
key: FeatureFlagKey[featureFlag],
value,
workspaceId,

View File

@ -99,12 +99,9 @@ export class FeatureFlagService {
),
);
// @ts-expect-error legacy noImplicitAny
const featureFlagKey = FeatureFlagKey[featureFlag];
if (shouldBePublic) {
publicFeatureFlagValidator.assertIsPublicFeatureFlag(
featureFlagKey,
featureFlag,
new FeatureFlagException(
'Invalid feature flag key, flag is not public',
FeatureFlagExceptionCode.INVALID_FEATURE_FLAG_KEY,
@ -114,7 +111,7 @@ export class FeatureFlagService {
const existingFeatureFlag = await this.featureFlagRepository.findOne({
where: {
key: featureFlagKey,
key: featureFlag,
workspaceId: workspaceId,
},
});
@ -125,7 +122,7 @@ export class FeatureFlagService {
value,
}
: {
key: featureFlagKey,
key: featureFlag,
value,
workspaceId: workspaceId,
};

View File

@ -1,12 +1,12 @@
import { CustomException } from 'src/utils/custom-exception';
import { featureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/feature-flag.validate';
import { CustomException } from 'src/utils/custom-exception';
describe('featureFlagValidator', () => {
describe('assertIsFeatureFlagKey', () => {
it('should not throw error if featureFlagKey is valid', () => {
expect(() =>
featureFlagValidator.assertIsFeatureFlagKey(
'IsWorkflowEnabled',
'IS_WORKFLOW_ENABLED',
new CustomException('Error', 'Error'),
),
).not.toThrow();

View File

@ -37,7 +37,7 @@ describe('FileStorageDriverFactory', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
if (key === 'STORAGE_TYPE') return StorageDriverType.Local;
if (key === 'STORAGE_TYPE') return StorageDriverType.LOCAL;
if (key === 'STORAGE_LOCAL_PATH') return storagePath;
return undefined;
@ -55,7 +55,7 @@ describe('FileStorageDriverFactory', () => {
it('should build config key for S3 storage', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockReturnValue(StorageDriverType.S3);
.mockReturnValue(StorageDriverType.S_3);
jest
.spyOn(factory as any, 'getConfigGroupHash')
.mockReturnValue('s3-hash-123');
@ -84,7 +84,7 @@ describe('FileStorageDriverFactory', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
if (key === 'STORAGE_TYPE') return StorageDriverType.Local;
if (key === 'STORAGE_TYPE') return StorageDriverType.LOCAL;
if (key === 'STORAGE_LOCAL_PATH') return storagePath;
return undefined;
@ -102,7 +102,7 @@ describe('FileStorageDriverFactory', () => {
.mockImplementation((key: string) => {
switch (key) {
case 'STORAGE_TYPE':
return StorageDriverType.S3;
return StorageDriverType.S_3;
case 'STORAGE_S3_NAME':
return 'test-bucket';
case 'STORAGE_S3_ENDPOINT':
@ -130,7 +130,7 @@ describe('FileStorageDriverFactory', () => {
.mockImplementation((key: string) => {
switch (key) {
case 'STORAGE_TYPE':
return StorageDriverType.S3;
return StorageDriverType.S_3;
case 'STORAGE_S3_NAME':
return 'test-bucket';
case 'STORAGE_S3_ENDPOINT':
@ -168,7 +168,7 @@ describe('FileStorageDriverFactory', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
if (key === 'STORAGE_TYPE') return StorageDriverType.Local;
if (key === 'STORAGE_TYPE') return StorageDriverType.LOCAL;
if (key === 'STORAGE_LOCAL_PATH') return storagePath;
return undefined;
@ -186,7 +186,7 @@ describe('FileStorageDriverFactory', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
if (key === 'STORAGE_TYPE') return StorageDriverType.Local;
if (key === 'STORAGE_TYPE') return StorageDriverType.LOCAL;
if (key === 'STORAGE_LOCAL_PATH') return storagePath;
return undefined;
@ -203,7 +203,7 @@ describe('FileStorageDriverFactory', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
if (key === 'STORAGE_TYPE') return StorageDriverType.Local;
if (key === 'STORAGE_TYPE') return StorageDriverType.LOCAL;
if (key === 'STORAGE_LOCAL_PATH') return '/tmp/storage1';
return undefined;
@ -215,7 +215,7 @@ describe('FileStorageDriverFactory', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
if (key === 'STORAGE_TYPE') return StorageDriverType.Local;
if (key === 'STORAGE_TYPE') return StorageDriverType.LOCAL;
if (key === 'STORAGE_LOCAL_PATH') return '/tmp/storage2';
return undefined;
@ -233,7 +233,7 @@ describe('FileStorageDriverFactory', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
if (key === 'STORAGE_TYPE') return StorageDriverType.Local;
if (key === 'STORAGE_TYPE') return StorageDriverType.LOCAL;
if (key === 'STORAGE_LOCAL_PATH') return '/tmp/storage';
return undefined;
@ -247,7 +247,7 @@ describe('FileStorageDriverFactory', () => {
.mockImplementation((key: string) => {
switch (key) {
case 'STORAGE_TYPE':
return StorageDriverType.S3;
return StorageDriverType.S_3;
case 'STORAGE_S3_NAME':
return 'test-bucket';
case 'STORAGE_S3_ENDPOINT':
@ -285,7 +285,7 @@ describe('FileStorageDriverFactory', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
if (key === 'STORAGE_TYPE') return StorageDriverType.Local;
if (key === 'STORAGE_TYPE') return StorageDriverType.LOCAL;
if (key === 'STORAGE_LOCAL_PATH') return storagePath;
return undefined;

View File

@ -21,13 +21,13 @@ export class FileStorageDriverFactory extends DriverFactoryBase<StorageDriver> {
protected buildConfigKey(): string {
const storageType = this.twentyConfigService.get('STORAGE_TYPE');
if (storageType === StorageDriverType.Local) {
if (storageType === StorageDriverType.LOCAL) {
const storagePath = this.twentyConfigService.get('STORAGE_LOCAL_PATH');
return `local|${storagePath}`;
}
if (storageType === StorageDriverType.S3) {
if (storageType === StorageDriverType.S_3) {
const storageConfigHash = this.getConfigGroupHash(
ConfigVariablesGroup.StorageConfig,
);
@ -42,7 +42,7 @@ export class FileStorageDriverFactory extends DriverFactoryBase<StorageDriver> {
const storageType = this.twentyConfigService.get('STORAGE_TYPE');
switch (storageType) {
case StorageDriverType.Local: {
case StorageDriverType.LOCAL: {
const storagePath = this.twentyConfigService.get('STORAGE_LOCAL_PATH');
return new LocalDriver({
@ -50,7 +50,7 @@ export class FileStorageDriverFactory extends DriverFactoryBase<StorageDriver> {
});
}
case StorageDriverType.S3: {
case StorageDriverType.S_3: {
const bucketName = this.twentyConfigService.get('STORAGE_S3_NAME');
const endpoint = this.twentyConfigService.get('STORAGE_S3_ENDPOINT');
const region = this.twentyConfigService.get('STORAGE_S3_REGION');

View File

@ -1,4 +1,4 @@
export enum StorageDriverType {
S3 = 's3',
Local = 'local',
S_3 = 'S_3',
LOCAL = 'LOCAL',
}

View File

@ -1,7 +1,7 @@
import { ModuleMetadata, FactoryProvider } from '@nestjs/common';
import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
export enum LLMChatModelDriver {
OpenAI = 'openai',
OPENAI = 'OPENAI',
}
export interface LLMChatModelModuleOptions {

View File

@ -8,8 +8,8 @@ export const llmChatModelModuleFactory = (
const driver = twentyConfigService.get('LLM_CHAT_MODEL_DRIVER');
switch (driver) {
case LLMChatModelDriver.OpenAI: {
return { type: LLMChatModelDriver.OpenAI };
case LLMChatModelDriver.OPENAI: {
return { type: LLMChatModelDriver.OPENAI };
}
default:
// `No LLM chat model driver (${driver})`);

View File

@ -5,8 +5,8 @@ import {
LLMChatModelModuleAsyncOptions,
} from 'src/engine/core-modules/llm-chat-model/interfaces/llm-chat-model.interface';
import { LLM_CHAT_MODEL_DRIVER } from 'src/engine/core-modules/llm-chat-model/llm-chat-model.constants';
import { OpenAIDriver } from 'src/engine/core-modules/llm-chat-model/drivers/openai.driver';
import { LLM_CHAT_MODEL_DRIVER } from 'src/engine/core-modules/llm-chat-model/llm-chat-model.constants';
import { LLMChatModelService } from 'src/engine/core-modules/llm-chat-model/llm-chat-model.service';
@Global()
@ -19,7 +19,7 @@ export class LLMChatModelModule {
const config = options.useFactory(...args);
switch (config?.type) {
case LLMChatModelDriver.OpenAI: {
case LLMChatModelDriver.OPENAI: {
return new OpenAIDriver();
}
}

View File

@ -1,19 +1,19 @@
import { ModuleMetadata, FactoryProvider } from '@nestjs/common';
import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
import { LangfuseDriverOptions } from 'src/engine/core-modules/llm-tracing/drivers/langfuse.driver';
export enum LLMTracingDriver {
Langfuse = 'langfuse',
Console = 'console',
LANGFUSE = 'LANGFUSE',
CONSOLE = 'CONSOLE',
}
export interface LangfuseDriverFactoryOptions {
type: LLMTracingDriver.Langfuse;
type: LLMTracingDriver.LANGFUSE;
options: LangfuseDriverOptions;
}
export interface ConsoleDriverFactoryOptions {
type: LLMTracingDriver.Console;
type: LLMTracingDriver.CONSOLE;
}
export type LLMTracingModuleOptions =

View File

@ -8,10 +8,10 @@ export const llmTracingModuleFactory = (
const driver = twentyConfigService.get('LLM_TRACING_DRIVER');
switch (driver) {
case LLMTracingDriver.Console: {
return { type: LLMTracingDriver.Console as const };
case LLMTracingDriver.CONSOLE: {
return { type: LLMTracingDriver.CONSOLE as const };
}
case LLMTracingDriver.Langfuse: {
case LLMTracingDriver.LANGFUSE: {
const secretKey = twentyConfigService.get('LANGFUSE_SECRET_KEY');
const publicKey = twentyConfigService.get('LANGFUSE_PUBLIC_KEY');
@ -22,7 +22,7 @@ export const llmTracingModuleFactory = (
}
return {
type: LLMTracingDriver.Langfuse as const,
type: LLMTracingDriver.LANGFUSE as const,
options: { secretKey, publicKey },
};
}

View File

@ -1,14 +1,14 @@
import { Global, DynamicModule } from '@nestjs/common';
import { DynamicModule, Global } from '@nestjs/common';
import {
LLMTracingModuleAsyncOptions,
LLMTracingDriver,
LLMTracingModuleAsyncOptions,
} from 'src/engine/core-modules/llm-tracing/interfaces/llm-tracing.interface';
import { LangfuseDriver } from 'src/engine/core-modules/llm-tracing/drivers/langfuse.driver';
import { ConsoleDriver } from 'src/engine/core-modules/llm-tracing/drivers/console.driver';
import { LLMTracingService } from 'src/engine/core-modules/llm-tracing/llm-tracing.service';
import { LangfuseDriver } from 'src/engine/core-modules/llm-tracing/drivers/langfuse.driver';
import { LLM_TRACING_DRIVER } from 'src/engine/core-modules/llm-tracing/llm-tracing.constants';
import { LLMTracingService } from 'src/engine/core-modules/llm-tracing/llm-tracing.service';
@Global()
export class LLMTracingModule {
@ -20,10 +20,10 @@ export class LLMTracingModule {
const config = options.useFactory(...args);
switch (config.type) {
case LLMTracingDriver.Langfuse: {
case LLMTracingDriver.LANGFUSE: {
return new LangfuseDriver(config.options);
}
case LLMTracingDriver.Console: {
case LLMTracingDriver.CONSOLE: {
return new ConsoleDriver();
}
}

View File

@ -1,11 +1,11 @@
import { LogLevel } from '@nestjs/common';
export enum LoggerDriverType {
Console = 'console',
CONSOLE = 'CONSOLE',
}
export interface ConsoleDriverFactoryOptions {
type: LoggerDriverType.Console;
type: LoggerDriverType.CONSOLE;
logLevels?: LogLevel[];
}

View File

@ -16,9 +16,9 @@ export const loggerModuleFactory = async (
const logLevels = twentyConfigService.get('LOG_LEVELS');
switch (driverType) {
case LoggerDriverType.Console: {
case LoggerDriverType.CONSOLE: {
return {
type: LoggerDriverType.Console,
type: LoggerDriverType.CONSOLE,
logLevels: logLevels,
};
}

View File

@ -19,7 +19,7 @@ export class LoggerModule extends ConfigurableModuleClass {
const provider = {
provide: LOGGER_DRIVER,
useValue:
options.type === LoggerDriverType.Console
options.type === LoggerDriverType.CONSOLE
? new ConsoleLogger()
: undefined,
};
@ -45,7 +45,7 @@ export class LoggerModule extends ConfigurableModuleClass {
const logLevels = config.logLevels ?? [];
const logger =
config?.type === LoggerDriverType.Console
config?.type === LoggerDriverType.CONSOLE
? new ConsoleLogger()
: undefined;

View File

@ -15,13 +15,13 @@ export const serverlessModuleFactory = async (
const options = { fileStorageService };
switch (driverType) {
case ServerlessDriverType.Local: {
case ServerlessDriverType.LOCAL: {
return {
type: ServerlessDriverType.Local,
type: ServerlessDriverType.LOCAL,
options,
};
}
case ServerlessDriverType.Lambda: {
case ServerlessDriverType.LAMBDA: {
const region = twentyConfigService.get('SERVERLESS_LAMBDA_REGION');
const accessKeyId = twentyConfigService.get(
'SERVERLESS_LAMBDA_ACCESS_KEY_ID',
@ -36,7 +36,7 @@ export const serverlessModuleFactory = async (
);
return {
type: ServerlessDriverType.Lambda,
type: ServerlessDriverType.LAMBDA,
options: {
...options,
credentials: accessKeyId

View File

@ -1,20 +1,20 @@
import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
import { LocalDriverOptions } from 'src/engine/core-modules/serverless/drivers/local.driver';
import { LambdaDriverOptions } from 'src/engine/core-modules/serverless/drivers/lambda.driver';
import { LocalDriverOptions } from 'src/engine/core-modules/serverless/drivers/local.driver';
export enum ServerlessDriverType {
Lambda = 'lambda',
Local = 'local',
LAMBDA = 'LAMBDA',
LOCAL = 'LOCAL',
}
export interface LocalDriverFactoryOptions {
type: ServerlessDriverType.Local;
type: ServerlessDriverType.LOCAL;
options: LocalDriverOptions;
}
export interface LambdaDriverFactoryOptions {
type: ServerlessDriverType.Lambda;
type: ServerlessDriverType.LAMBDA;
options: LambdaDriverOptions;
}

View File

@ -1,5 +1,6 @@
import { DynamicModule, Global } from '@nestjs/common';
import { AddPackagesCommand } from 'src/engine/core-modules/serverless/commands/add-packages.command';
import { LambdaDriver } from 'src/engine/core-modules/serverless/drivers/lambda.driver';
import { LocalDriver } from 'src/engine/core-modules/serverless/drivers/local.driver';
import { SERVERLESS_DRIVER } from 'src/engine/core-modules/serverless/serverless.constants';
@ -8,7 +9,6 @@ import {
ServerlessModuleAsyncOptions,
} from 'src/engine/core-modules/serverless/serverless.interface';
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
import { AddPackagesCommand } from 'src/engine/core-modules/serverless/commands/add-packages.command';
@Global()
export class ServerlessModule {
@ -19,7 +19,7 @@ export class ServerlessModule {
useFactory: async (...args: any[]) => {
const config = await options.useFactory(...args);
return config?.type === ServerlessDriverType.Local
return config?.type === ServerlessDriverType.LOCAL
? new LocalDriver(config.options)
: new LambdaDriver(config.options);
},

View File

@ -27,6 +27,7 @@ import { ServerlessDriverType } from 'src/engine/core-modules/serverless/serverl
import { CastToLogLevelArray } from 'src/engine/core-modules/twenty-config/decorators/cast-to-log-level-array.decorator';
import { CastToMeterDriverArray } from 'src/engine/core-modules/twenty-config/decorators/cast-to-meter-driver.decorator';
import { CastToPositiveNumber } from 'src/engine/core-modules/twenty-config/decorators/cast-to-positive-number.decorator';
import { CastToUpperSnakeCase } from 'src/engine/core-modules/twenty-config/decorators/cast-to-upper-snake-case.decorator';
import { ConfigVariablesMetadata } from 'src/engine/core-modules/twenty-config/decorators/config-variables-metadata.decorator';
import { IsAWSRegion } from 'src/engine/core-modules/twenty-config/decorators/is-aws-region.decorator';
import { IsDuration } from 'src/engine/core-modules/twenty-config/decorators/is-duration.decorator';
@ -301,7 +302,8 @@ export class ConfigVariables {
type: ConfigVariableType.ENUM,
options: Object.values(EmailDriver),
})
EMAIL_DRIVER: EmailDriver = EmailDriver.Logger;
@CastToUpperSnakeCase()
EMAIL_DRIVER: EmailDriver = EmailDriver.LOGGER;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.EmailSettings,
@ -349,14 +351,15 @@ export class ConfigVariables {
options: Object.values(StorageDriverType),
})
@IsOptional()
STORAGE_TYPE: StorageDriverType = StorageDriverType.Local;
@CastToUpperSnakeCase()
STORAGE_TYPE: StorageDriverType = StorageDriverType.LOCAL;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.StorageConfig,
description: 'Local path for storage when using local storage type',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.Local)
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.LOCAL)
STORAGE_LOCAL_PATH = '.local-storage';
@ConfigVariablesMetadata({
@ -364,7 +367,7 @@ export class ConfigVariables {
description: 'S3 region for storage when using S3 storage type',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S_3)
@IsAWSRegion()
STORAGE_S3_REGION: AwsRegion;
@ -373,7 +376,7 @@ export class ConfigVariables {
description: 'S3 bucket name for storage when using S3 storage type',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S_3)
STORAGE_S3_NAME: string;
@ConfigVariablesMetadata({
@ -381,7 +384,7 @@ export class ConfigVariables {
description: 'S3 endpoint for storage when using S3 storage type',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S_3)
@IsOptional()
STORAGE_S3_ENDPOINT: string;
@ -392,7 +395,7 @@ export class ConfigVariables {
'S3 access key ID for authentication when using S3 storage type',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S_3)
@IsOptional()
STORAGE_S3_ACCESS_KEY_ID: string;
@ -403,7 +406,7 @@ export class ConfigVariables {
'S3 secret access key for authentication when using S3 storage type',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S_3)
@IsOptional()
STORAGE_S3_SECRET_ACCESS_KEY: string;
@ -415,7 +418,8 @@ export class ConfigVariables {
isEnvOnly: true,
})
@IsOptional()
SERVERLESS_TYPE: ServerlessDriverType = ServerlessDriverType.Local;
@CastToUpperSnakeCase()
SERVERLESS_TYPE: ServerlessDriverType = ServerlessDriverType.LOCAL;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.ServerlessConfig,
@ -439,7 +443,7 @@ export class ConfigVariables {
description: 'Region for AWS Lambda functions',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.LAMBDA)
@IsAWSRegion()
SERVERLESS_LAMBDA_REGION: AwsRegion;
@ -448,7 +452,7 @@ export class ConfigVariables {
description: 'IAM role for AWS Lambda functions',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.LAMBDA)
SERVERLESS_LAMBDA_ROLE: string;
@ConfigVariablesMetadata({
@ -456,7 +460,7 @@ export class ConfigVariables {
description: 'Role to assume when hosting lambdas in dedicated AWS account',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.LAMBDA)
@IsOptional()
SERVERLESS_LAMBDA_SUBHOSTING_ROLE?: string;
@ -466,7 +470,7 @@ export class ConfigVariables {
description: 'Access key ID for AWS Lambda functions',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.LAMBDA)
@IsOptional()
SERVERLESS_LAMBDA_ACCESS_KEY_ID: string;
@ -476,7 +480,7 @@ export class ConfigVariables {
description: 'Secret access key for AWS Lambda functions',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.LAMBDA)
@IsOptional()
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY: string;
@ -634,8 +638,9 @@ export class ConfigVariables {
isEnvOnly: true,
})
@IsOptional()
@CastToUpperSnakeCase()
EXCEPTION_HANDLER_DRIVER: ExceptionHandlerDriver =
ExceptionHandlerDriver.Console;
ExceptionHandlerDriver.CONSOLE;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.Logging,
@ -676,7 +681,8 @@ export class ConfigVariables {
isEnvOnly: true,
})
@IsOptional()
LOGGER_DRIVER: LoggerDriverType = LoggerDriverType.Console;
@CastToUpperSnakeCase()
LOGGER_DRIVER: LoggerDriverType = LoggerDriverType.CONSOLE;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.ExceptionHandler,
@ -685,7 +691,7 @@ export class ConfigVariables {
isSensitive: true,
})
@ValidateIf(
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry,
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.SENTRY,
)
SENTRY_DSN: string;
@ -696,7 +702,7 @@ export class ConfigVariables {
isSensitive: true,
})
@ValidateIf(
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry,
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.SENTRY,
)
SENTRY_FRONT_DSN: string;
@ConfigVariablesMetadata({
@ -705,7 +711,7 @@ export class ConfigVariables {
type: ConfigVariableType.STRING,
})
@ValidateIf(
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry,
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.SENTRY,
)
@IsOptional()
SENTRY_ENVIRONMENT: string;
@ -717,7 +723,8 @@ export class ConfigVariables {
options: Object.values(SupportDriver),
})
@IsOptional()
SUPPORT_DRIVER: SupportDriver = SupportDriver.None;
@CastToUpperSnakeCase()
SUPPORT_DRIVER: SupportDriver = SupportDriver.NONE;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.SupportChatConfig,
@ -725,7 +732,7 @@ export class ConfigVariables {
description: 'Chat ID for the support front integration',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front)
@ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.FRONT)
SUPPORT_FRONT_CHAT_ID: string;
@ConfigVariablesMetadata({
@ -734,7 +741,7 @@ export class ConfigVariables {
description: 'HMAC key for the support front integration',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front)
@ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.FRONT)
SUPPORT_FRONT_HMAC_KEY: string;
@ConfigVariablesMetadata({
@ -840,7 +847,8 @@ export class ConfigVariables {
options: Object.values(NodeEnvironment),
isEnvOnly: true,
})
NODE_ENV: NodeEnvironment = NodeEnvironment.production;
// @CastToUpperSnakeCase()
NODE_ENV: NodeEnvironment = NodeEnvironment.PRODUCTION;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.ServerConfig,
@ -949,6 +957,7 @@ export class ConfigVariables {
options: Object.values(LLMChatModelDriver),
isEnvOnly: true,
})
@CastToUpperSnakeCase()
LLM_CHAT_MODEL_DRIVER: LLMChatModelDriver;
@ConfigVariablesMetadata({
@ -981,7 +990,8 @@ export class ConfigVariables {
options: Object.values(LLMTracingDriver),
isEnvOnly: true,
})
LLM_TRACING_DRIVER: LLMTracingDriver = LLMTracingDriver.Console;
@CastToUpperSnakeCase()
LLM_TRACING_DRIVER: LLMTracingDriver = LLMTracingDriver.CONSOLE;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.ServerConfig,
@ -1059,6 +1069,7 @@ export class ConfigVariables {
isEnvOnly: true,
})
@IsOptional()
@CastToUpperSnakeCase()
CAPTCHA_DRIVER?: CaptchaDriverType;
@ConfigVariablesMetadata({

View File

@ -0,0 +1,84 @@
import { plainToClass } from 'class-transformer';
import { CastToUpperSnakeCase } from 'src/engine/core-modules/twenty-config/decorators/cast-to-upper-snake-case.decorator';
class TestClass {
@CastToUpperSnakeCase()
value: string;
}
describe('CastToUpperSnakeCase Decorator', () => {
it('should transform lowercase string to UPPER_SNAKE_CASE', () => {
const result = plainToClass(TestClass, { value: 'local' });
expect(result.value).toBe('LOCAL');
});
it('should transform camelCase string to UPPER_SNAKE_CASE', () => {
const result = plainToClass(TestClass, { value: 'camelCase' });
expect(result.value).toBe('CAMEL_CASE');
});
it('should transform kebab-case string to UPPER_SNAKE_CASE', () => {
const result = plainToClass(TestClass, { value: 'kebab-case' });
expect(result.value).toBe('KEBAB_CASE');
});
it('should transform space-separated string to UPPER_SNAKE_CASE', () => {
const result = plainToClass(TestClass, { value: 'space separated' });
expect(result.value).toBe('SPACE_SEPARATED');
});
it('should handle already UPPER_SNAKE_CASE string', () => {
const result = plainToClass(TestClass, { value: 'ALREADY_UPPER_SNAKE' });
expect(result.value).toBe('ALREADY_UPPER_SNAKE');
});
it('should handle mixed case with numbers', () => {
const result = plainToClass(TestClass, { value: 'test123Value' });
expect(result.value).toBe('TEST_123_VALUE');
});
it('should trim whitespace', () => {
const result = plainToClass(TestClass, { value: ' local ' });
expect(result.value).toBe('LOCAL');
});
it('should handle empty string', () => {
const result = plainToClass(TestClass, { value: '' });
expect(result.value).toBe('');
});
it('should return undefined for non-string values', () => {
const result = plainToClass(TestClass, { value: 123 });
expect(result.value).toBeUndefined();
});
it('should return undefined for null values', () => {
const result = plainToClass(TestClass, { value: null });
expect(result.value).toBeUndefined();
});
it('should return undefined for undefined values', () => {
const result = plainToClass(TestClass, { value: undefined });
expect(result.value).toBeUndefined();
});
it('should handle complex mixed formats', () => {
const result = plainToClass(TestClass, {
value: 'Complex-Mixed_Format test123',
});
expect(result.value).toBe('COMPLEX_MIXED_FORMAT_TEST_123');
});
});

View File

@ -0,0 +1,13 @@
import { Transform } from 'class-transformer';
import snakeCase from 'lodash.snakecase';
export const CastToUpperSnakeCase = () =>
Transform(({ value }: { value: string }) => toUpperSnakeCase(value));
const toUpperSnakeCase = (value: unknown): string | undefined => {
if (typeof value === 'string') {
return snakeCase(value.trim()).toUpperCase();
}
return undefined;
};

View File

@ -1,5 +1,5 @@
export enum NodeEnvironment {
test = 'test',
development = 'development',
production = 'production',
TEST = 'test',
DEVELOPMENT = 'development',
PRODUCTION = 'production',
}

View File

@ -1,4 +1,4 @@
export enum SupportDriver {
None = 'none',
Front = 'front',
NONE = 'NONE',
FRONT = 'FRONT',
}

View File

@ -201,9 +201,9 @@ export class TwentyConfigService {
getLoggingConfig(): LoggerOptions {
switch (this.get('NODE_ENV')) {
case NodeEnvironment.development:
case NodeEnvironment.DEVELOPMENT:
return ['query', 'error'];
case NodeEnvironment.test:
case NodeEnvironment.TEST:
return [];
default:
return ['error'];

View File

@ -123,7 +123,7 @@ export class UserResolver {
) {
const isPermissionsV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsPermissionsV2Enabled,
FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED,
workspace.id,
);
@ -334,7 +334,7 @@ export class UserResolver {
})
supportUserHash(@Parent() parent: User): string | null {
if (
this.twentyConfigService.get('SUPPORT_DRIVER') !== SupportDriver.Front
this.twentyConfigService.get('SUPPORT_DRIVER') !== SupportDriver.FRONT
) {
return null;
}

View File

@ -206,7 +206,7 @@ export class PermissionsService {
const isPermissionsV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsPermissionsV2Enabled,
FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED,
workspaceId,
);
@ -234,7 +234,7 @@ export class PermissionsService {
}): Promise<boolean> {
const isPermissionsV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsPermissionsV2Enabled,
FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED,
workspaceId,
);

View File

@ -34,9 +34,9 @@ export const validateRemoteServerType = async (
const getFeatureFlagKey = (remoteServerType: RemoteServerType) => {
switch (remoteServerType) {
case RemoteServerType.POSTGRES_FDW:
return FeatureFlagKey.IsPostgreSQLIntegrationEnabled;
return FeatureFlagKey.IS_POSTGRESQL_INTEGRATION_ENABLED;
case RemoteServerType.STRIPE_FDW:
return FeatureFlagKey.IsStripeIntegrationEnabled;
return FeatureFlagKey.IS_STRIPE_INTEGRATION_ENABLED;
default:
throw new RemoteServerException(
`Type ${remoteServerType} is not supported.`,

View File

@ -205,7 +205,7 @@ export class RoleResolver {
private async validatePermissionsV2EnabledOrThrow(workspace: Workspace) {
const isPermissionsV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsPermissionsV2Enabled,
FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED,
workspace.id,
);

View File

@ -11,11 +11,11 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
import { ServerlessFunctionIdInput } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-id.input';
import { ExecuteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input';
import { GetServerlessFunctionSourceCodeInput } from 'src/engine/metadata-modules/serverless-function/dtos/get-serverless-function-source-code.input';
import { PublishServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/publish-serverless-function.input';
import { ServerlessFunctionExecutionResultDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
import { ServerlessFunctionIdInput } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-id.input';
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
import {
@ -37,7 +37,7 @@ export class ServerlessFunctionResolver {
async checkFeatureFlag(workspaceId: string) {
const isWorkflowEnabled = await this.featureFlagRepository.findOneBy({
workspaceId,
key: FeatureFlagKey.IsWorkflowEnabled,
key: FeatureFlagKey.IS_WORKFLOW_ENABLED,
value: true,
});

View File

@ -87,7 +87,7 @@ export class WorkspacePermissionsCacheService {
const isPermissionsV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsPermissionsV2Enabled,
FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED,
workspaceId,
);

View File

@ -42,13 +42,13 @@ describe('WorkspaceEntityManager', () => {
idByNameSingular: {},
},
featureFlagsMap: {
[FeatureFlagKey.IsPermissionsV2Enabled]: true,
[FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED]: true,
},
} as WorkspaceInternalContext;
mockDataSource = {
featureFlagMap: {
[FeatureFlagKey.IsPermissionsV2Enabled]: true,
[FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED]: true,
},
permissionsPerRoleId: {},
} as WorkspaceDataSource;
@ -118,7 +118,7 @@ describe('WorkspaceEntityManager', () => {
// Mock getFeatureFlagMap
jest.spyOn(entityManager as any, 'getFeatureFlagMap').mockReturnValue({
[FeatureFlagKey.IsPermissionsV2Enabled]: true,
[FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED]: true,
});
// Mock typeORM's EntityManager methods

View File

@ -94,7 +94,7 @@ export class WorkspaceEntityManager extends EntityManager {
const featureFlagMap = this.getFeatureFlagMap();
const isPermissionsV2Enabled =
featureFlagMap[FeatureFlagKey.IsPermissionsV2Enabled];
featureFlagMap[FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED];
if (permissionOptions?.roleId) {
const objectPermissionsByRoleId = dataSource.permissionsPerRoleId;
@ -159,7 +159,7 @@ export class WorkspaceEntityManager extends EntityManager {
const featureFlagMap = this.getFeatureFlagMap();
const isPermissionsV2Enabled =
featureFlagMap[FeatureFlagKey.IsPermissionsV2Enabled];
featureFlagMap[FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED];
if (!isPermissionsV2Enabled) {
return queryBuilder;
@ -371,7 +371,7 @@ export class WorkspaceEntityManager extends EntityManager {
const featureFlagMap = this.getFeatureFlagMap();
const isPermissionsV2Enabled =
featureFlagMap[FeatureFlagKey.IsPermissionsV2Enabled];
featureFlagMap[FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED];
if (!isPermissionsV2Enabled) {
return;

View File

@ -67,7 +67,7 @@ export class WorkspaceRepository<
queryRunner,
) as unknown as WorkspaceSelectQueryBuilder<U>;
const isPermissionsV2Enabled =
this.featureFlagMap[FeatureFlagKey.IsPermissionsV2Enabled];
this.featureFlagMap[FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED];
if (!isPermissionsV2Enabled) {
return queryBuilder;

View File

@ -137,7 +137,7 @@ const convertHttpExceptionToGraphql = (exception: HttpException) => {
}
// Only show the stack trace in development mode
if (process.env.NODE_ENV === NodeEnvironment.development) {
if (process.env.NODE_ENV === NodeEnvironment.DEVELOPMENT) {
error.stack = exception.stack;
error.extensions['response'] = exception.getResponse();
}

View File

@ -1,4 +1,4 @@
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { ViewOpenRecordInType } from 'src/modules/view/standard-objects/view.workspace-entity';
export interface ViewDefinition {
@ -11,14 +11,14 @@ export interface ViewDefinition {
icon?: string;
openRecordIn?: ViewOpenRecordInType;
kanbanFieldMetadataId?: string;
kanbanAggregateOperation?: AGGREGATE_OPERATIONS;
kanbanAggregateOperation?: AggregateOperations;
kanbanAggregateOperationFieldMetadataId?: string;
fields?: {
fieldMetadataId: string;
position: number;
isVisible: boolean;
size: number;
aggregateOperation?: AGGREGATE_OPERATIONS;
aggregateOperation?: AggregateOperations;
}[];
filters?: {
fieldMetadataId: string;

View File

@ -1,6 +1,6 @@
import { ObjectMetadataStandardIdToIdMap } from 'src/engine/metadata-modules/object-metadata/interfaces/object-metadata-standard-id-to-id-map';
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import {
BASE_OBJECT_STANDARD_FIELD_IDS,
COMPANY_STANDARD_FIELD_IDS,
@ -38,7 +38,7 @@ export const companiesAllView = (
position: 1,
isVisible: true,
size: 100,
aggregateOperation: AGGREGATE_OPERATIONS.count,
aggregateOperation: AggregateOperations.COUNT,
},
{
fieldMetadataId:
@ -75,7 +75,7 @@ export const companiesAllView = (
position: 5,
isVisible: true,
size: 150,
aggregateOperation: AGGREGATE_OPERATIONS.max,
aggregateOperation: AggregateOperations.MAX,
},
{
fieldMetadataId:
@ -85,7 +85,7 @@ export const companiesAllView = (
position: 6,
isVisible: true,
size: 170,
aggregateOperation: AGGREGATE_OPERATIONS.percentageEmpty,
aggregateOperation: AggregateOperations.PERCENTAGE_EMPTY,
},
{
fieldMetadataId:
@ -95,7 +95,7 @@ export const companiesAllView = (
position: 7,
isVisible: true,
size: 170,
aggregateOperation: AGGREGATE_OPERATIONS.countNotEmpty,
aggregateOperation: AggregateOperations.COUNT_NOT_EMPTY,
},
],
};

View File

@ -1,6 +1,6 @@
import { ObjectMetadataStandardIdToIdMap } from 'src/engine/metadata-modules/object-metadata/interfaces/object-metadata-standard-id-to-id-map';
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { OPPORTUNITY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
@ -33,7 +33,7 @@ export const opportunitiesAllView = (
position: 1,
isVisible: true,
size: 150,
aggregateOperation: AGGREGATE_OPERATIONS.avg,
aggregateOperation: AggregateOperations.AVG,
},
{
fieldMetadataId:
@ -50,7 +50,7 @@ export const opportunitiesAllView = (
position: 3,
isVisible: true,
size: 150,
aggregateOperation: AGGREGATE_OPERATIONS.min,
aggregateOperation: AggregateOperations.MIN,
},
{
fieldMetadataId:

View File

@ -1,6 +1,6 @@
import { ObjectMetadataStandardIdToIdMap } from 'src/engine/metadata-modules/object-metadata/interfaces/object-metadata-standard-id-to-id-map';
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { OPPORTUNITY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
@ -19,7 +19,7 @@ export const opportunitiesByStageView = (
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity].fields[
OPPORTUNITY_STANDARD_FIELD_IDS.stage
],
kanbanAggregateOperation: AGGREGATE_OPERATIONS.min,
kanbanAggregateOperation: AggregateOperations.MIN,
kanbanAggregateOperationFieldMetadataId:
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity].fields[
OPPORTUNITY_STANDARD_FIELD_IDS.amount

View File

@ -1,6 +1,6 @@
import { ObjectMetadataStandardIdToIdMap } from 'src/engine/metadata-modules/object-metadata/interfaces/object-metadata-standard-id-to-id-map';
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { OPPORTUNITY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
@ -36,7 +36,7 @@ export const opportunitiesTableByStageView = (
position: 1,
isVisible: true,
size: 150,
aggregateOperation: AGGREGATE_OPERATIONS.avg,
aggregateOperation: AggregateOperations.AVG,
},
{
fieldMetadataId:
@ -53,7 +53,7 @@ export const opportunitiesTableByStageView = (
position: 3,
isVisible: true,
size: 150,
aggregateOperation: AGGREGATE_OPERATIONS.max,
aggregateOperation: AggregateOperations.MAX,
},
{
fieldMetadataId:
@ -62,7 +62,7 @@ export const opportunitiesTableByStageView = (
position: 4,
isVisible: true,
size: 150,
aggregateOperation: AGGREGATE_OPERATIONS.count,
aggregateOperation: AggregateOperations.COUNT,
},
{
fieldMetadataId:

View File

@ -1,6 +1,6 @@
import { ObjectMetadataStandardIdToIdMap } from 'src/engine/metadata-modules/object-metadata/interfaces/object-metadata-standard-id-to-id-map';
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import {
BASE_OBJECT_STANDARD_FIELD_IDS,
PERSON_STANDARD_FIELD_IDS,
@ -38,7 +38,7 @@ export const peopleAllView = (
position: 1,
isVisible: true,
size: 150,
aggregateOperation: AGGREGATE_OPERATIONS.countUniqueValues,
aggregateOperation: AggregateOperations.COUNT_UNIQUE_VALUES,
},
{
fieldMetadataId:
@ -66,7 +66,7 @@ export const peopleAllView = (
position: 4,
isVisible: true,
size: 150,
aggregateOperation: AGGREGATE_OPERATIONS.percentageEmpty,
aggregateOperation: AggregateOperations.PERCENTAGE_EMPTY,
},
{
fieldMetadataId:
@ -76,7 +76,7 @@ export const peopleAllView = (
position: 5,
isVisible: true,
size: 150,
aggregateOperation: AGGREGATE_OPERATIONS.min,
aggregateOperation: AggregateOperations.MIN,
},
{
fieldMetadataId: