@@ -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);
|
||||||
|
});
|
||||||
@@ -13,6 +13,7 @@ import { PageModel } from './page';
|
|||||||
import { MODELS_SYMBOL } from './provider';
|
import { MODELS_SYMBOL } from './provider';
|
||||||
import { SessionModel } from './session';
|
import { SessionModel } from './session';
|
||||||
import { UserModel } from './user';
|
import { UserModel } from './user';
|
||||||
|
import { UserDocModel } from './user-doc';
|
||||||
import { UserFeatureModel } from './user-feature';
|
import { UserFeatureModel } from './user-feature';
|
||||||
import { VerificationTokenModel } from './verification-token';
|
import { VerificationTokenModel } from './verification-token';
|
||||||
import { WorkspaceModel } from './workspace';
|
import { WorkspaceModel } from './workspace';
|
||||||
@@ -28,6 +29,7 @@ const MODELS = {
|
|||||||
userFeature: UserFeatureModel,
|
userFeature: UserFeatureModel,
|
||||||
workspaceFeature: WorkspaceFeatureModel,
|
workspaceFeature: WorkspaceFeatureModel,
|
||||||
doc: DocModel,
|
doc: DocModel,
|
||||||
|
userDoc: UserDocModel,
|
||||||
};
|
};
|
||||||
|
|
||||||
type ModelsType = {
|
type ModelsType = {
|
||||||
@@ -85,6 +87,7 @@ export * from './feature';
|
|||||||
export * from './page';
|
export * from './page';
|
||||||
export * from './session';
|
export * from './session';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
|
export * from './user-doc';
|
||||||
export * from './user-feature';
|
export * from './user-feature';
|
||||||
export * from './verification-token';
|
export * from './verification-token';
|
||||||
export * from './workspace';
|
export * from './workspace';
|
||||||
|
|||||||
116
packages/backend/server/src/models/user-doc.ts
Normal file
116
packages/backend/server/src/models/user-doc.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user