From 687c26304aa9c5dfc39399c63cbf630dca8d1393 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Wed, 5 Mar 2025 12:10:28 +0000 Subject: [PATCH] refactor(server): split HistoryModel from DocModel (#10604) --- .../server/src/__tests__/models/doc.spec.ts | 155 +-------------- .../server/src/core/doc/adapters/workspace.ts | 8 +- packages/backend/server/src/core/doc/job.ts | 2 +- .../src/models/__tests__/history.spec.ts | 177 +++++++++++++++++ .../backend/server/src/models/common/index.ts | 1 + .../backend/server/src/models/common/user.ts | 7 + packages/backend/server/src/models/doc.ts | 182 +----------------- packages/backend/server/src/models/history.ts | 169 ++++++++++++++++ packages/backend/server/src/models/index.ts | 3 + 9 files changed, 374 insertions(+), 330 deletions(-) create mode 100644 packages/backend/server/src/models/__tests__/history.spec.ts create mode 100644 packages/backend/server/src/models/common/user.ts create mode 100644 packages/backend/server/src/models/history.ts diff --git a/packages/backend/server/src/__tests__/models/doc.spec.ts b/packages/backend/server/src/__tests__/models/doc.spec.ts index 66d9ffb970..ca745b1533 100644 --- a/packages/backend/server/src/__tests__/models/doc.spec.ts +++ b/packages/backend/server/src/__tests__/models/doc.spec.ts @@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto'; import ava, { TestFn } from 'ava'; import { Config } from '../../base/config'; -import { PublicDocMode } from '../../models'; +import { HistoryModel, PublicDocMode } from '../../models'; import { DocModel } from '../../models/doc'; import { type User, UserModel } from '../../models/user'; import { type Workspace, WorkspaceModel } from '../../models/workspace'; @@ -15,6 +15,7 @@ interface Context { user: UserModel; workspace: WorkspaceModel; doc: DocModel; + history: HistoryModel; } const test = ava as TestFn; @@ -25,6 +26,7 @@ test.before(async t => { 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; }); @@ -290,136 +292,6 @@ test('should get a doc authors', async t => { 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 => { const docId = randomUUID(); const snapshot = { @@ -430,7 +302,7 @@ test('should delete a doc, including histories, snapshots and updates', async t editorId: user.id, }; 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([ { 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); const foundSnapshot = await t.context.doc.get(workspace.id, docId); t.is(foundSnapshot, null); - const foundHistory = await t.context.doc.getLatestHistory( - workspace.id, - docId - ); + const foundHistory = await t.context.history.getLatest(workspace.id, docId); t.is(foundHistory, null); const foundUpdates = await t.context.doc.findUpdates(workspace.id, docId); t.is(foundUpdates.length, 0); @@ -470,7 +339,7 @@ test('should delete all docs in a workspace', async t => { editorId: user.id, }; 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([ { 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.createHistory(snapshot2, 1000); + await t.context.history.create(snapshot2, 1000); await t.context.doc.createUpdates([ { spaceId: workspace.id, @@ -495,19 +364,13 @@ test('should delete all docs in a workspace', async t => { t.is(deletedCount, 2); const foundSnapshot1 = await t.context.doc.get(workspace.id, docId1); t.is(foundSnapshot1, null); - const foundHistory1 = await t.context.doc.getLatestHistory( - workspace.id, - docId1 - ); + const foundHistory1 = await t.context.history.getLatest(workspace.id, docId1); t.is(foundHistory1, null); const foundUpdates1 = await t.context.doc.findUpdates(workspace.id, docId1); t.is(foundUpdates1.length, 0); const foundSnapshot2 = await t.context.doc.get(workspace.id, docId2); t.is(foundSnapshot2, null); - const foundHistory2 = await t.context.doc.getLatestHistory( - workspace.id, - docId2 - ); + const foundHistory2 = await t.context.history.getLatest(workspace.id, docId2); t.is(foundHistory2, null); const foundUpdates2 = await t.context.doc.findUpdates(workspace.id, docId2); t.is(foundUpdates2.length, 0); diff --git a/packages/backend/server/src/core/doc/adapters/workspace.ts b/packages/backend/server/src/core/doc/adapters/workspace.ts index 8cd74dcf49..9ed5ebc1bd 100644 --- a/packages/backend/server/src/core/doc/adapters/workspace.ts +++ b/packages/backend/server/src/core/doc/adapters/workspace.ts @@ -171,14 +171,14 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter { docId: string, query: HistoryFilter ) { - return await this.models.doc.findHistories(workspaceId, docId, { + return await this.models.history.findMany(workspaceId, docId, { before: query.before, take: query.limit, }); } async getDocHistory(workspaceId: string, docId: string, timestamp: number) { - const history = await this.models.doc.getHistory( + const history = await this.models.history.get( workspaceId, docId, timestamp @@ -282,7 +282,7 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter { } try { - await this.models.doc.createHistory( + await this.models.history.create( { spaceId: snapshot.spaceId, docId: snapshot.docId, @@ -372,7 +372,7 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter { } protected async lastDocHistory(workspaceId: string, id: string) { - return this.models.doc.getLatestHistory(workspaceId, id); + return this.models.history.getLatest(workspaceId, id); } // for auto merging diff --git a/packages/backend/server/src/core/doc/job.ts b/packages/backend/server/src/core/doc/job.ts index 028cc65be9..dc1cde21f7 100644 --- a/packages/backend/server/src/core/doc/job.ts +++ b/packages/backend/server/src/core/doc/job.ts @@ -32,7 +32,7 @@ export class DocStorageCronJob { @OnJob('nightly.cleanExpiredHistories') async cleanExpiredHistories() { - await this.models.doc.deleteExpiredHistories(); + await this.models.history.cleanExpired(); } @OnEvent('user.deleted') diff --git a/packages/backend/server/src/models/__tests__/history.spec.ts b/packages/backend/server/src/models/__tests__/history.spec.ts new file mode 100644 index 0000000000..f1db7ecfa4 --- /dev/null +++ b/packages/backend/server/src/models/__tests__/history.spec.ts @@ -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; + +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); +}); diff --git a/packages/backend/server/src/models/common/index.ts b/packages/backend/server/src/models/common/index.ts index 346b83cca9..d120be6a4a 100644 --- a/packages/backend/server/src/models/common/index.ts +++ b/packages/backend/server/src/models/common/index.ts @@ -1,3 +1,4 @@ export * from './doc'; export * from './feature'; export * from './role'; +export * from './user'; diff --git a/packages/backend/server/src/models/common/user.ts b/packages/backend/server/src/models/common/user.ts new file mode 100644 index 0000000000..242cd901c1 --- /dev/null +++ b/packages/backend/server/src/models/common/user.ts @@ -0,0 +1,7 @@ +import { Prisma } from '@prisma/client'; + +export const publicUserSelect = { + id: true, + name: true, + avatarUrl: true, +} satisfies Prisma.UserSelect; diff --git a/packages/backend/server/src/models/doc.ts b/packages/backend/server/src/models/doc.ts index 51d2677fb2..908f7af455 100644 --- a/packages/backend/server/src/models/doc.ts +++ b/packages/backend/server/src/models/doc.ts @@ -4,34 +4,10 @@ import type { Update } from '@prisma/client'; import { Prisma } from '@prisma/client'; import { BaseModel } from './base'; -import type { Doc, DocEditor } from './common'; +import { Doc, publicUserSelect } from './common'; 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< Prisma.WorkspaceDocUncheckedCreateInput, '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[]) { return await this.db.update.createMany({ data: updates.map(r => this.docRecordToUpdate(r)), @@ -148,148 +114,6 @@ export class DocModel extends BaseModel { // #endregion - // #region History - - /** - * Create a doc history with a max age. - */ - async createHistory( - snapshot: Doc, - maxAge: number - ): Promise { - 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 { - 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 { - 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 { - 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 /** @@ -356,8 +180,8 @@ export class DocModel extends BaseModel { select: { createdAt: true, updatedAt: true, - createdByUser: this.userSelectFields, - updatedByUser: this.userSelectFields, + createdByUser: { select: publicUserSelect }, + updatedByUser: { select: publicUserSelect }, }, }); } diff --git a/packages/backend/server/src/models/history.ts b/packages/backend/server/src/models/history.ts new file mode 100644 index 0000000000..d0dffb6a4e --- /dev/null +++ b/packages/backend/server/src/models/history.ts @@ -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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/packages/backend/server/src/models/index.ts b/packages/backend/server/src/models/index.ts index 799a7d8519..1954e1bc29 100644 --- a/packages/backend/server/src/models/index.ts +++ b/packages/backend/server/src/models/index.ts @@ -10,6 +10,7 @@ import { ApplyType } from '../base'; import { DocModel } from './doc'; import { DocUserModel } from './doc-user'; import { FeatureModel } from './feature'; +import { HistoryModel } from './history'; import { MODELS_SYMBOL } from './provider'; import { SessionModel } from './session'; import { UserModel } from './user'; @@ -32,6 +33,7 @@ const MODELS = { userDoc: UserDocModel, workspaceUser: WorkspaceUserModel, docUser: DocUserModel, + history: HistoryModel, }; type ModelsType = { @@ -87,6 +89,7 @@ export * from './common'; export * from './doc'; export * from './doc-user'; export * from './feature'; +export * from './history'; export * from './session'; export * from './user'; export * from './user-doc';