mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-24 04:27:27 +08:00
refactor(server): split HistoryModel from DocModel (#10604)
This commit is contained in:
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
|
|||||||
import ava, { TestFn } from 'ava';
|
import ava, { TestFn } from 'ava';
|
||||||
|
|
||||||
import { Config } from '../../base/config';
|
import { Config } from '../../base/config';
|
||||||
import { PublicDocMode } from '../../models';
|
import { HistoryModel, PublicDocMode } from '../../models';
|
||||||
import { DocModel } from '../../models/doc';
|
import { DocModel } from '../../models/doc';
|
||||||
import { type User, UserModel } from '../../models/user';
|
import { type User, UserModel } from '../../models/user';
|
||||||
import { type Workspace, WorkspaceModel } from '../../models/workspace';
|
import { type Workspace, WorkspaceModel } from '../../models/workspace';
|
||||||
@@ -15,6 +15,7 @@ interface Context {
|
|||||||
user: UserModel;
|
user: UserModel;
|
||||||
workspace: WorkspaceModel;
|
workspace: WorkspaceModel;
|
||||||
doc: DocModel;
|
doc: DocModel;
|
||||||
|
history: HistoryModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
const test = ava as TestFn<Context>;
|
const test = ava as TestFn<Context>;
|
||||||
@@ -25,6 +26,7 @@ test.before(async t => {
|
|||||||
t.context.user = module.get(UserModel);
|
t.context.user = module.get(UserModel);
|
||||||
t.context.workspace = module.get(WorkspaceModel);
|
t.context.workspace = module.get(WorkspaceModel);
|
||||||
t.context.doc = module.get(DocModel);
|
t.context.doc = module.get(DocModel);
|
||||||
|
t.context.history = module.get(HistoryModel);
|
||||||
t.context.config = module.get(Config);
|
t.context.config = module.get(Config);
|
||||||
t.context.module = module;
|
t.context.module = module;
|
||||||
});
|
});
|
||||||
@@ -290,136 +292,6 @@ test('should get a doc authors', async t => {
|
|||||||
t.is(notFoundMeta, null);
|
t.is(notFoundMeta, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create a history record', async t => {
|
|
||||||
const snapshot = {
|
|
||||||
spaceId: workspace.id,
|
|
||||||
docId: randomUUID(),
|
|
||||||
blob: Buffer.from('blob1'),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
editorId: user.id,
|
|
||||||
};
|
|
||||||
await t.context.doc.upsert(snapshot);
|
|
||||||
const created = await t.context.doc.createHistory(snapshot, 1000);
|
|
||||||
t.truthy(created);
|
|
||||||
t.deepEqual(created.timestamp, snapshot.timestamp);
|
|
||||||
t.deepEqual(created.editor, {
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
avatarUrl: user.avatarUrl,
|
|
||||||
});
|
|
||||||
const history = await t.context.doc.getHistory(
|
|
||||||
snapshot.spaceId,
|
|
||||||
snapshot.docId,
|
|
||||||
snapshot.timestamp
|
|
||||||
);
|
|
||||||
t.deepEqual(history, {
|
|
||||||
...created,
|
|
||||||
blob: snapshot.blob,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return null when history timestamp not match', async t => {
|
|
||||||
const snapshot = {
|
|
||||||
spaceId: workspace.id,
|
|
||||||
docId: randomUUID(),
|
|
||||||
blob: Buffer.from('blob1'),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
editorId: user.id,
|
|
||||||
};
|
|
||||||
await t.context.doc.upsert(snapshot);
|
|
||||||
await t.context.doc.createHistory(snapshot, 1000);
|
|
||||||
const history = await t.context.doc.getHistory(
|
|
||||||
snapshot.spaceId,
|
|
||||||
snapshot.docId,
|
|
||||||
snapshot.timestamp + 1
|
|
||||||
);
|
|
||||||
t.is(history, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should find history records', async t => {
|
|
||||||
const docId = randomUUID();
|
|
||||||
const snapshot1 = {
|
|
||||||
spaceId: workspace.id,
|
|
||||||
docId,
|
|
||||||
blob: Buffer.from('blob1'),
|
|
||||||
timestamp: Date.now() - 1000,
|
|
||||||
editorId: user.id,
|
|
||||||
};
|
|
||||||
const snapshot2 = {
|
|
||||||
spaceId: workspace.id,
|
|
||||||
docId,
|
|
||||||
blob: Buffer.from('blob2'),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
editorId: user.id,
|
|
||||||
};
|
|
||||||
await t.context.doc.createHistory(snapshot1, 1000);
|
|
||||||
await t.context.doc.createHistory(snapshot2, 1000);
|
|
||||||
let histories = await t.context.doc.findHistories(workspace.id, docId);
|
|
||||||
t.is(histories.length, 2);
|
|
||||||
t.deepEqual(histories[0].timestamp, snapshot2.timestamp);
|
|
||||||
t.deepEqual(histories[0].editor, {
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
avatarUrl: user.avatarUrl,
|
|
||||||
});
|
|
||||||
t.deepEqual(histories[1].timestamp, snapshot1.timestamp);
|
|
||||||
t.deepEqual(histories[1].editor, {
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
avatarUrl: user.avatarUrl,
|
|
||||||
});
|
|
||||||
// only take 1 history, order by timestamp desc
|
|
||||||
histories = await t.context.doc.findHistories(workspace.id, docId, {
|
|
||||||
take: 1,
|
|
||||||
});
|
|
||||||
t.is(histories.length, 1);
|
|
||||||
t.deepEqual(histories[0].timestamp, snapshot2.timestamp);
|
|
||||||
t.deepEqual(histories[0].editor, {
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
avatarUrl: user.avatarUrl,
|
|
||||||
});
|
|
||||||
// get empty history
|
|
||||||
histories = await t.context.doc.findHistories(workspace.id, docId, {
|
|
||||||
before: Date.now() - 1000000,
|
|
||||||
});
|
|
||||||
t.is(histories.length, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should get latest history', async t => {
|
|
||||||
const docId = randomUUID();
|
|
||||||
const snapshot1 = {
|
|
||||||
spaceId: workspace.id,
|
|
||||||
docId,
|
|
||||||
blob: Buffer.from('blob1'),
|
|
||||||
timestamp: Date.now() - 1000,
|
|
||||||
editorId: user.id,
|
|
||||||
};
|
|
||||||
const snapshot2 = {
|
|
||||||
spaceId: workspace.id,
|
|
||||||
docId,
|
|
||||||
blob: Buffer.from('blob2'),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
editorId: user.id,
|
|
||||||
};
|
|
||||||
await t.context.doc.createHistory(snapshot1, 1000);
|
|
||||||
await t.context.doc.createHistory(snapshot2, 1000);
|
|
||||||
const history = await t.context.doc.getLatestHistory(workspace.id, docId);
|
|
||||||
t.truthy(history);
|
|
||||||
t.deepEqual(history!.timestamp, snapshot2.timestamp);
|
|
||||||
t.deepEqual(history!.editor, {
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
avatarUrl: user.avatarUrl,
|
|
||||||
});
|
|
||||||
// return null when no history
|
|
||||||
const emptyHistory = await t.context.doc.getLatestHistory(
|
|
||||||
workspace.id,
|
|
||||||
randomUUID()
|
|
||||||
);
|
|
||||||
t.is(emptyHistory, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should delete a doc, including histories, snapshots and updates', async t => {
|
test('should delete a doc, including histories, snapshots and updates', async t => {
|
||||||
const docId = randomUUID();
|
const docId = randomUUID();
|
||||||
const snapshot = {
|
const snapshot = {
|
||||||
@@ -430,7 +302,7 @@ test('should delete a doc, including histories, snapshots and updates', async t
|
|||||||
editorId: user.id,
|
editorId: user.id,
|
||||||
};
|
};
|
||||||
await t.context.doc.upsert(snapshot);
|
await t.context.doc.upsert(snapshot);
|
||||||
await t.context.doc.createHistory(snapshot, 1000);
|
await t.context.history.create(snapshot, 1000);
|
||||||
await t.context.doc.createUpdates([
|
await t.context.doc.createUpdates([
|
||||||
{
|
{
|
||||||
spaceId: workspace.id,
|
spaceId: workspace.id,
|
||||||
@@ -443,10 +315,7 @@ test('should delete a doc, including histories, snapshots and updates', async t
|
|||||||
await t.context.doc.delete(workspace.id, docId);
|
await t.context.doc.delete(workspace.id, docId);
|
||||||
const foundSnapshot = await t.context.doc.get(workspace.id, docId);
|
const foundSnapshot = await t.context.doc.get(workspace.id, docId);
|
||||||
t.is(foundSnapshot, null);
|
t.is(foundSnapshot, null);
|
||||||
const foundHistory = await t.context.doc.getLatestHistory(
|
const foundHistory = await t.context.history.getLatest(workspace.id, docId);
|
||||||
workspace.id,
|
|
||||||
docId
|
|
||||||
);
|
|
||||||
t.is(foundHistory, null);
|
t.is(foundHistory, null);
|
||||||
const foundUpdates = await t.context.doc.findUpdates(workspace.id, docId);
|
const foundUpdates = await t.context.doc.findUpdates(workspace.id, docId);
|
||||||
t.is(foundUpdates.length, 0);
|
t.is(foundUpdates.length, 0);
|
||||||
@@ -470,7 +339,7 @@ test('should delete all docs in a workspace', async t => {
|
|||||||
editorId: user.id,
|
editorId: user.id,
|
||||||
};
|
};
|
||||||
await t.context.doc.upsert(snapshot1);
|
await t.context.doc.upsert(snapshot1);
|
||||||
await t.context.doc.createHistory(snapshot1, 1000);
|
await t.context.history.create(snapshot1, 1000);
|
||||||
await t.context.doc.createUpdates([
|
await t.context.doc.createUpdates([
|
||||||
{
|
{
|
||||||
spaceId: workspace.id,
|
spaceId: workspace.id,
|
||||||
@@ -481,7 +350,7 @@ test('should delete all docs in a workspace', async t => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
await t.context.doc.upsert(snapshot2);
|
await t.context.doc.upsert(snapshot2);
|
||||||
await t.context.doc.createHistory(snapshot2, 1000);
|
await t.context.history.create(snapshot2, 1000);
|
||||||
await t.context.doc.createUpdates([
|
await t.context.doc.createUpdates([
|
||||||
{
|
{
|
||||||
spaceId: workspace.id,
|
spaceId: workspace.id,
|
||||||
@@ -495,19 +364,13 @@ test('should delete all docs in a workspace', async t => {
|
|||||||
t.is(deletedCount, 2);
|
t.is(deletedCount, 2);
|
||||||
const foundSnapshot1 = await t.context.doc.get(workspace.id, docId1);
|
const foundSnapshot1 = await t.context.doc.get(workspace.id, docId1);
|
||||||
t.is(foundSnapshot1, null);
|
t.is(foundSnapshot1, null);
|
||||||
const foundHistory1 = await t.context.doc.getLatestHistory(
|
const foundHistory1 = await t.context.history.getLatest(workspace.id, docId1);
|
||||||
workspace.id,
|
|
||||||
docId1
|
|
||||||
);
|
|
||||||
t.is(foundHistory1, null);
|
t.is(foundHistory1, null);
|
||||||
const foundUpdates1 = await t.context.doc.findUpdates(workspace.id, docId1);
|
const foundUpdates1 = await t.context.doc.findUpdates(workspace.id, docId1);
|
||||||
t.is(foundUpdates1.length, 0);
|
t.is(foundUpdates1.length, 0);
|
||||||
const foundSnapshot2 = await t.context.doc.get(workspace.id, docId2);
|
const foundSnapshot2 = await t.context.doc.get(workspace.id, docId2);
|
||||||
t.is(foundSnapshot2, null);
|
t.is(foundSnapshot2, null);
|
||||||
const foundHistory2 = await t.context.doc.getLatestHistory(
|
const foundHistory2 = await t.context.history.getLatest(workspace.id, docId2);
|
||||||
workspace.id,
|
|
||||||
docId2
|
|
||||||
);
|
|
||||||
t.is(foundHistory2, null);
|
t.is(foundHistory2, null);
|
||||||
const foundUpdates2 = await t.context.doc.findUpdates(workspace.id, docId2);
|
const foundUpdates2 = await t.context.doc.findUpdates(workspace.id, docId2);
|
||||||
t.is(foundUpdates2.length, 0);
|
t.is(foundUpdates2.length, 0);
|
||||||
|
|||||||
@@ -171,14 +171,14 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
docId: string,
|
docId: string,
|
||||||
query: HistoryFilter
|
query: HistoryFilter
|
||||||
) {
|
) {
|
||||||
return await this.models.doc.findHistories(workspaceId, docId, {
|
return await this.models.history.findMany(workspaceId, docId, {
|
||||||
before: query.before,
|
before: query.before,
|
||||||
take: query.limit,
|
take: query.limit,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDocHistory(workspaceId: string, docId: string, timestamp: number) {
|
async getDocHistory(workspaceId: string, docId: string, timestamp: number) {
|
||||||
const history = await this.models.doc.getHistory(
|
const history = await this.models.history.get(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
docId,
|
docId,
|
||||||
timestamp
|
timestamp
|
||||||
@@ -282,7 +282,7 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.models.doc.createHistory(
|
await this.models.history.create(
|
||||||
{
|
{
|
||||||
spaceId: snapshot.spaceId,
|
spaceId: snapshot.spaceId,
|
||||||
docId: snapshot.docId,
|
docId: snapshot.docId,
|
||||||
@@ -372,7 +372,7 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async lastDocHistory(workspaceId: string, id: string) {
|
protected async lastDocHistory(workspaceId: string, id: string) {
|
||||||
return this.models.doc.getLatestHistory(workspaceId, id);
|
return this.models.history.getLatest(workspaceId, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// for auto merging
|
// for auto merging
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export class DocStorageCronJob {
|
|||||||
|
|
||||||
@OnJob('nightly.cleanExpiredHistories')
|
@OnJob('nightly.cleanExpiredHistories')
|
||||||
async cleanExpiredHistories() {
|
async cleanExpiredHistories() {
|
||||||
await this.models.doc.deleteExpiredHistories();
|
await this.models.history.cleanExpired();
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent('user.deleted')
|
@OnEvent('user.deleted')
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
|
import ava, { TestFn } from 'ava';
|
||||||
|
|
||||||
|
import { createTestingModule, type TestingModule } from '../../__tests__/utils';
|
||||||
|
import { Config } from '../../base';
|
||||||
|
import { DocModel } from '../doc';
|
||||||
|
import { HistoryModel } from '../history';
|
||||||
|
import { type User, UserModel } from '../user';
|
||||||
|
import { type Workspace, WorkspaceModel } from '../workspace';
|
||||||
|
|
||||||
|
interface Context {
|
||||||
|
config: Config;
|
||||||
|
module: TestingModule;
|
||||||
|
user: UserModel;
|
||||||
|
workspace: WorkspaceModel;
|
||||||
|
doc: DocModel;
|
||||||
|
history: HistoryModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const test = ava as TestFn<Context>;
|
||||||
|
|
||||||
|
test.before(async t => {
|
||||||
|
const module = await createTestingModule();
|
||||||
|
|
||||||
|
t.context.user = module.get(UserModel);
|
||||||
|
t.context.workspace = module.get(WorkspaceModel);
|
||||||
|
t.context.doc = module.get(DocModel);
|
||||||
|
t.context.history = module.get(HistoryModel);
|
||||||
|
t.context.config = module.get(Config);
|
||||||
|
t.context.module = module;
|
||||||
|
});
|
||||||
|
|
||||||
|
let user: User;
|
||||||
|
let workspace: Workspace;
|
||||||
|
|
||||||
|
test.beforeEach(async t => {
|
||||||
|
await t.context.module.initTestingDB();
|
||||||
|
user = await t.context.user.create({
|
||||||
|
email: 'test@affine.pro',
|
||||||
|
});
|
||||||
|
workspace = await t.context.workspace.create(user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after(async t => {
|
||||||
|
await t.context.module.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create a history record', async t => {
|
||||||
|
const snapshot = {
|
||||||
|
spaceId: workspace.id,
|
||||||
|
docId: randomUUID(),
|
||||||
|
blob: Buffer.from('blob1'),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
editorId: user.id,
|
||||||
|
};
|
||||||
|
await t.context.doc.upsert(snapshot);
|
||||||
|
const created = await t.context.history.create(snapshot, 1000);
|
||||||
|
t.truthy(created);
|
||||||
|
t.deepEqual(created.timestamp, snapshot.timestamp);
|
||||||
|
t.deepEqual(created.editor, {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
avatarUrl: user.avatarUrl,
|
||||||
|
});
|
||||||
|
const history = await t.context.history.get(
|
||||||
|
snapshot.spaceId,
|
||||||
|
snapshot.docId,
|
||||||
|
snapshot.timestamp
|
||||||
|
);
|
||||||
|
t.deepEqual(history, {
|
||||||
|
...created,
|
||||||
|
blob: snapshot.blob,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null when history timestamp not match', async t => {
|
||||||
|
const snapshot = {
|
||||||
|
spaceId: workspace.id,
|
||||||
|
docId: randomUUID(),
|
||||||
|
blob: Buffer.from('blob1'),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
editorId: user.id,
|
||||||
|
};
|
||||||
|
await t.context.doc.upsert(snapshot);
|
||||||
|
await t.context.history.create(snapshot, 1000);
|
||||||
|
const history = await t.context.history.get(
|
||||||
|
snapshot.spaceId,
|
||||||
|
snapshot.docId,
|
||||||
|
snapshot.timestamp + 1
|
||||||
|
);
|
||||||
|
t.is(history, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find history records', async t => {
|
||||||
|
const docId = randomUUID();
|
||||||
|
const snapshot1 = {
|
||||||
|
spaceId: workspace.id,
|
||||||
|
docId,
|
||||||
|
blob: Buffer.from('blob1'),
|
||||||
|
timestamp: Date.now() - 1000,
|
||||||
|
editorId: user.id,
|
||||||
|
};
|
||||||
|
const snapshot2 = {
|
||||||
|
spaceId: workspace.id,
|
||||||
|
docId,
|
||||||
|
blob: Buffer.from('blob2'),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
editorId: user.id,
|
||||||
|
};
|
||||||
|
await t.context.history.create(snapshot1, 1000);
|
||||||
|
await t.context.history.create(snapshot2, 1000);
|
||||||
|
let histories = await t.context.history.findMany(workspace.id, docId);
|
||||||
|
t.is(histories.length, 2);
|
||||||
|
t.deepEqual(histories[0].timestamp, snapshot2.timestamp);
|
||||||
|
t.deepEqual(histories[0].editor, {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
avatarUrl: user.avatarUrl,
|
||||||
|
});
|
||||||
|
t.deepEqual(histories[1].timestamp, snapshot1.timestamp);
|
||||||
|
t.deepEqual(histories[1].editor, {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
avatarUrl: user.avatarUrl,
|
||||||
|
});
|
||||||
|
// only take 1 history, order by timestamp desc
|
||||||
|
histories = await t.context.history.findMany(workspace.id, docId, {
|
||||||
|
take: 1,
|
||||||
|
});
|
||||||
|
t.is(histories.length, 1);
|
||||||
|
t.deepEqual(histories[0].timestamp, snapshot2.timestamp);
|
||||||
|
t.deepEqual(histories[0].editor, {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
avatarUrl: user.avatarUrl,
|
||||||
|
});
|
||||||
|
// get empty history
|
||||||
|
histories = await t.context.history.findMany(workspace.id, docId, {
|
||||||
|
before: Date.now() - 1000000,
|
||||||
|
});
|
||||||
|
t.is(histories.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get latest history', async t => {
|
||||||
|
const docId = randomUUID();
|
||||||
|
const snapshot1 = {
|
||||||
|
spaceId: workspace.id,
|
||||||
|
docId,
|
||||||
|
blob: Buffer.from('blob1'),
|
||||||
|
timestamp: Date.now() - 1000,
|
||||||
|
editorId: user.id,
|
||||||
|
};
|
||||||
|
const snapshot2 = {
|
||||||
|
spaceId: workspace.id,
|
||||||
|
docId,
|
||||||
|
blob: Buffer.from('blob2'),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
editorId: user.id,
|
||||||
|
};
|
||||||
|
await t.context.history.create(snapshot1, 1000);
|
||||||
|
await t.context.history.create(snapshot2, 1000);
|
||||||
|
const history = await t.context.history.getLatest(workspace.id, docId);
|
||||||
|
t.truthy(history);
|
||||||
|
t.deepEqual(history!.timestamp, snapshot2.timestamp);
|
||||||
|
t.deepEqual(history!.editor, {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
avatarUrl: user.avatarUrl,
|
||||||
|
});
|
||||||
|
// return null when no history
|
||||||
|
const emptyHistory = await t.context.history.getLatest(
|
||||||
|
workspace.id,
|
||||||
|
randomUUID()
|
||||||
|
);
|
||||||
|
t.is(emptyHistory, null);
|
||||||
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './doc';
|
export * from './doc';
|
||||||
export * from './feature';
|
export * from './feature';
|
||||||
export * from './role';
|
export * from './role';
|
||||||
|
export * from './user';
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
export const publicUserSelect = {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
avatarUrl: true,
|
||||||
|
} satisfies Prisma.UserSelect;
|
||||||
@@ -4,34 +4,10 @@ import type { Update } from '@prisma/client';
|
|||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
import { BaseModel } from './base';
|
import { BaseModel } from './base';
|
||||||
import type { Doc, DocEditor } from './common';
|
import { Doc, publicUserSelect } from './common';
|
||||||
|
|
||||||
export interface DocRecord extends Doc {}
|
export interface DocRecord extends Doc {}
|
||||||
|
|
||||||
export interface DocHistorySimple {
|
|
||||||
timestamp: number;
|
|
||||||
editor: DocEditor | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DocHistory {
|
|
||||||
blob: Buffer;
|
|
||||||
timestamp: number;
|
|
||||||
editor: DocEditor | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DocHistoryFilter {
|
|
||||||
/**
|
|
||||||
* timestamp to filter histories before.
|
|
||||||
*/
|
|
||||||
before?: number;
|
|
||||||
/**
|
|
||||||
* limit the number of histories to return.
|
|
||||||
*
|
|
||||||
* Default to `100`.
|
|
||||||
*/
|
|
||||||
take?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DocMetaUpsertInput = Omit<
|
export type DocMetaUpsertInput = Omit<
|
||||||
Prisma.WorkspaceDocUncheckedCreateInput,
|
Prisma.WorkspaceDocUncheckedCreateInput,
|
||||||
'workspaceId' | 'docId'
|
'workspaceId' | 'docId'
|
||||||
@@ -71,16 +47,6 @@ export class DocModel extends BaseModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private get userSelectFields() {
|
|
||||||
return {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
avatarUrl: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async createUpdates(updates: DocRecord[]) {
|
async createUpdates(updates: DocRecord[]) {
|
||||||
return await this.db.update.createMany({
|
return await this.db.update.createMany({
|
||||||
data: updates.map(r => this.docRecordToUpdate(r)),
|
data: updates.map(r => this.docRecordToUpdate(r)),
|
||||||
@@ -148,148 +114,6 @@ export class DocModel extends BaseModel {
|
|||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region History
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a doc history with a max age.
|
|
||||||
*/
|
|
||||||
async createHistory(
|
|
||||||
snapshot: Doc,
|
|
||||||
maxAge: number
|
|
||||||
): Promise<DocHistorySimple> {
|
|
||||||
const row = await this.db.snapshotHistory.create({
|
|
||||||
select: {
|
|
||||||
timestamp: true,
|
|
||||||
createdByUser: this.userSelectFields,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
workspaceId: snapshot.spaceId,
|
|
||||||
id: snapshot.docId,
|
|
||||||
timestamp: new Date(snapshot.timestamp),
|
|
||||||
blob: snapshot.blob,
|
|
||||||
createdBy: snapshot.editorId,
|
|
||||||
expiredAt: new Date(Date.now() + maxAge),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
timestamp: row.timestamp.getTime(),
|
|
||||||
editor: row.createdByUser,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find doc history by workspaceId and docId.
|
|
||||||
*
|
|
||||||
* Only including timestamp, createdByUser
|
|
||||||
*/
|
|
||||||
async findHistories(
|
|
||||||
workspaceId: string,
|
|
||||||
docId: string,
|
|
||||||
filter?: DocHistoryFilter
|
|
||||||
): Promise<DocHistorySimple[]> {
|
|
||||||
const rows = await this.db.snapshotHistory.findMany({
|
|
||||||
select: {
|
|
||||||
timestamp: true,
|
|
||||||
createdByUser: this.userSelectFields,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
workspaceId,
|
|
||||||
id: docId,
|
|
||||||
timestamp: {
|
|
||||||
lt: filter?.before ? new Date(filter.before) : new Date(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
timestamp: 'desc',
|
|
||||||
},
|
|
||||||
take: filter?.take ?? 100,
|
|
||||||
});
|
|
||||||
return rows.map(r => ({
|
|
||||||
timestamp: r.timestamp.getTime(),
|
|
||||||
editor: r.createdByUser,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the history of a doc at a specific timestamp.
|
|
||||||
*
|
|
||||||
* Including blob and createdByUser
|
|
||||||
*/
|
|
||||||
async getHistory(
|
|
||||||
workspaceId: string,
|
|
||||||
docId: string,
|
|
||||||
timestamp: number
|
|
||||||
): Promise<DocHistory | null> {
|
|
||||||
const row = await this.db.snapshotHistory.findUnique({
|
|
||||||
where: {
|
|
||||||
workspaceId_id_timestamp: {
|
|
||||||
workspaceId,
|
|
||||||
id: docId,
|
|
||||||
timestamp: new Date(timestamp),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
createdByUser: this.userSelectFields,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!row) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
blob: row.blob,
|
|
||||||
timestamp: row.timestamp.getTime(),
|
|
||||||
editor: row.createdByUser,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the latest history of a doc.
|
|
||||||
*
|
|
||||||
* Only including timestamp, createdByUser
|
|
||||||
*/
|
|
||||||
async getLatestHistory(
|
|
||||||
workspaceId: string,
|
|
||||||
docId: string
|
|
||||||
): Promise<DocHistorySimple | null> {
|
|
||||||
const row = await this.db.snapshotHistory.findFirst({
|
|
||||||
where: {
|
|
||||||
workspaceId,
|
|
||||||
id: docId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
timestamp: true,
|
|
||||||
createdByUser: this.userSelectFields,
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
timestamp: 'desc',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!row) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
timestamp: row.timestamp.getTime(),
|
|
||||||
editor: row.createdByUser,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete expired histories.
|
|
||||||
*/
|
|
||||||
async deleteExpiredHistories() {
|
|
||||||
const { count } = await this.db.snapshotHistory.deleteMany({
|
|
||||||
where: {
|
|
||||||
expiredAt: {
|
|
||||||
lte: new Date(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.logger.log(`Deleted ${count} expired histories`);
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// #region Doc
|
// #region Doc
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -356,8 +180,8 @@ export class DocModel extends BaseModel {
|
|||||||
select: {
|
select: {
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
createdByUser: this.userSelectFields,
|
createdByUser: { select: publicUserSelect },
|
||||||
updatedByUser: this.userSelectFields,
|
updatedByUser: { select: publicUserSelect },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { BaseModel } from './base';
|
||||||
|
import { Doc, DocEditor, publicUserSelect } from './common';
|
||||||
|
|
||||||
|
export interface DocHistorySimple {
|
||||||
|
timestamp: number;
|
||||||
|
editor: DocEditor | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocHistory {
|
||||||
|
blob: Buffer;
|
||||||
|
timestamp: number;
|
||||||
|
editor: DocEditor | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocHistoryFilter {
|
||||||
|
/**
|
||||||
|
* timestamp to filter histories before.
|
||||||
|
*/
|
||||||
|
before?: number;
|
||||||
|
/**
|
||||||
|
* limit the number of histories to return.
|
||||||
|
*
|
||||||
|
* Default to `100`.
|
||||||
|
*/
|
||||||
|
take?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HistoryModel extends BaseModel {
|
||||||
|
/**
|
||||||
|
* Create a doc history with a max age.
|
||||||
|
*/
|
||||||
|
async create(snapshot: Doc, maxAge: number): Promise<DocHistorySimple> {
|
||||||
|
const row = await this.db.snapshotHistory.create({
|
||||||
|
select: {
|
||||||
|
timestamp: true,
|
||||||
|
createdByUser: { select: publicUserSelect },
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
workspaceId: snapshot.spaceId,
|
||||||
|
id: snapshot.docId,
|
||||||
|
timestamp: new Date(snapshot.timestamp),
|
||||||
|
blob: snapshot.blob,
|
||||||
|
createdBy: snapshot.editorId,
|
||||||
|
expiredAt: new Date(Date.now() + maxAge),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logger.log(
|
||||||
|
`Created history ${row.timestamp} for ${snapshot.docId} in ${snapshot.spaceId}`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
timestamp: row.timestamp.getTime(),
|
||||||
|
editor: row.createdByUser,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find doc history by workspaceId and docId.
|
||||||
|
*
|
||||||
|
* Only including timestamp, createdByUser
|
||||||
|
*/
|
||||||
|
async findMany(
|
||||||
|
workspaceId: string,
|
||||||
|
docId: string,
|
||||||
|
filter?: DocHistoryFilter
|
||||||
|
): Promise<DocHistorySimple[]> {
|
||||||
|
const rows = await this.db.snapshotHistory.findMany({
|
||||||
|
select: {
|
||||||
|
timestamp: true,
|
||||||
|
createdByUser: { select: publicUserSelect },
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
id: docId,
|
||||||
|
timestamp: {
|
||||||
|
lt: filter?.before ? new Date(filter.before) : new Date(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
timestamp: 'desc',
|
||||||
|
},
|
||||||
|
take: filter?.take ?? 100,
|
||||||
|
});
|
||||||
|
return rows.map(r => ({
|
||||||
|
timestamp: r.timestamp.getTime(),
|
||||||
|
editor: r.createdByUser,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the history of a doc at a specific timestamp.
|
||||||
|
*
|
||||||
|
* Including blob and createdByUser
|
||||||
|
*/
|
||||||
|
async get(
|
||||||
|
workspaceId: string,
|
||||||
|
docId: string,
|
||||||
|
timestamp: number
|
||||||
|
): Promise<DocHistory | null> {
|
||||||
|
const row = await this.db.snapshotHistory.findUnique({
|
||||||
|
where: {
|
||||||
|
workspaceId_id_timestamp: {
|
||||||
|
workspaceId,
|
||||||
|
id: docId,
|
||||||
|
timestamp: new Date(timestamp),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
createdByUser: { select: publicUserSelect },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
blob: row.blob,
|
||||||
|
timestamp: row.timestamp.getTime(),
|
||||||
|
editor: row.createdByUser,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the latest history of a doc.
|
||||||
|
*
|
||||||
|
* Only including timestamp, createdByUser
|
||||||
|
*/
|
||||||
|
async getLatest(
|
||||||
|
workspaceId: string,
|
||||||
|
docId: string
|
||||||
|
): Promise<DocHistorySimple | null> {
|
||||||
|
const row = await this.db.snapshotHistory.findFirst({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
id: docId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
timestamp: true,
|
||||||
|
createdByUser: { select: publicUserSelect },
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
timestamp: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
timestamp: row.timestamp.getTime(),
|
||||||
|
editor: row.createdByUser,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean expired histories.
|
||||||
|
*/
|
||||||
|
async cleanExpired() {
|
||||||
|
const { count } = await this.db.snapshotHistory.deleteMany({
|
||||||
|
where: {
|
||||||
|
expiredAt: {
|
||||||
|
lte: new Date(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logger.log(`Deleted ${count} expired histories`);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { ApplyType } from '../base';
|
|||||||
import { DocModel } from './doc';
|
import { DocModel } from './doc';
|
||||||
import { DocUserModel } from './doc-user';
|
import { DocUserModel } from './doc-user';
|
||||||
import { FeatureModel } from './feature';
|
import { FeatureModel } from './feature';
|
||||||
|
import { HistoryModel } from './history';
|
||||||
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';
|
||||||
@@ -32,6 +33,7 @@ const MODELS = {
|
|||||||
userDoc: UserDocModel,
|
userDoc: UserDocModel,
|
||||||
workspaceUser: WorkspaceUserModel,
|
workspaceUser: WorkspaceUserModel,
|
||||||
docUser: DocUserModel,
|
docUser: DocUserModel,
|
||||||
|
history: HistoryModel,
|
||||||
};
|
};
|
||||||
|
|
||||||
type ModelsType = {
|
type ModelsType = {
|
||||||
@@ -87,6 +89,7 @@ export * from './common';
|
|||||||
export * from './doc';
|
export * from './doc';
|
||||||
export * from './doc-user';
|
export * from './doc-user';
|
||||||
export * from './feature';
|
export * from './feature';
|
||||||
|
export * from './history';
|
||||||
export * from './session';
|
export * from './session';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
export * from './user-doc';
|
export * from './user-doc';
|
||||||
|
|||||||
Reference in New Issue
Block a user