From 02978c002fcf830453cb91e7efda20c02b94cd48 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Tue, 24 Mar 2026 07:42:59 +0000 Subject: [PATCH] [sync] fix(backend): refresh base-node cache for v2 table creation (#1513) Synced from teableio/teable-ee@626773d --- .../v2/v2-base-node-compat.service.ts | 71 +++++++++++++++++++ .../src/features/v2/v2.module.ts | 2 + apps/nestjs-backend/test/table.e2e-spec.ts | 43 ++++++++++- 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 apps/nestjs-backend/src/features/v2/v2-base-node-compat.service.ts diff --git a/apps/nestjs-backend/src/features/v2/v2-base-node-compat.service.ts b/apps/nestjs-backend/src/features/v2/v2-base-node-compat.service.ts new file mode 100644 index 0000000000..d536b6c002 --- /dev/null +++ b/apps/nestjs-backend/src/features/v2/v2-base-node-compat.service.ts @@ -0,0 +1,71 @@ +import type { OnModuleInit } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; +import type { IBaseNodePresenceFlushPayload } from '@teable/openapi'; +import { + ProjectionHandler, + TableCreated, + ok, + type DomainError, + type IEventHandler, + type IExecutionContext, + type Result, +} from '@teable/v2-core'; +import type { DependencyContainer } from '@teable/v2-di'; +import { PerformanceCacheService } from '../../performance-cache'; +import { generateBaseNodeListCacheKey } from '../../performance-cache/generate-keys'; +import { ShareDbService } from '../../share-db/share-db.service'; +import { presenceHandler } from '../base-node/helper'; +import { V2ContainerService } from './v2-container.service'; +import type { IV2ProjectionRegistrar } from './v2-projection-registrar'; + +@ProjectionHandler(TableCreated) +class V2TableCreatedBaseNodeProjection implements IEventHandler { + constructor( + private readonly performanceCacheService: PerformanceCacheService, + private readonly shareDbService: ShareDbService + ) {} + + async handle( + _context: IExecutionContext, + event: TableCreated + ): Promise> { + const baseId = event.baseId.toString(); + this.performanceCacheService.del(generateBaseNodeListCacheKey(baseId)); + + if (this.shareDbService.shareDbAdapter.closed) { + return ok(undefined); + } + + presenceHandler(baseId, this.shareDbService, (presence) => { + presence.submit({ + event: 'flush', + }); + }); + + return ok(undefined); + } +} + +@Injectable() +export class V2BaseNodeCompatService implements IV2ProjectionRegistrar, OnModuleInit { + private readonly logger = new Logger(V2BaseNodeCompatService.name); + + constructor( + private readonly v2ContainerService: V2ContainerService, + private readonly performanceCacheService: PerformanceCacheService, + private readonly shareDbService: ShareDbService + ) {} + + onModuleInit(): void { + this.v2ContainerService.addProjectionRegistrar(this); + } + + registerProjections(container: DependencyContainer): void { + this.logger.log('Registering V2 base-node compatibility projections'); + + container.registerInstance( + V2TableCreatedBaseNodeProjection, + new V2TableCreatedBaseNodeProjection(this.performanceCacheService, this.shareDbService) + ); + } +} diff --git a/apps/nestjs-backend/src/features/v2/v2.module.ts b/apps/nestjs-backend/src/features/v2/v2.module.ts index a4da750a6b..acb5cbce9e 100644 --- a/apps/nestjs-backend/src/features/v2/v2.module.ts +++ b/apps/nestjs-backend/src/features/v2/v2.module.ts @@ -6,6 +6,7 @@ import { ShareDbModule } from '../../share-db/share-db.module'; import { UndoRedoStackService } from '../undo-redo/stack/undo-redo-stack.service'; import { ViewModule } from '../view/view.module'; import { V2ActionTriggerService } from './v2-action-trigger.service'; +import { V2BaseNodeCompatService } from './v2-base-node-compat.service'; import { V2ContainerService } from './v2-container.service'; import { V2Controller } from './v2.controller'; import { V2ExecutionContextFactory } from './v2-execution-context.factory'; @@ -98,6 +99,7 @@ const toErrorMessage = (body: unknown): string => { V2ContainerService, V2ExecutionContextFactory, V2ActionTriggerService, + V2BaseNodeCompatService, V2UserRenamePropagationService, V2FieldDeleteCompatService, V2RecordHistoryService, diff --git a/apps/nestjs-backend/test/table.e2e-spec.ts b/apps/nestjs-backend/test/table.e2e-spec.ts index 0d445d3309..019afa2b2c 100644 --- a/apps/nestjs-backend/test/table.e2e-spec.ts +++ b/apps/nestjs-backend/test/table.e2e-spec.ts @@ -5,6 +5,8 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { FieldKeyType, FieldType, Relationship, RowHeightLevel, ViewType } from '@teable/core'; import type { ICreateTableRo } from '@teable/openapi'; import { + BaseNodeResourceType, + getBaseNodeTree, updateTableDescription, updateTableIcon, updateTableName, @@ -31,6 +33,8 @@ import { getRecords, getTable, initApp, + createBase, + permanentDeleteBase, updateRecord, } from './utils/init-app'; @@ -142,7 +146,10 @@ describe('OpenAPI TableController (e2e)', () => { }); afterEach(async () => { - await permanentDeleteTable(baseId, tableId); + if (tableId) { + await permanentDeleteTable(baseId, tableId); + tableId = ''; + } }); async function processV2Outbox(times = 1): Promise { @@ -274,6 +281,40 @@ describe('OpenAPI TableController (e2e)', () => { expect(recordResult.records).toHaveLength(3); }); + it('should invalidate base-node tree cache after table creation', async () => { + const isolatedBase = await createBase({ + spaceId: globalThis.testConfig.spaceId, + name: `base-node-cache-${Date.now()}`, + }); + + try { + const initialTree = await getBaseNodeTree(isolatedBase.id).then((res) => res.data); + const initialTableNodeIds = new Set( + initialTree.nodes + .filter((node) => node.resourceType === BaseNodeResourceType.Table) + .map((node) => node.resourceId) + ); + + const createdTable = await createTable(isolatedBase.id, { + name: 'cache invalidation table', + fields: [{ name: 'Name', type: FieldType.SingleLineText }], + views: [{ name: 'Grid view', type: ViewType.Grid }], + records: [], + }); + + const refreshedTree = await getBaseNodeTree(isolatedBase.id).then((res) => res.data); + const createdNode = refreshedTree.nodes.find( + (node) => + node.resourceType === BaseNodeResourceType.Table && node.resourceId === createdTable.id + ); + + expect(initialTableNodeIds.has(createdTable.id)).toBe(false); + expect(createdNode).toBeDefined(); + } finally { + await permanentDeleteBase(isolatedBase.id); + } + }); + it('should refresh table lastModifyTime when add a record', async () => { const result = await createTable(baseId, { name: 'new table' }); tableId = result.id;