Twenty config admin panel integration (#11755)
closes https://github.com/twentyhq/core-team-issues/issues/761 closes https://github.com/twentyhq/core-team-issues/issues/762 --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
2
.github/workflows/preview-env-keepalive.yaml
vendored
2
.github/workflows/preview-env-keepalive.yaml
vendored
@ -33,6 +33,8 @@ jobs:
|
|||||||
echo "# === Randomly generated secrets ===" >> packages/twenty-docker/.env
|
echo "# === Randomly generated secrets ===" >> packages/twenty-docker/.env
|
||||||
echo "APP_SECRET=$(openssl rand -base64 32)" >> packages/twenty-docker/.env
|
echo "APP_SECRET=$(openssl rand -base64 32)" >> packages/twenty-docker/.env
|
||||||
echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 16)" >> packages/twenty-docker/.env
|
echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 16)" >> packages/twenty-docker/.env
|
||||||
|
# Remove line below when true becomes the default value (soon)
|
||||||
|
echo "CONFIG_VARIABLES_IN_DB_ENABLED=true" >> packages/twenty-docker/.env
|
||||||
|
|
||||||
echo "Docker compose build..."
|
echo "Docker compose build..."
|
||||||
cd packages/twenty-docker/
|
cd packages/twenty-docker/
|
||||||
|
|||||||
@ -301,6 +301,7 @@ export type ClientConfig = {
|
|||||||
defaultSubdomain?: Maybe<Scalars['String']>;
|
defaultSubdomain?: Maybe<Scalars['String']>;
|
||||||
frontDomain: Scalars['String'];
|
frontDomain: Scalars['String'];
|
||||||
isAttachmentPreviewEnabled: Scalars['Boolean'];
|
isAttachmentPreviewEnabled: Scalars['Boolean'];
|
||||||
|
isConfigVariablesInDbEnabled: Scalars['Boolean'];
|
||||||
isEmailVerificationRequired: Scalars['Boolean'];
|
isEmailVerificationRequired: Scalars['Boolean'];
|
||||||
isGoogleCalendarEnabled: Scalars['Boolean'];
|
isGoogleCalendarEnabled: Scalars['Boolean'];
|
||||||
isGoogleMessagingEnabled: Scalars['Boolean'];
|
isGoogleMessagingEnabled: Scalars['Boolean'];
|
||||||
@ -318,14 +319,32 @@ export type ComputeStepOutputSchemaInput = {
|
|||||||
step: Scalars['JSON'];
|
step: Scalars['JSON'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum ConfigSource {
|
||||||
|
DATABASE = 'DATABASE',
|
||||||
|
DEFAULT = 'DEFAULT',
|
||||||
|
ENVIRONMENT = 'ENVIRONMENT'
|
||||||
|
}
|
||||||
|
|
||||||
export type ConfigVariable = {
|
export type ConfigVariable = {
|
||||||
__typename?: 'ConfigVariable';
|
__typename?: 'ConfigVariable';
|
||||||
description: Scalars['String'];
|
description: Scalars['String'];
|
||||||
|
isEnvOnly: Scalars['Boolean'];
|
||||||
isSensitive: Scalars['Boolean'];
|
isSensitive: Scalars['Boolean'];
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
value: Scalars['String'];
|
options?: Maybe<Scalars['JSON']>;
|
||||||
|
source: ConfigSource;
|
||||||
|
type: ConfigVariableType;
|
||||||
|
value?: Maybe<Scalars['JSON']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum ConfigVariableType {
|
||||||
|
ARRAY = 'ARRAY',
|
||||||
|
BOOLEAN = 'BOOLEAN',
|
||||||
|
ENUM = 'ENUM',
|
||||||
|
NUMBER = 'NUMBER',
|
||||||
|
STRING = 'STRING'
|
||||||
|
}
|
||||||
|
|
||||||
export enum ConfigVariablesGroup {
|
export enum ConfigVariablesGroup {
|
||||||
AnalyticsConfig = 'AnalyticsConfig',
|
AnalyticsConfig = 'AnalyticsConfig',
|
||||||
BillingConfig = 'BillingConfig',
|
BillingConfig = 'BillingConfig',
|
||||||
@ -868,6 +887,7 @@ export type Mutation = {
|
|||||||
checkoutSession: BillingSessionOutput;
|
checkoutSession: BillingSessionOutput;
|
||||||
computeStepOutputSchema: Scalars['JSON'];
|
computeStepOutputSchema: Scalars['JSON'];
|
||||||
createApprovedAccessDomain: ApprovedAccessDomain;
|
createApprovedAccessDomain: ApprovedAccessDomain;
|
||||||
|
createDatabaseConfigVariable: Scalars['Boolean'];
|
||||||
createDraftFromWorkflowVersion: WorkflowVersion;
|
createDraftFromWorkflowVersion: WorkflowVersion;
|
||||||
createOIDCIdentityProvider: SetupSsoOutput;
|
createOIDCIdentityProvider: SetupSsoOutput;
|
||||||
createOneAppToken: AppToken;
|
createOneAppToken: AppToken;
|
||||||
@ -880,6 +900,7 @@ export type Mutation = {
|
|||||||
deactivateWorkflowVersion: Scalars['Boolean'];
|
deactivateWorkflowVersion: Scalars['Boolean'];
|
||||||
deleteApprovedAccessDomain: Scalars['Boolean'];
|
deleteApprovedAccessDomain: Scalars['Boolean'];
|
||||||
deleteCurrentWorkspace: Workspace;
|
deleteCurrentWorkspace: Workspace;
|
||||||
|
deleteDatabaseConfigVariable: Scalars['Boolean'];
|
||||||
deleteOneField: Field;
|
deleteOneField: Field;
|
||||||
deleteOneObject: Object;
|
deleteOneObject: Object;
|
||||||
deleteOneRole: Scalars['String'];
|
deleteOneRole: Scalars['String'];
|
||||||
@ -914,6 +935,7 @@ export type Mutation = {
|
|||||||
switchToYearlyInterval: BillingUpdateOutput;
|
switchToYearlyInterval: BillingUpdateOutput;
|
||||||
track: Analytics;
|
track: Analytics;
|
||||||
trackAnalytics: Analytics;
|
trackAnalytics: Analytics;
|
||||||
|
updateDatabaseConfigVariable: Scalars['Boolean'];
|
||||||
updateLabPublicFeatureFlag: FeatureFlagDto;
|
updateLabPublicFeatureFlag: FeatureFlagDto;
|
||||||
updateOneField: Field;
|
updateOneField: Field;
|
||||||
updateOneObject: Object;
|
updateOneObject: Object;
|
||||||
@ -971,6 +993,12 @@ export type MutationCreateApprovedAccessDomainArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateDatabaseConfigVariableArgs = {
|
||||||
|
key: Scalars['String'];
|
||||||
|
value: Scalars['JSON'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationCreateDraftFromWorkflowVersionArgs = {
|
export type MutationCreateDraftFromWorkflowVersionArgs = {
|
||||||
input: CreateDraftFromWorkflowVersionInput;
|
input: CreateDraftFromWorkflowVersionInput;
|
||||||
};
|
};
|
||||||
@ -1016,6 +1044,11 @@ export type MutationDeleteApprovedAccessDomainArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationDeleteDatabaseConfigVariableArgs = {
|
||||||
|
key: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationDeleteOneFieldArgs = {
|
export type MutationDeleteOneFieldArgs = {
|
||||||
input: DeleteOneFieldInput;
|
input: DeleteOneFieldInput;
|
||||||
};
|
};
|
||||||
@ -1162,6 +1195,12 @@ export type MutationTrackAnalyticsArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationUpdateDatabaseConfigVariableArgs = {
|
||||||
|
key: Scalars['String'];
|
||||||
|
value: Scalars['JSON'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationUpdateLabPublicFeatureFlagArgs = {
|
export type MutationUpdateLabPublicFeatureFlagArgs = {
|
||||||
input: UpdateLabPublicFeatureFlagInput;
|
input: UpdateLabPublicFeatureFlagInput;
|
||||||
};
|
};
|
||||||
@ -1485,6 +1524,7 @@ export type Query = {
|
|||||||
getApprovedAccessDomains: Array<ApprovedAccessDomain>;
|
getApprovedAccessDomains: Array<ApprovedAccessDomain>;
|
||||||
getAvailablePackages: Scalars['JSON'];
|
getAvailablePackages: Scalars['JSON'];
|
||||||
getConfigVariablesGrouped: ConfigVariablesOutput;
|
getConfigVariablesGrouped: ConfigVariablesOutput;
|
||||||
|
getDatabaseConfigVariable: ConfigVariable;
|
||||||
getIndicatorHealthStatus: AdminPanelHealthServiceData;
|
getIndicatorHealthStatus: AdminPanelHealthServiceData;
|
||||||
getMeteredProductsUsage: Array<BillingMeteredProductUsageOutput>;
|
getMeteredProductsUsage: Array<BillingMeteredProductUsageOutput>;
|
||||||
getPostgresCredentials?: Maybe<PostgresCredentials>;
|
getPostgresCredentials?: Maybe<PostgresCredentials>;
|
||||||
@ -1540,6 +1580,11 @@ export type QueryGetAvailablePackagesArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryGetDatabaseConfigVariableArgs = {
|
||||||
|
key: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueryGetIndicatorHealthStatusArgs = {
|
export type QueryGetIndicatorHealthStatusArgs = {
|
||||||
indicatorId: HealthIndicatorId;
|
indicatorId: HealthIndicatorId;
|
||||||
};
|
};
|
||||||
@ -2665,7 +2710,7 @@ export type SwitchSubscriptionToYearlyIntervalMutation = { __typename?: 'Mutatio
|
|||||||
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isEmailVerificationRequired: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, isAttachmentPreviewEnabled: boolean, chromeExtensionId?: string | null, canManageFeatureFlags: boolean, isMicrosoftMessagingEnabled: boolean, isMicrosoftCalendarEnabled: boolean, isGoogleMessagingEnabled: boolean, isGoogleCalendarEnabled: boolean, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, trialPeriods: Array<{ __typename?: 'BillingTrialPeriodDTO', duration: number, isCreditCardRequired: boolean }> }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number }, publicFeatureFlags: Array<{ __typename?: 'PublicFeatureFlag', key: FeatureFlagKey, metadata: { __typename?: 'PublicFeatureFlagMetadata', label: string, description: string, imagePath: string } }> } };
|
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isEmailVerificationRequired: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, isAttachmentPreviewEnabled: boolean, chromeExtensionId?: string | null, canManageFeatureFlags: boolean, isMicrosoftMessagingEnabled: boolean, isMicrosoftCalendarEnabled: boolean, isGoogleMessagingEnabled: boolean, isGoogleCalendarEnabled: boolean, isConfigVariablesInDbEnabled: boolean, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, trialPeriods: Array<{ __typename?: 'BillingTrialPeriodDTO', duration: number, isCreditCardRequired: boolean }> }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number }, publicFeatureFlags: Array<{ __typename?: 'PublicFeatureFlag', key: FeatureFlagKey, metadata: { __typename?: 'PublicFeatureFlagMetadata', label: string, description: string, imagePath: string } }> } };
|
||||||
|
|
||||||
export type SearchQueryVariables = Exact<{
|
export type SearchQueryVariables = Exact<{
|
||||||
searchInput: Scalars['String'];
|
searchInput: Scalars['String'];
|
||||||
@ -2683,6 +2728,41 @@ export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]
|
|||||||
|
|
||||||
export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } };
|
export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } };
|
||||||
|
|
||||||
|
export type CreateDatabaseConfigVariableMutationVariables = Exact<{
|
||||||
|
key: Scalars['String'];
|
||||||
|
value: Scalars['JSON'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type CreateDatabaseConfigVariableMutation = { __typename?: 'Mutation', createDatabaseConfigVariable: boolean };
|
||||||
|
|
||||||
|
export type DeleteDatabaseConfigVariableMutationVariables = Exact<{
|
||||||
|
key: Scalars['String'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type DeleteDatabaseConfigVariableMutation = { __typename?: 'Mutation', deleteDatabaseConfigVariable: boolean };
|
||||||
|
|
||||||
|
export type UpdateDatabaseConfigVariableMutationVariables = Exact<{
|
||||||
|
key: Scalars['String'];
|
||||||
|
value: Scalars['JSON'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type UpdateDatabaseConfigVariableMutation = { __typename?: 'Mutation', updateDatabaseConfigVariable: boolean };
|
||||||
|
|
||||||
|
export type GetConfigVariablesGroupedQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type GetConfigVariablesGroupedQuery = { __typename?: 'Query', getConfigVariablesGrouped: { __typename?: 'ConfigVariablesOutput', groups: Array<{ __typename?: 'ConfigVariablesGroupData', name: ConfigVariablesGroup, description: string, isHiddenOnLoad: boolean, variables: Array<{ __typename?: 'ConfigVariable', name: string, description: string, value?: any | null, isSensitive: boolean, isEnvOnly: boolean, type: ConfigVariableType, options?: any | null, source: ConfigSource }> }> } };
|
||||||
|
|
||||||
|
export type GetDatabaseConfigVariableQueryVariables = Exact<{
|
||||||
|
key: Scalars['String'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type GetDatabaseConfigVariableQuery = { __typename?: 'Query', getDatabaseConfigVariable: { __typename?: 'ConfigVariable', name: string, description: string, value?: any | null, isSensitive: boolean, isEnvOnly: boolean, type: ConfigVariableType, options?: any | null, source: ConfigSource } };
|
||||||
|
|
||||||
export type UpdateWorkspaceFeatureFlagMutationVariables = Exact<{
|
export type UpdateWorkspaceFeatureFlagMutationVariables = Exact<{
|
||||||
workspaceId: Scalars['String'];
|
workspaceId: Scalars['String'];
|
||||||
featureFlag: Scalars['String'];
|
featureFlag: Scalars['String'];
|
||||||
@ -2699,11 +2779,6 @@ export type UserLookupAdminPanelMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, allowImpersonation: boolean, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> }> } };
|
export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, allowImpersonation: boolean, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> }> } };
|
||||||
|
|
||||||
export type GetConfigVariablesGroupedQueryVariables = Exact<{ [key: string]: never; }>;
|
|
||||||
|
|
||||||
|
|
||||||
export type GetConfigVariablesGroupedQuery = { __typename?: 'Query', getConfigVariablesGrouped: { __typename?: 'ConfigVariablesOutput', groups: Array<{ __typename?: 'ConfigVariablesGroupData', name: ConfigVariablesGroup, description: string, isHiddenOnLoad: boolean, variables: Array<{ __typename?: 'ConfigVariable', name: string, description: string, value: string, isSensitive: boolean }> }> } };
|
|
||||||
|
|
||||||
export type GetVersionInfoQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetVersionInfoQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
@ -4510,6 +4585,7 @@ export const GetClientConfigDocument = gql`
|
|||||||
isMicrosoftCalendarEnabled
|
isMicrosoftCalendarEnabled
|
||||||
isGoogleMessagingEnabled
|
isGoogleMessagingEnabled
|
||||||
isGoogleCalendarEnabled
|
isGoogleCalendarEnabled
|
||||||
|
isConfigVariablesInDbEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -4622,6 +4698,191 @@ export function useSkipSyncEmailOnboardingStepMutation(baseOptions?: Apollo.Muta
|
|||||||
export type SkipSyncEmailOnboardingStepMutationHookResult = ReturnType<typeof useSkipSyncEmailOnboardingStepMutation>;
|
export type SkipSyncEmailOnboardingStepMutationHookResult = ReturnType<typeof useSkipSyncEmailOnboardingStepMutation>;
|
||||||
export type SkipSyncEmailOnboardingStepMutationResult = Apollo.MutationResult<SkipSyncEmailOnboardingStepMutation>;
|
export type SkipSyncEmailOnboardingStepMutationResult = Apollo.MutationResult<SkipSyncEmailOnboardingStepMutation>;
|
||||||
export type SkipSyncEmailOnboardingStepMutationOptions = Apollo.BaseMutationOptions<SkipSyncEmailOnboardingStepMutation, SkipSyncEmailOnboardingStepMutationVariables>;
|
export type SkipSyncEmailOnboardingStepMutationOptions = Apollo.BaseMutationOptions<SkipSyncEmailOnboardingStepMutation, SkipSyncEmailOnboardingStepMutationVariables>;
|
||||||
|
export const CreateDatabaseConfigVariableDocument = gql`
|
||||||
|
mutation CreateDatabaseConfigVariable($key: String!, $value: JSON!) {
|
||||||
|
createDatabaseConfigVariable(key: $key, value: $value)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type CreateDatabaseConfigVariableMutationFn = Apollo.MutationFunction<CreateDatabaseConfigVariableMutation, CreateDatabaseConfigVariableMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useCreateDatabaseConfigVariableMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useCreateDatabaseConfigVariableMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useCreateDatabaseConfigVariableMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [createDatabaseConfigVariableMutation, { data, loading, error }] = useCreateDatabaseConfigVariableMutation({
|
||||||
|
* variables: {
|
||||||
|
* key: // value for 'key'
|
||||||
|
* value: // value for 'value'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useCreateDatabaseConfigVariableMutation(baseOptions?: Apollo.MutationHookOptions<CreateDatabaseConfigVariableMutation, CreateDatabaseConfigVariableMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<CreateDatabaseConfigVariableMutation, CreateDatabaseConfigVariableMutationVariables>(CreateDatabaseConfigVariableDocument, options);
|
||||||
|
}
|
||||||
|
export type CreateDatabaseConfigVariableMutationHookResult = ReturnType<typeof useCreateDatabaseConfigVariableMutation>;
|
||||||
|
export type CreateDatabaseConfigVariableMutationResult = Apollo.MutationResult<CreateDatabaseConfigVariableMutation>;
|
||||||
|
export type CreateDatabaseConfigVariableMutationOptions = Apollo.BaseMutationOptions<CreateDatabaseConfigVariableMutation, CreateDatabaseConfigVariableMutationVariables>;
|
||||||
|
export const DeleteDatabaseConfigVariableDocument = gql`
|
||||||
|
mutation DeleteDatabaseConfigVariable($key: String!) {
|
||||||
|
deleteDatabaseConfigVariable(key: $key)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type DeleteDatabaseConfigVariableMutationFn = Apollo.MutationFunction<DeleteDatabaseConfigVariableMutation, DeleteDatabaseConfigVariableMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useDeleteDatabaseConfigVariableMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useDeleteDatabaseConfigVariableMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useDeleteDatabaseConfigVariableMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [deleteDatabaseConfigVariableMutation, { data, loading, error }] = useDeleteDatabaseConfigVariableMutation({
|
||||||
|
* variables: {
|
||||||
|
* key: // value for 'key'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useDeleteDatabaseConfigVariableMutation(baseOptions?: Apollo.MutationHookOptions<DeleteDatabaseConfigVariableMutation, DeleteDatabaseConfigVariableMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<DeleteDatabaseConfigVariableMutation, DeleteDatabaseConfigVariableMutationVariables>(DeleteDatabaseConfigVariableDocument, options);
|
||||||
|
}
|
||||||
|
export type DeleteDatabaseConfigVariableMutationHookResult = ReturnType<typeof useDeleteDatabaseConfigVariableMutation>;
|
||||||
|
export type DeleteDatabaseConfigVariableMutationResult = Apollo.MutationResult<DeleteDatabaseConfigVariableMutation>;
|
||||||
|
export type DeleteDatabaseConfigVariableMutationOptions = Apollo.BaseMutationOptions<DeleteDatabaseConfigVariableMutation, DeleteDatabaseConfigVariableMutationVariables>;
|
||||||
|
export const UpdateDatabaseConfigVariableDocument = gql`
|
||||||
|
mutation UpdateDatabaseConfigVariable($key: String!, $value: JSON!) {
|
||||||
|
updateDatabaseConfigVariable(key: $key, value: $value)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type UpdateDatabaseConfigVariableMutationFn = Apollo.MutationFunction<UpdateDatabaseConfigVariableMutation, UpdateDatabaseConfigVariableMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useUpdateDatabaseConfigVariableMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useUpdateDatabaseConfigVariableMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useUpdateDatabaseConfigVariableMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [updateDatabaseConfigVariableMutation, { data, loading, error }] = useUpdateDatabaseConfigVariableMutation({
|
||||||
|
* variables: {
|
||||||
|
* key: // value for 'key'
|
||||||
|
* value: // value for 'value'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useUpdateDatabaseConfigVariableMutation(baseOptions?: Apollo.MutationHookOptions<UpdateDatabaseConfigVariableMutation, UpdateDatabaseConfigVariableMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<UpdateDatabaseConfigVariableMutation, UpdateDatabaseConfigVariableMutationVariables>(UpdateDatabaseConfigVariableDocument, options);
|
||||||
|
}
|
||||||
|
export type UpdateDatabaseConfigVariableMutationHookResult = ReturnType<typeof useUpdateDatabaseConfigVariableMutation>;
|
||||||
|
export type UpdateDatabaseConfigVariableMutationResult = Apollo.MutationResult<UpdateDatabaseConfigVariableMutation>;
|
||||||
|
export type UpdateDatabaseConfigVariableMutationOptions = Apollo.BaseMutationOptions<UpdateDatabaseConfigVariableMutation, UpdateDatabaseConfigVariableMutationVariables>;
|
||||||
|
export const GetConfigVariablesGroupedDocument = gql`
|
||||||
|
query GetConfigVariablesGrouped {
|
||||||
|
getConfigVariablesGrouped {
|
||||||
|
groups {
|
||||||
|
name
|
||||||
|
description
|
||||||
|
isHiddenOnLoad
|
||||||
|
variables {
|
||||||
|
name
|
||||||
|
description
|
||||||
|
value
|
||||||
|
isSensitive
|
||||||
|
isEnvOnly
|
||||||
|
type
|
||||||
|
options
|
||||||
|
source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useGetConfigVariablesGroupedQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useGetConfigVariablesGroupedQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useGetConfigVariablesGroupedQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = useGetConfigVariablesGroupedQuery({
|
||||||
|
* variables: {
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useGetConfigVariablesGroupedQuery(baseOptions?: Apollo.QueryHookOptions<GetConfigVariablesGroupedQuery, GetConfigVariablesGroupedQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<GetConfigVariablesGroupedQuery, GetConfigVariablesGroupedQueryVariables>(GetConfigVariablesGroupedDocument, options);
|
||||||
|
}
|
||||||
|
export function useGetConfigVariablesGroupedLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetConfigVariablesGroupedQuery, GetConfigVariablesGroupedQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<GetConfigVariablesGroupedQuery, GetConfigVariablesGroupedQueryVariables>(GetConfigVariablesGroupedDocument, options);
|
||||||
|
}
|
||||||
|
export type GetConfigVariablesGroupedQueryHookResult = ReturnType<typeof useGetConfigVariablesGroupedQuery>;
|
||||||
|
export type GetConfigVariablesGroupedLazyQueryHookResult = ReturnType<typeof useGetConfigVariablesGroupedLazyQuery>;
|
||||||
|
export type GetConfigVariablesGroupedQueryResult = Apollo.QueryResult<GetConfigVariablesGroupedQuery, GetConfigVariablesGroupedQueryVariables>;
|
||||||
|
export const GetDatabaseConfigVariableDocument = gql`
|
||||||
|
query GetDatabaseConfigVariable($key: String!) {
|
||||||
|
getDatabaseConfigVariable(key: $key) {
|
||||||
|
name
|
||||||
|
description
|
||||||
|
value
|
||||||
|
isSensitive
|
||||||
|
isEnvOnly
|
||||||
|
type
|
||||||
|
options
|
||||||
|
source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useGetDatabaseConfigVariableQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useGetDatabaseConfigVariableQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useGetDatabaseConfigVariableQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = useGetDatabaseConfigVariableQuery({
|
||||||
|
* variables: {
|
||||||
|
* key: // value for 'key'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useGetDatabaseConfigVariableQuery(baseOptions: Apollo.QueryHookOptions<GetDatabaseConfigVariableQuery, GetDatabaseConfigVariableQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<GetDatabaseConfigVariableQuery, GetDatabaseConfigVariableQueryVariables>(GetDatabaseConfigVariableDocument, options);
|
||||||
|
}
|
||||||
|
export function useGetDatabaseConfigVariableLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetDatabaseConfigVariableQuery, GetDatabaseConfigVariableQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<GetDatabaseConfigVariableQuery, GetDatabaseConfigVariableQueryVariables>(GetDatabaseConfigVariableDocument, options);
|
||||||
|
}
|
||||||
|
export type GetDatabaseConfigVariableQueryHookResult = ReturnType<typeof useGetDatabaseConfigVariableQuery>;
|
||||||
|
export type GetDatabaseConfigVariableLazyQueryHookResult = ReturnType<typeof useGetDatabaseConfigVariableLazyQuery>;
|
||||||
|
export type GetDatabaseConfigVariableQueryResult = Apollo.QueryResult<GetDatabaseConfigVariableQuery, GetDatabaseConfigVariableQueryVariables>;
|
||||||
export const UpdateWorkspaceFeatureFlagDocument = gql`
|
export const UpdateWorkspaceFeatureFlagDocument = gql`
|
||||||
mutation UpdateWorkspaceFeatureFlag($workspaceId: String!, $featureFlag: String!, $value: Boolean!) {
|
mutation UpdateWorkspaceFeatureFlag($workspaceId: String!, $featureFlag: String!, $value: Boolean!) {
|
||||||
updateWorkspaceFeatureFlag(
|
updateWorkspaceFeatureFlag(
|
||||||
@ -4714,50 +4975,6 @@ export function useUserLookupAdminPanelMutation(baseOptions?: Apollo.MutationHoo
|
|||||||
export type UserLookupAdminPanelMutationHookResult = ReturnType<typeof useUserLookupAdminPanelMutation>;
|
export type UserLookupAdminPanelMutationHookResult = ReturnType<typeof useUserLookupAdminPanelMutation>;
|
||||||
export type UserLookupAdminPanelMutationResult = Apollo.MutationResult<UserLookupAdminPanelMutation>;
|
export type UserLookupAdminPanelMutationResult = Apollo.MutationResult<UserLookupAdminPanelMutation>;
|
||||||
export type UserLookupAdminPanelMutationOptions = Apollo.BaseMutationOptions<UserLookupAdminPanelMutation, UserLookupAdminPanelMutationVariables>;
|
export type UserLookupAdminPanelMutationOptions = Apollo.BaseMutationOptions<UserLookupAdminPanelMutation, UserLookupAdminPanelMutationVariables>;
|
||||||
export const GetConfigVariablesGroupedDocument = gql`
|
|
||||||
query GetConfigVariablesGrouped {
|
|
||||||
getConfigVariablesGrouped {
|
|
||||||
groups {
|
|
||||||
name
|
|
||||||
description
|
|
||||||
isHiddenOnLoad
|
|
||||||
variables {
|
|
||||||
name
|
|
||||||
description
|
|
||||||
value
|
|
||||||
isSensitive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* __useGetConfigVariablesGroupedQuery__
|
|
||||||
*
|
|
||||||
* To run a query within a React component, call `useGetConfigVariablesGroupedQuery` and pass it any options that fit your needs.
|
|
||||||
* When your component renders, `useGetConfigVariablesGroupedQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
|
||||||
* you can use to render your UI.
|
|
||||||
*
|
|
||||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const { data, loading, error } = useGetConfigVariablesGroupedQuery({
|
|
||||||
* variables: {
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
export function useGetConfigVariablesGroupedQuery(baseOptions?: Apollo.QueryHookOptions<GetConfigVariablesGroupedQuery, GetConfigVariablesGroupedQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useQuery<GetConfigVariablesGroupedQuery, GetConfigVariablesGroupedQueryVariables>(GetConfigVariablesGroupedDocument, options);
|
|
||||||
}
|
|
||||||
export function useGetConfigVariablesGroupedLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetConfigVariablesGroupedQuery, GetConfigVariablesGroupedQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useLazyQuery<GetConfigVariablesGroupedQuery, GetConfigVariablesGroupedQueryVariables>(GetConfigVariablesGroupedDocument, options);
|
|
||||||
}
|
|
||||||
export type GetConfigVariablesGroupedQueryHookResult = ReturnType<typeof useGetConfigVariablesGroupedQuery>;
|
|
||||||
export type GetConfigVariablesGroupedLazyQueryHookResult = ReturnType<typeof useGetConfigVariablesGroupedLazyQuery>;
|
|
||||||
export type GetConfigVariablesGroupedQueryResult = Apollo.QueryResult<GetConfigVariablesGroupedQuery, GetConfigVariablesGroupedQueryVariables>;
|
|
||||||
export const GetVersionInfoDocument = gql`
|
export const GetVersionInfoDocument = gql`
|
||||||
query GetVersionInfo {
|
query GetVersionInfo {
|
||||||
versionInfo {
|
versionInfo {
|
||||||
|
|||||||
@ -281,11 +281,11 @@ const SettingsAdminIndicatorHealthStatus = lazy(() =>
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const SettingsAdminSecondaryEnvVariables = lazy(() =>
|
const SettingsAdminConfigVariableDetails = lazy(() =>
|
||||||
import(
|
import(
|
||||||
'~/pages/settings/admin-panel/SettingsAdminSecondaryEnvVariables'
|
'~/pages/settings/admin-panel/SettingsAdminConfigVariableDetails'
|
||||||
).then((module) => ({
|
).then((module) => ({
|
||||||
default: module.SettingsAdminSecondaryEnvVariables,
|
default: module.SettingsAdminConfigVariableDetails,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -505,9 +505,10 @@ export const SettingsRoutes = ({
|
|||||||
path={SettingsPath.AdminPanelIndicatorHealthStatus}
|
path={SettingsPath.AdminPanelIndicatorHealthStatus}
|
||||||
element={<SettingsAdminIndicatorHealthStatus />}
|
element={<SettingsAdminIndicatorHealthStatus />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={SettingsPath.AdminPanelOtherEnvVariables}
|
path={SettingsPath.AdminPanelConfigVariableDetails}
|
||||||
element={<SettingsAdminSecondaryEnvVariables />}
|
element={<SettingsAdminConfigVariableDetails />}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionId
|
|||||||
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||||
import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
|
import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
|
||||||
import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState';
|
import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState';
|
||||||
|
import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState';
|
||||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||||
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
||||||
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
|
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
|
||||||
@ -21,8 +22,8 @@ import { supportChatState } from '@/client-config/states/supportChatState';
|
|||||||
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
|
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
import { useGetClientConfigQuery } from '~/generated/graphql';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { useGetClientConfigQuery } from '~/generated/graphql';
|
||||||
|
|
||||||
export const ClientConfigProviderEffect = () => {
|
export const ClientConfigProviderEffect = () => {
|
||||||
const setIsDebugMode = useSetRecoilState(isDebugModeState);
|
const setIsDebugMode = useSetRecoilState(isDebugModeState);
|
||||||
@ -82,6 +83,10 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
isAttachmentPreviewEnabledState,
|
isAttachmentPreviewEnabledState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setIsConfigVariablesInDbEnabled = useSetRecoilState(
|
||||||
|
isConfigVariablesInDbEnabledState,
|
||||||
|
);
|
||||||
|
|
||||||
const { data, loading, error } = useGetClientConfigQuery({
|
const { data, loading, error } = useGetClientConfigQuery({
|
||||||
skip: clientConfigApiStatus.isLoaded,
|
skip: clientConfigApiStatus.isLoaded,
|
||||||
});
|
});
|
||||||
@ -157,6 +162,9 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
setIsAttachmentPreviewEnabled(
|
setIsAttachmentPreviewEnabled(
|
||||||
data?.clientConfig?.isAttachmentPreviewEnabled,
|
data?.clientConfig?.isAttachmentPreviewEnabled,
|
||||||
);
|
);
|
||||||
|
setIsConfigVariablesInDbEnabled(
|
||||||
|
data?.clientConfig?.isConfigVariablesInDbEnabled,
|
||||||
|
);
|
||||||
}, [
|
}, [
|
||||||
data,
|
data,
|
||||||
setIsDebugMode,
|
setIsDebugMode,
|
||||||
@ -182,6 +190,7 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
setGoogleMessagingEnabled,
|
setGoogleMessagingEnabled,
|
||||||
setGoogleCalendarEnabled,
|
setGoogleCalendarEnabled,
|
||||||
setIsAttachmentPreviewEnabled,
|
setIsAttachmentPreviewEnabled,
|
||||||
|
setIsConfigVariablesInDbEnabled,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
@ -61,6 +61,7 @@ export const GET_CLIENT_CONFIG = gql`
|
|||||||
isMicrosoftCalendarEnabled
|
isMicrosoftCalendarEnabled
|
||||||
isGoogleMessagingEnabled
|
isGoogleMessagingEnabled
|
||||||
isGoogleCalendarEnabled
|
isGoogleCalendarEnabled
|
||||||
|
isConfigVariablesInDbEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { createState } from 'twenty-ui/utilities';
|
||||||
|
export const isConfigVariablesInDbEnabledState = createState<boolean>({
|
||||||
|
key: 'isConfigVariablesInDbEnabled',
|
||||||
|
defaultValue: false,
|
||||||
|
});
|
||||||
@ -20,8 +20,8 @@ export const SettingsAdminContent = () => {
|
|||||||
disabled: !canAccessFullAdminPanel && !canImpersonate,
|
disabled: !canAccessFullAdminPanel && !canImpersonate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: SETTINGS_ADMIN_TABS.ENV_VARIABLES,
|
id: SETTINGS_ADMIN_TABS.CONFIG_VARIABLES,
|
||||||
title: t`Env Variables`,
|
title: t`Config Variables`,
|
||||||
Icon: IconVariable,
|
Icon: IconVariable,
|
||||||
disabled: !canAccessFullAdminPanel,
|
disabled: !canAccessFullAdminPanel,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,68 +0,0 @@
|
|||||||
import { SettingsAdminEnvVariablesTable } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesTable';
|
|
||||||
import { SettingsAdminTabSkeletonLoader } from '@/settings/admin-panel/components/SettingsAdminTabSkeletonLoader';
|
|
||||||
import { SettingsListItemCardContent } from '@/settings/components/SettingsListItemCardContent';
|
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { t } from '@lingui/core/macro';
|
|
||||||
import { H2Title, IconHeartRateMonitor } from 'twenty-ui/display';
|
|
||||||
import { Card, Section } from 'twenty-ui/layout';
|
|
||||||
import { useGetConfigVariablesGroupedQuery } from '~/generated/graphql';
|
|
||||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
|
||||||
|
|
||||||
const StyledGroupContainer = styled.div``;
|
|
||||||
|
|
||||||
const StyledInfoText = styled.div`
|
|
||||||
color: ${({ theme }) => theme.font.color.secondary};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledCard = styled(Card)`
|
|
||||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SettingsAdminEnvVariables = () => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { data: configVariables, loading: configVariablesLoading } =
|
|
||||||
useGetConfigVariablesGroupedQuery({
|
|
||||||
fetchPolicy: 'network-only',
|
|
||||||
});
|
|
||||||
|
|
||||||
const visibleGroups =
|
|
||||||
configVariables?.getConfigVariablesGrouped.groups.filter(
|
|
||||||
(group) => !group.isHiddenOnLoad,
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
if (configVariablesLoading) {
|
|
||||||
return <SettingsAdminTabSkeletonLoader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Section>
|
|
||||||
<StyledInfoText>
|
|
||||||
{t`These are only the server values. Ensure your worker environment has the same variables and values, this is required for asynchronous tasks like email sync.`}
|
|
||||||
</StyledInfoText>
|
|
||||||
</Section>
|
|
||||||
{visibleGroups.map((group) => (
|
|
||||||
<StyledGroupContainer key={group.name}>
|
|
||||||
<H2Title title={group.name} description={group.description} />
|
|
||||||
{group.variables.length > 0 && (
|
|
||||||
<SettingsAdminEnvVariablesTable variables={group.variables} />
|
|
||||||
)}
|
|
||||||
</StyledGroupContainer>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Section>
|
|
||||||
<StyledCard rounded>
|
|
||||||
<SettingsListItemCardContent
|
|
||||||
label={t`Other Variables`}
|
|
||||||
to={getSettingsPath(SettingsPath.AdminPanelOtherEnvVariables)}
|
|
||||||
rightComponent={null}
|
|
||||||
LeftIcon={IconHeartRateMonitor}
|
|
||||||
LeftIconColor={theme.font.color.tertiary}
|
|
||||||
/>
|
|
||||||
</StyledCard>
|
|
||||||
</Section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,170 +0,0 @@
|
|||||||
import { SettingsAdminEnvCopyableText } from '@/settings/admin-panel/components/SettingsAdminEnvCopyableText';
|
|
||||||
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
|
|
||||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
|
||||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { IconChevronRight, IconEye, IconEyeOff } from 'twenty-ui/display';
|
|
||||||
import { LightIconButton } from 'twenty-ui/input';
|
|
||||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
|
||||||
|
|
||||||
type SettingsAdminEnvVariablesRowProps = {
|
|
||||||
variable: {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
value: string;
|
|
||||||
isSensitive: boolean;
|
|
||||||
};
|
|
||||||
isExpanded: boolean;
|
|
||||||
onExpandToggle: (name: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledTruncatedCell = styled(TableCell)`
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
cursor: pointer;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledButton = styled(motion.button)`
|
|
||||||
align-items: center;
|
|
||||||
border: none;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding-inline: ${({ theme }) => theme.spacing(1)};
|
|
||||||
background-color: transparent;
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
cursor: pointer;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MotionIconChevronDown = motion(IconChevronRight);
|
|
||||||
|
|
||||||
const StyledEllipsisLabel = styled.div`
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledValueContainer = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledTableRow = styled(TableRow)<{ isExpanded: boolean }>`
|
|
||||||
background-color: ${({ isExpanded, theme }) =>
|
|
||||||
isExpanded ? theme.background.transparent.light : 'transparent'};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledExpandableContainer = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
|
||||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SettingsAdminEnvVariablesRow = ({
|
|
||||||
variable,
|
|
||||||
isExpanded,
|
|
||||||
onExpandToggle,
|
|
||||||
}: SettingsAdminEnvVariablesRowProps) => {
|
|
||||||
const [showSensitiveValue, setShowSensitiveValue] = useState(false);
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const displayValue =
|
|
||||||
variable.value === ''
|
|
||||||
? 'null'
|
|
||||||
: variable.isSensitive && !showSensitiveValue
|
|
||||||
? '••••••'
|
|
||||||
: variable.value;
|
|
||||||
|
|
||||||
const handleToggleVisibility = (event: React.MouseEvent) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
setShowSensitiveValue(!showSensitiveValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const environmentVariablesDetails = [
|
|
||||||
{
|
|
||||||
label: 'Name',
|
|
||||||
value: <SettingsAdminEnvCopyableText text={variable.name} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Description',
|
|
||||||
value: (
|
|
||||||
<SettingsAdminEnvCopyableText
|
|
||||||
text={variable.description}
|
|
||||||
maxRows={1}
|
|
||||||
multiline={true}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Value',
|
|
||||||
value: (
|
|
||||||
<StyledValueContainer>
|
|
||||||
<SettingsAdminEnvCopyableText
|
|
||||||
text={variable.value}
|
|
||||||
displayText={displayValue}
|
|
||||||
multiline={true}
|
|
||||||
/>
|
|
||||||
{variable.isSensitive && variable.value !== '' && (
|
|
||||||
<LightIconButton
|
|
||||||
Icon={showSensitiveValue ? IconEyeOff : IconEye}
|
|
||||||
size="small"
|
|
||||||
accent="secondary"
|
|
||||||
onClick={handleToggleVisibility}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</StyledValueContainer>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StyledTableRow
|
|
||||||
onClick={() => onExpandToggle(variable.name)}
|
|
||||||
gridAutoColumns="5fr 4fr 3fr 1fr"
|
|
||||||
isExpanded={isExpanded}
|
|
||||||
>
|
|
||||||
<StyledTruncatedCell color={theme.font.color.primary}>
|
|
||||||
<StyledEllipsisLabel>{variable.name}</StyledEllipsisLabel>
|
|
||||||
</StyledTruncatedCell>
|
|
||||||
<StyledTruncatedCell>
|
|
||||||
<StyledEllipsisLabel>{variable.description}</StyledEllipsisLabel>
|
|
||||||
</StyledTruncatedCell>
|
|
||||||
<StyledTruncatedCell align="right">
|
|
||||||
<StyledEllipsisLabel>{displayValue}</StyledEllipsisLabel>
|
|
||||||
</StyledTruncatedCell>
|
|
||||||
<TableCell align="right">
|
|
||||||
<StyledButton
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onExpandToggle(variable.name);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MotionIconChevronDown
|
|
||||||
size={theme.icon.size.md}
|
|
||||||
color={theme.font.color.tertiary}
|
|
||||||
initial={false}
|
|
||||||
animate={{ rotate: isExpanded ? 90 : 0 }}
|
|
||||||
/>
|
|
||||||
</StyledButton>
|
|
||||||
</TableCell>
|
|
||||||
</StyledTableRow>
|
|
||||||
<AnimatedExpandableContainer isExpanded={isExpanded} mode="fit-content">
|
|
||||||
<StyledExpandableContainer>
|
|
||||||
<SettingsAdminTableCard
|
|
||||||
items={environmentVariablesDetails}
|
|
||||||
gridAutoColumns="1fr 4fr"
|
|
||||||
/>
|
|
||||||
</StyledExpandableContainer>
|
|
||||||
</AnimatedExpandableContainer>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
import { SettingsAdminEnvVariablesRow } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesRow';
|
|
||||||
import { Table } from '@/ui/layout/table/components/Table';
|
|
||||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
|
||||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
|
||||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
const StyledTableBody = styled(TableBody)`
|
|
||||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
|
||||||
`;
|
|
||||||
|
|
||||||
type SettingsAdminEnvVariablesTableProps = {
|
|
||||||
variables: Array<{
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
value: string;
|
|
||||||
isSensitive: boolean;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SettingsAdminEnvVariablesTable = ({
|
|
||||||
variables,
|
|
||||||
}: SettingsAdminEnvVariablesTableProps) => {
|
|
||||||
const [expandedRowName, setExpandedRowName] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleExpandToggle = (name: string) => {
|
|
||||||
setExpandedRowName(expandedRowName === name ? null : name);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table>
|
|
||||||
<TableRow gridAutoColumns="5fr 4fr 3fr 1fr">
|
|
||||||
<TableHeader>Name</TableHeader>
|
|
||||||
<TableHeader>Description</TableHeader>
|
|
||||||
<TableHeader align="right">Value</TableHeader>
|
|
||||||
<TableHeader align="right"></TableHeader>
|
|
||||||
</TableRow>
|
|
||||||
<StyledTableBody>
|
|
||||||
{variables.map((variable) => (
|
|
||||||
<SettingsAdminEnvVariablesRow
|
|
||||||
key={variable.name}
|
|
||||||
variable={variable}
|
|
||||||
isExpanded={expandedRowName === variable.name}
|
|
||||||
onExpandToggle={handleExpandToggle}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</StyledTableBody>
|
|
||||||
</Table>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { SettingsAdminEnvVariables } from '@/settings/admin-panel/components/SettingsAdminEnvVariables';
|
|
||||||
import { SettingsAdminGeneral } from '@/settings/admin-panel/components/SettingsAdminGeneral';
|
import { SettingsAdminGeneral } from '@/settings/admin-panel/components/SettingsAdminGeneral';
|
||||||
|
import { SettingsAdminConfigVariables } from '@/settings/admin-panel/config-variables/components/SettingsAdminConfigVariables';
|
||||||
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
|
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
|
||||||
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
|
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
|
||||||
import { SettingsAdminHealthStatus } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthStatus';
|
import { SettingsAdminHealthStatus } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthStatus';
|
||||||
@ -15,8 +15,8 @@ export const SettingsAdminTabContent = () => {
|
|||||||
switch (activeTabId) {
|
switch (activeTabId) {
|
||||||
case SETTINGS_ADMIN_TABS.GENERAL:
|
case SETTINGS_ADMIN_TABS.GENERAL:
|
||||||
return <SettingsAdminGeneral />;
|
return <SettingsAdminGeneral />;
|
||||||
case SETTINGS_ADMIN_TABS.ENV_VARIABLES:
|
case SETTINGS_ADMIN_TABS.CONFIG_VARIABLES:
|
||||||
return <SettingsAdminEnvVariables />;
|
return <SettingsAdminConfigVariables />;
|
||||||
case SETTINGS_ADMIN_TABS.HEALTH_STATUS:
|
case SETTINGS_ADMIN_TABS.HEALTH_STATUS:
|
||||||
return <SettingsAdminHealthStatus />;
|
return <SettingsAdminHealthStatus />;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -0,0 +1,62 @@
|
|||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState';
|
||||||
|
import {
|
||||||
|
IconDeviceFloppy,
|
||||||
|
IconPencil,
|
||||||
|
IconRefreshAlert,
|
||||||
|
} from 'twenty-ui/display';
|
||||||
|
import { Button } from 'twenty-ui/input';
|
||||||
|
import { ConfigSource, ConfigVariable } from '~/generated/graphql';
|
||||||
|
|
||||||
|
type ConfigVariableActionButtonsProps = {
|
||||||
|
variable: ConfigVariable;
|
||||||
|
isValueValid: boolean;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
onSave: () => void;
|
||||||
|
onReset: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfigVariableActionButtons = ({
|
||||||
|
variable,
|
||||||
|
isValueValid,
|
||||||
|
isSubmitting,
|
||||||
|
onSave,
|
||||||
|
onReset,
|
||||||
|
}: ConfigVariableActionButtonsProps) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const isConfigVariablesInDbEnabled = useRecoilValue(
|
||||||
|
isConfigVariablesInDbEnabledState,
|
||||||
|
);
|
||||||
|
const isFromDatabase = variable.source === ConfigSource.DATABASE;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isConfigVariablesInDbEnabled &&
|
||||||
|
variable.source === ConfigSource.DATABASE && (
|
||||||
|
<Button
|
||||||
|
title={t`Reset to Default`}
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
accent="danger"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={onReset}
|
||||||
|
Icon={IconRefreshAlert}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isConfigVariablesInDbEnabled && !variable.isEnvOnly && (
|
||||||
|
<Button
|
||||||
|
title={isFromDatabase ? t`Save` : t`Edit`}
|
||||||
|
variant="primary"
|
||||||
|
size="small"
|
||||||
|
accent="blue"
|
||||||
|
disabled={isSubmitting || !isValueValid}
|
||||||
|
onClick={onSave}
|
||||||
|
type="submit"
|
||||||
|
Icon={isFromDatabase ? IconDeviceFloppy : IconPencil}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,190 @@
|
|||||||
|
import { Select } from '@/ui/input/components/Select';
|
||||||
|
import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||||
|
import { TextArea } from '@/ui/input/components/TextArea';
|
||||||
|
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||||
|
import { SelectHotkeyScope } from '@/ui/input/types/SelectHotkeyScope';
|
||||||
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
import { ConfigVariableValue } from 'twenty-shared/types';
|
||||||
|
import { MenuItemMultiSelect } from 'twenty-ui/navigation';
|
||||||
|
import { ConfigVariableType } from '~/generated/graphql';
|
||||||
|
import { ConfigVariableOptions } from '../types/ConfigVariableOptions';
|
||||||
|
|
||||||
|
type ConfigVariableDatabaseInputProps = {
|
||||||
|
label: string;
|
||||||
|
value: ConfigVariableValue;
|
||||||
|
onChange: (value: string | number | boolean | string[] | null) => void;
|
||||||
|
type: ConfigVariableType;
|
||||||
|
options?: ConfigVariableOptions;
|
||||||
|
disabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfigVariableDatabaseInput = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
type,
|
||||||
|
options,
|
||||||
|
disabled,
|
||||||
|
placeholder,
|
||||||
|
}: ConfigVariableDatabaseInputProps) => {
|
||||||
|
const selectOptions =
|
||||||
|
options && Array.isArray(options)
|
||||||
|
? options.map((option) => ({
|
||||||
|
value: String(option),
|
||||||
|
label: String(option),
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const booleanOptions = [
|
||||||
|
{ value: 'true', label: 'true' },
|
||||||
|
{ value: 'false', label: 'false' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const isValueSelected = (optionValue: string) => {
|
||||||
|
if (!Array.isArray(value)) return false;
|
||||||
|
return value.includes(optionValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMultiSelectChange = (optionValue: string) => {
|
||||||
|
if (!Array.isArray(value)) return;
|
||||||
|
|
||||||
|
let newValues = [...value];
|
||||||
|
if (isValueSelected(optionValue)) {
|
||||||
|
newValues = newValues.filter((val) => val !== optionValue);
|
||||||
|
} else {
|
||||||
|
newValues.push(optionValue);
|
||||||
|
}
|
||||||
|
onChange(newValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case ConfigVariableType.BOOLEAN:
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
label={label}
|
||||||
|
value={String(value ?? '')}
|
||||||
|
onChange={(newValue: string) => onChange(newValue === 'true')}
|
||||||
|
disabled={disabled}
|
||||||
|
options={booleanOptions}
|
||||||
|
dropdownId="config-variable-boolean-select"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case ConfigVariableType.NUMBER:
|
||||||
|
return (
|
||||||
|
<TextInputV2
|
||||||
|
label={label}
|
||||||
|
value={value !== null && value !== undefined ? String(value) : ''}
|
||||||
|
onChange={(text) => {
|
||||||
|
const num = Number(text);
|
||||||
|
onChange(isNaN(num) ? text : num);
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={placeholder}
|
||||||
|
type="number"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case ConfigVariableType.ARRAY:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{options && Array.isArray(options) ? (
|
||||||
|
<Dropdown
|
||||||
|
dropdownId="config-variable-array-dropdown"
|
||||||
|
dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }}
|
||||||
|
dropdownPlacement="bottom-start"
|
||||||
|
dropdownOffset={{
|
||||||
|
y: 8,
|
||||||
|
}}
|
||||||
|
clickableComponent={
|
||||||
|
<SelectControl
|
||||||
|
selectedOption={{
|
||||||
|
value: '',
|
||||||
|
label:
|
||||||
|
Array.isArray(value) && value.length > 0
|
||||||
|
? value.join(', ')
|
||||||
|
: 'Select options',
|
||||||
|
}}
|
||||||
|
isDisabled={disabled}
|
||||||
|
hasRightElement={false}
|
||||||
|
selectSizeVariant="default"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
dropdownComponents={
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
{selectOptions.map((option) => (
|
||||||
|
<MenuItemMultiSelect
|
||||||
|
key={option.value}
|
||||||
|
text={option.label}
|
||||||
|
selected={isValueSelected(option.value)}
|
||||||
|
className="config-variable-array-menu-item-multi-select"
|
||||||
|
onSelectChange={() =>
|
||||||
|
handleMultiSelectChange(option.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TextArea
|
||||||
|
label={label}
|
||||||
|
value={
|
||||||
|
Array.isArray(value)
|
||||||
|
? JSON.stringify(value)
|
||||||
|
: String(value ?? '')
|
||||||
|
}
|
||||||
|
onChange={(text) => {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(text);
|
||||||
|
onChange(Array.isArray(arr) ? arr : value);
|
||||||
|
} catch {
|
||||||
|
onChange(text);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={placeholder || 'Enter JSON array'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case ConfigVariableType.ENUM:
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
label={label}
|
||||||
|
value={String(value ?? '')}
|
||||||
|
onChange={(newValue: string) => onChange(newValue)}
|
||||||
|
disabled={disabled}
|
||||||
|
options={selectOptions}
|
||||||
|
dropdownId="config-variable-enum-select"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case ConfigVariableType.STRING:
|
||||||
|
return (
|
||||||
|
<TextInputV2
|
||||||
|
label={label}
|
||||||
|
value={
|
||||||
|
typeof value === 'string'
|
||||||
|
? value
|
||||||
|
: value !== null && value !== undefined
|
||||||
|
? JSON.stringify(value)
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
onChange={(text) => onChange(text)}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={placeholder || 'Enter value'}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported type: ${type}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import { ConfigVariableSourceFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableSourceFilter';
|
||||||
|
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const StyledChipContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ConfigVariableFilterContainerProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
activeChips: {
|
||||||
|
label: string;
|
||||||
|
onRemove: () => void;
|
||||||
|
source?: ConfigVariableSourceFilter;
|
||||||
|
variant?: 'default' | 'danger';
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfigVariableFilterContainer = ({
|
||||||
|
children,
|
||||||
|
activeChips,
|
||||||
|
}: ConfigVariableFilterContainerProps) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{children}
|
||||||
|
{activeChips.length > 0 && (
|
||||||
|
<StyledChipContainer>
|
||||||
|
{activeChips.map((chip) => (
|
||||||
|
<SortOrFilterChip
|
||||||
|
key={chip.label + chip.source}
|
||||||
|
labelKey={chip.label}
|
||||||
|
onRemove={chip.onRemove}
|
||||||
|
labelValue={chip.source ?? ''}
|
||||||
|
variant={chip.variant}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</StyledChipContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
import { ConfigVariableFilterCategory } from '@/settings/admin-panel/config-variables/types/ConfigVariableFilterCategory';
|
||||||
|
import { ConfigVariableGroupFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableGroupFilter';
|
||||||
|
import { ConfigVariableSourceFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableSourceFilter';
|
||||||
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { IconSettings } from 'twenty-ui/display';
|
||||||
|
import { Button } from 'twenty-ui/input';
|
||||||
|
import { ConfigVariableOptionsDropdownContent } from './ConfigVariableOptionsDropdownContent';
|
||||||
|
|
||||||
|
type ConfigVariableFilterDropdownProps = {
|
||||||
|
sourceFilter: ConfigVariableSourceFilter;
|
||||||
|
groupFilter: ConfigVariableGroupFilter;
|
||||||
|
groupOptions: { value: string; label: string }[];
|
||||||
|
showHiddenGroupVariables: boolean;
|
||||||
|
onSourceFilterChange: (source: ConfigVariableSourceFilter) => void;
|
||||||
|
onGroupFilterChange: (group: ConfigVariableGroupFilter) => void;
|
||||||
|
onShowHiddenChange: (value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfigVariableFilterDropdown = ({
|
||||||
|
sourceFilter,
|
||||||
|
groupFilter,
|
||||||
|
groupOptions,
|
||||||
|
showHiddenGroupVariables,
|
||||||
|
onSourceFilterChange,
|
||||||
|
onGroupFilterChange,
|
||||||
|
onShowHiddenChange,
|
||||||
|
}: ConfigVariableFilterDropdownProps) => {
|
||||||
|
const [selectedCategory, setSelectedCategory] =
|
||||||
|
useState<ConfigVariableFilterCategory | null>(null);
|
||||||
|
|
||||||
|
const handleSelectCategory = (
|
||||||
|
category: ConfigVariableFilterCategory | null,
|
||||||
|
) => {
|
||||||
|
setSelectedCategory(category);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
clickableComponent={
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="medium"
|
||||||
|
title="Options"
|
||||||
|
Icon={IconSettings}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
dropdownId="env-var-options-dropdown"
|
||||||
|
dropdownHotkeyScope={{ scope: 'env-var-options' }}
|
||||||
|
dropdownOffset={{ x: 0, y: 10 }}
|
||||||
|
dropdownComponents={
|
||||||
|
<ConfigVariableOptionsDropdownContent
|
||||||
|
selectedCategory={selectedCategory}
|
||||||
|
onSelectCategory={handleSelectCategory}
|
||||||
|
sourceFilter={sourceFilter}
|
||||||
|
groupFilter={groupFilter}
|
||||||
|
groupOptions={groupOptions}
|
||||||
|
showHiddenGroupVariables={showHiddenGroupVariables}
|
||||||
|
onSourceFilterChange={onSourceFilterChange}
|
||||||
|
onGroupFilterChange={onGroupFilterChange}
|
||||||
|
onShowHiddenChange={onShowHiddenChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
|
||||||
|
import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { ConfigSource, ConfigVariable } from '~/generated/graphql';
|
||||||
|
|
||||||
|
const StyledHelpText = styled.div<{ color?: string }>`
|
||||||
|
color: ${({ theme, color }) => color || theme.font.color.tertiary};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
|
line-height: 1.5;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ConfigVariableHelpTextProps = {
|
||||||
|
variable: ConfigVariable;
|
||||||
|
hasValueChanged: boolean;
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfigVariableHelpText = ({
|
||||||
|
variable,
|
||||||
|
hasValueChanged,
|
||||||
|
}: ConfigVariableHelpTextProps) => {
|
||||||
|
const isConfigVariablesInDbEnabled = useRecoilValue(
|
||||||
|
isConfigVariablesInDbEnabledState,
|
||||||
|
);
|
||||||
|
const { t } = useLingui();
|
||||||
|
const isFromDatabase = variable.source === ConfigSource.DATABASE;
|
||||||
|
const isFromEnvironment = variable.source === ConfigSource.ENVIRONMENT;
|
||||||
|
const isReadOnly = !isConfigVariablesInDbEnabled;
|
||||||
|
|
||||||
|
if (isReadOnly) {
|
||||||
|
return (
|
||||||
|
<StyledHelpText>
|
||||||
|
{t`Database configuration is currently disabled.`}{' '}
|
||||||
|
{isFromEnvironment
|
||||||
|
? t`Value is set in the server environment, it may be a different value on the worker.`
|
||||||
|
: t`Using default application value. Configure via environment variables.`}
|
||||||
|
</StyledHelpText>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConfigVariablesInDbEnabled && variable.isEnvOnly) {
|
||||||
|
return (
|
||||||
|
<StyledHelpText>
|
||||||
|
{t`This setting can only be configured through environment variables.`}
|
||||||
|
</StyledHelpText>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConfigVariablesInDbEnabled && !variable.isEnvOnly && hasValueChanged) {
|
||||||
|
return (
|
||||||
|
<StyledHelpText>
|
||||||
|
{isFromDatabase
|
||||||
|
? t`Click on the checkmark to apply your changes.`
|
||||||
|
: t`This value will be saved to the database.`}
|
||||||
|
</StyledHelpText>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConfigVariablesInDbEnabled && !variable.isEnvOnly && !hasValueChanged) {
|
||||||
|
if (isFromDatabase) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledHelpText>
|
||||||
|
{t`This database value overrides environment settings. `}
|
||||||
|
</StyledHelpText>
|
||||||
|
<StyledHelpText>
|
||||||
|
{t`Clear the field or "X" to revert to environment/default value.`}
|
||||||
|
</StyledHelpText>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<StyledHelpText>
|
||||||
|
{isFromEnvironment
|
||||||
|
? t`Current value from server environment. Set a custom value to override.`
|
||||||
|
: t`Using default value. Set a custom value to override.`}
|
||||||
|
</StyledHelpText>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <StyledHelpText>{t`This should never happen`}</StyledHelpText>;
|
||||||
|
};
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState';
|
||||||
|
import { ConfigVariableSourceOptions } from '@/settings/admin-panel/config-variables/constants/ConfigVariableSourceOptions';
|
||||||
|
import { ConfigVariableFilterCategory } from '@/settings/admin-panel/config-variables/types/ConfigVariableFilterCategory';
|
||||||
|
import { ConfigVariableGroupFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableGroupFilter';
|
||||||
|
import { ConfigVariableSourceFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableSourceFilter';
|
||||||
|
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||||
|
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||||
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { IconChevronLeft, IconEye, IconEyeOff } from 'twenty-ui/display';
|
||||||
|
import { MenuItem, MenuItemSelectTag } from 'twenty-ui/navigation';
|
||||||
|
|
||||||
|
type ConfigVariableOptionsDropdownContentProps = {
|
||||||
|
selectedCategory: ConfigVariableFilterCategory | null;
|
||||||
|
onSelectCategory: (category: ConfigVariableFilterCategory | null) => void;
|
||||||
|
sourceFilter: ConfigVariableSourceFilter;
|
||||||
|
groupFilter: ConfigVariableGroupFilter;
|
||||||
|
groupOptions: { value: ConfigVariableGroupFilter; label: string }[];
|
||||||
|
showHiddenGroupVariables: boolean;
|
||||||
|
onSourceFilterChange: (source: ConfigVariableSourceFilter) => void;
|
||||||
|
onGroupFilterChange: (group: ConfigVariableGroupFilter) => void;
|
||||||
|
onShowHiddenChange: (value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfigVariableOptionsDropdownContent = ({
|
||||||
|
selectedCategory,
|
||||||
|
onSelectCategory,
|
||||||
|
sourceFilter,
|
||||||
|
groupFilter,
|
||||||
|
groupOptions,
|
||||||
|
showHiddenGroupVariables,
|
||||||
|
onSourceFilterChange,
|
||||||
|
onGroupFilterChange,
|
||||||
|
onShowHiddenChange,
|
||||||
|
}: ConfigVariableOptionsDropdownContentProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const isConfigVariablesInDbEnabled = useRecoilValue(
|
||||||
|
isConfigVariablesInDbEnabledState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const availableSourceOptions = ConfigVariableSourceOptions.filter(
|
||||||
|
(option) => isConfigVariablesInDbEnabled || option.value !== 'database',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!selectedCategory) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
<MenuItemSelectTag
|
||||||
|
text={t`Source`}
|
||||||
|
color="transparent"
|
||||||
|
onClick={() => onSelectCategory('source')}
|
||||||
|
/>
|
||||||
|
<MenuItemSelectTag
|
||||||
|
text={t`Group`}
|
||||||
|
color="transparent"
|
||||||
|
onClick={() => onSelectCategory('group')}
|
||||||
|
/>
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItemsContainer scrollable={false}>
|
||||||
|
<MenuItem
|
||||||
|
text={
|
||||||
|
showHiddenGroupVariables
|
||||||
|
? t`Hide hidden groups`
|
||||||
|
: t`Show hidden groups`
|
||||||
|
}
|
||||||
|
LeftIcon={() =>
|
||||||
|
showHiddenGroupVariables ? (
|
||||||
|
<IconEyeOff
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
stroke={theme.icon.stroke.sm}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<IconEye
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
stroke={theme.icon.stroke.sm}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={() => onShowHiddenChange(!showHiddenGroupVariables)}
|
||||||
|
/>
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownMenuHeader
|
||||||
|
StartComponent={
|
||||||
|
<DropdownMenuHeaderLeftComponent
|
||||||
|
onClick={() => onSelectCategory(null)}
|
||||||
|
Icon={IconChevronLeft}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{selectedCategory === 'source' && t`Select Source`}
|
||||||
|
{selectedCategory === 'group' && t`Select Group`}
|
||||||
|
</DropdownMenuHeader>
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
{selectedCategory === 'source' && (
|
||||||
|
<>
|
||||||
|
{availableSourceOptions.map((option) => (
|
||||||
|
<MenuItemSelectTag
|
||||||
|
key={option.value}
|
||||||
|
text={option.label}
|
||||||
|
color={option.color}
|
||||||
|
selected={option.value === sourceFilter}
|
||||||
|
onClick={() => {
|
||||||
|
onSourceFilterChange(option.value);
|
||||||
|
onSelectCategory(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{selectedCategory === 'group' && (
|
||||||
|
<>
|
||||||
|
{groupOptions.map((option) => (
|
||||||
|
<MenuItemSelectTag
|
||||||
|
key={option.value}
|
||||||
|
text={option.label}
|
||||||
|
color="transparent"
|
||||||
|
selected={option.value === groupFilter}
|
||||||
|
onClick={() => {
|
||||||
|
onGroupFilterChange(option.value);
|
||||||
|
onSelectCategory(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { IconSearch } from 'twenty-ui/display';
|
||||||
|
|
||||||
|
const StyledSearchInput = styled(TextInput)`
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ConfigVariableSearchInputProps = {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfigVariableSearchInput = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: ConfigVariableSearchInputProps) => {
|
||||||
|
return (
|
||||||
|
<StyledSearchInput
|
||||||
|
placeholder={t`Search config variables`}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
autoFocus={false}
|
||||||
|
LeftIcon={IconSearch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
|
||||||
|
import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState';
|
||||||
|
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { ConfigVariableValue } from 'twenty-shared/types';
|
||||||
|
import { ConfigVariable } from '~/generated/graphql';
|
||||||
|
import { ConfigVariableDatabaseInput } from './ConfigVariableDatabaseInput';
|
||||||
|
|
||||||
|
type ConfigVariableValueInputProps = {
|
||||||
|
variable: ConfigVariable;
|
||||||
|
value: ConfigVariableValue;
|
||||||
|
onChange: (value: string | number | boolean | string[] | null) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledValueContainer = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ConfigVariableValueInput = ({
|
||||||
|
variable,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
}: ConfigVariableValueInputProps) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const isConfigVariablesInDbEnabled = useRecoilValue(
|
||||||
|
isConfigVariablesInDbEnabledState,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledValueContainer>
|
||||||
|
{isConfigVariablesInDbEnabled && !variable.isEnvOnly ? (
|
||||||
|
<ConfigVariableDatabaseInput
|
||||||
|
label={t`Value`}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
type={variable.type}
|
||||||
|
options={variable.options}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={
|
||||||
|
disabled ? 'Undefined' : t`Enter a value to store in database`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TextInputV2
|
||||||
|
value={String(value)}
|
||||||
|
disabled
|
||||||
|
label={t`Value`}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</StyledValueContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -3,8 +3,15 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
|
||||||
import { IconCopy, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
import { IconCopy, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
|
type SettingsAdminConfigCopyableTextProps = {
|
||||||
|
text: string;
|
||||||
|
displayText?: React.ReactNode;
|
||||||
|
multiline?: boolean;
|
||||||
|
maxRows?: number;
|
||||||
|
};
|
||||||
|
|
||||||
const StyledEllipsisLabel = styled.div`
|
const StyledEllipsisLabel = styled.div`
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -20,17 +27,12 @@ const StyledExpandedEllipsisLabel = styled.div`
|
|||||||
const StyledCopyContainer = styled.span`
|
const StyledCopyContainer = styled.span`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`;
|
`;
|
||||||
export const SettingsAdminEnvCopyableText = ({
|
export const SettingsAdminConfigCopyableText = ({
|
||||||
text,
|
text,
|
||||||
displayText,
|
displayText,
|
||||||
multiline = false,
|
multiline = false,
|
||||||
maxRows,
|
maxRows,
|
||||||
}: {
|
}: SettingsAdminConfigCopyableTextProps) => {
|
||||||
text: string;
|
|
||||||
displayText?: React.ReactNode;
|
|
||||||
multiline?: boolean;
|
|
||||||
maxRows?: number;
|
|
||||||
}) => {
|
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
@ -0,0 +1,207 @@
|
|||||||
|
import { SettingsAdminTabSkeletonLoader } from '@/settings/admin-panel/components/SettingsAdminTabSkeletonLoader';
|
||||||
|
import { ConfigVariableFilterContainer } from '@/settings/admin-panel/config-variables/components/ConfigVariableFilterContainer';
|
||||||
|
import { ConfigVariableFilterDropdown } from '@/settings/admin-panel/config-variables/components/ConfigVariableFilterDropdown';
|
||||||
|
import { SettingsAdminConfigVariablesTable } from '@/settings/admin-panel/config-variables/components/SettingsAdminConfigVariablesTable';
|
||||||
|
import { ConfigVariableSourceOptions } from '@/settings/admin-panel/config-variables/constants/ConfigVariableSourceOptions';
|
||||||
|
import { ConfigVariableGroupFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableGroupFilter';
|
||||||
|
import { ConfigVariableSourceFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableSourceFilter';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { H2Title } from 'twenty-ui/display';
|
||||||
|
import { Section } from 'twenty-ui/layout';
|
||||||
|
import {
|
||||||
|
ConfigSource,
|
||||||
|
useGetConfigVariablesGroupedQuery,
|
||||||
|
} from '~/generated/graphql';
|
||||||
|
import { ConfigVariableSearchInput } from './ConfigVariableSearchInput';
|
||||||
|
|
||||||
|
const StyledControlsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: space-between;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTableContainer = styled.div`
|
||||||
|
margin-bottom: 24px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsAdminConfigVariables = () => {
|
||||||
|
const { data: configVariables, loading: configVariablesLoading } =
|
||||||
|
useGetConfigVariablesGroupedQuery({
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [showHiddenGroupVariables, setShowHiddenGroupVariables] =
|
||||||
|
useState(false);
|
||||||
|
const [sourceFilter, setSourceFilter] =
|
||||||
|
useState<ConfigVariableSourceFilter>('all');
|
||||||
|
const [groupFilter, setGroupFilter] =
|
||||||
|
useState<ConfigVariableGroupFilter>('all');
|
||||||
|
|
||||||
|
// Get all groups, not filtered by visibility
|
||||||
|
const allGroups = useMemo(
|
||||||
|
() => configVariables?.getConfigVariablesGrouped.groups ?? [],
|
||||||
|
[configVariables],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Compute group options from all groups, not just visible ones
|
||||||
|
const groupOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{ value: 'all', label: 'All Groups' },
|
||||||
|
...allGroups.map((group) => ({
|
||||||
|
value: group.name,
|
||||||
|
label: group.name,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
[allGroups],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Flatten all variables for filtering, attaching isHiddenOnLoad and groupName from group
|
||||||
|
const allVariables = useMemo(
|
||||||
|
() =>
|
||||||
|
configVariables?.getConfigVariablesGrouped.groups.flatMap((group) =>
|
||||||
|
group.variables.map((variable) => ({
|
||||||
|
...variable,
|
||||||
|
isHiddenOnLoad: group.isHiddenOnLoad,
|
||||||
|
groupName: group.name,
|
||||||
|
})),
|
||||||
|
) ?? [],
|
||||||
|
[configVariables],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filtering logic
|
||||||
|
const filteredVariables = useMemo(() => {
|
||||||
|
const isSearching = search.trim().length > 0;
|
||||||
|
const hasSelectedSpecificGroup = groupFilter !== 'all';
|
||||||
|
|
||||||
|
return allVariables.filter((v) => {
|
||||||
|
// Search filter
|
||||||
|
const matchesSearch =
|
||||||
|
v.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
(v.description?.toLowerCase() || '').includes(search.toLowerCase());
|
||||||
|
|
||||||
|
if (isSearching && !matchesSearch) return false;
|
||||||
|
|
||||||
|
// Group filter
|
||||||
|
const matchesGroup = hasSelectedSpecificGroup
|
||||||
|
? v.groupName === groupFilter
|
||||||
|
: true;
|
||||||
|
|
||||||
|
if (hasSelectedSpecificGroup && !matchesGroup) return false;
|
||||||
|
|
||||||
|
// Hidden filter - Only apply if:
|
||||||
|
// 1. User is not searching
|
||||||
|
// 2. Show hidden is off
|
||||||
|
// 3. Item is from a hidden group
|
||||||
|
// 4. No specific group is selected (if a specific group is selected, show all its variables)
|
||||||
|
if (
|
||||||
|
!isSearching &&
|
||||||
|
!showHiddenGroupVariables &&
|
||||||
|
v.isHiddenOnLoad &&
|
||||||
|
!hasSelectedSpecificGroup
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source filter
|
||||||
|
let matchesSource = true;
|
||||||
|
if (sourceFilter === 'database')
|
||||||
|
matchesSource = v.source === ConfigSource.DATABASE;
|
||||||
|
if (sourceFilter === 'environment')
|
||||||
|
matchesSource = v.source === ConfigSource.ENVIRONMENT;
|
||||||
|
if (sourceFilter === 'default')
|
||||||
|
matchesSource = v.source === ConfigSource.DEFAULT;
|
||||||
|
|
||||||
|
return matchesSource;
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
allVariables,
|
||||||
|
search,
|
||||||
|
showHiddenGroupVariables,
|
||||||
|
sourceFilter,
|
||||||
|
groupFilter,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Build activeChips for current filters
|
||||||
|
const activeChips = [];
|
||||||
|
if (sourceFilter !== 'all') {
|
||||||
|
activeChips.push({
|
||||||
|
label:
|
||||||
|
ConfigVariableSourceOptions.find((o) => o.value === sourceFilter)
|
||||||
|
?.label || '',
|
||||||
|
onRemove: () => setSourceFilter('all'),
|
||||||
|
variant: 'default' as const,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (groupFilter !== 'all') {
|
||||||
|
activeChips.push({
|
||||||
|
label: groupOptions.find((o) => o.value === groupFilter)?.label || '',
|
||||||
|
onRemove: () => setGroupFilter('all'),
|
||||||
|
variant: 'danger' as const,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group variables by groupName for rendering
|
||||||
|
const groupedVariables = useMemo(() => {
|
||||||
|
const groupMap = new Map();
|
||||||
|
filteredVariables.forEach((v) => {
|
||||||
|
if (!groupMap.has(v.groupName)) {
|
||||||
|
const group = allGroups.find((g) => g.name === v.groupName);
|
||||||
|
groupMap.set(v.groupName, {
|
||||||
|
variables: [],
|
||||||
|
description: group?.description || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
groupMap.get(v.groupName).variables.push(v);
|
||||||
|
});
|
||||||
|
return groupMap;
|
||||||
|
}, [filteredVariables, allGroups]);
|
||||||
|
|
||||||
|
if (configVariablesLoading) {
|
||||||
|
return <SettingsAdminTabSkeletonLoader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Section>
|
||||||
|
<H2Title title={t`Config Variables`} />
|
||||||
|
|
||||||
|
<ConfigVariableFilterContainer activeChips={activeChips}>
|
||||||
|
<StyledControlsContainer>
|
||||||
|
<ConfigVariableSearchInput value={search} onChange={setSearch} />
|
||||||
|
<ConfigVariableFilterDropdown
|
||||||
|
sourceFilter={sourceFilter}
|
||||||
|
groupFilter={groupFilter}
|
||||||
|
groupOptions={groupOptions}
|
||||||
|
showHiddenGroupVariables={showHiddenGroupVariables}
|
||||||
|
onSourceFilterChange={setSourceFilter}
|
||||||
|
onGroupFilterChange={setGroupFilter}
|
||||||
|
onShowHiddenChange={setShowHiddenGroupVariables}
|
||||||
|
/>
|
||||||
|
</StyledControlsContainer>
|
||||||
|
</ConfigVariableFilterContainer>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{groupedVariables.size === 0 && (
|
||||||
|
<StyledTableContainer>
|
||||||
|
<Section>
|
||||||
|
<H2Title
|
||||||
|
title={t`No variables found`}
|
||||||
|
description={t`No config variables match your current filters. Try adjusting your filters or search criteria.`}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
</StyledTableContainer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{[...groupedVariables.entries()].map(([groupName, groupData]) => (
|
||||||
|
<StyledTableContainer key={groupName}>
|
||||||
|
<H2Title title={groupName} description={groupData.description} />
|
||||||
|
|
||||||
|
<SettingsAdminConfigVariablesTable variables={groupData.variables} />
|
||||||
|
</StyledTableContainer>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||||
|
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconChevronRight } from 'twenty-ui/display';
|
||||||
|
import { ConfigVariable } from '~/generated/graphql';
|
||||||
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
|
|
||||||
|
type SettingsAdminConfigVariablesRowProps = {
|
||||||
|
variable: ConfigVariable;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledTruncatedCell = styled(TableCell)`
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTableRow = styled(TableRow)`
|
||||||
|
&:hover {
|
||||||
|
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEllipsisLabel = styled.div`
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsAdminConfigVariablesRow = ({
|
||||||
|
variable,
|
||||||
|
}: SettingsAdminConfigVariablesRowProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const displayValue =
|
||||||
|
variable.value === ''
|
||||||
|
? 'null'
|
||||||
|
: variable.isSensitive
|
||||||
|
? '••••••'
|
||||||
|
: typeof variable.value === 'boolean'
|
||||||
|
? variable.value
|
||||||
|
? 'true'
|
||||||
|
: 'false'
|
||||||
|
: variable.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledTableRow
|
||||||
|
gridAutoColumns="5fr 3fr 1fr"
|
||||||
|
to={getSettingsPath(SettingsPath.AdminPanelConfigVariableDetails, {
|
||||||
|
variableName: variable.name,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<StyledTruncatedCell color={theme.font.color.primary}>
|
||||||
|
<StyledEllipsisLabel>{variable.name}</StyledEllipsisLabel>
|
||||||
|
</StyledTruncatedCell>
|
||||||
|
<StyledTruncatedCell align="right">
|
||||||
|
<StyledEllipsisLabel>{displayValue}</StyledEllipsisLabel>
|
||||||
|
</StyledTruncatedCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<IconChevronRight
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
color={theme.font.color.tertiary}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</StyledTableRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { SettingsAdminConfigVariablesRow } from '@/settings/admin-panel/config-variables/components/SettingsAdminConfigVariablesRow';
|
||||||
|
import { Table } from '@/ui/layout/table/components/Table';
|
||||||
|
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||||
|
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||||
|
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { ConfigVariable } from '~/generated/graphql';
|
||||||
|
|
||||||
|
const StyledTableBody = styled(TableBody)`
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
`;
|
||||||
|
|
||||||
|
type SettingsAdminConfigVariablesTableProps = {
|
||||||
|
variables: ConfigVariable[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsAdminConfigVariablesTable = ({
|
||||||
|
variables,
|
||||||
|
}: SettingsAdminConfigVariablesTableProps) => {
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<TableRow gridAutoColumns="5fr 3fr 1fr">
|
||||||
|
<TableHeader>Name</TableHeader>
|
||||||
|
<TableHeader align="right">Value</TableHeader>
|
||||||
|
<TableHeader align="right"></TableHeader>
|
||||||
|
</TableRow>
|
||||||
|
<StyledTableBody>
|
||||||
|
{variables.map((variable) => (
|
||||||
|
<SettingsAdminConfigVariablesRow
|
||||||
|
key={variable.name}
|
||||||
|
variable={variable}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</StyledTableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { ConfigVariableSourceFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableSourceFilter';
|
||||||
|
import { ThemeColor } from 'twenty-ui/theme';
|
||||||
|
|
||||||
|
type ConfigVariableSourceOption = {
|
||||||
|
value: ConfigVariableSourceFilter;
|
||||||
|
label: string;
|
||||||
|
color: ThemeColor | 'transparent';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfigVariableSourceOptions: ConfigVariableSourceOption[] = [
|
||||||
|
{ value: 'all', label: 'All Sources', color: 'transparent' },
|
||||||
|
{ value: 'database', label: 'Database', color: 'blue' },
|
||||||
|
{ value: 'environment', label: 'Environment', color: 'green' },
|
||||||
|
{ value: 'default', label: 'Default', color: 'gray' },
|
||||||
|
];
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const CREATE_DATABASE_CONFIG_VARIABLE = gql`
|
||||||
|
mutation CreateDatabaseConfigVariable($key: String!, $value: JSON!) {
|
||||||
|
createDatabaseConfigVariable(key: $key, value: $value)
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const DELETE_DATABASE_CONFIG_VARIABLE = gql`
|
||||||
|
mutation DeleteDatabaseConfigVariable($key: String!) {
|
||||||
|
deleteDatabaseConfigVariable(key: $key)
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const UPDATE_DATABASE_CONFIG_VARIABLE = gql`
|
||||||
|
mutation UpdateDatabaseConfigVariable($key: String!, $value: JSON!) {
|
||||||
|
updateDatabaseConfigVariable(key: $key, value: $value)
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -12,6 +12,10 @@ export const GET_CONFIG_VARIABLES_GROUPED = gql`
|
|||||||
description
|
description
|
||||||
value
|
value
|
||||||
isSensitive
|
isSensitive
|
||||||
|
isEnvOnly
|
||||||
|
type
|
||||||
|
options
|
||||||
|
source
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const GET_DATABASE_CONFIG_VARIABLE = gql`
|
||||||
|
query GetDatabaseConfigVariable($key: String!) {
|
||||||
|
getDatabaseConfigVariable(key: $key) {
|
||||||
|
name
|
||||||
|
description
|
||||||
|
value
|
||||||
|
isSensitive
|
||||||
|
isEnvOnly
|
||||||
|
type
|
||||||
|
options
|
||||||
|
source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
|
||||||
|
import { GET_CLIENT_CONFIG } from '@/client-config/graphql/queries/getClientConfig';
|
||||||
|
import { GET_DATABASE_CONFIG_VARIABLE } from '@/settings/admin-panel/config-variables/graphql/queries/getDatabaseConfigVariable';
|
||||||
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { ConfigVariableValue } from 'twenty-shared/types';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import {
|
||||||
|
useCreateDatabaseConfigVariableMutation,
|
||||||
|
useDeleteDatabaseConfigVariableMutation,
|
||||||
|
useUpdateDatabaseConfigVariableMutation,
|
||||||
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
|
export const useConfigVariableActions = (variableName: string) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
|
const [updateDatabaseConfigVariable] =
|
||||||
|
useUpdateDatabaseConfigVariableMutation();
|
||||||
|
const [createDatabaseConfigVariable] =
|
||||||
|
useCreateDatabaseConfigVariableMutation();
|
||||||
|
const [deleteDatabaseConfigVariable] =
|
||||||
|
useDeleteDatabaseConfigVariableMutation();
|
||||||
|
|
||||||
|
const handleUpdateVariable = async (
|
||||||
|
value: ConfigVariableValue,
|
||||||
|
isFromDatabase: boolean,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
value === null ||
|
||||||
|
(typeof value === 'string' && value === '') ||
|
||||||
|
(Array.isArray(value) && value.length === 0)
|
||||||
|
) {
|
||||||
|
await handleDeleteVariable();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFromDatabase) {
|
||||||
|
await updateDatabaseConfigVariable({
|
||||||
|
variables: {
|
||||||
|
key: variableName,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
refetchQueries: [
|
||||||
|
{
|
||||||
|
query: GET_DATABASE_CONFIG_VARIABLE,
|
||||||
|
variables: { key: variableName },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: GET_CLIENT_CONFIG,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await createDatabaseConfigVariable({
|
||||||
|
variables: {
|
||||||
|
key: variableName,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
refetchQueries: [
|
||||||
|
{
|
||||||
|
query: GET_DATABASE_CONFIG_VARIABLE,
|
||||||
|
variables: { key: variableName },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: GET_CLIENT_CONFIG,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueueSnackBar(t`Variable updated successfully`, {
|
||||||
|
variant: SnackBarVariant.Success,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
enqueueSnackBar(t`Failed to update variable`, {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteVariable = async (e?: React.MouseEvent<HTMLElement>) => {
|
||||||
|
if (isDefined(e)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteDatabaseConfigVariable({
|
||||||
|
variables: {
|
||||||
|
key: variableName,
|
||||||
|
},
|
||||||
|
refetchQueries: [
|
||||||
|
{
|
||||||
|
query: GET_DATABASE_CONFIG_VARIABLE,
|
||||||
|
variables: { key: variableName },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: GET_CLIENT_CONFIG,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
enqueueSnackBar(t`Failed to remove override`, {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleUpdateVariable,
|
||||||
|
handleDeleteVariable,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { ConfigVariableValue } from 'twenty-shared/types';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { ConfigVariable } from '~/generated/graphql';
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
value: ConfigVariableValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useConfigVariableForm = (variable?: ConfigVariable) => {
|
||||||
|
const validationSchema = z.object({
|
||||||
|
value: z.union([
|
||||||
|
z.string(),
|
||||||
|
z.number(),
|
||||||
|
z.boolean(),
|
||||||
|
z.array(z.string()),
|
||||||
|
z.null(),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
watch,
|
||||||
|
} = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(validationSchema),
|
||||||
|
values: { value: variable?.value ?? null },
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentValue = watch('value');
|
||||||
|
const hasValueChanged = currentValue !== variable?.value;
|
||||||
|
const isValueValid = !!(
|
||||||
|
variable &&
|
||||||
|
!variable.isEnvOnly &&
|
||||||
|
hasValueChanged &&
|
||||||
|
((typeof currentValue === 'string' && currentValue.trim() !== '') ||
|
||||||
|
typeof currentValue === 'boolean' ||
|
||||||
|
typeof currentValue === 'number' ||
|
||||||
|
(Array.isArray(currentValue) && currentValue.length > 0))
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
isSubmitting,
|
||||||
|
watch,
|
||||||
|
currentValue,
|
||||||
|
hasValueChanged,
|
||||||
|
isValueValid,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export type ConfigVariableFilterCategory = 'source' | 'group';
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export type ConfigVariableGroupFilter = 'all' | string;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export type ConfigVariableOptions =
|
||||||
|
| readonly (string | number | boolean)[]
|
||||||
|
| Record<string, string>;
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export type ConfigVariableSourceFilter =
|
||||||
|
| 'all'
|
||||||
|
| 'database'
|
||||||
|
| 'environment'
|
||||||
|
| 'default';
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
|
||||||
|
import { ConfigSource } from '~/generated/graphql';
|
||||||
|
|
||||||
|
export const useSourceContent = (source: ConfigSource) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
switch (source) {
|
||||||
|
case ConfigSource.DATABASE:
|
||||||
|
return {
|
||||||
|
text: t`Stored in database`,
|
||||||
|
color: theme.color.blue50,
|
||||||
|
};
|
||||||
|
case ConfigSource.ENVIRONMENT:
|
||||||
|
return {
|
||||||
|
text: t`Environment variable`,
|
||||||
|
color: theme.color.green50,
|
||||||
|
};
|
||||||
|
case ConfigSource.DEFAULT:
|
||||||
|
return {
|
||||||
|
text: t`Default value`,
|
||||||
|
color: theme.font.color.tertiary,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown source: ${source}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,5 +1,5 @@
|
|||||||
export const SETTINGS_ADMIN_TABS = {
|
export const SETTINGS_ADMIN_TABS = {
|
||||||
GENERAL: 'general',
|
GENERAL: 'general',
|
||||||
ENV_VARIABLES: 'env-variables',
|
CONFIG_VARIABLES: 'config-variables',
|
||||||
HEALTH_STATUS: 'health-status',
|
HEALTH_STATUS: 'health-status',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,11 +3,11 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
||||||
|
import { BOOLEAN_DATA_MODEL_SELECT_OPTIONS } from '@/settings/data-model/fields/forms/boolean/constants/BooleanDataModelSelectOptions';
|
||||||
import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues';
|
import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues';
|
||||||
import { Select } from '@/ui/input/components/Select';
|
import { Select } from '@/ui/input/components/Select';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { IconCheck } from 'twenty-ui/display';
|
import { IconCheck } from 'twenty-ui/display';
|
||||||
import { BOOLEAN_DATA_MODEL_SELECT_OPTIONS } from '@/settings/data-model/fields/forms/boolean/constants/BooleanDataModelSelectOptions';
|
|
||||||
|
|
||||||
export const settingsDataModelFieldBooleanFormSchema = z.object({
|
export const settingsDataModelFieldBooleanFormSchema = z.object({
|
||||||
defaultValue: z.boolean(),
|
defaultValue: z.boolean(),
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export enum SettingsPath {
|
|||||||
AdminPanel = 'admin-panel',
|
AdminPanel = 'admin-panel',
|
||||||
AdminPanelHealthStatus = 'admin-panel#health-status',
|
AdminPanelHealthStatus = 'admin-panel#health-status',
|
||||||
AdminPanelIndicatorHealthStatus = 'admin-panel/health-status/:indicatorId',
|
AdminPanelIndicatorHealthStatus = 'admin-panel/health-status/:indicatorId',
|
||||||
AdminPanelOtherEnvVariables = 'admin-panel/other-env-variables',
|
AdminPanelConfigVariableDetails = 'admin-panel/config-variables/:variableName',
|
||||||
Lab = 'lab',
|
Lab = 'lab',
|
||||||
Roles = 'roles',
|
Roles = 'roles',
|
||||||
RoleCreate = 'roles/create',
|
RoleCreate = 'roles/create',
|
||||||
|
|||||||
@ -0,0 +1,211 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Form, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState';
|
||||||
|
import { ConfigVariableHelpText } from '@/settings/admin-panel/config-variables/components/ConfigVariableHelpText';
|
||||||
|
import { ConfigVariableValueInput } from '@/settings/admin-panel/config-variables/components/ConfigVariableValueInput';
|
||||||
|
import { useConfigVariableActions } from '@/settings/admin-panel/config-variables/hooks/useConfigVariableActions';
|
||||||
|
import { useConfigVariableForm } from '@/settings/admin-panel/config-variables/hooks/useConfigVariableForm';
|
||||||
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
|
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||||
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { ConfigVariableValue } from 'twenty-shared/types';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { H3Title, IconCheck, IconPencil, IconX } from 'twenty-ui/display';
|
||||||
|
import { Button } from 'twenty-ui/input';
|
||||||
|
import {
|
||||||
|
ConfigSource,
|
||||||
|
useGetDatabaseConfigVariableQuery,
|
||||||
|
} from '~/generated/graphql';
|
||||||
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
|
|
||||||
|
const StyledForm = styled(Form)`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(4)};
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledH3Title = styled(H3Title)`
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledRow = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledButtonContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
& > :not(:first-of-type) > button {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsAdminConfigVariableDetails = () => {
|
||||||
|
const { variableName } = useParams();
|
||||||
|
const { t } = useLingui();
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false);
|
||||||
|
const isConfigVariablesInDbEnabled = useRecoilValue(
|
||||||
|
isConfigVariablesInDbEnabledState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: configVariableData, loading } =
|
||||||
|
useGetDatabaseConfigVariableQuery({
|
||||||
|
variables: { key: variableName ?? '' },
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const variable = configVariableData?.getDatabaseConfigVariable;
|
||||||
|
|
||||||
|
const { handleUpdateVariable, handleDeleteVariable } =
|
||||||
|
useConfigVariableActions(variable?.name ?? '');
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
isSubmitting,
|
||||||
|
watch,
|
||||||
|
hasValueChanged,
|
||||||
|
isValueValid,
|
||||||
|
} = useConfigVariableForm(variable);
|
||||||
|
|
||||||
|
if (loading === true || isDefined(variable) === false) {
|
||||||
|
return <SettingsSkeletonLoader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEnvOnly = variable.isEnvOnly;
|
||||||
|
const isFromDatabase = variable.source === ConfigSource.DATABASE;
|
||||||
|
|
||||||
|
const onSubmit = async (formData: { value: ConfigVariableValue }) => {
|
||||||
|
await handleUpdateVariable(formData.value, isFromDatabase);
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditClick = () => {
|
||||||
|
setIsEditing(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleXButtonClick = () => {
|
||||||
|
if (isFromDatabase && hasValueChanged) {
|
||||||
|
setValue('value', variable.value);
|
||||||
|
setIsEditing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFromDatabase && !hasValueChanged) {
|
||||||
|
setIsConfirmationModalOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue('value', variable.value);
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmReset = () => {
|
||||||
|
handleDeleteVariable();
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SubMenuTopBarContainer
|
||||||
|
links={[
|
||||||
|
{
|
||||||
|
children: t`Other`,
|
||||||
|
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: t`Admin Panel`,
|
||||||
|
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: t`Config Variables`,
|
||||||
|
href: getSettingsPath(
|
||||||
|
SettingsPath.AdminPanel,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
'config-variables',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: variable.name,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SettingsPageContainer>
|
||||||
|
<StyledH3Title
|
||||||
|
title={variable.name}
|
||||||
|
description={variable.description}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StyledForm onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<StyledRow>
|
||||||
|
<ConfigVariableValueInput
|
||||||
|
variable={variable}
|
||||||
|
value={watch('value')}
|
||||||
|
onChange={(value) => setValue('value', value)}
|
||||||
|
disabled={isEnvOnly || !isEditing}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isEditing ? (
|
||||||
|
<Button
|
||||||
|
Icon={IconPencil}
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleEditClick}
|
||||||
|
type="button"
|
||||||
|
disabled={isEnvOnly || !isConfigVariablesInDbEnabled}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<StyledButtonContainer>
|
||||||
|
<Button
|
||||||
|
Icon={IconCheck}
|
||||||
|
variant="secondary"
|
||||||
|
position="left"
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting || !isValueValid || !hasValueChanged}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
Icon={IconX}
|
||||||
|
variant="secondary"
|
||||||
|
position="right"
|
||||||
|
onClick={handleXButtonClick}
|
||||||
|
type="button"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</StyledButtonContainer>
|
||||||
|
)}
|
||||||
|
</StyledRow>
|
||||||
|
|
||||||
|
<ConfigVariableHelpText
|
||||||
|
variable={variable}
|
||||||
|
hasValueChanged={hasValueChanged}
|
||||||
|
/>
|
||||||
|
</StyledForm>
|
||||||
|
</SettingsPageContainer>
|
||||||
|
</SubMenuTopBarContainer>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
isOpen={isConfirmationModalOpen}
|
||||||
|
setIsOpen={(isOpen) => {
|
||||||
|
setIsConfirmationModalOpen(isOpen);
|
||||||
|
if (!isOpen) {
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={t`Reset variable`}
|
||||||
|
subtitle={t`This will revert the database value to environment/default value. The database override will be removed and the system will use the environment settings.`}
|
||||||
|
onConfirmClick={handleConfirmReset}
|
||||||
|
confirmButtonText={t`Reset`}
|
||||||
|
confirmButtonAccent="danger"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,60 +0,0 @@
|
|||||||
import { SettingsAdminEnvVariablesTable } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesTable';
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
|
||||||
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
|
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { t } from '@lingui/core/macro';
|
|
||||||
import { H2Title } from 'twenty-ui/display';
|
|
||||||
import { useGetConfigVariablesGroupedQuery } from '~/generated/graphql';
|
|
||||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
|
||||||
|
|
||||||
const StyledGroupContainer = styled.div``;
|
|
||||||
|
|
||||||
export const SettingsAdminSecondaryEnvVariables = () => {
|
|
||||||
const {
|
|
||||||
data: secondaryConfigVariables,
|
|
||||||
loading: secondaryConfigVariablesLoading,
|
|
||||||
} = useGetConfigVariablesGroupedQuery({
|
|
||||||
fetchPolicy: 'network-only',
|
|
||||||
});
|
|
||||||
|
|
||||||
const hiddenGroups =
|
|
||||||
secondaryConfigVariables?.getConfigVariablesGrouped.groups.filter(
|
|
||||||
(group) => group.isHiddenOnLoad,
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
if (secondaryConfigVariablesLoading) {
|
|
||||||
return <SettingsSkeletonLoader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SubMenuTopBarContainer
|
|
||||||
title={t`Other Environment Variables`}
|
|
||||||
links={[
|
|
||||||
{
|
|
||||||
children: t`Other`,
|
|
||||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: t`Admin Panel`,
|
|
||||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: t`Other Environment Variables`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<SettingsPageContainer>
|
|
||||||
{hiddenGroups.map((group) => (
|
|
||||||
<StyledGroupContainer key={group.name}>
|
|
||||||
<H2Title title={group.name} description={group.description} />
|
|
||||||
{group.variables.length > 0 && (
|
|
||||||
<SettingsAdminEnvVariablesTable variables={group.variables} />
|
|
||||||
)}
|
|
||||||
</StyledGroupContainer>
|
|
||||||
))}
|
|
||||||
</SettingsPageContainer>
|
|
||||||
</SubMenuTopBarContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -58,4 +58,5 @@ export const mockedClientConfig: ClientConfig = {
|
|||||||
isGoogleMessagingEnabled: true,
|
isGoogleMessagingEnabled: true,
|
||||||
isGoogleCalendarEnabled: true,
|
isGoogleCalendarEnabled: true,
|
||||||
isAttachmentPreviewEnabled: true,
|
isAttachmentPreviewEnabled: true,
|
||||||
|
isConfigVariablesInDbEnabled: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export const getSettingsPath = <T extends SettingsPath>(
|
|||||||
[key in PathParam<`/${AppPath.Settings}/${T}`>]: string | null;
|
[key in PathParam<`/${AppPath.Settings}/${T}`>]: string | null;
|
||||||
},
|
},
|
||||||
queryParams?: Record<string, any>,
|
queryParams?: Record<string, any>,
|
||||||
|
hash?: string,
|
||||||
) => {
|
) => {
|
||||||
let path = `/${AppPath.Settings}/${to}`;
|
let path = `/${AppPath.Settings}/${to}`;
|
||||||
|
|
||||||
@ -32,5 +33,9 @@ export const getSettingsPath = <T extends SettingsPath>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDefined(hash)) {
|
||||||
|
path += `#${hash.replace(/^#/, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { User } from 'src/engine/core-modules/user/user.entity';
|
|||||||
const UserFindOneMock = jest.fn();
|
const UserFindOneMock = jest.fn();
|
||||||
const LoginTokenServiceGenerateLoginTokenMock = jest.fn();
|
const LoginTokenServiceGenerateLoginTokenMock = jest.fn();
|
||||||
const TwentyConfigServiceGetAllMock = jest.fn();
|
const TwentyConfigServiceGetAllMock = jest.fn();
|
||||||
|
const TwentyConfigServiceGetVariableWithMetadataMock = jest.fn();
|
||||||
|
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'src/engine/core-modules/twenty-config/constants/config-variables-group-metadata',
|
'src/engine/core-modules/twenty-config/constants/config-variables-group-metadata',
|
||||||
@ -72,6 +73,8 @@ describe('AdminPanelService', () => {
|
|||||||
provide: TwentyConfigService,
|
provide: TwentyConfigService,
|
||||||
useValue: {
|
useValue: {
|
||||||
getAll: TwentyConfigServiceGetAllMock,
|
getAll: TwentyConfigServiceGetAllMock,
|
||||||
|
getVariableWithMetadata:
|
||||||
|
TwentyConfigServiceGetVariableWithMetadataMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -165,14 +168,20 @@ describe('AdminPanelService', () => {
|
|||||||
metadata: {
|
metadata: {
|
||||||
group: 'SERVER_CONFIG',
|
group: 'SERVER_CONFIG',
|
||||||
description: 'Server URL',
|
description: 'Server URL',
|
||||||
|
type: 'string',
|
||||||
|
options: undefined,
|
||||||
},
|
},
|
||||||
|
source: 'env',
|
||||||
},
|
},
|
||||||
RATE_LIMIT_TTL: {
|
RATE_LIMIT_TTL: {
|
||||||
value: '60',
|
value: 60,
|
||||||
metadata: {
|
metadata: {
|
||||||
group: 'RATE_LIMITING',
|
group: 'RATE_LIMITING',
|
||||||
description: 'Rate limit TTL',
|
description: 'Rate limit TTL',
|
||||||
|
type: 'number',
|
||||||
|
options: undefined,
|
||||||
},
|
},
|
||||||
|
source: 'env',
|
||||||
},
|
},
|
||||||
API_KEY: {
|
API_KEY: {
|
||||||
value: 'secret-key',
|
value: 'secret-key',
|
||||||
@ -180,14 +189,20 @@ describe('AdminPanelService', () => {
|
|||||||
group: 'SERVER_CONFIG',
|
group: 'SERVER_CONFIG',
|
||||||
description: 'API Key',
|
description: 'API Key',
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
|
type: 'string',
|
||||||
|
options: undefined,
|
||||||
},
|
},
|
||||||
|
source: 'env',
|
||||||
},
|
},
|
||||||
OTHER_VAR: {
|
OTHER_VAR: {
|
||||||
value: 'other',
|
value: 'other',
|
||||||
metadata: {
|
metadata: {
|
||||||
group: 'OTHER',
|
group: 'OTHER',
|
||||||
description: 'Other var',
|
description: 'Other var',
|
||||||
|
type: 'string',
|
||||||
|
options: undefined,
|
||||||
},
|
},
|
||||||
|
source: 'env',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -205,12 +220,20 @@ describe('AdminPanelService', () => {
|
|||||||
value: 'secret-key',
|
value: 'secret-key',
|
||||||
description: 'API Key',
|
description: 'API Key',
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
|
isEnvOnly: false,
|
||||||
|
type: 'string',
|
||||||
|
options: undefined,
|
||||||
|
source: 'env',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'SERVER_URL',
|
name: 'SERVER_URL',
|
||||||
value: 'http://localhost',
|
value: 'http://localhost',
|
||||||
description: 'Server URL',
|
description: 'Server URL',
|
||||||
isSensitive: false,
|
isSensitive: false,
|
||||||
|
isEnvOnly: false,
|
||||||
|
type: 'string',
|
||||||
|
options: undefined,
|
||||||
|
source: 'env',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -221,9 +244,13 @@ describe('AdminPanelService', () => {
|
|||||||
variables: [
|
variables: [
|
||||||
{
|
{
|
||||||
name: 'RATE_LIMIT_TTL',
|
name: 'RATE_LIMIT_TTL',
|
||||||
value: '60',
|
value: 60,
|
||||||
description: 'Rate limit TTL',
|
description: 'Rate limit TTL',
|
||||||
isSensitive: false,
|
isSensitive: false,
|
||||||
|
isEnvOnly: false,
|
||||||
|
type: 'number',
|
||||||
|
options: undefined,
|
||||||
|
source: 'env',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -237,6 +264,10 @@ describe('AdminPanelService', () => {
|
|||||||
value: 'other',
|
value: 'other',
|
||||||
description: 'Other var',
|
description: 'Other var',
|
||||||
isSensitive: false,
|
isSensitive: false,
|
||||||
|
isEnvOnly: false,
|
||||||
|
type: 'string',
|
||||||
|
options: undefined,
|
||||||
|
source: 'env',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -264,7 +295,10 @@ describe('AdminPanelService', () => {
|
|||||||
value: 'test',
|
value: 'test',
|
||||||
metadata: {
|
metadata: {
|
||||||
group: 'SERVER_CONFIG',
|
group: 'SERVER_CONFIG',
|
||||||
|
type: 'string',
|
||||||
|
options: undefined,
|
||||||
},
|
},
|
||||||
|
source: 'env',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -275,6 +309,10 @@ describe('AdminPanelService', () => {
|
|||||||
value: 'test',
|
value: 'test',
|
||||||
description: undefined,
|
description: undefined,
|
||||||
isSensitive: false,
|
isSensitive: false,
|
||||||
|
isEnvOnly: false,
|
||||||
|
options: undefined,
|
||||||
|
source: 'env',
|
||||||
|
type: 'string',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -376,4 +414,42 @@ describe('AdminPanelService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getConfigVariable', () => {
|
||||||
|
it('should return config variable with all fields', () => {
|
||||||
|
TwentyConfigServiceGetVariableWithMetadataMock.mockReturnValue({
|
||||||
|
value: 'test-value',
|
||||||
|
metadata: {
|
||||||
|
group: 'SERVER_CONFIG',
|
||||||
|
description: 'Test description',
|
||||||
|
isSensitive: true,
|
||||||
|
isEnvOnly: true,
|
||||||
|
type: 'string',
|
||||||
|
options: ['option1', 'option2'],
|
||||||
|
},
|
||||||
|
source: 'env',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = service.getConfigVariable('SERVER_URL');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
name: 'SERVER_URL',
|
||||||
|
value: 'test-value',
|
||||||
|
description: 'Test description',
|
||||||
|
isSensitive: true,
|
||||||
|
isEnvOnly: true,
|
||||||
|
type: 'string',
|
||||||
|
options: ['option1', 'option2'],
|
||||||
|
source: 'env',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when variable not found', () => {
|
||||||
|
TwentyConfigServiceGetVariableWithMetadataMock.mockReturnValue(undefined);
|
||||||
|
|
||||||
|
expect(() => service.getConfigVariable('INVALID_VAR')).toThrow(
|
||||||
|
'Config variable INVALID_VAR not found',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import { UseFilters, UseGuards } from '@nestjs/common';
|
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import GraphQLJSON from 'graphql-type-json';
|
||||||
|
|
||||||
import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/admin-panel-health.service';
|
import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/admin-panel-health.service';
|
||||||
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
|
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
|
||||||
|
import { ConfigVariable } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
|
||||||
import { ConfigVariablesOutput } from 'src/engine/core-modules/admin-panel/dtos/config-variables.output';
|
import { ConfigVariablesOutput } from 'src/engine/core-modules/admin-panel/dtos/config-variables.output';
|
||||||
import { ImpersonateInput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.input';
|
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 { ImpersonateOutput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.output';
|
||||||
@ -18,6 +21,9 @@ import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/service
|
|||||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||||
import { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
|
import { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
|
||||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||||
|
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
||||||
|
import { ConfigVariableGraphqlApiExceptionFilter } from 'src/engine/core-modules/twenty-config/filters/config-variable-graphql-api-exception.filter';
|
||||||
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
import { AdminPanelGuard } from 'src/engine/guards/admin-panel-guard';
|
import { AdminPanelGuard } from 'src/engine/guards/admin-panel-guard';
|
||||||
import { ImpersonateGuard } from 'src/engine/guards/impersonate-guard';
|
import { ImpersonateGuard } from 'src/engine/guards/impersonate-guard';
|
||||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||||
@ -27,12 +33,16 @@ import { AdminPanelHealthServiceData } from './dtos/admin-panel-health-service-d
|
|||||||
import { QueueMetricsData } from './dtos/queue-metrics-data.dto';
|
import { QueueMetricsData } from './dtos/queue-metrics-data.dto';
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
@UseFilters(AuthGraphqlApiExceptionFilter)
|
@UseFilters(
|
||||||
|
AuthGraphqlApiExceptionFilter,
|
||||||
|
ConfigVariableGraphqlApiExceptionFilter,
|
||||||
|
)
|
||||||
export class AdminPanelResolver {
|
export class AdminPanelResolver {
|
||||||
constructor(
|
constructor(
|
||||||
private adminService: AdminPanelService,
|
private adminService: AdminPanelService,
|
||||||
private adminPanelHealthService: AdminPanelHealthService,
|
private adminPanelHealthService: AdminPanelHealthService,
|
||||||
private featureFlagService: FeatureFlagService,
|
private featureFlagService: FeatureFlagService,
|
||||||
|
private readonly twentyConfigService: TwentyConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard)
|
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard)
|
||||||
@ -119,4 +129,48 @@ export class AdminPanelResolver {
|
|||||||
async versionInfo(): Promise<VersionInfo> {
|
async versionInfo(): Promise<VersionInfo> {
|
||||||
return this.adminService.getVersionInfo();
|
return this.adminService.getVersionInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
|
||||||
|
@Query(() => ConfigVariable)
|
||||||
|
async getDatabaseConfigVariable(
|
||||||
|
@Args('key', { type: () => String }) key: keyof ConfigVariables,
|
||||||
|
): Promise<ConfigVariable> {
|
||||||
|
this.twentyConfigService.validateConfigVariableExists(key as string);
|
||||||
|
|
||||||
|
return this.adminService.getConfigVariable(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
|
||||||
|
@Mutation(() => Boolean)
|
||||||
|
async createDatabaseConfigVariable(
|
||||||
|
@Args('key', { type: () => String }) key: keyof ConfigVariables,
|
||||||
|
@Args('value', { type: () => GraphQLJSON })
|
||||||
|
value: ConfigVariables[keyof ConfigVariables],
|
||||||
|
): Promise<boolean> {
|
||||||
|
await this.twentyConfigService.set(key, value);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
|
||||||
|
@Mutation(() => Boolean)
|
||||||
|
async updateDatabaseConfigVariable(
|
||||||
|
@Args('key', { type: () => String }) key: keyof ConfigVariables,
|
||||||
|
@Args('value', { type: () => GraphQLJSON })
|
||||||
|
value: ConfigVariables[keyof ConfigVariables],
|
||||||
|
): Promise<boolean> {
|
||||||
|
await this.twentyConfigService.update(key, value);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
|
||||||
|
@Mutation(() => Boolean)
|
||||||
|
async deleteDatabaseConfigVariable(
|
||||||
|
@Args('key', { type: () => String }) key: keyof ConfigVariables,
|
||||||
|
): Promise<boolean> {
|
||||||
|
await this.twentyConfigService.delete(key);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/l
|
|||||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
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 { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
|
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
||||||
import { CONFIG_VARIABLES_GROUP_METADATA } from 'src/engine/core-modules/twenty-config/constants/config-variables-group-metadata';
|
import { CONFIG_VARIABLES_GROUP_METADATA } from 'src/engine/core-modules/twenty-config/constants/config-variables-group-metadata';
|
||||||
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
@ -127,14 +128,20 @@ export class AdminPanelService {
|
|||||||
const rawEnvVars = this.twentyConfigService.getAll();
|
const rawEnvVars = this.twentyConfigService.getAll();
|
||||||
const groupedData = new Map<ConfigVariablesGroup, ConfigVariable[]>();
|
const groupedData = new Map<ConfigVariablesGroup, ConfigVariable[]>();
|
||||||
|
|
||||||
for (const [varName, { value, metadata }] of Object.entries(rawEnvVars)) {
|
for (const [varName, { value, metadata, source }] of Object.entries(
|
||||||
|
rawEnvVars,
|
||||||
|
)) {
|
||||||
const { group, description } = metadata;
|
const { group, description } = metadata;
|
||||||
|
|
||||||
const envVar: ConfigVariable = {
|
const envVar: ConfigVariable = {
|
||||||
name: varName,
|
name: varName,
|
||||||
description,
|
description,
|
||||||
value: String(value),
|
value: value ?? null,
|
||||||
isSensitive: metadata.isSensitive ?? false,
|
isSensitive: metadata.isSensitive ?? false,
|
||||||
|
isEnvOnly: metadata.isEnvOnly ?? false,
|
||||||
|
type: metadata.type,
|
||||||
|
options: metadata.options,
|
||||||
|
source,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!groupedData.has(group)) {
|
if (!groupedData.has(group)) {
|
||||||
@ -161,6 +168,30 @@ export class AdminPanelService {
|
|||||||
return { groups };
|
return { groups };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getConfigVariable(key: string): ConfigVariable {
|
||||||
|
const variableWithMetadata =
|
||||||
|
this.twentyConfigService.getVariableWithMetadata(
|
||||||
|
key as keyof ConfigVariables,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!variableWithMetadata) {
|
||||||
|
throw new Error(`Config variable ${key} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value, metadata, source } = variableWithMetadata;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: key,
|
||||||
|
description: metadata.description ?? '',
|
||||||
|
value: value ?? null,
|
||||||
|
isSensitive: metadata.isSensitive ?? false,
|
||||||
|
isEnvOnly: metadata.isEnvOnly ?? false,
|
||||||
|
type: metadata.type,
|
||||||
|
options: metadata.options,
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async getVersionInfo(): Promise<VersionInfo> {
|
async getVersionInfo(): Promise<VersionInfo> {
|
||||||
const currentVersion = this.twentyConfigService.get('APP_VERSION');
|
const currentVersion = this.twentyConfigService.get('APP_VERSION');
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,19 @@
|
|||||||
import { Field, ObjectType } from '@nestjs/graphql';
|
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import GraphQLJSON from 'graphql-type-json';
|
||||||
|
import { ConfigVariableValue } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
import { ConfigSource } from 'src/engine/core-modules/twenty-config/enums/config-source.enum';
|
||||||
|
import { ConfigVariableType } from 'src/engine/core-modules/twenty-config/enums/config-variable-type.enum';
|
||||||
|
import { ConfigVariableOptions } from 'src/engine/core-modules/twenty-config/types/config-variable-options.type';
|
||||||
|
|
||||||
|
registerEnumType(ConfigSource, {
|
||||||
|
name: 'ConfigSource',
|
||||||
|
});
|
||||||
|
|
||||||
|
registerEnumType(ConfigVariableType, {
|
||||||
|
name: 'ConfigVariableType',
|
||||||
|
});
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class ConfigVariable {
|
export class ConfigVariable {
|
||||||
@ -8,9 +23,21 @@ export class ConfigVariable {
|
|||||||
@Field()
|
@Field()
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
@Field()
|
@Field(() => GraphQLJSON, { nullable: true })
|
||||||
value: string;
|
value: ConfigVariableValue;
|
||||||
|
|
||||||
@Field()
|
@Field()
|
||||||
isSensitive: boolean;
|
isSensitive: boolean;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
source: ConfigSource;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
isEnvOnly: boolean;
|
||||||
|
|
||||||
|
@Field(() => ConfigVariableType)
|
||||||
|
type: ConfigVariableType;
|
||||||
|
|
||||||
|
@Field(() => GraphQLJSON, { nullable: true })
|
||||||
|
options?: ConfigVariableOptions;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -142,4 +142,7 @@ export class ClientConfig {
|
|||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
isGoogleCalendarEnabled: boolean;
|
isGoogleCalendarEnabled: boolean;
|
||||||
|
|
||||||
|
@Field(() => Boolean)
|
||||||
|
isConfigVariablesInDbEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,6 +97,9 @@ export class ClientConfigResolver {
|
|||||||
isGoogleCalendarEnabled: this.twentyConfigService.get(
|
isGoogleCalendarEnabled: this.twentyConfigService.get(
|
||||||
'CALENDAR_PROVIDER_GOOGLE_ENABLED',
|
'CALENDAR_PROVIDER_GOOGLE_ENABLED',
|
||||||
),
|
),
|
||||||
|
isConfigVariablesInDbEnabled: this.twentyConfigService.get(
|
||||||
|
'IS_CONFIG_VARIABLES_IN_DB_ENABLED',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
return Promise.resolve(clientConfig);
|
return Promise.resolve(clientConfig);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
|
import { Injectable, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ConfigCacheEntry,
|
ConfigCacheEntry,
|
||||||
@ -8,7 +8,6 @@ import {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ConfigCacheService implements OnModuleDestroy {
|
export class ConfigCacheService implements OnModuleDestroy {
|
||||||
private readonly logger = new Logger(ConfigCacheService.name);
|
|
||||||
private readonly foundConfigValuesCache: Map<
|
private readonly foundConfigValuesCache: Map<
|
||||||
ConfigKey,
|
ConfigKey,
|
||||||
ConfigCacheEntry<ConfigKey>
|
ConfigCacheEntry<ConfigKey>
|
||||||
|
|||||||
@ -33,13 +33,18 @@ import { IsDuration } from 'src/engine/core-modules/twenty-config/decorators/is-
|
|||||||
import { IsOptionalOrEmptyString } from 'src/engine/core-modules/twenty-config/decorators/is-optional-or-empty-string.decorator';
|
import { IsOptionalOrEmptyString } from 'src/engine/core-modules/twenty-config/decorators/is-optional-or-empty-string.decorator';
|
||||||
import { IsStrictlyLowerThan } from 'src/engine/core-modules/twenty-config/decorators/is-strictly-lower-than.decorator';
|
import { IsStrictlyLowerThan } from 'src/engine/core-modules/twenty-config/decorators/is-strictly-lower-than.decorator';
|
||||||
import { IsTwentySemVer } from 'src/engine/core-modules/twenty-config/decorators/is-twenty-semver.decorator';
|
import { IsTwentySemVer } from 'src/engine/core-modules/twenty-config/decorators/is-twenty-semver.decorator';
|
||||||
|
import { ConfigVariableType } from 'src/engine/core-modules/twenty-config/enums/config-variable-type.enum';
|
||||||
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
||||||
|
import {
|
||||||
|
ConfigVariableException,
|
||||||
|
ConfigVariableExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/twenty-config/twenty-config.exception';
|
||||||
|
|
||||||
export class ConfigVariables {
|
export class ConfigVariables {
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.Other,
|
group: ConfigVariablesGroup.Other,
|
||||||
description: 'Enable or disable password authentication for users',
|
description: 'Enable or disable password authentication for users',
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
AUTH_PASSWORD_ENABLED = true;
|
AUTH_PASSWORD_ENABLED = true;
|
||||||
@ -48,7 +53,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.Other,
|
group: ConfigVariablesGroup.Other,
|
||||||
description:
|
description:
|
||||||
'Prefills tim@apple.dev in the login form, used in local development for quicker sign-in',
|
'Prefills tim@apple.dev in the login form, used in local development for quicker sign-in',
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ValidateIf((env) => env.AUTH_PASSWORD_ENABLED)
|
@ValidateIf((env) => env.AUTH_PASSWORD_ENABLED)
|
||||||
@ -57,7 +62,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.Other,
|
group: ConfigVariablesGroup.Other,
|
||||||
description: 'Require email verification for user accounts',
|
description: 'Require email verification for user accounts',
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
IS_EMAIL_VERIFICATION_REQUIRED = false;
|
IS_EMAIL_VERIFICATION_REQUIRED = false;
|
||||||
@ -65,7 +70,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.TokensDuration,
|
group: ConfigVariablesGroup.TokensDuration,
|
||||||
description: 'Duration for which the email verification token is valid',
|
description: 'Duration for which the email verification token is valid',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsDuration()
|
@IsDuration()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -74,7 +79,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.TokensDuration,
|
group: ConfigVariablesGroup.TokensDuration,
|
||||||
description: 'Duration for which the password reset token is valid',
|
description: 'Duration for which the password reset token is valid',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsDuration()
|
@IsDuration()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -83,30 +88,31 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.GoogleAuth,
|
group: ConfigVariablesGroup.GoogleAuth,
|
||||||
description: 'Enable or disable the Google Calendar integration',
|
description: 'Enable or disable the Google Calendar integration',
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
CALENDAR_PROVIDER_GOOGLE_ENABLED = false;
|
CALENDAR_PROVIDER_GOOGLE_ENABLED = false;
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.GoogleAuth,
|
group: ConfigVariablesGroup.GoogleAuth,
|
||||||
description: 'Callback URL for Google Auth APIs',
|
description: 'Callback URL for Google Auth APIs',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
|
isSensitive: false,
|
||||||
})
|
})
|
||||||
AUTH_GOOGLE_APIS_CALLBACK_URL: string;
|
AUTH_GOOGLE_APIS_CALLBACK_URL: string;
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.GoogleAuth,
|
group: ConfigVariablesGroup.GoogleAuth,
|
||||||
description: 'Enable or disable Google Single Sign-On (SSO)',
|
description: 'Enable or disable Google Single Sign-On (SSO)',
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
AUTH_GOOGLE_ENABLED = false;
|
AUTH_GOOGLE_ENABLED = false;
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.GoogleAuth,
|
group: ConfigVariablesGroup.GoogleAuth,
|
||||||
isSensitive: true,
|
isSensitive: false,
|
||||||
description: 'Client ID for Google authentication',
|
description: 'Client ID for Google authentication',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED)
|
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED)
|
||||||
AUTH_GOOGLE_CLIENT_ID: string;
|
AUTH_GOOGLE_CLIENT_ID: string;
|
||||||
@ -115,16 +121,16 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.GoogleAuth,
|
group: ConfigVariablesGroup.GoogleAuth,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'Client secret for Google authentication',
|
description: 'Client secret for Google authentication',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED)
|
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED)
|
||||||
AUTH_GOOGLE_CLIENT_SECRET: string;
|
AUTH_GOOGLE_CLIENT_SECRET: string;
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.GoogleAuth,
|
group: ConfigVariablesGroup.GoogleAuth,
|
||||||
isSensitive: true,
|
isSensitive: false,
|
||||||
description: 'Callback URL for Google authentication',
|
description: 'Callback URL for Google authentication',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsUrl({ require_tld: false, require_protocol: true })
|
@IsUrl({ require_tld: false, require_protocol: true })
|
||||||
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED)
|
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED)
|
||||||
@ -133,23 +139,23 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.GoogleAuth,
|
group: ConfigVariablesGroup.GoogleAuth,
|
||||||
description: 'Enable or disable the Gmail messaging integration',
|
description: 'Enable or disable the Gmail messaging integration',
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
MESSAGING_PROVIDER_GMAIL_ENABLED = false;
|
MESSAGING_PROVIDER_GMAIL_ENABLED = false;
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.MicrosoftAuth,
|
group: ConfigVariablesGroup.MicrosoftAuth,
|
||||||
description: 'Enable or disable Microsoft authentication',
|
description: 'Enable or disable Microsoft authentication',
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
AUTH_MICROSOFT_ENABLED = false;
|
AUTH_MICROSOFT_ENABLED = false;
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.MicrosoftAuth,
|
group: ConfigVariablesGroup.MicrosoftAuth,
|
||||||
isSensitive: true,
|
isSensitive: false,
|
||||||
description: 'Client ID for Microsoft authentication',
|
description: 'Client ID for Microsoft authentication',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
|
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
|
||||||
AUTH_MICROSOFT_CLIENT_ID: string;
|
AUTH_MICROSOFT_CLIENT_ID: string;
|
||||||
@ -158,16 +164,16 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.MicrosoftAuth,
|
group: ConfigVariablesGroup.MicrosoftAuth,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'Client secret for Microsoft authentication',
|
description: 'Client secret for Microsoft authentication',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
|
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
|
||||||
AUTH_MICROSOFT_CLIENT_SECRET: string;
|
AUTH_MICROSOFT_CLIENT_SECRET: string;
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.MicrosoftAuth,
|
group: ConfigVariablesGroup.MicrosoftAuth,
|
||||||
isSensitive: true,
|
isSensitive: false,
|
||||||
description: 'Callback URL for Microsoft authentication',
|
description: 'Callback URL for Microsoft authentication',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsUrl({ require_tld: false, require_protocol: true })
|
@IsUrl({ require_tld: false, require_protocol: true })
|
||||||
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
|
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
|
||||||
@ -175,9 +181,9 @@ export class ConfigVariables {
|
|||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.MicrosoftAuth,
|
group: ConfigVariablesGroup.MicrosoftAuth,
|
||||||
isSensitive: true,
|
isSensitive: false,
|
||||||
description: 'Callback URL for Microsoft APIs',
|
description: 'Callback URL for Microsoft APIs',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsUrl({ require_tld: false, require_protocol: true })
|
@IsUrl({ require_tld: false, require_protocol: true })
|
||||||
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
|
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
|
||||||
@ -186,14 +192,14 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.MicrosoftAuth,
|
group: ConfigVariablesGroup.MicrosoftAuth,
|
||||||
description: 'Enable or disable the Microsoft messaging integration',
|
description: 'Enable or disable the Microsoft messaging integration',
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
MESSAGING_PROVIDER_MICROSOFT_ENABLED = false;
|
MESSAGING_PROVIDER_MICROSOFT_ENABLED = false;
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.MicrosoftAuth,
|
group: ConfigVariablesGroup.MicrosoftAuth,
|
||||||
description: 'Enable or disable the Microsoft Calendar integration',
|
description: 'Enable or disable the Microsoft Calendar integration',
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
CALENDAR_PROVIDER_MICROSOFT_ENABLED = false;
|
CALENDAR_PROVIDER_MICROSOFT_ENABLED = false;
|
||||||
|
|
||||||
@ -202,7 +208,7 @@ export class ConfigVariables {
|
|||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description:
|
description:
|
||||||
'Legacy variable to be deprecated when all API Keys expire. Replaced by APP_KEY',
|
'Legacy variable to be deprecated when all API Keys expire. Replaced by APP_KEY',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
ACCESS_TOKEN_SECRET: string;
|
ACCESS_TOKEN_SECRET: string;
|
||||||
@ -210,7 +216,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.TokensDuration,
|
group: ConfigVariablesGroup.TokensDuration,
|
||||||
description: 'Duration for which the access token is valid',
|
description: 'Duration for which the access token is valid',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsDuration()
|
@IsDuration()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -219,7 +225,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.TokensDuration,
|
group: ConfigVariablesGroup.TokensDuration,
|
||||||
description: 'Duration for which the refresh token is valid',
|
description: 'Duration for which the refresh token is valid',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
REFRESH_TOKEN_EXPIRES_IN = '60d';
|
REFRESH_TOKEN_EXPIRES_IN = '60d';
|
||||||
@ -227,7 +233,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.TokensDuration,
|
group: ConfigVariablesGroup.TokensDuration,
|
||||||
description: 'Cooldown period for refreshing tokens',
|
description: 'Cooldown period for refreshing tokens',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsDuration()
|
@IsDuration()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -236,7 +242,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.TokensDuration,
|
group: ConfigVariablesGroup.TokensDuration,
|
||||||
description: 'Duration for which the login token is valid',
|
description: 'Duration for which the login token is valid',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsDuration()
|
@IsDuration()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -245,7 +251,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.TokensDuration,
|
group: ConfigVariablesGroup.TokensDuration,
|
||||||
description: 'Duration for which the file token is valid',
|
description: 'Duration for which the file token is valid',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsDuration()
|
@IsDuration()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -254,7 +260,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.TokensDuration,
|
group: ConfigVariablesGroup.TokensDuration,
|
||||||
description: 'Duration for which the invitation token is valid',
|
description: 'Duration for which the invitation token is valid',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsDuration()
|
@IsDuration()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -263,35 +269,35 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.TokensDuration,
|
group: ConfigVariablesGroup.TokensDuration,
|
||||||
description: 'Duration for which the short-term token is valid',
|
description: 'Duration for which the short-term token is valid',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
SHORT_TERM_TOKEN_EXPIRES_IN = '5m';
|
SHORT_TERM_TOKEN_EXPIRES_IN = '5m';
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.EmailSettings,
|
group: ConfigVariablesGroup.EmailSettings,
|
||||||
description: 'Email address used as the sender for outgoing emails',
|
description: 'Email address used as the sender for outgoing emails',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
EMAIL_FROM_ADDRESS = 'noreply@yourdomain.com';
|
EMAIL_FROM_ADDRESS = 'noreply@yourdomain.com';
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.EmailSettings,
|
group: ConfigVariablesGroup.EmailSettings,
|
||||||
description: 'Email address used for system notifications',
|
description: 'Email address used for system notifications',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
EMAIL_SYSTEM_ADDRESS = 'system@yourdomain.com';
|
EMAIL_SYSTEM_ADDRESS = 'system@yourdomain.com';
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.EmailSettings,
|
group: ConfigVariablesGroup.EmailSettings,
|
||||||
description: 'Name used in the From header for outgoing emails',
|
description: 'Name used in the From header for outgoing emails',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
EMAIL_FROM_NAME = 'Felix from Twenty';
|
EMAIL_FROM_NAME = 'Felix from Twenty';
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.EmailSettings,
|
group: ConfigVariablesGroup.EmailSettings,
|
||||||
description: 'Email driver to use for sending emails',
|
description: 'Email driver to use for sending emails',
|
||||||
type: 'enum',
|
type: ConfigVariableType.ENUM,
|
||||||
options: Object.values(EmailDriver),
|
options: Object.values(EmailDriver),
|
||||||
})
|
})
|
||||||
EMAIL_DRIVER: EmailDriver = EmailDriver.Logger;
|
EMAIL_DRIVER: EmailDriver = EmailDriver.Logger;
|
||||||
@ -299,14 +305,14 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.EmailSettings,
|
group: ConfigVariablesGroup.EmailSettings,
|
||||||
description: 'SMTP host for sending emails',
|
description: 'SMTP host for sending emails',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
EMAIL_SMTP_HOST: string;
|
EMAIL_SMTP_HOST: string;
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.EmailSettings,
|
group: ConfigVariablesGroup.EmailSettings,
|
||||||
description: 'Use unsecure connection for SMTP',
|
description: 'Use unsecure connection for SMTP',
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
EMAIL_SMTP_NO_TLS = false;
|
EMAIL_SMTP_NO_TLS = false;
|
||||||
@ -314,7 +320,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.EmailSettings,
|
group: ConfigVariablesGroup.EmailSettings,
|
||||||
description: 'SMTP port for sending emails',
|
description: 'SMTP port for sending emails',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
EMAIL_SMTP_PORT = 587;
|
EMAIL_SMTP_PORT = 587;
|
||||||
@ -322,7 +328,8 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.EmailSettings,
|
group: ConfigVariablesGroup.EmailSettings,
|
||||||
description: 'SMTP user for authentication',
|
description: 'SMTP user for authentication',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
|
isSensitive: true,
|
||||||
})
|
})
|
||||||
EMAIL_SMTP_USER: string;
|
EMAIL_SMTP_USER: string;
|
||||||
|
|
||||||
@ -330,14 +337,14 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.EmailSettings,
|
group: ConfigVariablesGroup.EmailSettings,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'SMTP password for authentication',
|
description: 'SMTP password for authentication',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
EMAIL_SMTP_PASSWORD: string;
|
EMAIL_SMTP_PASSWORD: string;
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.StorageConfig,
|
group: ConfigVariablesGroup.StorageConfig,
|
||||||
description: 'Type of storage to use (local or S3)',
|
description: 'Type of storage to use (local or S3)',
|
||||||
type: 'enum',
|
type: ConfigVariableType.ENUM,
|
||||||
options: Object.values(StorageDriverType),
|
options: Object.values(StorageDriverType),
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -346,7 +353,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.StorageConfig,
|
group: ConfigVariablesGroup.StorageConfig,
|
||||||
description: 'Local path for storage when using local storage type',
|
description: 'Local path for storage when using local storage type',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.Local)
|
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.Local)
|
||||||
STORAGE_LOCAL_PATH = '.local-storage';
|
STORAGE_LOCAL_PATH = '.local-storage';
|
||||||
@ -354,7 +361,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.StorageConfig,
|
group: ConfigVariablesGroup.StorageConfig,
|
||||||
description: 'S3 region for storage when using S3 storage type',
|
description: 'S3 region for storage when using S3 storage type',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
||||||
@IsAWSRegion()
|
@IsAWSRegion()
|
||||||
@ -363,7 +370,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.StorageConfig,
|
group: ConfigVariablesGroup.StorageConfig,
|
||||||
description: 'S3 bucket name for storage when using S3 storage type',
|
description: 'S3 bucket name for storage when using S3 storage type',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
||||||
STORAGE_S3_NAME: string;
|
STORAGE_S3_NAME: string;
|
||||||
@ -371,7 +378,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.StorageConfig,
|
group: ConfigVariablesGroup.StorageConfig,
|
||||||
description: 'S3 endpoint for storage when using S3 storage type',
|
description: 'S3 endpoint for storage when using S3 storage type',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -382,7 +389,7 @@ export class ConfigVariables {
|
|||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description:
|
description:
|
||||||
'S3 access key ID for authentication when using S3 storage type',
|
'S3 access key ID for authentication when using S3 storage type',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -393,7 +400,7 @@ export class ConfigVariables {
|
|||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description:
|
description:
|
||||||
'S3 secret access key for authentication when using S3 storage type',
|
'S3 secret access key for authentication when using S3 storage type',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -402,7 +409,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ServerlessConfig,
|
group: ConfigVariablesGroup.ServerlessConfig,
|
||||||
description: 'Type of serverless execution (local or Lambda)',
|
description: 'Type of serverless execution (local or Lambda)',
|
||||||
type: 'enum',
|
type: ConfigVariableType.ENUM,
|
||||||
options: Object.values(ServerlessDriverType),
|
options: Object.values(ServerlessDriverType),
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -411,7 +418,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ServerlessConfig,
|
group: ConfigVariablesGroup.ServerlessConfig,
|
||||||
description: 'Throttle limit for serverless function execution',
|
description: 'Throttle limit for serverless function execution',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
SERVERLESS_FUNCTION_EXEC_THROTTLE_LIMIT = 10;
|
SERVERLESS_FUNCTION_EXEC_THROTTLE_LIMIT = 10;
|
||||||
@ -420,7 +427,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ServerlessConfig,
|
group: ConfigVariablesGroup.ServerlessConfig,
|
||||||
description: 'Time-to-live for serverless function execution throttle',
|
description: 'Time-to-live for serverless function execution throttle',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
SERVERLESS_FUNCTION_EXEC_THROTTLE_TTL = 1000;
|
SERVERLESS_FUNCTION_EXEC_THROTTLE_TTL = 1000;
|
||||||
@ -428,7 +435,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ServerlessConfig,
|
group: ConfigVariablesGroup.ServerlessConfig,
|
||||||
description: 'Region for AWS Lambda functions',
|
description: 'Region for AWS Lambda functions',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
|
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
|
||||||
@IsAWSRegion()
|
@IsAWSRegion()
|
||||||
@ -437,7 +444,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ServerlessConfig,
|
group: ConfigVariablesGroup.ServerlessConfig,
|
||||||
description: 'IAM role for AWS Lambda functions',
|
description: 'IAM role for AWS Lambda functions',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
|
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
|
||||||
SERVERLESS_LAMBDA_ROLE: string;
|
SERVERLESS_LAMBDA_ROLE: string;
|
||||||
@ -445,7 +452,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ServerlessConfig,
|
group: ConfigVariablesGroup.ServerlessConfig,
|
||||||
description: 'Role to assume when hosting lambdas in dedicated AWS account',
|
description: 'Role to assume when hosting lambdas in dedicated AWS account',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
|
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -455,7 +462,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.ServerlessConfig,
|
group: ConfigVariablesGroup.ServerlessConfig,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'Access key ID for AWS Lambda functions',
|
description: 'Access key ID for AWS Lambda functions',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
|
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -465,7 +472,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.ServerlessConfig,
|
group: ConfigVariablesGroup.ServerlessConfig,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'Secret access key for AWS Lambda functions',
|
description: 'Secret access key for AWS Lambda functions',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
|
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -474,7 +481,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.AnalyticsConfig,
|
group: ConfigVariablesGroup.AnalyticsConfig,
|
||||||
description: 'Enable or disable analytics for telemetry',
|
description: 'Enable or disable analytics for telemetry',
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
ANALYTICS_ENABLED = false;
|
ANALYTICS_ENABLED = false;
|
||||||
@ -482,7 +489,8 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.AnalyticsConfig,
|
group: ConfigVariablesGroup.AnalyticsConfig,
|
||||||
description: 'Clickhouse host for analytics',
|
description: 'Clickhouse host for analytics',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
|
isSensitive: true,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsUrl({
|
@IsUrl({
|
||||||
@ -495,7 +503,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.Logging,
|
group: ConfigVariablesGroup.Logging,
|
||||||
description: 'Enable or disable telemetry logging',
|
description: 'Enable or disable telemetry logging',
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
TELEMETRY_ENABLED = true;
|
TELEMETRY_ENABLED = true;
|
||||||
@ -503,7 +511,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.BillingConfig,
|
group: ConfigVariablesGroup.BillingConfig,
|
||||||
description: 'Enable or disable billing features',
|
description: 'Enable or disable billing features',
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
IS_BILLING_ENABLED = false;
|
IS_BILLING_ENABLED = false;
|
||||||
@ -511,7 +519,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.BillingConfig,
|
group: ConfigVariablesGroup.BillingConfig,
|
||||||
description: 'Link required for billing plan',
|
description: 'Link required for billing plan',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
||||||
BILLING_PLAN_REQUIRED_LINK: string;
|
BILLING_PLAN_REQUIRED_LINK: string;
|
||||||
@ -519,7 +527,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.BillingConfig,
|
group: ConfigVariablesGroup.BillingConfig,
|
||||||
description: 'Duration of free trial with credit card in days',
|
description: 'Duration of free trial with credit card in days',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -529,7 +537,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.BillingConfig,
|
group: ConfigVariablesGroup.BillingConfig,
|
||||||
description: 'Duration of free trial without credit card in days',
|
description: 'Duration of free trial without credit card in days',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -539,7 +547,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.BillingConfig,
|
group: ConfigVariablesGroup.BillingConfig,
|
||||||
description: 'Amount of money in cents to trigger a billing threshold',
|
description: 'Amount of money in cents to trigger a billing threshold',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
||||||
@ -548,7 +556,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.BillingConfig,
|
group: ConfigVariablesGroup.BillingConfig,
|
||||||
description: 'Amount of credits for the free trial without credit card',
|
description: 'Amount of credits for the free trial without credit card',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
||||||
@ -557,7 +565,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.BillingConfig,
|
group: ConfigVariablesGroup.BillingConfig,
|
||||||
description: 'Amount of credits for the free trial with credit card',
|
description: 'Amount of credits for the free trial with credit card',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
||||||
@ -567,7 +575,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.BillingConfig,
|
group: ConfigVariablesGroup.BillingConfig,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'Stripe API key for billing',
|
description: 'Stripe API key for billing',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
||||||
BILLING_STRIPE_API_KEY: string;
|
BILLING_STRIPE_API_KEY: string;
|
||||||
@ -576,7 +584,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.BillingConfig,
|
group: ConfigVariablesGroup.BillingConfig,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'Stripe webhook secret for billing',
|
description: 'Stripe webhook secret for billing',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
||||||
BILLING_STRIPE_WEBHOOK_SECRET: string;
|
BILLING_STRIPE_WEBHOOK_SECRET: string;
|
||||||
@ -584,7 +592,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ServerConfig,
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
description: 'Url for the frontend application',
|
description: 'Url for the frontend application',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsUrl({ require_tld: false, require_protocol: true })
|
@IsUrl({ require_tld: false, require_protocol: true })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -594,7 +602,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.ServerConfig,
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
description:
|
description:
|
||||||
'Default subdomain for the frontend when multi-workspace is enabled',
|
'Default subdomain for the frontend when multi-workspace is enabled',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.IS_MULTIWORKSPACE_ENABLED)
|
@ValidateIf((env) => env.IS_MULTIWORKSPACE_ENABLED)
|
||||||
DEFAULT_SUBDOMAIN = 'app';
|
DEFAULT_SUBDOMAIN = 'app';
|
||||||
@ -602,7 +610,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.Other,
|
group: ConfigVariablesGroup.Other,
|
||||||
description: 'ID for the Chrome extension',
|
description: 'ID for the Chrome extension',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
CHROME_EXTENSION_ID: string;
|
CHROME_EXTENSION_ID: string;
|
||||||
@ -610,7 +618,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.Logging,
|
group: ConfigVariablesGroup.Logging,
|
||||||
description: 'Enable or disable buffering for logs before sending',
|
description: 'Enable or disable buffering for logs before sending',
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
LOGGER_IS_BUFFER_ENABLED = true;
|
LOGGER_IS_BUFFER_ENABLED = true;
|
||||||
@ -618,7 +626,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.Logging,
|
group: ConfigVariablesGroup.Logging,
|
||||||
description: 'Driver used for handling exceptions (Console or Sentry)',
|
description: 'Driver used for handling exceptions (Console or Sentry)',
|
||||||
type: 'enum',
|
type: ConfigVariableType.ENUM,
|
||||||
options: Object.values(ExceptionHandlerDriver),
|
options: Object.values(ExceptionHandlerDriver),
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -628,8 +636,8 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.Logging,
|
group: ConfigVariablesGroup.Logging,
|
||||||
description: 'Levels of logging to be captured',
|
description: 'Levels of logging to be captured',
|
||||||
type: 'array',
|
type: ConfigVariableType.ARRAY,
|
||||||
options: ['log', 'error', 'warn'],
|
options: ['log', 'error', 'warn', 'debug', 'verbose'],
|
||||||
})
|
})
|
||||||
@CastToLogLevelArray()
|
@CastToLogLevelArray()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -638,7 +646,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.Metering,
|
group: ConfigVariablesGroup.Metering,
|
||||||
description: 'Driver used for collect metrics (OpenTelemetry or Console)',
|
description: 'Driver used for collect metrics (OpenTelemetry or Console)',
|
||||||
type: 'array',
|
type: ConfigVariableType.ARRAY,
|
||||||
options: ['OpenTelemetry', 'Console'],
|
options: ['OpenTelemetry', 'Console'],
|
||||||
})
|
})
|
||||||
@CastToMeterDriverArray()
|
@CastToMeterDriverArray()
|
||||||
@ -648,7 +656,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.Metering,
|
group: ConfigVariablesGroup.Metering,
|
||||||
description: 'Endpoint URL for the OpenTelemetry collector',
|
description: 'Endpoint URL for the OpenTelemetry collector',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
OTLP_COLLECTOR_ENDPOINT_URL: string;
|
OTLP_COLLECTOR_ENDPOINT_URL: string;
|
||||||
@ -656,7 +664,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ExceptionHandler,
|
group: ConfigVariablesGroup.ExceptionHandler,
|
||||||
description: 'Driver used for logging (only console for now)',
|
description: 'Driver used for logging (only console for now)',
|
||||||
type: 'enum',
|
type: ConfigVariableType.ENUM,
|
||||||
options: Object.values(LoggerDriverType),
|
options: Object.values(LoggerDriverType),
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -665,7 +673,8 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ExceptionHandler,
|
group: ConfigVariablesGroup.ExceptionHandler,
|
||||||
description: 'Data Source Name (DSN) for Sentry logging',
|
description: 'Data Source Name (DSN) for Sentry logging',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
|
isSensitive: true,
|
||||||
})
|
})
|
||||||
@ValidateIf(
|
@ValidateIf(
|
||||||
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry,
|
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry,
|
||||||
@ -675,7 +684,8 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ExceptionHandler,
|
group: ConfigVariablesGroup.ExceptionHandler,
|
||||||
description: 'Front-end DSN for Sentry logging',
|
description: 'Front-end DSN for Sentry logging',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
|
isSensitive: true,
|
||||||
})
|
})
|
||||||
@ValidateIf(
|
@ValidateIf(
|
||||||
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry,
|
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry,
|
||||||
@ -684,7 +694,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ExceptionHandler,
|
group: ConfigVariablesGroup.ExceptionHandler,
|
||||||
description: 'Environment name for Sentry logging',
|
description: 'Environment name for Sentry logging',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf(
|
@ValidateIf(
|
||||||
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry,
|
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry,
|
||||||
@ -695,7 +705,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.SupportChatConfig,
|
group: ConfigVariablesGroup.SupportChatConfig,
|
||||||
description: 'Driver used for support chat integration',
|
description: 'Driver used for support chat integration',
|
||||||
type: 'enum',
|
type: ConfigVariableType.ENUM,
|
||||||
options: Object.values(SupportDriver),
|
options: Object.values(SupportDriver),
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -705,7 +715,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.SupportChatConfig,
|
group: ConfigVariablesGroup.SupportChatConfig,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'Chat ID for the support front integration',
|
description: 'Chat ID for the support front integration',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front)
|
@ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front)
|
||||||
SUPPORT_FRONT_CHAT_ID: string;
|
SUPPORT_FRONT_CHAT_ID: string;
|
||||||
@ -714,7 +724,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.SupportChatConfig,
|
group: ConfigVariablesGroup.SupportChatConfig,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'HMAC key for the support front integration',
|
description: 'HMAC key for the support front integration',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front)
|
@ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front)
|
||||||
SUPPORT_FRONT_HMAC_KEY: string;
|
SUPPORT_FRONT_HMAC_KEY: string;
|
||||||
@ -723,7 +733,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.ServerConfig,
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'Database connection URL',
|
description: 'Database connection URL',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
isEnvOnly: true,
|
isEnvOnly: true,
|
||||||
})
|
})
|
||||||
@IsDefined()
|
@IsDefined()
|
||||||
@ -740,7 +750,7 @@ export class ConfigVariables {
|
|||||||
description:
|
description:
|
||||||
'Allow connections to a database with self-signed certificates',
|
'Allow connections to a database with self-signed certificates',
|
||||||
isEnvOnly: true,
|
isEnvOnly: true,
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
PG_SSL_ALLOW_SELF_SIGNED = false;
|
PG_SSL_ALLOW_SELF_SIGNED = false;
|
||||||
@ -749,7 +759,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.ServerConfig,
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
description: 'Enable configuration variables to be stored in the database',
|
description: 'Enable configuration variables to be stored in the database',
|
||||||
isEnvOnly: true,
|
isEnvOnly: true,
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
IS_CONFIG_VARIABLES_IN_DB_ENABLED = false;
|
IS_CONFIG_VARIABLES_IN_DB_ENABLED = false;
|
||||||
@ -757,7 +767,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.TokensDuration,
|
group: ConfigVariablesGroup.TokensDuration,
|
||||||
description: 'Time-to-live for cache storage in seconds',
|
description: 'Time-to-live for cache storage in seconds',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
CACHE_STORAGE_TTL: number = 3600 * 24 * 7;
|
CACHE_STORAGE_TTL: number = 3600 * 24 * 7;
|
||||||
@ -767,7 +777,7 @@ export class ConfigVariables {
|
|||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'URL for cache storage (e.g., Redis connection URL)',
|
description: 'URL for cache storage (e.g., Redis connection URL)',
|
||||||
isEnvOnly: true,
|
isEnvOnly: true,
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsUrl({
|
@IsUrl({
|
||||||
@ -780,7 +790,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ServerConfig,
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
description: 'Node environment (development, production, etc.)',
|
description: 'Node environment (development, production, etc.)',
|
||||||
type: 'enum',
|
type: ConfigVariableType.ENUM,
|
||||||
options: Object.values(NodeEnvironment),
|
options: Object.values(NodeEnvironment),
|
||||||
})
|
})
|
||||||
NODE_ENV: NodeEnvironment = NodeEnvironment.production;
|
NODE_ENV: NodeEnvironment = NodeEnvironment.production;
|
||||||
@ -788,7 +798,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ServerConfig,
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
description: 'Port for the node server',
|
description: 'Port for the node server',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -797,7 +807,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ServerConfig,
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
description: 'Base URL for the server',
|
description: 'Base URL for the server',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsUrl({ require_tld: false, require_protocol: true })
|
@IsUrl({ require_tld: false, require_protocol: true })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -808,14 +818,14 @@ export class ConfigVariables {
|
|||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'Secret key for the application',
|
description: 'Secret key for the application',
|
||||||
isEnvOnly: true,
|
isEnvOnly: true,
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
APP_SECRET: string;
|
APP_SECRET: string;
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.RateLimiting,
|
group: ConfigVariablesGroup.RateLimiting,
|
||||||
description: 'Maximum number of records affected by mutations',
|
description: 'Maximum number of records affected by mutations',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -824,7 +834,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.RateLimiting,
|
group: ConfigVariablesGroup.RateLimiting,
|
||||||
description: 'Time-to-live for API rate limiting in milliseconds',
|
description: 'Time-to-live for API rate limiting in milliseconds',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
API_RATE_LIMITING_TTL = 100;
|
API_RATE_LIMITING_TTL = 100;
|
||||||
@ -833,7 +843,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.RateLimiting,
|
group: ConfigVariablesGroup.RateLimiting,
|
||||||
description:
|
description:
|
||||||
'Maximum number of requests allowed in the rate limiting window',
|
'Maximum number of requests allowed in the rate limiting window',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
API_RATE_LIMITING_LIMIT = 500;
|
API_RATE_LIMITING_LIMIT = 500;
|
||||||
@ -841,7 +851,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.SSL,
|
group: ConfigVariablesGroup.SSL,
|
||||||
description: 'Path to the SSL key for enabling HTTPS in local development',
|
description: 'Path to the SSL key for enabling HTTPS in local development',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
SSL_KEY_PATH: string;
|
SSL_KEY_PATH: string;
|
||||||
@ -850,7 +860,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.SSL,
|
group: ConfigVariablesGroup.SSL,
|
||||||
description:
|
description:
|
||||||
'Path to the SSL certificate for enabling HTTPS in local development',
|
'Path to the SSL certificate for enabling HTTPS in local development',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
SSL_CERT_PATH: string;
|
SSL_CERT_PATH: string;
|
||||||
@ -859,7 +869,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.CloudflareConfig,
|
group: ConfigVariablesGroup.CloudflareConfig,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'API key for Cloudflare integration',
|
description: 'API key for Cloudflare integration',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.CLOUDFLARE_ZONE_ID)
|
@ValidateIf((env) => env.CLOUDFLARE_ZONE_ID)
|
||||||
CLOUDFLARE_API_KEY: string;
|
CLOUDFLARE_API_KEY: string;
|
||||||
@ -867,7 +877,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.CloudflareConfig,
|
group: ConfigVariablesGroup.CloudflareConfig,
|
||||||
description: 'Zone ID for Cloudflare integration',
|
description: 'Zone ID for Cloudflare integration',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@ValidateIf((env) => env.CLOUDFLARE_API_KEY)
|
@ValidateIf((env) => env.CLOUDFLARE_API_KEY)
|
||||||
CLOUDFLARE_ZONE_ID: string;
|
CLOUDFLARE_ZONE_ID: string;
|
||||||
@ -875,7 +885,8 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.Other,
|
group: ConfigVariablesGroup.Other,
|
||||||
description: 'Random string to validate queries from Cloudflare',
|
description: 'Random string to validate queries from Cloudflare',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
|
isSensitive: true,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
CLOUDFLARE_WEBHOOK_SECRET: string;
|
CLOUDFLARE_WEBHOOK_SECRET: string;
|
||||||
@ -883,7 +894,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.LLM,
|
group: ConfigVariablesGroup.LLM,
|
||||||
description: 'Driver for the LLM chat model',
|
description: 'Driver for the LLM chat model',
|
||||||
type: 'enum',
|
type: ConfigVariableType.ENUM,
|
||||||
options: Object.values(LLMChatModelDriver),
|
options: Object.values(LLMChatModelDriver),
|
||||||
})
|
})
|
||||||
LLM_CHAT_MODEL_DRIVER: LLMChatModelDriver;
|
LLM_CHAT_MODEL_DRIVER: LLMChatModelDriver;
|
||||||
@ -892,7 +903,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.LLM,
|
group: ConfigVariablesGroup.LLM,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'API key for OpenAI integration',
|
description: 'API key for OpenAI integration',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
OPENAI_API_KEY: string;
|
OPENAI_API_KEY: string;
|
||||||
|
|
||||||
@ -900,21 +911,21 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.LLM,
|
group: ConfigVariablesGroup.LLM,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'Secret key for Langfuse integration',
|
description: 'Secret key for Langfuse integration',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
LANGFUSE_SECRET_KEY: string;
|
LANGFUSE_SECRET_KEY: string;
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.LLM,
|
group: ConfigVariablesGroup.LLM,
|
||||||
description: 'Public key for Langfuse integration',
|
description: 'Public key for Langfuse integration',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
LANGFUSE_PUBLIC_KEY: string;
|
LANGFUSE_PUBLIC_KEY: string;
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.LLM,
|
group: ConfigVariablesGroup.LLM,
|
||||||
description: 'Driver for LLM tracing',
|
description: 'Driver for LLM tracing',
|
||||||
type: 'enum',
|
type: ConfigVariableType.ENUM,
|
||||||
options: Object.values(LLMTracingDriver),
|
options: Object.values(LLMTracingDriver),
|
||||||
})
|
})
|
||||||
LLM_TRACING_DRIVER: LLMTracingDriver = LLMTracingDriver.Console;
|
LLM_TRACING_DRIVER: LLMTracingDriver = LLMTracingDriver.Console;
|
||||||
@ -922,7 +933,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ServerConfig,
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
description: 'Enable or disable multi-workspace support',
|
description: 'Enable or disable multi-workspace support',
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
IS_MULTIWORKSPACE_ENABLED = false;
|
IS_MULTIWORKSPACE_ENABLED = false;
|
||||||
@ -931,7 +942,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.Other,
|
group: ConfigVariablesGroup.Other,
|
||||||
description:
|
description:
|
||||||
'Number of inactive days before sending a deletion warning for workspaces. Used in the workspace deletion cron job to determine when to send warning emails.',
|
'Number of inactive days before sending a deletion warning for workspaces. Used in the workspace deletion cron job to determine when to send warning emails.',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
@IsStrictlyLowerThan('WORKSPACE_INACTIVE_DAYS_BEFORE_SOFT_DELETION', {
|
@IsStrictlyLowerThan('WORKSPACE_INACTIVE_DAYS_BEFORE_SOFT_DELETION', {
|
||||||
@ -943,7 +954,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.Other,
|
group: ConfigVariablesGroup.Other,
|
||||||
description: 'Number of inactive days before soft deleting workspaces',
|
description: 'Number of inactive days before soft deleting workspaces',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
@IsStrictlyLowerThan('WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION', {
|
@IsStrictlyLowerThan('WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION', {
|
||||||
@ -955,7 +966,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.Other,
|
group: ConfigVariablesGroup.Other,
|
||||||
description: 'Number of inactive days before deleting workspaces',
|
description: 'Number of inactive days before deleting workspaces',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION = 21;
|
WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION = 21;
|
||||||
@ -964,7 +975,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.Other,
|
group: ConfigVariablesGroup.Other,
|
||||||
description:
|
description:
|
||||||
'Maximum number of workspaces that can be deleted in a single execution',
|
'Maximum number of workspaces that can be deleted in a single execution',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
@ValidateIf((env) => env.MAX_NUMBER_OF_WORKSPACES_DELETED_PER_EXECUTION > 0)
|
@ValidateIf((env) => env.MAX_NUMBER_OF_WORKSPACES_DELETED_PER_EXECUTION > 0)
|
||||||
@ -973,7 +984,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.RateLimiting,
|
group: ConfigVariablesGroup.RateLimiting,
|
||||||
description: 'Throttle limit for workflow execution',
|
description: 'Throttle limit for workflow execution',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
WORKFLOW_EXEC_THROTTLE_LIMIT = 500;
|
WORKFLOW_EXEC_THROTTLE_LIMIT = 500;
|
||||||
@ -981,7 +992,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.RateLimiting,
|
group: ConfigVariablesGroup.RateLimiting,
|
||||||
description: 'Time-to-live for workflow execution throttle in milliseconds',
|
description: 'Time-to-live for workflow execution throttle in milliseconds',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
WORKFLOW_EXEC_THROTTLE_TTL = 1000;
|
WORKFLOW_EXEC_THROTTLE_TTL = 1000;
|
||||||
@ -989,7 +1000,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.CaptchaConfig,
|
group: ConfigVariablesGroup.CaptchaConfig,
|
||||||
description: 'Driver for captcha integration',
|
description: 'Driver for captcha integration',
|
||||||
type: 'enum',
|
type: ConfigVariableType.ENUM,
|
||||||
options: Object.values(CaptchaDriverType),
|
options: Object.values(CaptchaDriverType),
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -999,7 +1010,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.CaptchaConfig,
|
group: ConfigVariablesGroup.CaptchaConfig,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'Site key for captcha integration',
|
description: 'Site key for captcha integration',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
CAPTCHA_SITE_KEY?: string;
|
CAPTCHA_SITE_KEY?: string;
|
||||||
@ -1008,7 +1019,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.CaptchaConfig,
|
group: ConfigVariablesGroup.CaptchaConfig,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'Secret key for captcha integration',
|
description: 'Secret key for captcha integration',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
CAPTCHA_SECRET_KEY?: string;
|
CAPTCHA_SECRET_KEY?: string;
|
||||||
@ -1017,7 +1028,7 @@ export class ConfigVariables {
|
|||||||
group: ConfigVariablesGroup.ServerConfig,
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
description: 'License key for the Enterprise version',
|
description: 'License key for the Enterprise version',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
ENTERPRISE_KEY: string;
|
ENTERPRISE_KEY: string;
|
||||||
@ -1025,7 +1036,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.Other,
|
group: ConfigVariablesGroup.Other,
|
||||||
description: 'Health monitoring time window in minutes',
|
description: 'Health monitoring time window in minutes',
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
})
|
})
|
||||||
@CastToPositiveNumber()
|
@CastToPositiveNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -1034,7 +1045,7 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.Other,
|
group: ConfigVariablesGroup.Other,
|
||||||
description: 'Enable or disable the attachment preview feature',
|
description: 'Enable or disable the attachment preview feature',
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
IS_ATTACHMENT_PREVIEW_ENABLED = true;
|
IS_ATTACHMENT_PREVIEW_ENABLED = true;
|
||||||
@ -1042,7 +1053,8 @@ export class ConfigVariables {
|
|||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ServerConfig,
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
description: 'Twenty server version',
|
description: 'Twenty server version',
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
|
isEnvOnly: true,
|
||||||
})
|
})
|
||||||
@IsOptionalOrEmptyString()
|
@IsOptionalOrEmptyString()
|
||||||
@IsTwentySemVer()
|
@IsTwentySemVer()
|
||||||
@ -1076,7 +1088,10 @@ export const validate = (config: Record<string, unknown>): ConfigVariables => {
|
|||||||
|
|
||||||
if (validationErrors.length > 0) {
|
if (validationErrors.length > 0) {
|
||||||
logValidatonErrors(validationErrors, 'error');
|
logValidatonErrors(validationErrors, 'error');
|
||||||
throw new Error('Config variables validation failed');
|
throw new ConfigVariableException(
|
||||||
|
'Config variables validation failed',
|
||||||
|
ConfigVariableExceptionCode.VALIDATION_FAILED,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return validatedConfig;
|
return validatedConfig;
|
||||||
|
|||||||
@ -4,11 +4,11 @@ import { Test, TestingModule } from '@nestjs/testing';
|
|||||||
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
||||||
import { CONFIG_VARIABLES_INSTANCE_TOKEN } from 'src/engine/core-modules/twenty-config/constants/config-variables-instance-tokens.constants';
|
import { CONFIG_VARIABLES_INSTANCE_TOKEN } from 'src/engine/core-modules/twenty-config/constants/config-variables-instance-tokens.constants';
|
||||||
import { ConfigValueConverterService } from 'src/engine/core-modules/twenty-config/conversion/config-value-converter.service';
|
import { ConfigValueConverterService } from 'src/engine/core-modules/twenty-config/conversion/config-value-converter.service';
|
||||||
|
import { ConfigVariableType } from 'src/engine/core-modules/twenty-config/enums/config-variable-type.enum';
|
||||||
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
||||||
import { configTransformers } from 'src/engine/core-modules/twenty-config/utils/config-transformers.util';
|
import { configTransformers } from 'src/engine/core-modules/twenty-config/utils/config-transformers.util';
|
||||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||||
|
|
||||||
// Mock configTransformers for type validation tests
|
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'src/engine/core-modules/twenty-config/utils/config-transformers.util',
|
'src/engine/core-modules/twenty-config/utils/config-transformers.util',
|
||||||
() => {
|
() => {
|
||||||
@ -19,7 +19,6 @@ jest.mock(
|
|||||||
return {
|
return {
|
||||||
configTransformers: {
|
configTransformers: {
|
||||||
...originalModule.configTransformers,
|
...originalModule.configTransformers,
|
||||||
// These mocked versions can be overridden in specific tests
|
|
||||||
_mockedBoolean: jest.fn(),
|
_mockedBoolean: jest.fn(),
|
||||||
_mockedNumber: jest.fn(),
|
_mockedNumber: jest.fn(),
|
||||||
_mockedString: jest.fn(),
|
_mockedString: jest.fn(),
|
||||||
@ -56,7 +55,7 @@ describe('ConfigValueConverterService', () => {
|
|||||||
// Mock the metadata
|
// Mock the metadata
|
||||||
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
||||||
AUTH_PASSWORD_ENABLED: {
|
AUTH_PASSWORD_ENABLED: {
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
group: ConfigVariablesGroup.Other,
|
group: ConfigVariablesGroup.Other,
|
||||||
description: 'Enable or disable password authentication for users',
|
description: 'Enable or disable password authentication for users',
|
||||||
},
|
},
|
||||||
@ -116,7 +115,7 @@ describe('ConfigValueConverterService', () => {
|
|||||||
it('should convert string to number based on metadata', () => {
|
it('should convert string to number based on metadata', () => {
|
||||||
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
||||||
NODE_PORT: {
|
NODE_PORT: {
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
group: ConfigVariablesGroup.ServerConfig,
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
description: 'Port for the node server',
|
description: 'Port for the node server',
|
||||||
},
|
},
|
||||||
@ -146,7 +145,7 @@ describe('ConfigValueConverterService', () => {
|
|||||||
it('should convert string to array based on metadata', () => {
|
it('should convert string to array based on metadata', () => {
|
||||||
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
||||||
LOG_LEVELS: {
|
LOG_LEVELS: {
|
||||||
type: 'array',
|
type: ConfigVariableType.ARRAY,
|
||||||
group: ConfigVariablesGroup.Logging,
|
group: ConfigVariablesGroup.Logging,
|
||||||
description: 'Levels of logging to be captured',
|
description: 'Levels of logging to be captured',
|
||||||
},
|
},
|
||||||
@ -161,7 +160,7 @@ describe('ConfigValueConverterService', () => {
|
|||||||
|
|
||||||
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
||||||
LOG_LEVELS: {
|
LOG_LEVELS: {
|
||||||
type: 'array',
|
type: ConfigVariableType.ARRAY,
|
||||||
group: ConfigVariablesGroup.Logging,
|
group: ConfigVariablesGroup.Logging,
|
||||||
description: 'Levels of logging to be captured',
|
description: 'Levels of logging to be captured',
|
||||||
},
|
},
|
||||||
@ -188,7 +187,7 @@ describe('ConfigValueConverterService', () => {
|
|||||||
it('should handle various input types', () => {
|
it('should handle various input types', () => {
|
||||||
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
||||||
AUTH_PASSWORD_ENABLED: {
|
AUTH_PASSWORD_ENABLED: {
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
group: ConfigVariablesGroup.Other,
|
group: ConfigVariablesGroup.Other,
|
||||||
description: 'Enable or disable password authentication for users',
|
description: 'Enable or disable password authentication for users',
|
||||||
},
|
},
|
||||||
@ -202,7 +201,7 @@ describe('ConfigValueConverterService', () => {
|
|||||||
|
|
||||||
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
||||||
NODE_PORT: {
|
NODE_PORT: {
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
group: ConfigVariablesGroup.ServerConfig,
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
description: 'Port for the node server',
|
description: 'Port for the node server',
|
||||||
},
|
},
|
||||||
@ -216,7 +215,7 @@ describe('ConfigValueConverterService', () => {
|
|||||||
|
|
||||||
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
||||||
LOG_LEVELS: {
|
LOG_LEVELS: {
|
||||||
type: 'array',
|
type: ConfigVariableType.ARRAY,
|
||||||
group: ConfigVariablesGroup.Logging,
|
group: ConfigVariablesGroup.Logging,
|
||||||
description: 'Levels of logging to be captured',
|
description: 'Levels of logging to be captured',
|
||||||
},
|
},
|
||||||
@ -259,7 +258,7 @@ describe('ConfigValueConverterService', () => {
|
|||||||
it('should throw error if boolean converter returns non-boolean', () => {
|
it('should throw error if boolean converter returns non-boolean', () => {
|
||||||
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
||||||
AUTH_PASSWORD_ENABLED: {
|
AUTH_PASSWORD_ENABLED: {
|
||||||
type: 'boolean',
|
type: ConfigVariableType.BOOLEAN,
|
||||||
group: ConfigVariablesGroup.Other,
|
group: ConfigVariablesGroup.Other,
|
||||||
description: 'Test boolean',
|
description: 'Test boolean',
|
||||||
},
|
},
|
||||||
@ -284,7 +283,7 @@ describe('ConfigValueConverterService', () => {
|
|||||||
it('should throw error if number converter returns non-number', () => {
|
it('should throw error if number converter returns non-number', () => {
|
||||||
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
||||||
NODE_PORT: {
|
NODE_PORT: {
|
||||||
type: 'number',
|
type: ConfigVariableType.NUMBER,
|
||||||
group: ConfigVariablesGroup.ServerConfig,
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
description: 'Test number',
|
description: 'Test number',
|
||||||
},
|
},
|
||||||
@ -309,7 +308,7 @@ describe('ConfigValueConverterService', () => {
|
|||||||
it('should throw error if string converter returns non-string', () => {
|
it('should throw error if string converter returns non-string', () => {
|
||||||
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
||||||
EMAIL_FROM_ADDRESS: {
|
EMAIL_FROM_ADDRESS: {
|
||||||
type: 'string',
|
type: ConfigVariableType.STRING,
|
||||||
group: ConfigVariablesGroup.EmailSettings,
|
group: ConfigVariablesGroup.EmailSettings,
|
||||||
description: 'Test string',
|
description: 'Test string',
|
||||||
},
|
},
|
||||||
@ -332,7 +331,7 @@ describe('ConfigValueConverterService', () => {
|
|||||||
it('should throw error if array conversion produces non-array', () => {
|
it('should throw error if array conversion produces non-array', () => {
|
||||||
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
||||||
LOG_LEVELS: {
|
LOG_LEVELS: {
|
||||||
type: 'array',
|
type: ConfigVariableType.ARRAY,
|
||||||
group: ConfigVariablesGroup.Logging,
|
group: ConfigVariablesGroup.Logging,
|
||||||
description: 'Test array',
|
description: 'Test array',
|
||||||
},
|
},
|
||||||
@ -358,7 +357,7 @@ describe('ConfigValueConverterService', () => {
|
|||||||
it('should handle array with option validation', () => {
|
it('should handle array with option validation', () => {
|
||||||
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
||||||
LOG_LEVELS: {
|
LOG_LEVELS: {
|
||||||
type: 'array',
|
type: ConfigVariableType.ARRAY,
|
||||||
group: ConfigVariablesGroup.Logging,
|
group: ConfigVariablesGroup.Logging,
|
||||||
description: 'Test array with options',
|
description: 'Test array with options',
|
||||||
options: ['log', 'error', 'warn', 'debug'],
|
options: ['log', 'error', 'warn', 'debug'],
|
||||||
@ -374,7 +373,7 @@ describe('ConfigValueConverterService', () => {
|
|||||||
|
|
||||||
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
||||||
LOG_LEVELS: {
|
LOG_LEVELS: {
|
||||||
type: 'array',
|
type: ConfigVariableType.ARRAY,
|
||||||
group: ConfigVariablesGroup.Logging,
|
group: ConfigVariablesGroup.Logging,
|
||||||
description: 'Test array with options',
|
description: 'Test array with options',
|
||||||
options: ['log', 'error', 'warn', 'debug'],
|
options: ['log', 'error', 'warn', 'debug'],
|
||||||
@ -392,7 +391,7 @@ describe('ConfigValueConverterService', () => {
|
|||||||
it('should properly handle enum with options', () => {
|
it('should properly handle enum with options', () => {
|
||||||
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
||||||
LOG_LEVEL: {
|
LOG_LEVEL: {
|
||||||
type: 'enum',
|
type: ConfigVariableType.ENUM,
|
||||||
group: ConfigVariablesGroup.Logging,
|
group: ConfigVariablesGroup.Logging,
|
||||||
description: 'Test enum',
|
description: 'Test enum',
|
||||||
options: ['log', 'error', 'warn', 'debug'],
|
options: ['log', 'error', 'warn', 'debug'],
|
||||||
@ -408,7 +407,7 @@ describe('ConfigValueConverterService', () => {
|
|||||||
|
|
||||||
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValueOnce({
|
||||||
LOG_LEVEL: {
|
LOG_LEVEL: {
|
||||||
type: 'enum',
|
type: ConfigVariableType.ENUM,
|
||||||
group: ConfigVariablesGroup.Logging,
|
group: ConfigVariablesGroup.Logging,
|
||||||
description: 'Test enum',
|
description: 'Test enum',
|
||||||
options: ['log', 'error', 'warn', 'debug'],
|
options: ['log', 'error', 'warn', 'debug'],
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
|
|||||||
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
||||||
import { CONFIG_VARIABLES_INSTANCE_TOKEN } from 'src/engine/core-modules/twenty-config/constants/config-variables-instance-tokens.constants';
|
import { CONFIG_VARIABLES_INSTANCE_TOKEN } from 'src/engine/core-modules/twenty-config/constants/config-variables-instance-tokens.constants';
|
||||||
import { ConfigVariablesMetadataMap } from 'src/engine/core-modules/twenty-config/decorators/config-variables-metadata.decorator';
|
import { ConfigVariablesMetadataMap } from 'src/engine/core-modules/twenty-config/decorators/config-variables-metadata.decorator';
|
||||||
|
import { ConfigVariableType } from 'src/engine/core-modules/twenty-config/enums/config-variable-type.enum';
|
||||||
import { ConfigVariableOptions } from 'src/engine/core-modules/twenty-config/types/config-variable-options.type';
|
import { ConfigVariableOptions } from 'src/engine/core-modules/twenty-config/types/config-variable-options.type';
|
||||||
import { ConfigVariableType } from 'src/engine/core-modules/twenty-config/types/config-variable-type.type';
|
|
||||||
import { configTransformers } from 'src/engine/core-modules/twenty-config/utils/config-transformers.util';
|
import { configTransformers } from 'src/engine/core-modules/twenty-config/utils/config-transformers.util';
|
||||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ export class ConfigValueConverterService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
switch (configType) {
|
switch (configType) {
|
||||||
case 'boolean': {
|
case ConfigVariableType.BOOLEAN: {
|
||||||
const result = configTransformers.boolean(dbValue);
|
const result = configTransformers.boolean(dbValue);
|
||||||
|
|
||||||
if (result !== undefined && typeof result !== 'boolean') {
|
if (result !== undefined && typeof result !== 'boolean') {
|
||||||
@ -43,7 +43,7 @@ export class ConfigValueConverterService {
|
|||||||
return result as ConfigVariables[T];
|
return result as ConfigVariables[T];
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'number': {
|
case ConfigVariableType.NUMBER: {
|
||||||
const result = configTransformers.number(dbValue);
|
const result = configTransformers.number(dbValue);
|
||||||
|
|
||||||
if (result !== undefined && typeof result !== 'number') {
|
if (result !== undefined && typeof result !== 'number') {
|
||||||
@ -55,7 +55,7 @@ export class ConfigValueConverterService {
|
|||||||
return result as ConfigVariables[T];
|
return result as ConfigVariables[T];
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'string': {
|
case ConfigVariableType.STRING: {
|
||||||
const result = configTransformers.string(dbValue);
|
const result = configTransformers.string(dbValue);
|
||||||
|
|
||||||
if (result !== undefined && typeof result !== 'string') {
|
if (result !== undefined && typeof result !== 'string') {
|
||||||
@ -67,7 +67,7 @@ export class ConfigValueConverterService {
|
|||||||
return result as ConfigVariables[T];
|
return result as ConfigVariables[T];
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'array': {
|
case ConfigVariableType.ARRAY: {
|
||||||
const result = this.convertToArray(dbValue, options);
|
const result = this.convertToArray(dbValue, options);
|
||||||
|
|
||||||
if (result !== undefined && !Array.isArray(result)) {
|
if (result !== undefined && !Array.isArray(result)) {
|
||||||
@ -79,7 +79,7 @@ export class ConfigValueConverterService {
|
|||||||
return result as ConfigVariables[T];
|
return result as ConfigVariables[T];
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'enum': {
|
case ConfigVariableType.ENUM: {
|
||||||
const result = this.convertToEnum(dbValue, options);
|
const result = this.convertToEnum(dbValue, options);
|
||||||
|
|
||||||
return result as ConfigVariables[T];
|
return result as ConfigVariables[T];
|
||||||
@ -204,10 +204,10 @@ export class ConfigValueConverterService {
|
|||||||
): ConfigVariableType {
|
): ConfigVariableType {
|
||||||
const defaultValue = this.configVariables[key];
|
const defaultValue = this.configVariables[key];
|
||||||
|
|
||||||
if (typeof defaultValue === 'boolean') return 'boolean';
|
if (typeof defaultValue === 'boolean') return ConfigVariableType.BOOLEAN;
|
||||||
if (typeof defaultValue === 'number') return 'number';
|
if (typeof defaultValue === 'number') return ConfigVariableType.NUMBER;
|
||||||
if (Array.isArray(defaultValue)) return 'array';
|
if (Array.isArray(defaultValue)) return ConfigVariableType.ARRAY;
|
||||||
|
|
||||||
return 'string';
|
return ConfigVariableType.STRING;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import {
|
|||||||
ValidationOptions,
|
ValidationOptions,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
|
import { ConfigVariableType } from 'src/engine/core-modules/twenty-config/enums/config-variable-type.enum';
|
||||||
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
||||||
import { ConfigVariableOptions } from 'src/engine/core-modules/twenty-config/types/config-variable-options.type';
|
import { ConfigVariableOptions } from 'src/engine/core-modules/twenty-config/types/config-variable-options.type';
|
||||||
import { ConfigVariableType } from 'src/engine/core-modules/twenty-config/types/config-variable-type.type';
|
|
||||||
import { applyBasicValidators } from 'src/engine/core-modules/twenty-config/utils/apply-basic-validators.util';
|
import { applyBasicValidators } from 'src/engine/core-modules/twenty-config/utils/apply-basic-validators.util';
|
||||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ export interface ConfigVariablesMetadataOptions {
|
|||||||
description: string;
|
description: string;
|
||||||
isSensitive?: boolean;
|
isSensitive?: boolean;
|
||||||
isEnvOnly?: boolean;
|
isEnvOnly?: boolean;
|
||||||
type?: ConfigVariableType;
|
type: ConfigVariableType;
|
||||||
options?: ConfigVariableOptions;
|
options?: ConfigVariableOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,14 +51,12 @@ export function ConfigVariablesMetadata(
|
|||||||
IsOptional()(target, propertyKey);
|
IsOptional()(target, propertyKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.type) {
|
applyBasicValidators(
|
||||||
applyBasicValidators(
|
options.type,
|
||||||
options.type,
|
target,
|
||||||
target,
|
propertyKey.toString(),
|
||||||
propertyKey.toString(),
|
options.options,
|
||||||
options.options,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
registerDecorator({
|
registerDecorator({
|
||||||
name: propertyKey.toString(),
|
name: propertyKey.toString(),
|
||||||
|
|||||||
@ -19,12 +19,10 @@ const CONFIG_ENV_ONLY_KEY = 'ENV_ONLY_VAR';
|
|||||||
const CONFIG_PORT_KEY = 'NODE_PORT';
|
const CONFIG_PORT_KEY = 'NODE_PORT';
|
||||||
|
|
||||||
class TestDatabaseConfigDriver extends DatabaseConfigDriver {
|
class TestDatabaseConfigDriver extends DatabaseConfigDriver {
|
||||||
// Expose the protected/private property for testing
|
|
||||||
public get testAllPossibleConfigKeys(): Array<keyof ConfigVariables> {
|
public get testAllPossibleConfigKeys(): Array<keyof ConfigVariables> {
|
||||||
return this['allPossibleConfigKeys'];
|
return this['allPossibleConfigKeys'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override Object.keys usage in constructor with our test keys
|
|
||||||
constructor(
|
constructor(
|
||||||
configCache: ConfigCacheService,
|
configCache: ConfigCacheService,
|
||||||
configStorage: ConfigStorageService,
|
configStorage: ConfigStorageService,
|
||||||
@ -213,51 +211,6 @@ describe('DatabaseConfigDriver', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fetchAndCacheConfigVariable', () => {
|
|
||||||
it('should refresh config variable from storage', async () => {
|
|
||||||
const value = true;
|
|
||||||
|
|
||||||
jest.spyOn(configStorage, 'get').mockResolvedValue(value);
|
|
||||||
|
|
||||||
await driver.fetchAndCacheConfigVariable(CONFIG_PASSWORD_KEY);
|
|
||||||
|
|
||||||
expect(configStorage.get).toHaveBeenCalledWith(CONFIG_PASSWORD_KEY);
|
|
||||||
expect(configCache.set).toHaveBeenCalledWith(CONFIG_PASSWORD_KEY, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should mark key as missing when value is undefined', async () => {
|
|
||||||
jest.spyOn(configStorage, 'get').mockResolvedValue(undefined);
|
|
||||||
|
|
||||||
await driver.fetchAndCacheConfigVariable(CONFIG_PASSWORD_KEY);
|
|
||||||
|
|
||||||
expect(configStorage.get).toHaveBeenCalledWith(CONFIG_PASSWORD_KEY);
|
|
||||||
expect(configCache.markKeyAsMissing).toHaveBeenCalledWith(
|
|
||||||
CONFIG_PASSWORD_KEY,
|
|
||||||
);
|
|
||||||
expect(configCache.set).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should mark key as missing when storage fetch fails', async () => {
|
|
||||||
const error = new Error('Storage error');
|
|
||||||
|
|
||||||
jest.spyOn(configStorage, 'get').mockRejectedValue(error);
|
|
||||||
const loggerSpy = jest
|
|
||||||
.spyOn(driver['logger'], 'error')
|
|
||||||
.mockImplementation();
|
|
||||||
|
|
||||||
await driver.fetchAndCacheConfigVariable(CONFIG_PASSWORD_KEY);
|
|
||||||
|
|
||||||
expect(configStorage.get).toHaveBeenCalledWith(CONFIG_PASSWORD_KEY);
|
|
||||||
expect(configCache.markKeyAsMissing).toHaveBeenCalledWith(
|
|
||||||
CONFIG_PASSWORD_KEY,
|
|
||||||
);
|
|
||||||
expect(loggerSpy).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining('Failed to fetch config'),
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('cache operations', () => {
|
describe('cache operations', () => {
|
||||||
it('should return cache info', () => {
|
it('should return cache info', () => {
|
||||||
const cacheInfo = {
|
const cacheInfo = {
|
||||||
|
|||||||
@ -56,6 +56,18 @@ export class DatabaseConfigDriver
|
|||||||
return this.configCache.get(key);
|
return this.configCache.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async set<T extends keyof ConfigVariables>(
|
||||||
|
key: T,
|
||||||
|
value: ConfigVariables[T],
|
||||||
|
): Promise<void> {
|
||||||
|
if (isEnvOnlyConfigVar(key)) {
|
||||||
|
throw new Error(`Cannot set environment-only variable: ${key as string}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.configStorage.set(key, value);
|
||||||
|
this.configCache.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
async update<T extends keyof ConfigVariables>(
|
async update<T extends keyof ConfigVariables>(
|
||||||
key: T,
|
key: T,
|
||||||
value: ConfigVariables[T],
|
value: ConfigVariables[T],
|
||||||
@ -66,43 +78,8 @@ export class DatabaseConfigDriver
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await this.configStorage.set(key, value);
|
||||||
await this.configStorage.set(key, value);
|
this.configCache.set(key, value);
|
||||||
this.configCache.set(key, value);
|
|
||||||
this.logger.debug(
|
|
||||||
`[UPDATE] Config variable ${key as string} updated successfully`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(
|
|
||||||
`[UPDATE] Failed to update config variable ${key as string}`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchAndCacheConfigVariable(key: keyof ConfigVariables): Promise<void> {
|
|
||||||
try {
|
|
||||||
const value = await this.configStorage.get(key);
|
|
||||||
|
|
||||||
if (value !== undefined) {
|
|
||||||
this.configCache.set(key, value);
|
|
||||||
this.logger.debug(
|
|
||||||
`[FETCH] Config variable ${key as string} loaded from database`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.configCache.markKeyAsMissing(key);
|
|
||||||
this.logger.debug(
|
|
||||||
`[FETCH] Config variable ${key as string} not found in database, marked as missing`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(
|
|
||||||
`[FETCH] Failed to fetch config variable ${key as string} from database`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
this.configCache.markKeyAsMissing(key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCacheInfo(): {
|
getCacheInfo(): {
|
||||||
@ -114,39 +91,29 @@ export class DatabaseConfigDriver
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async loadAllConfigVarsFromDb(): Promise<number> {
|
private async loadAllConfigVarsFromDb(): Promise<number> {
|
||||||
try {
|
const configVars = await this.configStorage.loadAll();
|
||||||
this.logger.debug('[LOAD] Fetching all config variables from database');
|
|
||||||
const configVars = await this.configStorage.loadAll();
|
|
||||||
|
|
||||||
this.logger.debug(
|
for (const [key, value] of configVars.entries()) {
|
||||||
`[LOAD] Processing ${this.allPossibleConfigKeys.length} possible config variables`,
|
this.configCache.set(key, value);
|
||||||
);
|
|
||||||
|
|
||||||
for (const [key, value] of configVars.entries()) {
|
|
||||||
this.configCache.set(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key of this.allPossibleConfigKeys) {
|
|
||||||
if (!configVars.has(key)) {
|
|
||||||
this.configCache.markKeyAsMissing(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const missingKeysCount =
|
|
||||||
this.allPossibleConfigKeys.length - configVars.size;
|
|
||||||
|
|
||||||
this.logger.debug(
|
|
||||||
`[LOAD] Cached ${configVars.size} config variables, marked ${missingKeysCount} keys as missing`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return configVars.size;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(
|
|
||||||
'[LOAD] Failed to load config variables from database',
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const key of this.allPossibleConfigKeys) {
|
||||||
|
if (!configVars.has(key)) {
|
||||||
|
this.configCache.markKeyAsMissing(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return configVars.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(key: keyof ConfigVariables): Promise<void> {
|
||||||
|
if (isEnvOnlyConfigVar(key)) {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot delete environment-only variable: ${key as string}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this.configStorage.delete(key);
|
||||||
|
this.configCache.markKeyAsMissing(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -157,16 +124,8 @@ export class DatabaseConfigDriver
|
|||||||
@Cron(CONFIG_VARIABLES_REFRESH_CRON_INTERVAL)
|
@Cron(CONFIG_VARIABLES_REFRESH_CRON_INTERVAL)
|
||||||
async refreshAllCache(): Promise<void> {
|
async refreshAllCache(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.logger.debug(
|
|
||||||
'[REFRESH] Starting scheduled refresh of config variables',
|
|
||||||
);
|
|
||||||
|
|
||||||
const dbValues = await this.configStorage.loadAll();
|
const dbValues = await this.configStorage.loadAll();
|
||||||
|
|
||||||
this.logger.debug(
|
|
||||||
`[REFRESH] Processing ${this.allPossibleConfigKeys.length} possible config variables`,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const [key, value] of dbValues.entries()) {
|
for (const [key, value] of dbValues.entries()) {
|
||||||
if (!isEnvOnlyConfigVar(key)) {
|
if (!isEnvOnlyConfigVar(key)) {
|
||||||
this.configCache.set(key, value);
|
this.configCache.set(key, value);
|
||||||
@ -178,16 +137,12 @@ export class DatabaseConfigDriver
|
|||||||
this.configCache.markKeyAsMissing(key);
|
this.configCache.markKeyAsMissing(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const missingKeysCount =
|
|
||||||
this.allPossibleConfigKeys.length - dbValues.size;
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`[REFRESH] Config variables refreshed: ${dbValues.size} values updated, ${missingKeysCount} marked as missing`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('[REFRESH] Failed to refresh config variables', error);
|
|
||||||
// Error is caught and logged but not rethrown to prevent the cron job from crashing
|
// Error is caught and logged but not rethrown to prevent the cron job from crashing
|
||||||
|
this.logger.error(
|
||||||
|
'Failed to refresh config variables from database',
|
||||||
|
error instanceof Error ? error.stack : error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-va
|
|||||||
import { CONFIG_VARIABLES_INSTANCE_TOKEN } from 'src/engine/core-modules/twenty-config/constants/config-variables-instance-tokens.constants';
|
import { CONFIG_VARIABLES_INSTANCE_TOKEN } from 'src/engine/core-modules/twenty-config/constants/config-variables-instance-tokens.constants';
|
||||||
import { ConfigValueConverterService } from 'src/engine/core-modules/twenty-config/conversion/config-value-converter.service';
|
import { ConfigValueConverterService } from 'src/engine/core-modules/twenty-config/conversion/config-value-converter.service';
|
||||||
import { DatabaseConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/database-config.driver';
|
import { DatabaseConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/database-config.driver';
|
||||||
|
import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver';
|
||||||
import { ConfigStorageService } from 'src/engine/core-modules/twenty-config/storage/config-storage.service';
|
import { ConfigStorageService } from 'src/engine/core-modules/twenty-config/storage/config-storage.service';
|
||||||
|
|
||||||
@Module({})
|
@Module({})
|
||||||
@ -24,6 +25,7 @@ export class DatabaseConfigModule {
|
|||||||
ConfigCacheService,
|
ConfigCacheService,
|
||||||
ConfigStorageService,
|
ConfigStorageService,
|
||||||
ConfigValueConverterService,
|
ConfigValueConverterService,
|
||||||
|
EnvironmentConfigDriver,
|
||||||
{
|
{
|
||||||
provide: CONFIG_VARIABLES_INSTANCE_TOKEN,
|
provide: CONFIG_VARIABLES_INSTANCE_TOKEN,
|
||||||
useValue: new ConfigVariables(),
|
useValue: new ConfigVariables(),
|
||||||
|
|||||||
@ -19,11 +19,6 @@ export interface DatabaseConfigDriverInterface {
|
|||||||
value: ConfigVariables[T],
|
value: ConfigVariables[T],
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch and cache a specific configuration from its source
|
|
||||||
*/
|
|
||||||
fetchAndCacheConfigVariable(key: keyof ConfigVariables): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refreshes all entries in the config cache
|
* Refreshes all entries in the config cache
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
export enum ConfigVariableType {
|
||||||
|
BOOLEAN = 'boolean',
|
||||||
|
NUMBER = 'number',
|
||||||
|
ARRAY = 'array',
|
||||||
|
STRING = 'string',
|
||||||
|
ENUM = 'enum',
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import { Catch, ExceptionFilter } from '@nestjs/common';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ForbiddenError,
|
||||||
|
InternalServerError,
|
||||||
|
NotFoundError,
|
||||||
|
UserInputError,
|
||||||
|
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||||
|
import {
|
||||||
|
ConfigVariableException,
|
||||||
|
ConfigVariableExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/twenty-config/twenty-config.exception';
|
||||||
|
|
||||||
|
@Catch(ConfigVariableException)
|
||||||
|
export class ConfigVariableGraphqlApiExceptionFilter
|
||||||
|
implements ExceptionFilter
|
||||||
|
{
|
||||||
|
catch(exception: ConfigVariableException) {
|
||||||
|
switch (exception.code) {
|
||||||
|
case ConfigVariableExceptionCode.VARIABLE_NOT_FOUND:
|
||||||
|
throw new NotFoundError(exception.message);
|
||||||
|
case ConfigVariableExceptionCode.ENVIRONMENT_ONLY_VARIABLE:
|
||||||
|
throw new ForbiddenError(exception.message);
|
||||||
|
case ConfigVariableExceptionCode.DATABASE_CONFIG_DISABLED:
|
||||||
|
throw new UserInputError(exception.message);
|
||||||
|
case ConfigVariableExceptionCode.INTERNAL_ERROR:
|
||||||
|
default:
|
||||||
|
throw new InternalServerError(exception.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,20 +3,31 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { DeleteResult, IsNull, Repository } from 'typeorm';
|
import { DeleteResult, IsNull, Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import * as authUtils from 'src/engine/core-modules/auth/auth.util';
|
||||||
import {
|
import {
|
||||||
KeyValuePair,
|
KeyValuePair,
|
||||||
KeyValuePairType,
|
KeyValuePairType,
|
||||||
} from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
} from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||||
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
||||||
import { ConfigValueConverterService } from 'src/engine/core-modules/twenty-config/conversion/config-value-converter.service';
|
import { ConfigValueConverterService } from 'src/engine/core-modules/twenty-config/conversion/config-value-converter.service';
|
||||||
|
import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver';
|
||||||
|
import { ConfigVariableType } from 'src/engine/core-modules/twenty-config/enums/config-variable-type.enum';
|
||||||
|
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
||||||
import { ConfigStorageService } from 'src/engine/core-modules/twenty-config/storage/config-storage.service';
|
import { ConfigStorageService } from 'src/engine/core-modules/twenty-config/storage/config-storage.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||||
|
|
||||||
|
jest.mock('src/engine/core-modules/auth/auth.util', () => ({
|
||||||
|
encryptText: jest.fn((text) => `encrypted:${text}`),
|
||||||
|
decryptText: jest.fn((text) => text.replace('encrypted:', '')),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('ConfigStorageService', () => {
|
describe('ConfigStorageService', () => {
|
||||||
let service: ConfigStorageService;
|
let service: ConfigStorageService;
|
||||||
let keyValuePairRepository: Repository<KeyValuePair>;
|
let keyValuePairRepository: Repository<KeyValuePair>;
|
||||||
let configValueConverter: ConfigValueConverterService;
|
let configValueConverter: ConfigValueConverterService;
|
||||||
|
let environmentConfigDriver: EnvironmentConfigDriver;
|
||||||
|
|
||||||
const createMockKeyValuePair = (
|
const createMockKeyValuePair = (
|
||||||
key: string,
|
key: string,
|
||||||
@ -47,6 +58,12 @@ describe('ConfigStorageService', () => {
|
|||||||
convertAppValueToDbValue: jest.fn(),
|
convertAppValueToDbValue: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: EnvironmentConfigDriver,
|
||||||
|
useValue: {
|
||||||
|
get: jest.fn().mockReturnValue('test-secret'),
|
||||||
|
},
|
||||||
|
},
|
||||||
ConfigVariables,
|
ConfigVariables,
|
||||||
{
|
{
|
||||||
provide: getRepositoryToken(KeyValuePair, 'core'),
|
provide: getRepositoryToken(KeyValuePair, 'core'),
|
||||||
@ -68,6 +85,9 @@ describe('ConfigStorageService', () => {
|
|||||||
configValueConverter = module.get<ConfigValueConverterService>(
|
configValueConverter = module.get<ConfigValueConverterService>(
|
||||||
ConfigValueConverterService,
|
ConfigValueConverterService,
|
||||||
);
|
);
|
||||||
|
environmentConfigDriver = module.get<EnvironmentConfigDriver>(
|
||||||
|
EnvironmentConfigDriver,
|
||||||
|
);
|
||||||
|
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
@ -136,6 +156,188 @@ describe('ConfigStorageService', () => {
|
|||||||
|
|
||||||
await expect(service.get(key)).rejects.toThrow('Conversion error');
|
await expect(service.get(key)).rejects.toThrow('Conversion error');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should decrypt sensitive string values', async () => {
|
||||||
|
const key = 'SENSITIVE_CONFIG' as keyof ConfigVariables;
|
||||||
|
const originalValue = 'sensitive-value';
|
||||||
|
const encryptedValue = 'encrypted:sensitive-value';
|
||||||
|
|
||||||
|
const mockRecord = createMockKeyValuePair(key as string, encryptedValue);
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(keyValuePairRepository, 'findOne')
|
||||||
|
.mockResolvedValue(mockRecord);
|
||||||
|
|
||||||
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValue({
|
||||||
|
[key]: {
|
||||||
|
isSensitive: true,
|
||||||
|
type: ConfigVariableType.STRING,
|
||||||
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
|
description: 'Test sensitive config',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
configValueConverter.convertDbValueToAppValue as jest.Mock
|
||||||
|
).mockReturnValue(encryptedValue);
|
||||||
|
|
||||||
|
const result = await service.get(key);
|
||||||
|
|
||||||
|
expect(result).toBe(originalValue);
|
||||||
|
expect(environmentConfigDriver.get).toHaveBeenCalledWith('APP_SECRET');
|
||||||
|
expect(authUtils.decryptText).toHaveBeenCalledWith(
|
||||||
|
encryptedValue,
|
||||||
|
'test-secret',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle decryption errors gracefully', async () => {
|
||||||
|
const key = 'SENSITIVE_CONFIG' as keyof ConfigVariables;
|
||||||
|
const encryptedValue = 'encrypted-value';
|
||||||
|
const convertedValue = 'converted-value';
|
||||||
|
|
||||||
|
const mockRecord = createMockKeyValuePair(key as string, encryptedValue);
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(keyValuePairRepository, 'findOne')
|
||||||
|
.mockResolvedValue(mockRecord);
|
||||||
|
|
||||||
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValue({
|
||||||
|
[key]: {
|
||||||
|
isSensitive: true,
|
||||||
|
type: ConfigVariableType.STRING,
|
||||||
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
|
description: 'Test sensitive config',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
configValueConverter.convertDbValueToAppValue as jest.Mock
|
||||||
|
).mockReturnValue(convertedValue);
|
||||||
|
|
||||||
|
const result = await service.get(key);
|
||||||
|
|
||||||
|
expect(result).toBe(convertedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle decryption failure in get() by returning original value', async () => {
|
||||||
|
const key = 'SENSITIVE_CONFIG' as keyof ConfigVariables;
|
||||||
|
const encryptedValue = 'encrypted:sensitive-value';
|
||||||
|
|
||||||
|
const mockRecord = createMockKeyValuePair(key as string, encryptedValue);
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(keyValuePairRepository, 'findOne')
|
||||||
|
.mockResolvedValue(mockRecord);
|
||||||
|
|
||||||
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValue({
|
||||||
|
[key]: {
|
||||||
|
isSensitive: true,
|
||||||
|
type: ConfigVariableType.STRING,
|
||||||
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
|
description: 'Test sensitive config',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
configValueConverter.convertDbValueToAppValue as jest.Mock
|
||||||
|
).mockReturnValue(encryptedValue);
|
||||||
|
|
||||||
|
// Mock decryption to throw an error
|
||||||
|
(authUtils.decryptText as jest.Mock).mockImplementationOnce(() => {
|
||||||
|
throw new Error('Decryption failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.get(key);
|
||||||
|
|
||||||
|
expect(result).toBe(encryptedValue); // Should fall back to encrypted value
|
||||||
|
expect(environmentConfigDriver.get).toHaveBeenCalledWith('APP_SECRET');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip decryption for non-string sensitive values', async () => {
|
||||||
|
const key = 'SENSITIVE_CONFIG' as keyof ConfigVariables;
|
||||||
|
const value = { someKey: 'someValue' };
|
||||||
|
|
||||||
|
const mockRecord = createMockKeyValuePair(
|
||||||
|
key as string,
|
||||||
|
JSON.stringify(value),
|
||||||
|
);
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(keyValuePairRepository, 'findOne')
|
||||||
|
.mockResolvedValue(mockRecord);
|
||||||
|
|
||||||
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValue({
|
||||||
|
[key]: {
|
||||||
|
isSensitive: true,
|
||||||
|
type: ConfigVariableType.ARRAY,
|
||||||
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
|
description: 'Test sensitive config',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
configValueConverter.convertDbValueToAppValue as jest.Mock
|
||||||
|
).mockReturnValue(value);
|
||||||
|
|
||||||
|
const result = await service.get(key);
|
||||||
|
|
||||||
|
expect(result).toBe(value);
|
||||||
|
expect(authUtils.decryptText).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle decryption failure in loadAll() by skipping failed values', async () => {
|
||||||
|
const configVars: KeyValuePair[] = [
|
||||||
|
createMockKeyValuePair('SENSITIVE_CONFIG_1', 'encrypted:value1'),
|
||||||
|
createMockKeyValuePair('SENSITIVE_CONFIG_2', 'encrypted:value2'),
|
||||||
|
createMockKeyValuePair('NORMAL_CONFIG', 'normal-value'),
|
||||||
|
];
|
||||||
|
|
||||||
|
jest.spyOn(keyValuePairRepository, 'find').mockResolvedValue(configVars);
|
||||||
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValue({
|
||||||
|
SENSITIVE_CONFIG_1: {
|
||||||
|
isSensitive: true,
|
||||||
|
type: ConfigVariableType.STRING,
|
||||||
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
|
description: 'Test sensitive config 1',
|
||||||
|
},
|
||||||
|
SENSITIVE_CONFIG_2: {
|
||||||
|
isSensitive: true,
|
||||||
|
type: ConfigVariableType.STRING,
|
||||||
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
|
description: 'Test sensitive config 2',
|
||||||
|
},
|
||||||
|
NORMAL_CONFIG: {
|
||||||
|
type: ConfigVariableType.STRING,
|
||||||
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
|
description: 'Test normal config',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
configValueConverter.convertDbValueToAppValue as jest.Mock
|
||||||
|
).mockImplementation((value) => value);
|
||||||
|
|
||||||
|
// Mock decryption to fail for the second sensitive value
|
||||||
|
(authUtils.decryptText as jest.Mock)
|
||||||
|
.mockImplementationOnce((text) => text.replace('encrypted:', ''))
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new Error('Decryption failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.loadAll();
|
||||||
|
|
||||||
|
expect(result.size).toBe(3);
|
||||||
|
expect(result.get('SENSITIVE_CONFIG_1' as keyof ConfigVariables)).toBe(
|
||||||
|
'value1',
|
||||||
|
);
|
||||||
|
expect(result.get('SENSITIVE_CONFIG_2' as keyof ConfigVariables)).toBe(
|
||||||
|
'encrypted:value2',
|
||||||
|
); // Original encrypted value
|
||||||
|
expect(result.get('NORMAL_CONFIG' as keyof ConfigVariables)).toBe(
|
||||||
|
'normal-value',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('set', () => {
|
describe('set', () => {
|
||||||
@ -197,6 +399,77 @@ describe('ConfigStorageService', () => {
|
|||||||
|
|
||||||
await expect(service.set(key, value)).rejects.toThrow('Conversion error');
|
await expect(service.set(key, value)).rejects.toThrow('Conversion error');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should encrypt sensitive string values', async () => {
|
||||||
|
const key = 'SENSITIVE_CONFIG' as keyof ConfigVariables;
|
||||||
|
const value = 'sensitive-value';
|
||||||
|
const convertedValue = 'sensitive-value';
|
||||||
|
const encryptedValue = 'encrypted:sensitive-value';
|
||||||
|
|
||||||
|
jest.spyOn(keyValuePairRepository, 'findOne').mockResolvedValue(null);
|
||||||
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValue({
|
||||||
|
[key]: {
|
||||||
|
isSensitive: true,
|
||||||
|
type: ConfigVariableType.STRING,
|
||||||
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
|
description: 'Test sensitive config',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
configValueConverter.convertAppValueToDbValue as jest.Mock
|
||||||
|
).mockReturnValue(convertedValue);
|
||||||
|
|
||||||
|
await service.set(key, value);
|
||||||
|
|
||||||
|
expect(keyValuePairRepository.insert).toHaveBeenCalledWith({
|
||||||
|
key: key as string,
|
||||||
|
value: encryptedValue,
|
||||||
|
userId: null,
|
||||||
|
workspaceId: null,
|
||||||
|
type: KeyValuePairType.CONFIG_VARIABLE,
|
||||||
|
});
|
||||||
|
expect(environmentConfigDriver.get).toHaveBeenCalledWith('APP_SECRET');
|
||||||
|
expect(authUtils.encryptText).toHaveBeenCalledWith(
|
||||||
|
convertedValue,
|
||||||
|
'test-secret',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle encryption errors gracefully', async () => {
|
||||||
|
const key = 'SENSITIVE_CONFIG' as keyof ConfigVariables;
|
||||||
|
const value = 'sensitive-value';
|
||||||
|
const convertedValue = 'sensitive-value';
|
||||||
|
|
||||||
|
jest.spyOn(keyValuePairRepository, 'findOne').mockResolvedValue(null);
|
||||||
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValue({
|
||||||
|
[key]: {
|
||||||
|
isSensitive: true,
|
||||||
|
type: ConfigVariableType.STRING,
|
||||||
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
|
description: 'Test sensitive config',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
configValueConverter.convertAppValueToDbValue as jest.Mock
|
||||||
|
).mockReturnValue(convertedValue);
|
||||||
|
|
||||||
|
// Mock encryption to throw an error
|
||||||
|
(authUtils.encryptText as jest.Mock).mockImplementationOnce(() => {
|
||||||
|
throw new Error('Encryption failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.set(key, value);
|
||||||
|
|
||||||
|
expect(keyValuePairRepository.insert).toHaveBeenCalledWith({
|
||||||
|
key: key as string,
|
||||||
|
value: convertedValue, // Should fall back to unconverted value
|
||||||
|
userId: null,
|
||||||
|
workspaceId: null,
|
||||||
|
type: KeyValuePairType.CONFIG_VARIABLE,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('delete', () => {
|
describe('delete', () => {
|
||||||
@ -315,6 +588,47 @@ describe('ConfigStorageService', () => {
|
|||||||
).toHaveBeenCalledTimes(1); // Only called for non-null value
|
).toHaveBeenCalledTimes(1); // Only called for non-null value
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should decrypt sensitive string values in loadAll', async () => {
|
||||||
|
const configVars: KeyValuePair[] = [
|
||||||
|
createMockKeyValuePair('SENSITIVE_CONFIG', 'encrypted:sensitive-value'),
|
||||||
|
createMockKeyValuePair('NORMAL_CONFIG', 'normal-value'),
|
||||||
|
];
|
||||||
|
|
||||||
|
jest.spyOn(keyValuePairRepository, 'find').mockResolvedValue(configVars);
|
||||||
|
jest.spyOn(TypedReflect, 'getMetadata').mockReturnValue({
|
||||||
|
SENSITIVE_CONFIG: {
|
||||||
|
isSensitive: true,
|
||||||
|
type: ConfigVariableType.STRING,
|
||||||
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
|
description: 'Test sensitive config',
|
||||||
|
},
|
||||||
|
NORMAL_CONFIG: {
|
||||||
|
type: ConfigVariableType.STRING,
|
||||||
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
|
description: 'Test normal config',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
configValueConverter.convertDbValueToAppValue as jest.Mock
|
||||||
|
).mockImplementation((value) => value);
|
||||||
|
|
||||||
|
const result = await service.loadAll();
|
||||||
|
|
||||||
|
expect(result.size).toBe(2);
|
||||||
|
expect(result.get('SENSITIVE_CONFIG' as keyof ConfigVariables)).toBe(
|
||||||
|
'sensitive-value',
|
||||||
|
);
|
||||||
|
expect(result.get('NORMAL_CONFIG' as keyof ConfigVariables)).toBe(
|
||||||
|
'normal-value',
|
||||||
|
);
|
||||||
|
expect(environmentConfigDriver.get).toHaveBeenCalledWith('APP_SECRET');
|
||||||
|
expect(authUtils.decryptText).toHaveBeenCalledWith(
|
||||||
|
'encrypted:sensitive-value',
|
||||||
|
'test-secret',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Edge Cases and Additional Scenarios', () => {
|
describe('Edge Cases and Additional Scenarios', () => {
|
||||||
|
|||||||
@ -3,12 +3,19 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { FindOptionsWhere, IsNull, Repository } from 'typeorm';
|
import { FindOptionsWhere, IsNull, Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
decryptText,
|
||||||
|
encryptText,
|
||||||
|
} from 'src/engine/core-modules/auth/auth.util';
|
||||||
import {
|
import {
|
||||||
KeyValuePair,
|
KeyValuePair,
|
||||||
KeyValuePairType,
|
KeyValuePairType,
|
||||||
} from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
} from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||||
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
||||||
import { ConfigValueConverterService } from 'src/engine/core-modules/twenty-config/conversion/config-value-converter.service';
|
import { ConfigValueConverterService } from 'src/engine/core-modules/twenty-config/conversion/config-value-converter.service';
|
||||||
|
import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver';
|
||||||
|
import { ConfigVariableType } from 'src/engine/core-modules/twenty-config/enums/config-variable-type.enum';
|
||||||
|
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||||
|
|
||||||
import { ConfigStorageInterface } from './interfaces/config-storage.interface';
|
import { ConfigStorageInterface } from './interfaces/config-storage.interface';
|
||||||
|
|
||||||
@ -20,6 +27,7 @@ export class ConfigStorageService implements ConfigStorageInterface {
|
|||||||
@InjectRepository(KeyValuePair, 'core')
|
@InjectRepository(KeyValuePair, 'core')
|
||||||
private readonly keyValuePairRepository: Repository<KeyValuePair>,
|
private readonly keyValuePairRepository: Repository<KeyValuePair>,
|
||||||
private readonly configValueConverter: ConfigValueConverterService,
|
private readonly configValueConverter: ConfigValueConverterService,
|
||||||
|
private readonly environmentConfigDriver: EnvironmentConfigDriver,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private getConfigVariableWhereClause(
|
private getConfigVariableWhereClause(
|
||||||
@ -33,6 +41,67 @@ export class ConfigStorageService implements ConfigStorageInterface {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getAppSecret(): string {
|
||||||
|
return this.environmentConfigDriver.get('APP_SECRET');
|
||||||
|
}
|
||||||
|
|
||||||
|
private getConfigMetadata<T extends keyof ConfigVariables>(key: T) {
|
||||||
|
return TypedReflect.getMetadata('config-variables', ConfigVariables)?.[
|
||||||
|
key as string
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private logAndRethrow(message: string, error: any): never {
|
||||||
|
this.logger.error(message, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async convertAndSecureValue<T extends keyof ConfigVariables>(
|
||||||
|
value: any,
|
||||||
|
key: T,
|
||||||
|
isDecrypt = false,
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const convertedValue = isDecrypt
|
||||||
|
? this.configValueConverter.convertDbValueToAppValue(value, key)
|
||||||
|
: this.configValueConverter.convertAppValueToDbValue(value);
|
||||||
|
|
||||||
|
const metadata = this.getConfigMetadata(key);
|
||||||
|
const isSensitiveString =
|
||||||
|
metadata?.isSensitive &&
|
||||||
|
metadata.type === ConfigVariableType.STRING &&
|
||||||
|
typeof convertedValue === 'string';
|
||||||
|
|
||||||
|
if (!isSensitiveString) {
|
||||||
|
return convertedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appSecret = this.getAppSecret();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return isDecrypt
|
||||||
|
? decryptText(convertedValue, appSecret)
|
||||||
|
: encryptText(convertedValue, appSecret);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to ${isDecrypt ? 'decrypt' : 'encrypt'} value for key ${
|
||||||
|
key as string
|
||||||
|
}`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
|
||||||
|
return convertedValue;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logAndRethrow(
|
||||||
|
`Failed to convert value ${
|
||||||
|
isDecrypt ? 'from DB' : 'to DB'
|
||||||
|
} for key ${key as string}`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async get<T extends keyof ConfigVariables>(
|
async get<T extends keyof ConfigVariables>(
|
||||||
key: T,
|
key: T,
|
||||||
): Promise<ConfigVariables[T] | undefined> {
|
): Promise<ConfigVariables[T] | undefined> {
|
||||||
@ -45,25 +114,13 @@ export class ConfigStorageService implements ConfigStorageInterface {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
this.logger.debug(
|
||||||
this.logger.debug(
|
`Fetching config for ${key as string} in database: ${result?.value}`,
|
||||||
`Fetching config for ${key as string} in database: ${result?.value}`,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
return this.configValueConverter.convertDbValueToAppValue(
|
return await this.convertAndSecureValue(result.value, key, true);
|
||||||
result.value,
|
|
||||||
key,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(
|
|
||||||
`Failed to convert value to app type for key ${key as string}`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to get config for ${key as string}`, error);
|
this.logAndRethrow(`Failed to get config for ${key as string}`, error);
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,18 +129,7 @@ export class ConfigStorageService implements ConfigStorageInterface {
|
|||||||
value: ConfigVariables[T],
|
value: ConfigVariables[T],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
let processedValue;
|
const dbValue = await this.convertAndSecureValue(value, key, false);
|
||||||
|
|
||||||
try {
|
|
||||||
processedValue =
|
|
||||||
this.configValueConverter.convertAppValueToDbValue(value);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(
|
|
||||||
`Failed to convert value to storage type for key ${key as string}`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingRecord = await this.keyValuePairRepository.findOne({
|
const existingRecord = await this.keyValuePairRepository.findOne({
|
||||||
where: this.getConfigVariableWhereClause(key as string),
|
where: this.getConfigVariableWhereClause(key as string),
|
||||||
@ -92,20 +138,19 @@ export class ConfigStorageService implements ConfigStorageInterface {
|
|||||||
if (existingRecord) {
|
if (existingRecord) {
|
||||||
await this.keyValuePairRepository.update(
|
await this.keyValuePairRepository.update(
|
||||||
{ id: existingRecord.id },
|
{ id: existingRecord.id },
|
||||||
{ value: processedValue },
|
{ value: dbValue },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await this.keyValuePairRepository.insert({
|
await this.keyValuePairRepository.insert({
|
||||||
key: key as string,
|
key: key as string,
|
||||||
value: processedValue,
|
value: dbValue,
|
||||||
userId: null,
|
userId: null,
|
||||||
workspaceId: null,
|
workspaceId: null,
|
||||||
type: KeyValuePairType.CONFIG_VARIABLE,
|
type: KeyValuePairType.CONFIG_VARIABLE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to set config for ${key as string}`, error);
|
this.logAndRethrow(`Failed to set config for ${key as string}`, error);
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,8 +160,7 @@ export class ConfigStorageService implements ConfigStorageInterface {
|
|||||||
this.getConfigVariableWhereClause(key as string),
|
this.getConfigVariableWhereClause(key as string),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to delete config for ${key as string}`, error);
|
this.logAndRethrow(`Failed to delete config for ${key as string}`, error);
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,9 +182,10 @@ export class ConfigStorageService implements ConfigStorageInterface {
|
|||||||
const key = configVar.key as keyof ConfigVariables;
|
const key = configVar.key as keyof ConfigVariables;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const value = this.configValueConverter.convertDbValueToAppValue(
|
const value = await this.convertAndSecureValue(
|
||||||
configVar.value,
|
configVar.value,
|
||||||
key,
|
key,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
@ -148,7 +193,7 @@ export class ConfigStorageService implements ConfigStorageInterface {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Failed to convert value to app type for key ${key as string}`,
|
`Failed to process config value for key ${key as string}`,
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@ -158,8 +203,7 @@ export class ConfigStorageService implements ConfigStorageInterface {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Failed to load all config variables', error);
|
this.logAndRethrow('Failed to load all config variables', error);
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { CustomException } from 'src/utils/custom-exception';
|
||||||
|
|
||||||
|
export class ConfigVariableException extends CustomException {
|
||||||
|
constructor(message: string, code: ConfigVariableExceptionCode) {
|
||||||
|
super(message, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ConfigVariableExceptionCode {
|
||||||
|
DATABASE_CONFIG_DISABLED = 'DATABASE_CONFIG_DISABLED',
|
||||||
|
ENVIRONMENT_ONLY_VARIABLE = 'ENVIRONMENT_ONLY_VARIABLE',
|
||||||
|
VARIABLE_NOT_FOUND = 'VARIABLE_NOT_FOUND',
|
||||||
|
VALIDATION_FAILED = 'VALIDATION_FAILED',
|
||||||
|
UNSUPPORTED_CONFIG_TYPE = 'UNSUPPORTED_CONFIG_TYPE',
|
||||||
|
METADATA_NOT_FOUND = 'METADATA_NOT_FOUND',
|
||||||
|
INTERNAL_ERROR = 'INTERNAL_ERROR',
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
||||||
|
import { CONFIG_VARIABLES_INSTANCE_TOKEN } from 'src/engine/core-modules/twenty-config/constants/config-variables-instance-tokens.constants';
|
||||||
import { DatabaseConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/database-config.driver';
|
import { DatabaseConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/database-config.driver';
|
||||||
import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver';
|
import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver';
|
||||||
import { ConfigSource } from 'src/engine/core-modules/twenty-config/enums/config-source.enum';
|
import { ConfigSource } from 'src/engine/core-modules/twenty-config/enums/config-source.enum';
|
||||||
@ -58,7 +59,6 @@ const mockConfigVarMetadata = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Setup with database driver
|
|
||||||
const setupTestModule = async (isDatabaseConfigEnabled = true) => {
|
const setupTestModule = async (isDatabaseConfigEnabled = true) => {
|
||||||
const configServiceMock = {
|
const configServiceMock = {
|
||||||
get: jest.fn().mockImplementation((key) => {
|
get: jest.fn().mockImplementation((key) => {
|
||||||
@ -70,6 +70,13 @@ const setupTestModule = async (isDatabaseConfigEnabled = true) => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockConfigVariablesInstance = {
|
||||||
|
TEST_VAR: 'test value',
|
||||||
|
ENV_ONLY_VAR: 'env only value',
|
||||||
|
SENSITIVE_VAR: 'sensitive value',
|
||||||
|
NO_METADATA_KEY: 'value without metadata',
|
||||||
|
};
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
TwentyConfigService,
|
TwentyConfigService,
|
||||||
@ -77,8 +84,10 @@ const setupTestModule = async (isDatabaseConfigEnabled = true) => {
|
|||||||
provide: DatabaseConfigDriver,
|
provide: DatabaseConfigDriver,
|
||||||
useValue: {
|
useValue: {
|
||||||
get: jest.fn(),
|
get: jest.fn(),
|
||||||
|
set: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
getCacheInfo: jest.fn(),
|
getCacheInfo: jest.fn(),
|
||||||
|
delete: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -93,6 +102,10 @@ const setupTestModule = async (isDatabaseConfigEnabled = true) => {
|
|||||||
provide: ConfigService,
|
provide: ConfigService,
|
||||||
useValue: configServiceMock,
|
useValue: configServiceMock,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: CONFIG_VARIABLES_INSTANCE_TOKEN,
|
||||||
|
useValue: mockConfigVariablesInstance,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
@ -104,10 +117,10 @@ const setupTestModule = async (isDatabaseConfigEnabled = true) => {
|
|||||||
EnvironmentConfigDriver,
|
EnvironmentConfigDriver,
|
||||||
),
|
),
|
||||||
configService: module.get<ConfigService>(ConfigService),
|
configService: module.get<ConfigService>(ConfigService),
|
||||||
|
configVariablesInstance: module.get(CONFIG_VARIABLES_INSTANCE_TOKEN),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Setup without database driver
|
|
||||||
const setupTestModuleWithoutDb = async () => {
|
const setupTestModuleWithoutDb = async () => {
|
||||||
const configServiceMock = {
|
const configServiceMock = {
|
||||||
get: jest.fn().mockImplementation((key) => {
|
get: jest.fn().mockImplementation((key) => {
|
||||||
@ -119,6 +132,13 @@ const setupTestModuleWithoutDb = async () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockConfigVariablesInstance = {
|
||||||
|
TEST_VAR: 'test value',
|
||||||
|
ENV_ONLY_VAR: 'env only value',
|
||||||
|
SENSITIVE_VAR: 'sensitive value',
|
||||||
|
NO_METADATA_KEY: 'value without metadata',
|
||||||
|
};
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
TwentyConfigService,
|
TwentyConfigService,
|
||||||
@ -134,6 +154,10 @@ const setupTestModuleWithoutDb = async () => {
|
|||||||
provide: ConfigService,
|
provide: ConfigService,
|
||||||
useValue: configServiceMock,
|
useValue: configServiceMock,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: CONFIG_VARIABLES_INSTANCE_TOKEN,
|
||||||
|
useValue: mockConfigVariablesInstance,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
@ -143,6 +167,7 @@ const setupTestModuleWithoutDb = async () => {
|
|||||||
EnvironmentConfigDriver,
|
EnvironmentConfigDriver,
|
||||||
),
|
),
|
||||||
configService: module.get<ConfigService>(ConfigService),
|
configService: module.get<ConfigService>(ConfigService),
|
||||||
|
configVariablesInstance: module.get(CONFIG_VARIABLES_INSTANCE_TOKEN),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -278,6 +303,10 @@ describe('TwentyConfigService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(service, 'validateConfigVariableExists').mockReturnValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw error when database driver is not active', async () => {
|
it('should throw error when database driver is not active', async () => {
|
||||||
setPrivateProps(service, { isDatabaseDriverActive: false });
|
setPrivateProps(service, { isDatabaseDriverActive: false });
|
||||||
|
|
||||||
@ -468,4 +497,51 @@ describe('TwentyConfigService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('validateConfigVariableExists', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be called by set, update, and delete methods', async () => {
|
||||||
|
const validateSpy = jest
|
||||||
|
.spyOn(service, 'validateConfigVariableExists')
|
||||||
|
.mockReturnValue(true);
|
||||||
|
|
||||||
|
setPrivateProps(service, { isDatabaseDriverActive: true });
|
||||||
|
jest
|
||||||
|
.spyOn(service as any, 'validateNotEnvOnly')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
|
await service.set('TEST_VAR' as keyof ConfigVariables, 'test value');
|
||||||
|
await service.update(
|
||||||
|
'TEST_VAR' as keyof ConfigVariables,
|
||||||
|
'updated value',
|
||||||
|
);
|
||||||
|
await service.delete('TEST_VAR' as keyof ConfigVariables);
|
||||||
|
|
||||||
|
expect(validateSpy).toHaveBeenCalledTimes(3);
|
||||||
|
expect(validateSpy).toHaveBeenCalledWith('TEST_VAR');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for valid config variables with metadata', () => {
|
||||||
|
jest.spyOn(service, 'validateConfigVariableExists').mockRestore();
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(service as any, 'getMetadata')
|
||||||
|
.mockReturnValue(mockConfigVarMetadata.TEST_VAR);
|
||||||
|
|
||||||
|
expect(service.validateConfigVariableExists('TEST_VAR')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when config variable does not exist', () => {
|
||||||
|
jest.spyOn(service, 'validateConfigVariableExists').mockRestore();
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
service.validateConfigVariableExists('MISSING_KEY');
|
||||||
|
}).toThrow(
|
||||||
|
'Config variable "MISSING_KEY" does not exist in ConfigVariables',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
import { Injectable, Logger, Optional } from '@nestjs/common';
|
import { Inject, Injectable, Logger, Optional } from '@nestjs/common';
|
||||||
|
|
||||||
import { isString } from 'class-validator';
|
import { isString } from 'class-validator';
|
||||||
|
|
||||||
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
||||||
|
import { CONFIG_VARIABLES_INSTANCE_TOKEN } from 'src/engine/core-modules/twenty-config/constants/config-variables-instance-tokens.constants';
|
||||||
import { CONFIG_VARIABLES_MASKING_CONFIG } from 'src/engine/core-modules/twenty-config/constants/config-variables-masking-config';
|
import { CONFIG_VARIABLES_MASKING_CONFIG } from 'src/engine/core-modules/twenty-config/constants/config-variables-masking-config';
|
||||||
import { ConfigVariablesMetadataOptions } from 'src/engine/core-modules/twenty-config/decorators/config-variables-metadata.decorator';
|
import { ConfigVariablesMetadataOptions } from 'src/engine/core-modules/twenty-config/decorators/config-variables-metadata.decorator';
|
||||||
import { DatabaseConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/database-config.driver';
|
import { DatabaseConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/database-config.driver';
|
||||||
import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver';
|
import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver';
|
||||||
import { ConfigSource } from 'src/engine/core-modules/twenty-config/enums/config-source.enum';
|
import { ConfigSource } from 'src/engine/core-modules/twenty-config/enums/config-source.enum';
|
||||||
import { ConfigVariablesMaskingStrategies } from 'src/engine/core-modules/twenty-config/enums/config-variables-masking-strategies.enum';
|
import { ConfigVariablesMaskingStrategies } from 'src/engine/core-modules/twenty-config/enums/config-variables-masking-strategies.enum';
|
||||||
|
import {
|
||||||
|
ConfigVariableException,
|
||||||
|
ConfigVariableExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/twenty-config/twenty-config.exception';
|
||||||
import { configVariableMaskSensitiveData } from 'src/engine/core-modules/twenty-config/utils/config-variable-mask-sensitive-data.util';
|
import { configVariableMaskSensitiveData } from 'src/engine/core-modules/twenty-config/utils/config-variable-mask-sensitive-data.util';
|
||||||
import { isEnvOnlyConfigVar } from 'src/engine/core-modules/twenty-config/utils/is-env-only-config-var.util';
|
import { isEnvOnlyConfigVar } from 'src/engine/core-modules/twenty-config/utils/is-env-only-config-var.util';
|
||||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||||
@ -21,6 +26,8 @@ export class TwentyConfigService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly environmentConfigDriver: EnvironmentConfigDriver,
|
private readonly environmentConfigDriver: EnvironmentConfigDriver,
|
||||||
@Optional() private readonly databaseConfigDriver: DatabaseConfigDriver,
|
@Optional() private readonly databaseConfigDriver: DatabaseConfigDriver,
|
||||||
|
@Inject(CONFIG_VARIABLES_INSTANCE_TOKEN)
|
||||||
|
private readonly configVariablesInstance: ConfigVariables,
|
||||||
) {
|
) {
|
||||||
const isConfigVariablesInDbEnabled = this.environmentConfigDriver.get(
|
const isConfigVariablesInDbEnabled = this.environmentConfigDriver.get(
|
||||||
'IS_CONFIG_VARIABLES_IN_DB_ENABLED',
|
'IS_CONFIG_VARIABLES_IN_DB_ENABLED',
|
||||||
@ -59,49 +66,37 @@ export class TwentyConfigService {
|
|||||||
if (cachedValueFromDb !== undefined) {
|
if (cachedValueFromDb !== undefined) {
|
||||||
return cachedValueFromDb;
|
return cachedValueFromDb;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.environmentConfigDriver.get(key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.environmentConfigDriver.get(key);
|
return this.environmentConfigDriver.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async set<T extends keyof ConfigVariables>(
|
||||||
|
key: T,
|
||||||
|
value: ConfigVariables[T],
|
||||||
|
): Promise<void> {
|
||||||
|
this.validateDatabaseDriverActive('set');
|
||||||
|
this.validateNotEnvOnly(key, 'create');
|
||||||
|
this.validateConfigVariableExists(key as string);
|
||||||
|
|
||||||
|
await this.databaseConfigDriver.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
async update<T extends keyof ConfigVariables>(
|
async update<T extends keyof ConfigVariables>(
|
||||||
key: T,
|
key: T,
|
||||||
value: ConfigVariables[T],
|
value: ConfigVariables[T],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!this.isDatabaseDriverActive) {
|
this.validateDatabaseDriverActive('update');
|
||||||
throw new Error(
|
this.validateNotEnvOnly(key, 'update');
|
||||||
'Database configuration is disabled or unavailable, cannot update configuration',
|
this.validateConfigVariableExists(key as string);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata =
|
await this.databaseConfigDriver.update(key, value);
|
||||||
TypedReflect.getMetadata('config-variables', ConfigVariables) ?? {};
|
|
||||||
const envMetadata = metadata[key];
|
|
||||||
|
|
||||||
if (envMetadata?.isEnvOnly) {
|
|
||||||
throw new Error(
|
|
||||||
`Cannot update environment-only variable: ${key as string}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.databaseConfigDriver.update(key, value);
|
|
||||||
this.logger.debug(`Updated config variable: ${key as string}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to update config for ${key as string}`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getMetadata(
|
getMetadata(
|
||||||
key: keyof ConfigVariables,
|
key: keyof ConfigVariables,
|
||||||
): ConfigVariablesMetadataOptions | undefined {
|
): ConfigVariablesMetadataOptions | undefined {
|
||||||
const metadata =
|
return this.getConfigMetadata()[key as string];
|
||||||
TypedReflect.getMetadata('config-variables', ConfigVariables) ?? {};
|
|
||||||
|
|
||||||
return metadata[key];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll(): Record<
|
getAll(): Record<
|
||||||
@ -121,44 +116,14 @@ export class TwentyConfigService {
|
|||||||
}
|
}
|
||||||
> = {};
|
> = {};
|
||||||
|
|
||||||
const configVars = new ConfigVariables();
|
const metadata = this.getConfigMetadata();
|
||||||
const metadata =
|
|
||||||
TypedReflect.getMetadata('config-variables', ConfigVariables) ?? {};
|
|
||||||
|
|
||||||
Object.entries(metadata).forEach(([key, envMetadata]) => {
|
Object.entries(metadata).forEach(([key, envMetadata]) => {
|
||||||
let value = this.get(key as keyof ConfigVariables) ?? '';
|
const typedKey = key as keyof ConfigVariables;
|
||||||
let source = ConfigSource.ENVIRONMENT;
|
let value = this.get(typedKey) ?? '';
|
||||||
|
const source = this.determineConfigSource(typedKey, value, envMetadata);
|
||||||
|
|
||||||
if (!this.isDatabaseDriverActive || envMetadata.isEnvOnly) {
|
value = this.maskSensitiveValue(typedKey, value);
|
||||||
if (value === configVars[key as keyof ConfigVariables]) {
|
|
||||||
source = ConfigSource.DEFAULT;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const dbValue = value;
|
|
||||||
|
|
||||||
source =
|
|
||||||
dbValue !== configVars[key as keyof ConfigVariables]
|
|
||||||
? ConfigSource.DATABASE
|
|
||||||
: ConfigSource.DEFAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isString(value) && key in CONFIG_VARIABLES_MASKING_CONFIG) {
|
|
||||||
const varMaskingConfig =
|
|
||||||
CONFIG_VARIABLES_MASKING_CONFIG[
|
|
||||||
key as keyof typeof CONFIG_VARIABLES_MASKING_CONFIG
|
|
||||||
];
|
|
||||||
const options =
|
|
||||||
varMaskingConfig.strategy ===
|
|
||||||
ConfigVariablesMaskingStrategies.LAST_N_CHARS
|
|
||||||
? { chars: varMaskingConfig.chars }
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
value = configVariableMaskSensitiveData(
|
|
||||||
value,
|
|
||||||
varMaskingConfig.strategy,
|
|
||||||
{ ...options, variableName: key },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
result[key] = {
|
result[key] = {
|
||||||
value,
|
value,
|
||||||
@ -170,6 +135,29 @@ export class TwentyConfigService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVariableWithMetadata(key: keyof ConfigVariables): {
|
||||||
|
value: ConfigVariables[keyof ConfigVariables];
|
||||||
|
metadata: ConfigVariablesMetadataOptions;
|
||||||
|
source: ConfigSource;
|
||||||
|
} | null {
|
||||||
|
const metadata = this.getMetadata(key);
|
||||||
|
|
||||||
|
if (!metadata) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = this.get(key) ?? '';
|
||||||
|
const source = this.determineConfigSource(key, value, metadata);
|
||||||
|
|
||||||
|
value = this.maskSensitiveValue(key, value);
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
metadata,
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
getCacheInfo(): {
|
getCacheInfo(): {
|
||||||
usingDatabaseDriver: boolean;
|
usingDatabaseDriver: boolean;
|
||||||
cacheStats?: {
|
cacheStats?: {
|
||||||
@ -191,4 +179,100 @@ export class TwentyConfigService {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async delete(key: keyof ConfigVariables): Promise<void> {
|
||||||
|
this.validateDatabaseDriverActive('delete');
|
||||||
|
this.validateConfigVariableExists(key as string);
|
||||||
|
await this.databaseConfigDriver.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getConfigMetadata(): Record<string, ConfigVariablesMetadataOptions> {
|
||||||
|
return TypedReflect.getMetadata('config-variables', ConfigVariables) ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateDatabaseDriverActive(operation: string): void {
|
||||||
|
if (!this.isDatabaseDriverActive) {
|
||||||
|
throw new ConfigVariableException(
|
||||||
|
`Database configuration is disabled or unavailable, cannot ${operation} configuration`,
|
||||||
|
ConfigVariableExceptionCode.DATABASE_CONFIG_DISABLED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateNotEnvOnly<T extends keyof ConfigVariables>(
|
||||||
|
key: T,
|
||||||
|
operation: string,
|
||||||
|
): void {
|
||||||
|
const metadata = this.getConfigMetadata();
|
||||||
|
const envMetadata = metadata[key as string];
|
||||||
|
|
||||||
|
if (envMetadata?.isEnvOnly) {
|
||||||
|
throw new ConfigVariableException(
|
||||||
|
`Cannot ${operation} environment-only variable: ${key as string}`,
|
||||||
|
ConfigVariableExceptionCode.ENVIRONMENT_ONLY_VARIABLE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private determineConfigSource<T extends keyof ConfigVariables>(
|
||||||
|
key: T,
|
||||||
|
value: ConfigVariables[T],
|
||||||
|
metadata: ConfigVariablesMetadataOptions,
|
||||||
|
): ConfigSource {
|
||||||
|
const configVars = new ConfigVariables();
|
||||||
|
|
||||||
|
if (!this.isDatabaseDriverActive || metadata.isEnvOnly) {
|
||||||
|
return value === configVars[key]
|
||||||
|
? ConfigSource.DEFAULT
|
||||||
|
: ConfigSource.ENVIRONMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbValue = this.databaseConfigDriver.get(key);
|
||||||
|
|
||||||
|
if (dbValue !== undefined) {
|
||||||
|
return ConfigSource.DATABASE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value === configVars[key]
|
||||||
|
? ConfigSource.DEFAULT
|
||||||
|
: ConfigSource.ENVIRONMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private maskSensitiveValue<T extends keyof ConfigVariables>(
|
||||||
|
key: T,
|
||||||
|
value: any,
|
||||||
|
): any {
|
||||||
|
if (!isString(value) || !(key in CONFIG_VARIABLES_MASKING_CONFIG)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const varMaskingConfig =
|
||||||
|
CONFIG_VARIABLES_MASKING_CONFIG[
|
||||||
|
key as keyof typeof CONFIG_VARIABLES_MASKING_CONFIG
|
||||||
|
];
|
||||||
|
const options =
|
||||||
|
varMaskingConfig.strategy ===
|
||||||
|
ConfigVariablesMaskingStrategies.LAST_N_CHARS
|
||||||
|
? { chars: varMaskingConfig.chars }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return configVariableMaskSensitiveData(value, varMaskingConfig.strategy, {
|
||||||
|
...options,
|
||||||
|
variableName: key as string,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
validateConfigVariableExists(key: string): boolean {
|
||||||
|
const metadata = this.getConfigMetadata();
|
||||||
|
const keyExists = key in metadata;
|
||||||
|
|
||||||
|
if (!keyExists) {
|
||||||
|
throw new ConfigVariableException(
|
||||||
|
`Config variable "${key}" does not exist in ConfigVariables`,
|
||||||
|
ConfigVariableExceptionCode.VARIABLE_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
export type ConfigVariableType =
|
|
||||||
| 'boolean'
|
|
||||||
| 'number'
|
|
||||||
| 'array'
|
|
||||||
| 'string'
|
|
||||||
| 'enum';
|
|
||||||
@ -7,6 +7,7 @@ import {
|
|||||||
IsString,
|
IsString,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
|
import { ConfigVariableType } from 'src/engine/core-modules/twenty-config/enums/config-variable-type.enum';
|
||||||
import { applyBasicValidators } from 'src/engine/core-modules/twenty-config/utils/apply-basic-validators.util';
|
import { applyBasicValidators } from 'src/engine/core-modules/twenty-config/utils/apply-basic-validators.util';
|
||||||
import { configTransformers } from 'src/engine/core-modules/twenty-config/utils/config-transformers.util';
|
import { configTransformers } from 'src/engine/core-modules/twenty-config/utils/config-transformers.util';
|
||||||
|
|
||||||
@ -50,7 +51,11 @@ describe('applyBasicValidators', () => {
|
|||||||
return jest.fn();
|
return jest.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
applyBasicValidators('boolean', mockTarget, mockPropertyKey);
|
applyBasicValidators(
|
||||||
|
ConfigVariableType.BOOLEAN,
|
||||||
|
mockTarget,
|
||||||
|
mockPropertyKey,
|
||||||
|
);
|
||||||
|
|
||||||
expect(Transform).toHaveBeenCalled();
|
expect(Transform).toHaveBeenCalled();
|
||||||
expect(IsBoolean).toHaveBeenCalled();
|
expect(IsBoolean).toHaveBeenCalled();
|
||||||
@ -81,7 +86,11 @@ describe('applyBasicValidators', () => {
|
|||||||
return jest.fn();
|
return jest.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
applyBasicValidators('number', mockTarget, mockPropertyKey);
|
applyBasicValidators(
|
||||||
|
ConfigVariableType.NUMBER,
|
||||||
|
mockTarget,
|
||||||
|
mockPropertyKey,
|
||||||
|
);
|
||||||
|
|
||||||
expect(Transform).toHaveBeenCalled();
|
expect(Transform).toHaveBeenCalled();
|
||||||
expect(IsNumber).toHaveBeenCalled();
|
expect(IsNumber).toHaveBeenCalled();
|
||||||
@ -104,7 +113,11 @@ describe('applyBasicValidators', () => {
|
|||||||
|
|
||||||
describe('string type', () => {
|
describe('string type', () => {
|
||||||
it('should apply string validator', () => {
|
it('should apply string validator', () => {
|
||||||
applyBasicValidators('string', mockTarget, mockPropertyKey);
|
applyBasicValidators(
|
||||||
|
ConfigVariableType.STRING,
|
||||||
|
mockTarget,
|
||||||
|
mockPropertyKey,
|
||||||
|
);
|
||||||
|
|
||||||
expect(IsString).toHaveBeenCalled();
|
expect(IsString).toHaveBeenCalled();
|
||||||
expect(Transform).not.toHaveBeenCalled(); // String doesn't need a transform
|
expect(Transform).not.toHaveBeenCalled(); // String doesn't need a transform
|
||||||
@ -115,7 +128,12 @@ describe('applyBasicValidators', () => {
|
|||||||
it('should apply enum validator with string array options', () => {
|
it('should apply enum validator with string array options', () => {
|
||||||
const enumOptions = ['option1', 'option2', 'option3'];
|
const enumOptions = ['option1', 'option2', 'option3'];
|
||||||
|
|
||||||
applyBasicValidators('enum', mockTarget, mockPropertyKey, enumOptions);
|
applyBasicValidators(
|
||||||
|
ConfigVariableType.ENUM,
|
||||||
|
mockTarget,
|
||||||
|
mockPropertyKey,
|
||||||
|
enumOptions,
|
||||||
|
);
|
||||||
|
|
||||||
expect(IsEnum).toHaveBeenCalledWith(enumOptions);
|
expect(IsEnum).toHaveBeenCalledWith(enumOptions);
|
||||||
expect(Transform).not.toHaveBeenCalled(); // Enum doesn't need a transform
|
expect(Transform).not.toHaveBeenCalled(); // Enum doesn't need a transform
|
||||||
@ -128,14 +146,23 @@ describe('applyBasicValidators', () => {
|
|||||||
Option3 = 'value3',
|
Option3 = 'value3',
|
||||||
}
|
}
|
||||||
|
|
||||||
applyBasicValidators('enum', mockTarget, mockPropertyKey, TestEnum);
|
applyBasicValidators(
|
||||||
|
ConfigVariableType.ENUM,
|
||||||
|
mockTarget,
|
||||||
|
mockPropertyKey,
|
||||||
|
TestEnum,
|
||||||
|
);
|
||||||
|
|
||||||
expect(IsEnum).toHaveBeenCalledWith(TestEnum);
|
expect(IsEnum).toHaveBeenCalledWith(TestEnum);
|
||||||
expect(Transform).not.toHaveBeenCalled(); // Enum doesn't need a transform
|
expect(Transform).not.toHaveBeenCalled(); // Enum doesn't need a transform
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not apply enum validator without options', () => {
|
it('should not apply enum validator without options', () => {
|
||||||
applyBasicValidators('enum', mockTarget, mockPropertyKey);
|
applyBasicValidators(
|
||||||
|
ConfigVariableType.ENUM,
|
||||||
|
mockTarget,
|
||||||
|
mockPropertyKey,
|
||||||
|
);
|
||||||
|
|
||||||
expect(IsEnum).not.toHaveBeenCalled();
|
expect(IsEnum).not.toHaveBeenCalled();
|
||||||
expect(Transform).not.toHaveBeenCalled();
|
expect(Transform).not.toHaveBeenCalled();
|
||||||
@ -144,7 +171,11 @@ describe('applyBasicValidators', () => {
|
|||||||
|
|
||||||
describe('array type', () => {
|
describe('array type', () => {
|
||||||
it('should apply array validator', () => {
|
it('should apply array validator', () => {
|
||||||
applyBasicValidators('array', mockTarget, mockPropertyKey);
|
applyBasicValidators(
|
||||||
|
ConfigVariableType.ARRAY,
|
||||||
|
mockTarget,
|
||||||
|
mockPropertyKey,
|
||||||
|
);
|
||||||
|
|
||||||
expect(IsArray).toHaveBeenCalled();
|
expect(IsArray).toHaveBeenCalled();
|
||||||
expect(Transform).not.toHaveBeenCalled(); // Array doesn't need a transform
|
expect(Transform).not.toHaveBeenCalled(); // Array doesn't need a transform
|
||||||
|
|||||||
@ -7,8 +7,12 @@ import {
|
|||||||
IsString,
|
IsString,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
|
import { ConfigVariableType } from 'src/engine/core-modules/twenty-config/enums/config-variable-type.enum';
|
||||||
|
import {
|
||||||
|
ConfigVariableException,
|
||||||
|
ConfigVariableExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/twenty-config/twenty-config.exception';
|
||||||
import { ConfigVariableOptions } from 'src/engine/core-modules/twenty-config/types/config-variable-options.type';
|
import { ConfigVariableOptions } from 'src/engine/core-modules/twenty-config/types/config-variable-options.type';
|
||||||
import { ConfigVariableType } from 'src/engine/core-modules/twenty-config/types/config-variable-type.type';
|
|
||||||
import { configTransformers } from 'src/engine/core-modules/twenty-config/utils/config-transformers.util';
|
import { configTransformers } from 'src/engine/core-modules/twenty-config/utils/config-transformers.util';
|
||||||
|
|
||||||
export function applyBasicValidators(
|
export function applyBasicValidators(
|
||||||
@ -18,7 +22,7 @@ export function applyBasicValidators(
|
|||||||
options?: ConfigVariableOptions,
|
options?: ConfigVariableOptions,
|
||||||
): void {
|
): void {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'boolean':
|
case ConfigVariableType.BOOLEAN:
|
||||||
Transform(({ value }) => {
|
Transform(({ value }) => {
|
||||||
const result = configTransformers.boolean(value);
|
const result = configTransformers.boolean(value);
|
||||||
|
|
||||||
@ -27,7 +31,7 @@ export function applyBasicValidators(
|
|||||||
IsBoolean()(target, propertyKey);
|
IsBoolean()(target, propertyKey);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'number':
|
case ConfigVariableType.NUMBER:
|
||||||
Transform(({ value }) => {
|
Transform(({ value }) => {
|
||||||
const result = configTransformers.number(value);
|
const result = configTransformers.number(value);
|
||||||
|
|
||||||
@ -36,21 +40,24 @@ export function applyBasicValidators(
|
|||||||
IsNumber()(target, propertyKey);
|
IsNumber()(target, propertyKey);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'string':
|
case ConfigVariableType.STRING:
|
||||||
IsString()(target, propertyKey);
|
IsString()(target, propertyKey);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'enum':
|
case ConfigVariableType.ENUM:
|
||||||
if (options) {
|
if (options) {
|
||||||
IsEnum(options)(target, propertyKey);
|
IsEnum(options)(target, propertyKey);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'array':
|
case ConfigVariableType.ARRAY:
|
||||||
IsArray()(target, propertyKey);
|
IsArray()(target, propertyKey);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported config variable type: ${type}`);
|
throw new ConfigVariableException(
|
||||||
|
`Unsupported config variable type: ${type}`,
|
||||||
|
ConfigVariableExceptionCode.UNSUPPORTED_CONFIG_TYPE,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/twenty-shared/src/types/ConfigVariableValue.ts
Normal file
1
packages/twenty-shared/src/types/ConfigVariableValue.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type ConfigVariableValue = string | number | boolean | string[] | null;
|
||||||
@ -7,6 +7,7 @@
|
|||||||
* |___/
|
* |___/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export type { ConfigVariableValue } from './ConfigVariableValue';
|
||||||
export { ConnectedAccountProvider } from './ConnectedAccountProvider';
|
export { ConnectedAccountProvider } from './ConnectedAccountProvider';
|
||||||
export { FieldMetadataType } from './FieldMetadataType';
|
export { FieldMetadataType } from './FieldMetadataType';
|
||||||
export type { IsExactly } from './IsExactly';
|
export type { IsExactly } from './IsExactly';
|
||||||
|
|||||||
@ -238,6 +238,7 @@ export {
|
|||||||
IconPuzzle,
|
IconPuzzle,
|
||||||
IconQuestionMark,
|
IconQuestionMark,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
|
IconRefreshAlert,
|
||||||
IconRefreshDot,
|
IconRefreshDot,
|
||||||
IconRelationManyToMany,
|
IconRelationManyToMany,
|
||||||
IconRelationOneToMany,
|
IconRelationOneToMany,
|
||||||
|
|||||||
@ -299,6 +299,7 @@ export {
|
|||||||
IconPuzzle,
|
IconPuzzle,
|
||||||
IconQuestionMark,
|
IconQuestionMark,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
|
IconRefreshAlert,
|
||||||
IconRefreshDot,
|
IconRefreshDot,
|
||||||
IconRelationManyToMany,
|
IconRelationManyToMany,
|
||||||
IconRelationOneToMany,
|
IconRelationOneToMany,
|
||||||
|
|||||||
@ -1,11 +1,18 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { OverflowingTextWithTooltip } from '@ui/display/tooltip/OverflowingTextWithTooltip';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
type H3TitleProps = {
|
type H3TitleProps = {
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
|
description?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
const StyledH3Title = styled.h3`
|
const StyledH3Title = styled.h3`
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
font-size: ${({ theme }) => theme.font.size.lg};
|
font-size: ${({ theme }) => theme.font.size.lg};
|
||||||
@ -13,6 +20,29 @@ const StyledH3Title = styled.h3`
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const H3Title = ({ title, className }: H3TitleProps) => {
|
const StyledDescription = styled.h4`
|
||||||
return <StyledH3Title className={className}>{title}</StyledH3Title>;
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||||
|
margin: 0;
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const H3Title = ({ title, description, className }: H3TitleProps) => {
|
||||||
|
return (
|
||||||
|
<StyledContainer className={className}>
|
||||||
|
<StyledH3Title>{title}</StyledH3Title>
|
||||||
|
{description && (
|
||||||
|
// Design rule: Never set a description for H3 if there are any H2 in the page
|
||||||
|
// (in that case, each H2 must have its own description)
|
||||||
|
<StyledDescription>
|
||||||
|
<OverflowingTextWithTooltip
|
||||||
|
text={description}
|
||||||
|
displayedMaxRows={2}
|
||||||
|
isTooltipMultiline={true}
|
||||||
|
/>
|
||||||
|
</StyledDescription>
|
||||||
|
)}
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user