chore(admin): remove useless config diff (#12545)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added a GraphQL mutation to validate multiple app configuration updates, returning detailed validation results for each item.
  - Extended the API schema to support validation feedback, enabling client-side checks before applying changes.
  - Introduced a detailed, parameterized error message system for configuration validation errors.
  - Enabled validation of configuration inputs via the admin UI with clear, descriptive error messages.

- **Improvements**
  - Enhanced error reporting with specific, context-rich messages for invalid app configurations.
  - Simplified admin settings UI by removing the confirmation dialog and streamlining save actions.
  - Improved clarity and maintainability of validation logic and error handling components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
forehalo
2025-05-27 06:07:26 +00:00
parent eed95366c9
commit 2f139bd02c
19 changed files with 291 additions and 185 deletions
@@ -1,6 +1,7 @@
import test from 'ava';
import { createModule } from '../../../__tests__/create-module';
import { InvalidAppConfig } from '../../error';
import { ConfigFactory, ConfigModule } from '..';
import { Config } from '../config';
import { override } from '../register';
@@ -64,30 +65,29 @@ test('should override config', async t => {
test('should validate config', t => {
const config = module.get(ConfigFactory);
t.notThrows(() =>
t.is(
config.validate([
{
module: 'auth',
key: 'passwordRequirements',
value: { max: 10, min: 6 },
},
])
]),
null
);
t.throws(
() =>
config.validate([
{
module: 'auth',
key: 'passwordRequirements',
value: { max: 10, min: 10 },
},
]),
const [error] = config.validate([
{
message: `Invalid config for module [auth] with key [passwordRequirements]
Value: {"max":10,"min":10}
Error: Minimum length of password must be less than maximum length`,
}
module: 'auth',
key: 'passwordRequirements',
value: { max: 10, min: 10 },
},
])!;
t.true(error instanceof InvalidAppConfig);
t.is(
error.message,
'Invalid app config for module `auth` with key `passwordRequirements`. Minimum length of password must be less than maximum length.'
);
});
@@ -33,13 +33,17 @@ export class ConfigFactory {
}
validate(updates: Array<{ module: string; key: string; value: any }>) {
const errors: string[] = [];
const errors: InvalidAppConfig[] = [];
updates.forEach(update => {
const descriptor = APP_CONFIG_DESCRIPTORS[update.module]?.[update.key];
if (!descriptor) {
errors.push(
`Invalid config for module [${update.module}] with unknown key [${update.key}]`
new InvalidAppConfig({
module: update.module,
key: update.key,
hint: `Unknown config [${update.key}]`,
})
);
return;
}
@@ -47,16 +51,18 @@ export class ConfigFactory {
const { success, error } = descriptor.validate(update.value);
if (!success) {
error.issues.forEach(issue => {
errors.push(`Invalid config for module [${update.module}] with key [${update.key}]
Value: ${JSON.stringify(update.value)}
Error: ${issue.message}`);
errors.push(
new InvalidAppConfig({
module: update.module,
key: update.key,
hint: issue.message,
})
);
});
}
});
if (errors.length > 0) {
throw new InvalidAppConfig(errors.join('\n'));
}
return errors.length > 0 ? errors : null;
}
private loadDefault() {
@@ -877,7 +877,14 @@ export const USER_FRIENDLY_ERRORS = {
// app config
invalid_app_config: {
type: 'invalid_input',
message: 'Invalid app config.',
args: { module: 'string', key: 'string', hint: 'string' },
message: ({ module, key, hint }) =>
`Invalid app config for module \`${module}\` with key \`${key}\`. ${hint}.`,
},
invalid_app_config_input: {
type: 'invalid_input',
args: { message: 'string' },
message: ({ message }) => `Invalid app config input: ${message}`,
},
// indexer errors
@@ -1012,10 +1012,26 @@ export class MentionUserOneselfDenied extends UserFriendlyError {
super('action_forbidden', 'mention_user_oneself_denied', message);
}
}
@ObjectType()
class InvalidAppConfigDataType {
@Field() module!: string
@Field() key!: string
@Field() hint!: string
}
export class InvalidAppConfig extends UserFriendlyError {
constructor(message?: string) {
super('invalid_input', 'invalid_app_config', message);
constructor(args: InvalidAppConfigDataType, message?: string | ((args: InvalidAppConfigDataType) => string)) {
super('invalid_input', 'invalid_app_config', message, args);
}
}
@ObjectType()
class InvalidAppConfigInputDataType {
@Field() message!: string
}
export class InvalidAppConfigInput extends UserFriendlyError {
constructor(args: InvalidAppConfigInputDataType, message?: string | ((args: InvalidAppConfigInputDataType) => string)) {
super('invalid_input', 'invalid_app_config_input', message, args);
}
}
@@ -1176,6 +1192,7 @@ export enum ErrorNames {
MENTION_USER_DOC_ACCESS_DENIED,
MENTION_USER_ONESELF_DENIED,
INVALID_APP_CONFIG,
INVALID_APP_CONFIG_INPUT,
SEARCH_PROVIDER_NOT_FOUND,
INVALID_SEARCH_PROVIDER_REQUEST,
INVALID_INDEXER_INPUT
@@ -1187,5 +1204,5 @@ registerEnumType(ErrorNames, {
export const ErrorDataUnionType = createUnionType({
name: 'ErrorDataUnion',
types: () =>
[GraphqlBadRequestDataType, HttpRequestErrorDataType, QueryTooLongDataType, ValidationErrorDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, InvalidOauthCallbackCodeDataType, MissingOauthQueryParameterDataType, InvalidOauthResponseDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, DocUpdateBlockedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, NoMoreSeatDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderNotSupportedDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, CopilotFailedToMatchGlobalContextDataType, CopilotFailedToAddWorkspaceFileEmbeddingDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseToActivateDataType, InvalidLicenseUpdateParamsDataType, UnsupportedClientVersionDataType, MentionUserDocAccessDeniedDataType, InvalidSearchProviderRequestDataType, InvalidIndexerInputDataType] as const,
[GraphqlBadRequestDataType, HttpRequestErrorDataType, QueryTooLongDataType, ValidationErrorDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, InvalidOauthCallbackCodeDataType, MissingOauthQueryParameterDataType, InvalidOauthResponseDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, DocUpdateBlockedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, NoMoreSeatDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderNotSupportedDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, CopilotFailedToMatchGlobalContextDataType, CopilotFailedToAddWorkspaceFileEmbeddingDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseToActivateDataType, InvalidLicenseUpdateParamsDataType, UnsupportedClientVersionDataType, MentionUserDocAccessDeniedDataType, InvalidAppConfigDataType, InvalidAppConfigInputDataType, InvalidSearchProviderRequestDataType, InvalidIndexerInputDataType] as const,
});