Skip to content

Commit d39f3a7

Browse files
committed
feat: implement folder depth validation and enhance node movement logic in BaseNodeService
1 parent 8b298a4 commit d39f3a7

File tree

3 files changed

+205
-54
lines changed

3 files changed

+205
-54
lines changed

apps/nestjs-backend/src/features/base-node/base-node.service.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -412,11 +412,8 @@ export class BaseNodeService {
412412
throw new CustomHttpException('Parent must be a folder', HttpErrorCode.VALIDATION_ERROR);
413413
}
414414

415-
if (parentNode) {
416-
const depth = await this.getFolderDepth(baseId, parentNode.id);
417-
if (depth > maxFolderDepth + 1) {
418-
throw new CustomHttpException('Folder depth exceeded', HttpErrorCode.VALIDATION_ERROR);
419-
}
415+
if (parentNode && resourceType === BaseNodeResourceType.Folder) {
416+
await this.assertFolderDepth(baseId, parentNode.id);
420417
}
421418

422419
switch (resourceType) {
@@ -656,12 +653,12 @@ export class BaseNodeService {
656653
}
657654

658655
let newNode: IBaseNodeEntry;
659-
if (parentId === null) {
656+
if (anchorId) {
657+
newNode = await this.moveNodeTo(baseId, node.id, { anchorId, position });
658+
} else if (parentId === null) {
660659
newNode = await this.moveNodeToRoot(baseId, node.id);
661660
} else if (parentId) {
662661
newNode = await this.moveNodeToFolder(baseId, node.id, parentId);
663-
} else if (anchorId) {
664-
newNode = await this.moveNodeTo(baseId, node.id, { anchorId, position });
665662
} else {
666663
throw new CustomHttpException(
667664
'At least one of parentId or anchorId must be provided',
@@ -757,6 +754,10 @@ export class BaseNodeService {
757754
throw new CustomHttpException(`Anchor ${anchorId} not found`, HttpErrorCode.NOT_FOUND);
758755
});
759756

757+
if (node.resourceType === BaseNodeResourceType.Folder && anchor.parentId) {
758+
await this.assertFolderDepth(baseId, anchor.parentId);
759+
}
760+
760761
await updateOrder({
761762
query: baseId,
762763
position: position ?? 'after',
@@ -1087,17 +1088,30 @@ export class BaseNodeService {
10871088
return entry;
10881089
}
10891090

1091+
private async assertFolderDepth(baseId: string, id: string) {
1092+
const folderDepth = await this.getFolderDepth(baseId, id);
1093+
console.log('folderDepth', folderDepth, 'maxFolderDepth', maxFolderDepth);
1094+
if (folderDepth >= maxFolderDepth) {
1095+
throw new CustomHttpException('Folder depth exceeded', HttpErrorCode.VALIDATION_ERROR);
1096+
}
1097+
}
1098+
10901099
private async getFolderDepth(baseId: string, id: string) {
10911100
const prisma = this.prismaService.txClient();
10921101
const allFolders = await prisma.baseNode.findMany({
10931102
where: { baseId, resourceType: BaseNodeResourceType.Folder },
10941103
select: { id: true, parentId: true },
10951104
});
10961105

1106+
let depth = 0;
1107+
if (allFolders.length === 0) {
1108+
return depth;
1109+
}
1110+
10971111
const folderMap = keyBy(allFolders, 'id');
1098-
let depth = 1;
10991112
let current = id;
11001113
while (current) {
1114+
depth++;
11011115
const folder = folderMap[current];
11021116
if (!folder) {
11031117
throw new CustomHttpException('Folder not found', HttpErrorCode.NOT_FOUND);
@@ -1106,7 +1120,6 @@ export class BaseNodeService {
11061120
throw new CustomHttpException('Folder is itself', HttpErrorCode.VALIDATION_ERROR);
11071121
}
11081122
current = folder.parentId ?? '';
1109-
depth++;
11101123
}
11111124
return depth;
11121125
}

apps/nestjs-backend/src/features/dashboard/dashboard.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -525,14 +525,14 @@ export class DashboardService {
525525
dashboard.name = newName;
526526

527527
return this.prismaService.$tx(async () => {
528-
const { dashboardMap } = await this.baseImportService.createDashboard(
528+
const { dashboardIdMap } = await this.baseImportService.createDashboard(
529529
baseId,
530530
[dashboard],
531531
{},
532532
{}
533533
);
534534

535-
const newDashboardId = dashboardMap[dashboardId];
535+
const newDashboardId = dashboardIdMap[dashboardId];
536536

537537
return {
538538
id: newDashboardId,

apps/nestjs-backend/test/base-node.e2e-spec.ts

Lines changed: 180 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,7 @@ describe('BaseNodeController (e2e) /api/base/:baseId/node', () => {
9797
});
9898

9999
afterEach(async () => {
100-
try {
101-
await deleteBaseNode(baseId, testNodeId);
102-
} catch (e) {
103-
// Node might already be deleted
104-
}
100+
await deleteBaseNode(baseId, testNodeId);
105101
});
106102

107103
it('should get single node successfully', async () => {
@@ -133,11 +129,7 @@ describe('BaseNodeController (e2e) /api/base/:baseId/node', () => {
133129
afterEach(async () => {
134130
// Cleanup created nodes
135131
for (const nodeId of nodesToCleanup) {
136-
try {
137-
await deleteBaseNode(baseId, nodeId);
138-
} catch (e) {
139-
// Ignore cleanup errors
140-
}
132+
await deleteBaseNode(baseId, nodeId);
141133
}
142134
nodesToCleanup.length = 0;
143135
});
@@ -293,11 +285,7 @@ describe('BaseNodeController (e2e) /api/base/:baseId/node', () => {
293285
});
294286

295287
afterEach(async () => {
296-
try {
297-
await deleteBaseNode(baseId, testNodeId);
298-
} catch (e) {
299-
// Node might already be deleted
300-
}
288+
await deleteBaseNode(baseId, testNodeId);
301289
});
302290

303291
it('should update node name successfully', async () => {
@@ -400,11 +388,7 @@ describe('BaseNodeController (e2e) /api/base/:baseId/node', () => {
400388

401389
afterEach(async () => {
402390
for (const nodeId of nodesToCleanup) {
403-
try {
404-
await deleteBaseNode(baseId, nodeId);
405-
} catch (e) {
406-
// Ignore cleanup errors
407-
}
391+
await deleteBaseNode(baseId, nodeId);
408392
}
409393
nodesToCleanup.length = 0;
410394
});
@@ -686,11 +670,7 @@ describe('BaseNodeController (e2e) /api/base/:baseId/node', () => {
686670

687671
afterEach(async () => {
688672
for (const nodeId of nodesToCleanup) {
689-
try {
690-
await deleteBaseNode(baseId, nodeId);
691-
} catch (e) {
692-
// Ignore cleanup errors
693-
}
673+
await deleteBaseNode(baseId, nodeId);
694674
}
695675
nodesToCleanup.length = 0;
696676
});
@@ -760,11 +740,7 @@ describe('BaseNodeController (e2e) /api/base/:baseId/node', () => {
760740

761741
afterEach(async () => {
762742
for (const nodeId of nodesToCleanup) {
763-
try {
764-
await deleteBaseNode(baseId, nodeId);
765-
} catch (e) {
766-
// Ignore cleanup errors
767-
}
743+
await deleteBaseNode(baseId, nodeId);
768744
}
769745
nodesToCleanup.length = 0;
770746
});
@@ -802,14 +778,12 @@ describe('BaseNodeController (e2e) /api/base/:baseId/node', () => {
802778
});
803779

804780
it('should handle complex folder hierarchy', async () => {
805-
// Create root folder
806781
const root = await createBaseNode(baseId, {
807782
resourceType: BaseNodeResourceType.Folder,
808783
name: 'Root',
809784
});
810785
nodesToCleanup.push(root.data.id);
811786

812-
// Create level 1 children
813787
const child1 = await createBaseNode(baseId, {
814788
resourceType: BaseNodeResourceType.Folder,
815789
name: 'Child 1',
@@ -824,13 +798,14 @@ describe('BaseNodeController (e2e) /api/base/:baseId/node', () => {
824798
});
825799
nodesToCleanup.push(child2.data.id);
826800

827-
// Create level 2 children
828-
const grandchild = await createBaseNode(baseId, {
829-
resourceType: BaseNodeResourceType.Folder,
830-
name: 'Grandchild',
801+
const child1Table = await createBaseNode(baseId, {
802+
resourceType: BaseNodeResourceType.Table,
803+
name: 'Child 1 Table',
831804
parentId: child1.data.id,
805+
fields: [{ name: 'Field1', type: FieldType.SingleLineText }],
806+
views: [{ name: 'Grid view', type: ViewType.Grid }],
832807
});
833-
nodesToCleanup.push(grandchild.data.id);
808+
nodesToCleanup.push(child1Table.data.id);
834809

835810
// Verify structure
836811
const tree = await getBaseNodeTree(baseId);
@@ -908,6 +883,173 @@ describe('BaseNodeController (e2e) /api/base/:baseId/node', () => {
908883
});
909884
});
910885

886+
describe('Folder depth limitation', () => {
887+
const nodesToCleanup: string[] = [];
888+
889+
afterEach(async () => {
890+
// Cleanup nodes in reverse order to handle hierarchy
891+
for (const nodeId of [...nodesToCleanup].reverse()) {
892+
await deleteBaseNode(baseId, nodeId);
893+
}
894+
nodesToCleanup.length = 0;
895+
});
896+
897+
it('should allow creating folders up to max depth (3 levels)', async () => {
898+
// Create level 1 folder
899+
const level1 = await createBaseNode(baseId, {
900+
resourceType: BaseNodeResourceType.Folder,
901+
name: 'Level 1 Folder',
902+
});
903+
nodesToCleanup.push(level1.data.id);
904+
expect(level1.data.parentId).toBeNull();
905+
906+
// Create level 2 folder
907+
const level2 = await createBaseNode(baseId, {
908+
resourceType: BaseNodeResourceType.Folder,
909+
name: 'Level 2 Folder',
910+
parentId: level1.data.id,
911+
});
912+
nodesToCleanup.push(level2.data.id);
913+
expect(level2.data.parentId).toBe(level1.data.id);
914+
});
915+
916+
it('should fail when creating folder exceeding max depth (4th level)', async () => {
917+
// Create 3 levels of folders
918+
const level1 = await createBaseNode(baseId, {
919+
resourceType: BaseNodeResourceType.Folder,
920+
name: 'Depth Limit Level 1',
921+
});
922+
nodesToCleanup.push(level1.data.id);
923+
924+
const level2 = await createBaseNode(baseId, {
925+
resourceType: BaseNodeResourceType.Folder,
926+
name: 'Depth Limit Level 2',
927+
parentId: level1.data.id,
928+
});
929+
nodesToCleanup.push(level2.data.id);
930+
931+
// Try to create level 4 folder (should fail)
932+
const error = await getError(() =>
933+
createBaseNode(baseId, {
934+
resourceType: BaseNodeResourceType.Folder,
935+
name: 'Depth Limit Level 3',
936+
parentId: level2.data.id,
937+
})
938+
);
939+
940+
expect(error?.status).toBe(400);
941+
});
942+
943+
it('should allow creating table in folder at max depth', async () => {
944+
// Create 2 levels of folders
945+
const level1 = await createBaseNode(baseId, {
946+
resourceType: BaseNodeResourceType.Folder,
947+
name: 'Table Depth Level 1',
948+
});
949+
nodesToCleanup.push(level1.data.id);
950+
951+
const level2 = await createBaseNode(baseId, {
952+
resourceType: BaseNodeResourceType.Folder,
953+
name: 'Table Depth Level 2',
954+
parentId: level1.data.id,
955+
});
956+
nodesToCleanup.push(level2.data.id);
957+
958+
// Create table in level 2 folder (should succeed - tables don't count as depth)
959+
const table = await createBaseNode(baseId, {
960+
resourceType: BaseNodeResourceType.Table,
961+
name: 'Table in Max Depth',
962+
parentId: level2.data.id,
963+
fields: [{ name: 'Field1', type: FieldType.SingleLineText }],
964+
views: [{ name: 'Grid view', type: ViewType.Grid }],
965+
});
966+
nodesToCleanup.push(table.data.id);
967+
expect(table.data.parentId).toBe(level2.data.id);
968+
});
969+
970+
it('should fail when moving folder to exceed max depth using anchorId', async () => {
971+
// Create 3 levels of folders
972+
const level1 = await createBaseNode(baseId, {
973+
resourceType: BaseNodeResourceType.Folder,
974+
name: 'Move Depth Level 1',
975+
});
976+
nodesToCleanup.push(level1.data.id);
977+
978+
const level2 = await createBaseNode(baseId, {
979+
resourceType: BaseNodeResourceType.Folder,
980+
name: 'Move Depth Level 2',
981+
parentId: level1.data.id,
982+
});
983+
nodesToCleanup.push(level2.data.id);
984+
985+
const level3 = await createBaseNode(baseId, {
986+
resourceType: BaseNodeResourceType.Table,
987+
name: 'Table in Move Depth Level 3',
988+
parentId: level2.data.id,
989+
fields: [{ name: 'Field1', type: FieldType.SingleLineText }],
990+
views: [{ name: 'Grid view', type: ViewType.Grid }],
991+
});
992+
nodesToCleanup.push(level3.data.id);
993+
994+
// Create a folder at root level to move
995+
const folderToMove = await createBaseNode(baseId, {
996+
resourceType: BaseNodeResourceType.Folder,
997+
name: 'Folder to Move',
998+
});
999+
nodesToCleanup.push(folderToMove.data.id);
1000+
1001+
// Try to move folder next to level2 (which would make it level 3 if it had the same parent)
1002+
// Using anchorId with position should check depth
1003+
const error = await getError(() =>
1004+
moveBaseNode(baseId, folderToMove.data.id, {
1005+
anchorId: level3.data.id,
1006+
position: 'after',
1007+
})
1008+
);
1009+
1010+
expect(error?.status).toBe(400);
1011+
});
1012+
1013+
it('should allow moving folder within valid depth using anchorId', async () => {
1014+
// Create 2 levels of folders
1015+
const level1 = await createBaseNode(baseId, {
1016+
resourceType: BaseNodeResourceType.Folder,
1017+
name: 'Valid Move Level 1',
1018+
});
1019+
nodesToCleanup.push(level1.data.id);
1020+
1021+
const level2 = await createBaseNode(baseId, {
1022+
resourceType: BaseNodeResourceType.Folder,
1023+
name: 'Valid Move Level 2',
1024+
parentId: level1.data.id,
1025+
});
1026+
nodesToCleanup.push(level2.data.id);
1027+
1028+
// Create a folder at root level
1029+
const folderToMove = await createBaseNode(baseId, {
1030+
resourceType: BaseNodeResourceType.Folder,
1031+
name: 'Folder to Move Valid',
1032+
});
1033+
nodesToCleanup.push(folderToMove.data.id);
1034+
1035+
// Move folder next to level2 (which makes it level 3 - still valid)
1036+
const response = await moveBaseNode(baseId, folderToMove.data.id, {
1037+
anchorId: level2.data.id,
1038+
position: 'after',
1039+
});
1040+
1041+
expect(response.data.id).toBe(folderToMove.data.id);
1042+
expect(response.data.parentId).toBe(level1.data.id);
1043+
});
1044+
1045+
it('should return maxFolderDepth in tree response', async () => {
1046+
const response = await getBaseNodeTree(baseId);
1047+
1048+
expect(response.data).toHaveProperty('maxFolderDepth');
1049+
expect(response.data.maxFolderDepth).toBe(2);
1050+
});
1051+
});
1052+
9111053
describe('Permission tests', () => {
9121054
let permissionSpaceId: string;
9131055
let permissionBaseId: string;
@@ -1181,11 +1323,7 @@ describe('BaseNodeController (e2e) /api/base/:baseId/node', () => {
11811323

11821324
afterEach(async () => {
11831325
for (const nodeId of creatorNodesToCleanup) {
1184-
try {
1185-
await deleteBaseNode(permissionBaseId, nodeId);
1186-
} catch (e) {
1187-
// Ignore cleanup errors
1188-
}
1326+
await deleteBaseNode(permissionBaseId, nodeId);
11891327
}
11901328
creatorNodesToCleanup.length = 0;
11911329
});

0 commit comments

Comments
 (0)