feat(server): userDoc model (#9835)

close CLOUD-104
This commit is contained in:
fengmk2
2025-02-06 02:50:28 +00:00
parent b40f007ccf
commit 8e7cfb6115
3 changed files with 215 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
import { randomUUID } from 'node:crypto';
import ava, { TestFn } from 'ava';
import { Config } from '../../base/config';
import { type User, UserModel } from '../../models/user';
import { UserDocModel } from '../../models/user-doc';
import { createTestingModule, type TestingModule } from '../utils';
interface Context {
config: Config;
module: TestingModule;
user: UserModel;
doc: UserDocModel;
}
const test = ava as TestFn<Context>;
test.before(async t => {
const module = await createTestingModule();
t.context.user = module.get(UserModel);
t.context.doc = module.get(UserDocModel);
t.context.config = module.get(Config);
t.context.module = module;
});
let user: User;
test.beforeEach(async t => {
await t.context.module.initTestingDB();
user = await t.context.user.create({
email: 'test@affine.pro',
});
});
test.after(async t => {
await t.context.module.close();
});
test('should upsert a doc', async t => {
const docId = randomUUID();
const doc = await t.context.doc.upsert({
spaceId: user.id,
docId,
blob: Buffer.from('hello'),
timestamp: Date.now(),
editorId: user.id,
});
t.truthy(doc.updatedAt);
// add a new one
const docId2 = randomUUID();
const doc2 = await t.context.doc.upsert({
spaceId: user.id,
docId: docId2,
blob: Buffer.from('world'),
timestamp: Date.now(),
editorId: user.id,
});
t.truthy(doc2.updatedAt);
// update the first one
const doc3 = await t.context.doc.upsert({
spaceId: user.id,
docId,
blob: Buffer.from('world'),
timestamp: Date.now() + 1000,
editorId: user.id,
});
t.truthy(doc3.updatedAt);
t.true(doc3.updatedAt > doc.updatedAt);
// get all docs timestamps
const timestamps = await t.context.doc.findTimestampsByUserId(user.id);
t.deepEqual(timestamps, {
[docId]: doc3.updatedAt.getTime(),
[docId2]: doc2.updatedAt.getTime(),
});
});
test('should get a doc', async t => {
const docId = randomUUID();
const doc = await t.context.doc.upsert({
spaceId: user.id,
docId,
blob: Buffer.from('hello'),
timestamp: Date.now(),
editorId: user.id,
});
t.truthy(doc.updatedAt);
const doc2 = await t.context.doc.get(user.id, docId);
t.truthy(doc2);
t.is(doc2!.docId, docId);
t.deepEqual(doc2!.blob, Buffer.from('hello'));
// get a non-exist doc
const doc3 = await t.context.doc.get(user.id, randomUUID());
t.is(doc3, null);
});

View File

@@ -13,6 +13,7 @@ import { PageModel } from './page';
import { MODELS_SYMBOL } from './provider';
import { SessionModel } from './session';
import { UserModel } from './user';
import { UserDocModel } from './user-doc';
import { UserFeatureModel } from './user-feature';
import { VerificationTokenModel } from './verification-token';
import { WorkspaceModel } from './workspace';
@@ -28,6 +29,7 @@ const MODELS = {
userFeature: UserFeatureModel,
workspaceFeature: WorkspaceFeatureModel,
doc: DocModel,
userDoc: UserDocModel,
};
type ModelsType = {
@@ -85,6 +87,7 @@ export * from './feature';
export * from './page';
export * from './session';
export * from './user';
export * from './user-doc';
export * from './user-feature';
export * from './verification-token';
export * from './workspace';

View File

@@ -0,0 +1,116 @@
import { Injectable } from '@nestjs/common';
import { BaseModel } from './base';
import { Doc } from './common';
/**
* User Doc Model
*/
@Injectable()
export class UserDocModel extends BaseModel {
async upsert(doc: Doc) {
const row = await this.db.userSnapshot.upsert({
where: {
userId_id: {
userId: doc.spaceId,
id: doc.docId,
},
},
update: {
blob: doc.blob,
updatedAt: new Date(doc.timestamp),
},
create: {
userId: doc.spaceId,
id: doc.docId,
blob: doc.blob,
createdAt: new Date(doc.timestamp),
updatedAt: new Date(doc.timestamp),
},
select: {
updatedAt: true,
},
});
return row;
}
async get(userId: string, docId: string): Promise<Doc | null> {
const row = await this.db.userSnapshot.findUnique({
where: {
userId_id: {
userId,
id: docId,
},
},
});
if (!row) {
return null;
}
return {
spaceId: row.userId,
docId: row.id,
blob: row.blob,
timestamp: row.updatedAt.getTime(),
editorId: row.userId,
};
}
/**
* Find the timestamps of user docs by userId.
*
* @param after Only return timestamps after this timestamp.
*/
async findTimestampsByUserId(userId: string, after?: number) {
const snapshots = await this.db.userSnapshot.findMany({
select: {
id: true,
updatedAt: true,
},
where: {
userId,
...(after
? {
updatedAt: {
gt: new Date(after),
},
}
: {}),
},
});
const result: Record<string, number> = {};
snapshots.forEach(s => {
result[s.id] = s.updatedAt.getTime();
});
return result;
}
/**
* Delete a user doc by userId and docId.
*/
async delete(userId: string, docId: string) {
await this.db.userSnapshot.deleteMany({
where: {
userId,
id: docId,
},
});
this.logger.log(`Deleted user ${userId} doc ${docId}`);
}
/**
* Delete all user docs by userId.
*/
async deleteAllByUserId(userId: string) {
const { count } = await this.db.userSnapshot.deleteMany({
where: {
userId,
},
});
this.logger.log(`Deleted user ${userId} ${count} docs`);
return count;
}
}