• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

teableio / teable / 21931418853

12 Feb 2026 02:38AM UTC coverage: 64.11% (+0.02%) from 64.087%
21931418853

Pull #2595

github

web-flow
Merge cd2dbff7b into 367739c6d
Pull Request #2595: [sync] feat: support base share (node) T1873 T1122 T730 T682 (#1122)

5067 of 6630 branches covered (76.43%)

385 of 582 new or added lines in 30 files covered. (66.15%)

15 existing lines in 2 files now uncovered.

23274 of 36303 relevant lines covered (64.11%)

9079.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.0
/apps/nestjs-backend/src/features/base-node/base-node.controller.ts
1
/* eslint-disable sonarjs/no-duplicate-string */
2
import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
3
import type { IBaseNodeTreeVo, IBaseNodeVo, IDeleteBaseNodeVo } from '@teable/openapi';
4
import {
5
  moveBaseNodeRoSchema,
6
  createBaseNodeRoSchema,
7
  duplicateBaseNodeRoSchema,
8
  ICreateBaseNodeRo,
9
  IDuplicateBaseNodeRo,
10
  IMoveBaseNodeRo,
11
  updateBaseNodeRoSchema,
12
  IUpdateBaseNodeRo,
13
} from '@teable/openapi';
14
import { ClsService } from 'nestjs-cls';
15
import { EmitControllerEvent } from '../../event-emitter/decorators/emit-controller-event.decorator';
16
import { Events } from '../../event-emitter/events';
17
import type { IClsStore } from '../../types/cls';
18
import { ZodValidationPipe } from '../../zod.validation.pipe';
19
import { AllowAnonymous, AllowAnonymousType } from '../auth/decorators/allow-anonymous.decorator';
20
import { BaseNodePermissions } from '../auth/decorators/base-node-permissions.decorator';
21
import { Permissions } from '../auth/decorators/permissions.decorator';
22
import { BaseNodePermissionGuard } from '../auth/guard/base-node-permission.guard';
23
import { checkBaseNodePermission } from './base-node.permission.helper';
24
import { BaseNodeService } from './base-node.service';
25
import { BaseNodeAction } from './types';
26

