mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
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:
@@ -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,
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import Sinon from 'sinon';
|
||||
|
||||
import { createModule } from '../../../__tests__/create-module';
|
||||
import { Mockers } from '../../../__tests__/mocks';
|
||||
import { InvalidAppConfigInput } from '../../../base';
|
||||
import { Models } from '../../../models';
|
||||
import { ServerService } from '../service';
|
||||
|
||||
@@ -47,9 +48,7 @@ test('should validate config before update', async t => {
|
||||
},
|
||||
]),
|
||||
{
|
||||
message: `Invalid config for module [server] with key [externalUrl]
|
||||
Value: "invalid-url@some-domain.com"
|
||||
Error: Invalid url`,
|
||||
instanceOf: InvalidAppConfigInput,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -64,7 +63,7 @@ Error: Invalid url`,
|
||||
},
|
||||
]),
|
||||
{
|
||||
message: `Invalid config for module [auth] with unknown key [unknown-key]`,
|
||||
instanceOf: InvalidAppConfigInput,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -182,6 +182,24 @@ class UpdateAppConfigInput {
|
||||
value!: any;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
class AppConfigValidateResult {
|
||||
@Field()
|
||||
module!: string;
|
||||
|
||||
@Field()
|
||||
key!: string;
|
||||
|
||||
@Field(() => GraphQLJSON)
|
||||
value!: any;
|
||||
|
||||
@Field()
|
||||
valid!: boolean;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@Admin()
|
||||
@Resolver(() => GraphQLJSONObject)
|
||||
export class AppConfigResolver {
|
||||
@@ -204,4 +222,28 @@ export class AppConfigResolver {
|
||||
): Promise<DeepPartial<AppConfig>> {
|
||||
return await this.service.updateConfig(me.id, updates);
|
||||
}
|
||||
|
||||
@Mutation(() => [AppConfigValidateResult], {
|
||||
description: 'validate app configuration',
|
||||
})
|
||||
async validateAppConfig(
|
||||
@Args('updates', { type: () => [UpdateAppConfigInput] })
|
||||
updates: UpdateAppConfigInput[]
|
||||
): Promise<AppConfigValidateResult[]> {
|
||||
const errors = this.service.validateConfig(updates);
|
||||
|
||||
return updates.map(update => {
|
||||
const error = errors?.find(
|
||||
error =>
|
||||
error.data.module === update.module && error.data.key === update.key
|
||||
);
|
||||
return {
|
||||
module: update.module,
|
||||
key: update.key,
|
||||
value: update.value,
|
||||
valid: !error,
|
||||
error: error?.data.hint,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
||||
import { set } from 'lodash-es';
|
||||
|
||||
import { ConfigFactory, EventBus, OnEvent } from '../../base';
|
||||
import {
|
||||
ConfigFactory,
|
||||
EventBus,
|
||||
InvalidAppConfigInput,
|
||||
OnEvent,
|
||||
} from '../../base';
|
||||
import { Models } from '../../models';
|
||||
import { ServerFeature } from './types';
|
||||
|
||||
@@ -60,11 +65,21 @@ export class ServerService implements OnApplicationBootstrap {
|
||||
return this.configFactory.clone();
|
||||
}
|
||||
|
||||
validateConfig(updates: Array<{ module: string; key: string; value: any }>) {
|
||||
return this.configFactory.validate(updates);
|
||||
}
|
||||
|
||||
async updateConfig(
|
||||
user: string,
|
||||
updates: Array<{ module: string; key: string; value: any }>
|
||||
): Promise<DeepPartial<AppConfig>> {
|
||||
this.configFactory.validate(updates);
|
||||
const errors = this.configFactory.validate(updates);
|
||||
|
||||
if (errors?.length) {
|
||||
throw new InvalidAppConfigInput({
|
||||
message: errors.map(error => error.message).join('\n'),
|
||||
});
|
||||
}
|
||||
|
||||
const promises = await this.models.appConfig.save(
|
||||
user,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { resolve } from 'node:path';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { Command, CommandRunner } from 'nest-commander';
|
||||
|
||||
import { ConfigFactory } from '../../base';
|
||||
import { ConfigFactory, InvalidAppConfigInput } from '../../base';
|
||||
import { Models } from '../../models';
|
||||
|
||||
@Command({
|
||||
@@ -50,7 +50,13 @@ export class ImportConfigCommand extends CommandRunner {
|
||||
});
|
||||
});
|
||||
|
||||
this.configFactory.validate(forValidation);
|
||||
const errors = this.configFactory.validate(forValidation);
|
||||
|
||||
if (errors?.length) {
|
||||
throw new InvalidAppConfigInput({
|
||||
message: errors.map(error => error.message).join('\n '),
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-expect-error null as user id
|
||||
await this.models.appConfig.save(null, forSaving);
|
||||
|
||||
@@ -71,6 +71,14 @@ type AlreadyInSpaceDataType {
|
||||
spaceId: String!
|
||||
}
|
||||
|
||||
type AppConfigValidateResult {
|
||||
error: String
|
||||
key: String!
|
||||
module: String!
|
||||
valid: Boolean!
|
||||
value: JSON!
|
||||
}
|
||||
|
||||
type BlobNotFoundDataType {
|
||||
blobId: String!
|
||||
spaceId: String!
|
||||
@@ -523,7 +531,7 @@ type EditorType {
|
||||
name: String!
|
||||
}
|
||||
|
||||
union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotContextFileNotSupportedDataType | CopilotDocNotFoundDataType | CopilotFailedToAddWorkspaceFileEmbeddingDataType | CopilotFailedToMatchContextDataType | CopilotFailedToMatchGlobalContextDataType | CopilotFailedToModifyContextDataType | CopilotInvalidContextDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderNotSupportedDataType | CopilotProviderSideErrorDataType | DocActionDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | DocUpdateBlockedDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | GraphqlBadRequestDataType | HttpRequestErrorDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidIndexerInputDataType | InvalidLicenseToActivateDataType | InvalidLicenseUpdateParamsDataType | InvalidOauthCallbackCodeDataType | InvalidOauthResponseDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | InvalidSearchProviderRequestDataType | MemberNotFoundInSpaceDataType | MentionUserDocAccessDeniedDataType | MissingOauthQueryParameterDataType | NoMoreSeatDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | ValidationErrorDataType | VersionRejectedDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType
|
||||
union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotContextFileNotSupportedDataType | CopilotDocNotFoundDataType | CopilotFailedToAddWorkspaceFileEmbeddingDataType | CopilotFailedToMatchContextDataType | CopilotFailedToMatchGlobalContextDataType | CopilotFailedToModifyContextDataType | CopilotInvalidContextDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderNotSupportedDataType | CopilotProviderSideErrorDataType | DocActionDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | DocUpdateBlockedDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | GraphqlBadRequestDataType | HttpRequestErrorDataType | InvalidAppConfigDataType | InvalidAppConfigInputDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidIndexerInputDataType | InvalidLicenseToActivateDataType | InvalidLicenseUpdateParamsDataType | InvalidOauthCallbackCodeDataType | InvalidOauthResponseDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | InvalidSearchProviderRequestDataType | MemberNotFoundInSpaceDataType | MentionUserDocAccessDeniedDataType | MissingOauthQueryParameterDataType | NoMoreSeatDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | ValidationErrorDataType | VersionRejectedDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType
|
||||
|
||||
enum ErrorNames {
|
||||
ACCESS_DENIED
|
||||
@@ -589,6 +597,7 @@ enum ErrorNames {
|
||||
HTTP_REQUEST_ERROR
|
||||
INTERNAL_SERVER_ERROR
|
||||
INVALID_APP_CONFIG
|
||||
INVALID_APP_CONFIG_INPUT
|
||||
INVALID_AUTH_STATE
|
||||
INVALID_CHECKOUT_PARAMETERS
|
||||
INVALID_EMAIL
|
||||
@@ -729,6 +738,16 @@ input ImportUsersInput {
|
||||
users: [CreateUserInput!]!
|
||||
}
|
||||
|
||||
type InvalidAppConfigDataType {
|
||||
hint: String!
|
||||
key: String!
|
||||
module: String!
|
||||
}
|
||||
|
||||
type InvalidAppConfigInputDataType {
|
||||
message: String!
|
||||
}
|
||||
|
||||
type InvalidEmailDataType {
|
||||
email: String!
|
||||
}
|
||||
@@ -1198,6 +1217,9 @@ type Mutation {
|
||||
|
||||
"""Upload user avatar"""
|
||||
uploadAvatar(avatar: Upload!): UserType!
|
||||
|
||||
"""validate app configuration"""
|
||||
validateAppConfig(updates: [UpdateAppConfigInput!]!): [AppConfigValidateResult!]!
|
||||
verifyEmail(token: String!): Boolean!
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user