feat(server): add settings resolver (#10797)

close CLOUD-166
This commit is contained in:
fengmk2
2025-03-18 00:41:21 +00:00
parent 5dcbae6f86
commit 3f0981a6fa
8 changed files with 231 additions and 4 deletions

View File

@@ -2,6 +2,7 @@ export * from './blobs';
export * from './invite';
export * from './notification';
export * from './permission';
export * from './settings';
export * from './testing-app';
export * from './testing-module';
export * from './user';

View File

@@ -0,0 +1,33 @@
import type { SettingsType, UpdateSettingsInput } from '../../core/user/types';
import type { TestingApp } from './testing-app';
export async function getSettings(app: TestingApp): Promise<SettingsType> {
const res = await app.gql(
`
query settings {
currentUser {
settings {
receiveInvitationEmail
receiveMentionEmail
}
}
}
`
);
return res.currentUser.settings;
}
export async function updateSettings(
app: TestingApp,
input: UpdateSettingsInput
): Promise<boolean> {
const res = await app.gql(
`
mutation updateSettings($input: UpdateSettingsInput!) {
updateSettings(input: $input)
}
`,
{ input }
);
return res.updateSettings;
}

View File

@@ -0,0 +1,86 @@
import test from 'ava';
import {
createTestingApp,
getSettings,
TestingApp,
updateSettings,
} from '../../../__tests__/utils';
let app: TestingApp;
test.before(async () => {
app = await createTestingApp();
});
test.after.always(async () => {
await app.close();
});
test('should get user settings', async t => {
await app.signup();
const settings = await getSettings(app);
t.deepEqual(settings, {
receiveInvitationEmail: true,
receiveMentionEmail: true,
});
});
test('should update user settings', async t => {
await app.signup();
await updateSettings(app, {
receiveInvitationEmail: false,
receiveMentionEmail: false,
});
const settings = await getSettings(app);
t.deepEqual(settings, {
receiveInvitationEmail: false,
receiveMentionEmail: false,
});
await updateSettings(app, {
receiveMentionEmail: true,
});
const settings2 = await getSettings(app);
t.deepEqual(settings2, {
receiveInvitationEmail: false,
receiveMentionEmail: true,
});
await updateSettings(app, {
// ignore undefined value
receiveInvitationEmail: undefined,
});
const settings3 = await getSettings(app);
t.deepEqual(settings3, {
receiveInvitationEmail: false,
receiveMentionEmail: true,
});
});
test('should throw error when update user settings with invalid input', async t => {
await app.signup();
await t.throwsAsync(
updateSettings(app, {
receiveInvitationEmail: false,
// @ts-expect-error invalid value
receiveMentionEmail: null,
}),
{
message: /Expected boolean, received null/,
}
);
});
test('should not update user settings when not logged in', async t => {
await app.logout();
await t.throwsAsync(
updateSettings(app, {
receiveInvitationEmail: false,
receiveMentionEmail: false,
}),
{
message: 'You must sign in first to access this resource.',
}
);
});

View File

@@ -4,11 +4,20 @@ import { PermissionModule } from '../permission';
import { StorageModule } from '../storage';
import { UserAvatarController } from './controller';
import { UserEventsListener } from './event';
import { UserManagementResolver, UserResolver } from './resolver';
import {
UserManagementResolver,
UserResolver,
UserSettingsResolver,
} from './resolver';
@Module({
imports: [StorageModule, PermissionModule],
providers: [UserResolver, UserManagementResolver, UserEventsListener],
providers: [
UserResolver,
UserManagementResolver,
UserEventsListener,
UserSettingsResolver,
],
controllers: [UserAvatarController],
})
export class UserModule {}

View File

@@ -7,6 +7,7 @@ import {
Mutation,
ObjectType,
Query,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { PrismaClient } from '@prisma/client';
@@ -19,7 +20,7 @@ import {
Throttle,
UserNotFound,
} from '../../base';
import { Models } from '../../models';
import { Models, SettingsSchema } from '../../models';
import { Public } from '../auth/guard';
import { sessionUser } from '../auth/service';
import { CurrentUser } from '../auth/session';
@@ -31,6 +32,8 @@ import {
ManageUserInput,
PublicUserType,
RemoveAvatar,
SettingsType,
UpdateSettingsInput,
UpdateUserInput,
UserOrLimitedUser,
UserType,
@@ -155,6 +158,33 @@ export class UserResolver {
}
}
@Resolver(() => UserType)
export class UserSettingsResolver {
constructor(private readonly models: Models) {}
@Mutation(() => Boolean, {
name: 'updateSettings',
description: 'Update user settings',
})
async updateSettings(
@CurrentUser() user: CurrentUser,
@Args('input', { type: () => UpdateSettingsInput })
input: UpdateSettingsInput
) {
SettingsSchema.parse(input);
await this.models.settings.set(user.id, input);
return true;
}
@ResolveField(() => SettingsType, {
name: 'settings',
description: 'Get user settings',
})
async getSettings(@CurrentUser() me: CurrentUser): Promise<SettingsType> {
return await this.models.settings.get(me.id);
}
}
@InputType()
class ListUserInput {
@Field(() => Int, { nullable: true, defaultValue: 0 })

View File

@@ -7,7 +7,12 @@ import {
} from '@nestjs/graphql';
import type { User } from '@prisma/client';
import { PublicUser, WorkspaceUser } from '../../models';
import {
PublicUser,
Settings,
SettingsInput,
WorkspaceUser,
} from '../../models';
import { type CurrentUser } from '../auth/session';
@ObjectType()
@@ -109,6 +114,15 @@ export class RemoveAvatar {
success!: boolean;
}
@ObjectType()
export class SettingsType implements Settings {
@Field({ description: 'Receive invitation email' })
receiveInvitationEmail!: boolean;
@Field({ description: 'Receive mention email' })
receiveMentionEmail!: boolean;
}
@InputType()
export class UpdateUserInput implements Partial<User> {
@Field({ description: 'User name', nullable: true })
@@ -123,3 +137,12 @@ export class ManageUserInput {
@Field({ description: 'User name', nullable: true })
name?: string;
}
@InputType()
export class UpdateSettingsInput implements SettingsInput {
@Field({ description: 'Receive invitation email', nullable: true })
receiveInvitationEmail?: boolean;
@Field({ description: 'Receive mention email', nullable: true })
receiveMentionEmail?: boolean;
}

View File

@@ -943,6 +943,9 @@ type Mutation {
"""update multiple server runtime configurable settings"""
updateRuntimeConfigs(updates: JSONObject!): [ServerRuntimeConfigType!]!
"""Update user settings"""
updateSettings(input: UpdateSettingsInput!): Boolean!
updateSubscriptionRecurring(idempotencyKey: String @deprecated(reason: "use header `Idempotency-Key`"), plan: SubscriptionPlan = Pro, recurring: SubscriptionRecurring!, workspaceId: String): SubscriptionType!
"""Update an user"""
@@ -1278,6 +1281,14 @@ type ServerServiceConfig {
name: String!
}
type SettingsType {
"""Receive invitation email"""
receiveInvitationEmail: Boolean!
"""Receive mention email"""
receiveMentionEmail: Boolean!
}
type SpaceAccessDeniedDataType {
spaceId: String!
}
@@ -1403,6 +1414,14 @@ input UpdateDocUserRoleInput {
workspaceId: String!
}
input UpdateSettingsInput {
"""Receive invitation email"""
receiveInvitationEmail: Boolean
"""Receive mention email"""
receiveMentionEmail: Boolean
}
input UpdateUserInput {
"""User name"""
name: String
@@ -1495,6 +1514,9 @@ type UserType {
notifications(pagination: PaginationInput!): PaginatedNotificationObjectType!
quota: UserQuotaType!
quotaUsage: UserQuotaUsageType!
"""Get user settings"""
settings: SettingsType!
subscriptions: [SubscriptionType!]!
token: tokenType! @deprecated(reason: "use [/api/auth/sign-in?native=true] instead")
}