mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 01:09:54 +08:00
@@ -0,0 +1,12 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "settings" (
|
||||||
|
"user_id" VARCHAR NOT NULL,
|
||||||
|
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||||
|
"payload" JSONB NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "settings_pkey" PRIMARY KEY ("user_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "settings" ADD CONSTRAINT "settings_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -39,6 +39,7 @@ model User {
|
|||||||
createdHistory SnapshotHistory[] @relation("createdHistory")
|
createdHistory SnapshotHistory[] @relation("createdHistory")
|
||||||
// receive notifications
|
// receive notifications
|
||||||
notifications Notification[] @relation("user_notifications")
|
notifications Notification[] @relation("user_notifications")
|
||||||
|
settings Settings?
|
||||||
|
|
||||||
@@index([email])
|
@@index([email])
|
||||||
@@map("users")
|
@@map("users")
|
||||||
@@ -706,3 +707,14 @@ model Notification {
|
|||||||
@@index([userId, createdAt, read])
|
@@index([userId, createdAt, read])
|
||||||
@@map("notifications")
|
@@map("notifications")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Settings {
|
||||||
|
userId String @id @map("user_id") @db.VarChar
|
||||||
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||||
|
payload Json @db.JsonB
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map("settings")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { mock } from 'node:test';
|
||||||
|
|
||||||
|
import ava, { TestFn } from 'ava';
|
||||||
|
import { ZodError } from 'zod';
|
||||||
|
|
||||||
|
import { createTestingModule, type TestingModule } from '../../__tests__/utils';
|
||||||
|
import { Config } from '../../base/config';
|
||||||
|
import { Models, User } from '..';
|
||||||
|
|
||||||
|
interface Context {
|
||||||
|
config: Config;
|
||||||
|
module: TestingModule;
|
||||||
|
models: Models;
|
||||||
|
}
|
||||||
|
|
||||||
|
const test = ava as TestFn<Context>;
|
||||||
|
|
||||||
|
test.before(async t => {
|
||||||
|
const module = await createTestingModule();
|
||||||
|
|
||||||
|
t.context.models = module.get(Models);
|
||||||
|
t.context.config = module.get(Config);
|
||||||
|
t.context.module = module;
|
||||||
|
await t.context.module.initTestingDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
let user: User;
|
||||||
|
|
||||||
|
test.beforeEach(async t => {
|
||||||
|
user = await t.context.models.user.create({
|
||||||
|
email: `test-${randomUUID()}@affine.pro`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach.always(() => {
|
||||||
|
mock.reset();
|
||||||
|
mock.timers.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after(async t => {
|
||||||
|
await t.context.module.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get a user settings with default value', async t => {
|
||||||
|
const settings = await t.context.models.settings.get(user.id);
|
||||||
|
t.deepEqual(settings, {
|
||||||
|
receiveInvitationEmail: true,
|
||||||
|
receiveMentionEmail: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update a user settings', async t => {
|
||||||
|
const settings = await t.context.models.settings.set(user.id, {
|
||||||
|
receiveInvitationEmail: false,
|
||||||
|
});
|
||||||
|
t.deepEqual(settings, {
|
||||||
|
receiveInvitationEmail: false,
|
||||||
|
receiveMentionEmail: true,
|
||||||
|
});
|
||||||
|
const settings2 = await t.context.models.settings.get(user.id);
|
||||||
|
t.deepEqual(settings2, settings);
|
||||||
|
|
||||||
|
// update existing setting
|
||||||
|
const setting3 = await t.context.models.settings.set(user.id, {
|
||||||
|
receiveInvitationEmail: true,
|
||||||
|
});
|
||||||
|
t.deepEqual(setting3, {
|
||||||
|
receiveInvitationEmail: true,
|
||||||
|
receiveMentionEmail: true,
|
||||||
|
});
|
||||||
|
const setting4 = await t.context.models.settings.get(user.id);
|
||||||
|
t.deepEqual(setting4, setting3);
|
||||||
|
|
||||||
|
const setting5 = await t.context.models.settings.set(user.id, {
|
||||||
|
receiveMentionEmail: false,
|
||||||
|
receiveInvitationEmail: false,
|
||||||
|
});
|
||||||
|
t.deepEqual(setting5, {
|
||||||
|
receiveInvitationEmail: false,
|
||||||
|
receiveMentionEmail: false,
|
||||||
|
});
|
||||||
|
const setting6 = await t.context.models.settings.get(user.id);
|
||||||
|
t.deepEqual(setting6, setting5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw error when update settings with invalid payload', async t => {
|
||||||
|
await t.throwsAsync(
|
||||||
|
t.context.models.settings.set(user.id, {
|
||||||
|
// @ts-expect-error invalid setting input types
|
||||||
|
receiveInvitationEmail: 1,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
instanceOf: ZodError,
|
||||||
|
message: /Expected boolean, received number/,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -14,6 +14,7 @@ import { HistoryModel } from './history';
|
|||||||
import { NotificationModel } from './notification';
|
import { NotificationModel } from './notification';
|
||||||
import { MODELS_SYMBOL } from './provider';
|
import { MODELS_SYMBOL } from './provider';
|
||||||
import { SessionModel } from './session';
|
import { SessionModel } from './session';
|
||||||
|
import { SettingsModel } from './settings';
|
||||||
import { UserModel } from './user';
|
import { UserModel } from './user';
|
||||||
import { UserDocModel } from './user-doc';
|
import { UserDocModel } from './user-doc';
|
||||||
import { UserFeatureModel } from './user-feature';
|
import { UserFeatureModel } from './user-feature';
|
||||||
@@ -36,6 +37,7 @@ const MODELS = {
|
|||||||
docUser: DocUserModel,
|
docUser: DocUserModel,
|
||||||
history: HistoryModel,
|
history: HistoryModel,
|
||||||
notification: NotificationModel,
|
notification: NotificationModel,
|
||||||
|
settings: SettingsModel,
|
||||||
};
|
};
|
||||||
|
|
||||||
type ModelsType = {
|
type ModelsType = {
|
||||||
@@ -94,6 +96,7 @@ export * from './feature';
|
|||||||
export * from './history';
|
export * from './history';
|
||||||
export * from './notification';
|
export * from './notification';
|
||||||
export * from './session';
|
export * from './session';
|
||||||
|
export * from './settings';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
export * from './user-doc';
|
export * from './user-doc';
|
||||||
export * from './user-feature';
|
export * from './user-feature';
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Transactional } from '@nestjs-cls/transactional';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
import { BaseModel } from './base';
|
||||||
|
|
||||||
|
export const SettingsSchema = z.object({
|
||||||
|
receiveInvitationEmail: z.boolean().default(true),
|
||||||
|
receiveMentionEmail: z.boolean().default(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SettingsInput = z.input<typeof SettingsSchema>;
|
||||||
|
export type Settings = z.infer<typeof SettingsSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings Model
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class SettingsModel extends BaseModel {
|
||||||
|
@Transactional()
|
||||||
|
async set(userId: string, setting: SettingsInput) {
|
||||||
|
const existsSetting = await this.get(userId);
|
||||||
|
const payload = SettingsSchema.parse({
|
||||||
|
...existsSetting,
|
||||||
|
...setting,
|
||||||
|
});
|
||||||
|
await this.db.settings.upsert({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
payload,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
userId,
|
||||||
|
payload,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logger.log(`Settings updated for user ${userId}`);
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(userId: string): Promise<Settings> {
|
||||||
|
const row = await this.db.settings.findUnique({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return SettingsSchema.parse(row?.payload ?? {});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user