27
@Controller('api/base/:baseId/node')
28
@UseGuards(BaseNodePermissionGuard)
29
@AllowAnonymous(AllowAnonymousType.RESOURCE)
30
export class BaseNodeController {
31
  constructor(
32
    private readonly baseNodeService: BaseNodeService,
292✔
33
    private readonly cls: ClsService<IClsStore>
292✔
34
  ) {}
35

36
  @Get('list')
37
  @Permissions('base|read')
38
  async getList(@Param('baseId') baseId: string): Promise<IBaseNodeVo[]> {
39
    const permissionContext = await this.getPermissionContext(baseId);
20✔
40
    const nodeList = await this.baseNodeService.getList(baseId);
20✔
41
    const allowedNodeIds = this.getAllowedNodeIds(nodeList, permissionContext.shareNodeId);
20✔
42
    return nodeList.filter((node) => this.filterNode(node, permissionContext, allowedNodeIds));
64✔
43
  }
44

45
  @Get('tree')
46
  @Permissions('base|read')
47
  async getTree(@Param('baseId') baseId: string): Promise<IBaseNodeTreeVo> {
48
    const permissionContext = await this.getPermissionContext(baseId);
44✔
49
    const tree = await this.baseNodeService.getTree(baseId);
44✔
50
    const allowedNodeIds = this.getAllowedNodeIds(tree.nodes, permissionContext.shareNodeId);
44✔
51
    return {
44✔
52
      ...tree,
53
      nodes: tree.nodes.filter((node) => this.filterNode(node, permissionContext, allowedNodeIds)),
156✔
54
    };
55
  }
56

57
  private filterNode(
58
    node: IBaseNodeVo,
59
    permissionContext: { permissionSet: Set<string>; shareNodeId?: string },
60
    allowedNodeIds?: Set<string>
61
  ): boolean {
62
    if (allowedNodeIds && !allowedNodeIds.has(node.id)) {
220✔
63
      return false;
10✔
64
    }
65

66
    // Then check standard permissions
67
    return checkBaseNodePermission(
210✔
68
      { resourceType: node.resourceType, resourceId: node.resourceId },
69
      BaseNodeAction.Read,
70
      permissionContext
71
    );
72
  }
73

74
  protected getAllowedNodeIds(nodes: IBaseNodeVo[], shareNodeId?: string) {
75
    if (!shareNodeId) {
64✔
76
      return undefined;
56✔
77
    }
78
    const nodeIds = new Set(nodes.map((node) => node.id));
24✔
79
    if (!nodeIds.has(shareNodeId)) {
8✔
NEW
80
      return new Set<string>();
×
81
    }
82
    const childrenByParent = new Map<string, string[]>();
8✔
83
    for (const node of nodes) {
8✔
84
      if (!node.parentId) {
24✔
85
        continue;
16✔
86
      }
87
      const current = childrenByParent.get(node.parentId) ?? [];
8✔
88
      current.push(node.id);
24✔
89
      childrenByParent.set(node.parentId, current);
24✔
90
    }
91
    const allowed = new Set<string>();
8✔
92
    const queue = [shareNodeId];
8✔
93
    while (queue.length) {
8✔
94
      const current = queue.shift();
14✔
95
      if (!current || allowed.has(current)) {
14✔
NEW
96
        continue;
×
97
      }
98
      allowed.add(current);
14✔
99
      const children = childrenByParent.get(current) ?? [];
14✔
100
      for (const childId of children) {
14✔
101
        if (!allowed.has(childId)) {
6✔
102
          queue.push(childId);
6✔
103
        }
104
      }
105
    }
106
    return allowed;
8✔
107
  }
108

109
  @Get(':nodeId')
110
  @Permissions('base|read')
111
  @BaseNodePermissions(BaseNodeAction.Read)
112
  async getNode(
113
    @Param('baseId') baseId: string,
114
    @Param('nodeId') nodeId: string
115
  ): Promise<IBaseNodeVo> {
116
    return this.baseNodeService.getNodeVo(baseId, nodeId);
16✔
117
  }
118

119
  @Post()
120
  @Permissions('base|read')
121
  @BaseNodePermissions(BaseNodeAction.Create)
122
  @EmitControllerEvent(Events.BASE_NODE_CREATE)
123
  async create(
124
    @Param('baseId') baseId: string,
125
    @Body(new ZodValidationPipe(createBaseNodeRoSchema)) ro: ICreateBaseNodeRo
126
  ): Promise<IBaseNodeVo> {
127
    return this.baseNodeService.create(baseId, ro);
244✔
128
  }
129

130
  @Post(':nodeId/duplicate')
131
  @Permissions('base|read')
132
  @BaseNodePermissions(BaseNodeAction.Read, BaseNodeAction.Create)
133
  @EmitControllerEvent(Events.BASE_NODE_CREATE)
134
  async duplicate(
135
    @Param('baseId') baseId: string,
136
    @Param('nodeId') nodeId: string,
137
    @Body(new ZodValidationPipe(duplicateBaseNodeRoSchema)) ro: IDuplicateBaseNodeRo
138
  ): Promise<IBaseNodeVo> {
139
    return this.baseNodeService.duplicate(baseId, nodeId, ro);
10✔
140
  }
141

142
  @Put(':nodeId')
143
  @Permissions('base|read')
144
  @BaseNodePermissions(BaseNodeAction.Update)
145
  @EmitControllerEvent(Events.BASE_NODE_UPDATE)
146
  async update(
147
    @Param('baseId') baseId: string,
148
    @Param('nodeId') nodeId: string,
149
    @Body(new ZodValidationPipe(updateBaseNodeRoSchema)) ro: IUpdateBaseNodeRo
150
  ): Promise<IBaseNodeVo> {
151
    return this.baseNodeService.update(baseId, nodeId, ro);
14✔
152
  }
153

154
  @Put(':nodeId/move')
155
  @Permissions('base|update')
156
  async move(
157
    @Param('baseId') baseId: string,
158
    @Param('nodeId') nodeId: string,
159
    @Body(new ZodValidationPipe(moveBaseNodeRoSchema)) ro: IMoveBaseNodeRo
160
  ): Promise<IBaseNodeVo> {
161
    return this.baseNodeService.move(baseId, nodeId, ro);
58✔
162
  }
163

164
  @Delete(':nodeId')
165
  @Permissions('base|read')
166
  @BaseNodePermissions(BaseNodeAction.Delete)
167
  @EmitControllerEvent(Events.BASE_NODE_DELETE)
168
  async delete(
169
    @Param('baseId') baseId: string,
170
    @Param('nodeId') nodeId: string
171
  ): Promise<IDeleteBaseNodeVo> {
172
    return this.baseNodeService.delete(baseId, nodeId);
198✔
173
  }
174

175
  @Delete(':nodeId/permanent')
176
  @Permissions('base|read')
177
  @BaseNodePermissions(BaseNodeAction.Delete)
178
  @EmitControllerEvent(Events.BASE_NODE_DELETE)
179
  async permanentDelete(
180
    @Param('baseId') baseId: string,
181
    @Param('nodeId') nodeId: string
182
  ): Promise<IDeleteBaseNodeVo> {
183
    const result = await this.baseNodeService.delete(baseId, nodeId, true);
×
184
    return { ...result, permanent: true };
×
185
  }
186

187
  protected async getPermissionContext(_baseId: string) {
188
    const permissions = this.cls.get('permissions');
64✔
189
    const permissionSet = new Set(permissions);
64✔
190
    const baseShare = this.cls.get('baseShare');
64✔
191
    return {
64✔
192
      permissionSet,
193
      shareNodeId: baseShare?.nodeId,
194
    };
195
  }
196
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc