Skip to content

Commit a72f9bb

Browse files
[sync] fix(backend): refresh base-node cache for v2 table creation (#1513) (#2821)
Synced from teableio/teable-ee@626773d Co-authored-by: nichenqin <nichenqin@hotmail.com>
1 parent 09e8cc4 commit a72f9bb

3 files changed

Lines changed: 115 additions & 1 deletion

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type { OnModuleInit } from '@nestjs/common';
2+
import { Injectable, Logger } from '@nestjs/common';
3+
import type { IBaseNodePresenceFlushPayload } from '@teable/openapi';
4+
import {
5+
ProjectionHandler,
6+
TableCreated,
7+
ok,
8+
type DomainError,
9+
type IEventHandler,
10+
type IExecutionContext,
11+
type Result,
12+
} from '@teable/v2-core';
13+
import type { DependencyContainer } from '@teable/v2-di';
14+
import { PerformanceCacheService } from '../../performance-cache';
15+
import { generateBaseNodeListCacheKey } from '../../performance-cache/generate-keys';
16+
import { ShareDbService } from '../../share-db/share-db.service';
17+
import { presenceHandler } from '../base-node/helper';
18+
import { V2ContainerService } from './v2-container.service';
19+
import type { IV2ProjectionRegistrar } from './v2-projection-registrar';
20+
21+
@ProjectionHandler(TableCreated)
22+
class V2TableCreatedBaseNodeProjection implements IEventHandler<TableCreated> {
23+
constructor(
24+
private readonly performanceCacheService: PerformanceCacheService,
25+
private readonly shareDbService: ShareDbService
26+
) {}
27+
28+
async handle(
29+
_context: IExecutionContext,
30+
event: TableCreated
31+
): Promise<Result<void, DomainError>> {
32+
const baseId = event.baseId.toString();
33+
this.performanceCacheService.del(generateBaseNodeListCacheKey(baseId));
34+
35+
if (this.shareDbService.shareDbAdapter.closed) {
36+
return ok(undefined);
37+
}
38+
39+
presenceHandler<IBaseNodePresenceFlushPayload>(baseId, this.shareDbService, (presence) => {
40+
presence.submit({
41+
event: 'flush',
42+
});
43+
});
44+
45+
return ok(undefined);
46+
}
47+
}
48+
49+
@Injectable()
50+
export class V2BaseNodeCompatService implements IV2ProjectionRegistrar, OnModuleInit {
51+
private readonly logger = new Logger(V2BaseNodeCompatService.name);
52+
53+
constructor(
54+
private readonly v2ContainerService: V2ContainerService,
55+
private readonly performanceCacheService: PerformanceCacheService,
56+
private readonly shareDbService: ShareDbService
57+
) {}
58+
59+
onModuleInit(): void {
60+
this.v2ContainerService.addProjectionRegistrar(this);
61+
}
62+
63+
registerProjections(container: DependencyContainer): void {
64+
this.logger.log('Registering V2 base-node compatibility projections');
65+
66+
container.registerInstance(
67+
V2TableCreatedBaseNodeProjection,
68+
new V2TableCreatedBaseNodeProjection(this.performanceCacheService, this.shareDbService)
69+
);
70+
}
71+
}

apps/nestjs-backend/src/features/v2/v2.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ShareDbModule } from '../../share-db/share-db.module';
66
import { UndoRedoStackService } from '../undo-redo/stack/undo-redo-stack.service';
77
import { ViewModule } from '../view/view.module';
88
import { V2ActionTriggerService } from './v2-action-trigger.service';
9+
import { V2BaseNodeCompatService } from './v2-base-node-compat.service';
910
import { V2ContainerService } from './v2-container.service';
1011
import { V2Controller } from './v2.controller';
1112
import { V2ExecutionContextFactory } from './v2-execution-context.factory';
@@ -98,6 +99,7 @@ const toErrorMessage = (body: unknown): string => {
9899
V2ContainerService,
99100
V2ExecutionContextFactory,
100101
V2ActionTriggerService,
102+
V2BaseNodeCompatService,
101103
V2UserRenamePropagationService,
102104
V2FieldDeleteCompatService,
103105
V2RecordHistoryService,

apps/nestjs-backend/test/table.e2e-spec.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
55
import { FieldKeyType, FieldType, Relationship, RowHeightLevel, ViewType } from '@teable/core';
66
import type { ICreateTableRo } from '@teable/openapi';
77
import {
8+
BaseNodeResourceType,
9+
getBaseNodeTree,
810
updateTableDescription,
911
updateTableIcon,
1012
updateTableName,
@@ -31,6 +33,8 @@ import {
3133
getRecords,
3234
getTable,
3335
initApp,
36+
createBase,
37+
permanentDeleteBase,
3438
updateRecord,
3539
} from './utils/init-app';
3640

@@ -142,7 +146,10 @@ describe('OpenAPI TableController (e2e)', () => {
142146
});
143147

144148
afterEach(async () => {
145-
await permanentDeleteTable(baseId, tableId);
149+
if (tableId) {
150+
await permanentDeleteTable(baseId, tableId);
151+
tableId = '';
152+
}
146153
});
147154

148155
async function processV2Outbox(times = 1): Promise<void> {
@@ -274,6 +281,40 @@ describe('OpenAPI TableController (e2e)', () => {
274281
expect(recordResult.records).toHaveLength(3);
275282
});
276283

284+
it('should invalidate base-node tree cache after table creation', async () => {
285+
const isolatedBase = await createBase({
286+
spaceId: globalThis.testConfig.spaceId,
287+
name: `base-node-cache-${Date.now()}`,
288+
});
289+
290+
try {
291+
const initialTree = await getBaseNodeTree(isolatedBase.id).then((res) => res.data);
292+
const initialTableNodeIds = new Set(
293+
initialTree.nodes
294+
.filter((node) => node.resourceType === BaseNodeResourceType.Table)
295+
.map((node) => node.resourceId)
296+
);
297+
298+
const createdTable = await createTable(isolatedBase.id, {
299+
name: 'cache invalidation table',
300+
fields: [{ name: 'Name', type: FieldType.SingleLineText }],
301+
views: [{ name: 'Grid view', type: ViewType.Grid }],
302+
records: [],
303+
});
304+
305+
const refreshedTree = await getBaseNodeTree(isolatedBase.id).then((res) => res.data);
306+
const createdNode = refreshedTree.nodes.find(
307+
(node) =>
308+
node.resourceType === BaseNodeResourceType.Table && node.resourceId === createdTable.id
309+
);
310+
311+
expect(initialTableNodeIds.has(createdTable.id)).toBe(false);
312+
expect(createdNode).toBeDefined();
313+
} finally {
314+
await permanentDeleteBase(isolatedBase.id);
315+
}
316+
});
317+
277318
it('should refresh table lastModifyTime when add a record', async () => {
278319
const result = await createTable(baseId, { name: 'new table' });
279320
tableId = result.id;

0 commit comments

Comments
 (0)