Skip to content

Commit 6ab37b0

Browse files
authored
Merge pull request #164 from import-ai/refactor/resources
refactor(resources): update listChildren
2 parents 150a061 + f2b9921 commit 6ab37b0

File tree

10 files changed

+184
-123
lines changed

10 files changed

+184
-123
lines changed

src/app/app.module.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ClassSerializerInterceptor,
23
DynamicModule,
34
MiddlewareConsumer,
45
Module,
@@ -54,13 +55,17 @@ export class AppModule implements NestModule {
5455
module: AppModule,
5556
controllers: [AppController],
5657
providers: [
58+
{
59+
provide: APP_PIPE,
60+
useClass: ValidationPipe,
61+
},
5762
{
5863
provide: APP_INTERCEPTOR,
5964
useClass: SnakeCaseInterceptor,
6065
},
6166
{
62-
provide: APP_PIPE,
63-
useClass: ValidationPipe,
67+
provide: APP_INTERCEPTOR,
68+
useClass: ClassSerializerInterceptor,
6469
},
6570
],
6671
imports: [
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Expose } from 'class-transformer';
2+
import { ResourceMetaDto } from 'omniboxd/resources/dto/resource-meta.dto';
3+
4+
export class ListChildrenRespDto extends ResourceMetaDto {
5+
@Expose({ name: 'has_children' })
6+
hasChildren: boolean;
7+
8+
constructor(resourceMeta: ResourceMetaDto, hasChildren: boolean) {
9+
super();
10+
Object.assign(this, resourceMeta);
11+
this.hasChildren = hasChildren;
12+
}
13+
}

src/namespace-resources/dto/resource.dto.ts

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ResourcePermission } from 'omniboxd/permissions/resource-permission.enum';
2+
import { ResourceMetaDto } from 'omniboxd/resources/dto/resource-meta.dto';
23
import {
34
Resource,
45
ResourceType,
@@ -10,30 +11,6 @@ export enum SpaceType {
1011
TEAM = 'teamspace',
1112
}
1213

13-
export class ResourceMetaDto {
14-
id: string;
15-
parent_id: string | null;
16-
name: string;
17-
resource_type: string;
18-
attrs: Record<string, any>;
19-
tags: TagDto[];
20-
created_at: string;
21-
updated_at: string;
22-
23-
static fromEntity(resource: Resource, tags: TagDto[] = []) {
24-
const dto = new ResourceMetaDto();
25-
dto.id = resource.id;
26-
dto.parent_id = resource.parentId;
27-
dto.name = resource.name;
28-
dto.resource_type = resource.resourceType;
29-
dto.attrs = resource.attrs;
30-
dto.tags = tags;
31-
dto.created_at = resource.createdAt.toISOString();
32-
dto.updated_at = resource.updatedAt.toISOString();
33-
return dto;
34-
}
35-
}
36-
3714
export class ResourceDto {
3815
id: string;
3916
namespace_id: string;

src/namespace-resources/namespace-resources.controller.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import {
1717
} from '@nestjs/common';
1818
import { UserId } from 'omniboxd/decorators/user-id.decorator';
1919
import { Request } from 'express';
20-
import { ResourceMetaDto } from 'omniboxd/namespace-resources/dto/resource.dto';
20+
import { ResourceMetaDto } from 'omniboxd/resources/dto/resource-meta.dto';
21+
import { ListChildrenRespDto } from './dto/list-children-resp.dto';
2122

2223
@Controller('api/v1/namespaces/:namespaceId/resources')
2324
export class NamespaceResourcesController {
@@ -91,7 +92,7 @@ export class NamespaceResourcesController {
9192
@UserId() userId: string,
9293
@Param('namespaceId') namespaceId: string,
9394
@Param('resourceId') resourceId: string,
94-
): Promise<ResourceMetaDto[]> {
95+
): Promise<ListChildrenRespDto[]> {
9596
return this.namespaceResourcesService.listChildren(
9697
namespaceId,
9798
resourceId,

src/namespace-resources/namespace-resources.e2e-spec.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -676,17 +676,6 @@ describe('ResourcesController (e2e)', () => {
676676
);
677677
expect(childResource).toBeDefined();
678678
expect(childResource.name).toBe('Child Resource for List');
679-
expect(childResource.attrs).toEqual(childAttrs);
680-
681-
// Check for required timestamp fields
682-
expect(childResource).toHaveProperty('created_at');
683-
expect(childResource).toHaveProperty('updated_at');
684-
expect(childResource.created_at).toMatch(
685-
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/,
686-
);
687-
expect(childResource.updated_at).toMatch(
688-
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/,
689-
);
690679
});
691680

692681
it('should return empty array for resource with no children', async () => {
@@ -836,7 +825,6 @@ describe('ResourcesController (e2e)', () => {
836825
);
837826
expect(foundResource).toBeDefined();
838827
expect(foundResource.name).toBe('Searchable Document');
839-
expect(foundResource.attrs).toEqual(searchAttrs);
840828
});
841829

842830
it('should search resources by partial name and validate attrs match source', async () => {
@@ -852,7 +840,6 @@ describe('ResourcesController (e2e)', () => {
852840
(r: any) => r.id === searchableResourceId,
853841
);
854842
expect(foundResource).toBeDefined();
855-
expect(foundResource.attrs).toEqual(searchAttrs);
856843
});
857844

858845
it('should return empty array for non-matching search', async () => {
@@ -1070,7 +1057,6 @@ describe('ResourcesController (e2e)', () => {
10701057
(r: any) => r.id === testResourceId,
10711058
);
10721059
expect(foundResource).toBeDefined();
1073-
expect(foundResource.attrs).toEqual(complexAttrs);
10741060
});
10751061

10761062
it('should preserve attrs in search endpoint', async () => {
@@ -1085,7 +1071,6 @@ describe('ResourcesController (e2e)', () => {
10851071
(r: any) => r.id === testResourceId,
10861072
);
10871073
expect(foundResource).toBeDefined();
1088-
expect(foundResource.attrs).toEqual(complexAttrs);
10891074
});
10901075

10911076
it('should preserve attrs after update', async () => {

src/namespace-resources/namespace-resources.service.ts

Lines changed: 88 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ import { PermissionsService } from 'omniboxd/permissions/permissions.service';
3333
import { PrivateSearchResourceDto } from 'omniboxd/wizard/dto/agent-request.dto';
3434
import { ResourcePermission } from 'omniboxd/permissions/resource-permission.enum';
3535
import { Response } from 'express';
36-
import { ResourceDto, ResourceMetaDto, SpaceType } from './dto/resource.dto';
36+
import { ResourceDto, SpaceType } from './dto/resource.dto';
3737
import { Namespace } from 'omniboxd/namespaces/entities/namespace.entity';
3838
import { TagService } from 'omniboxd/tag/tag.service';
3939
import { TagDto } from 'omniboxd/tag/dto/tag.dto';
4040
import { ResourceAttachmentsService } from 'omniboxd/resource-attachments/resource-attachments.service';
4141
import { ResourcesService } from 'omniboxd/resources/resources.service';
42+
import { ResourceMetaDto } from 'omniboxd/resources/dto/resource-meta.dto';
43+
import { ListChildrenRespDto } from './dto/list-children-resp.dto';
4244

4345
const TASK_PRIORITY = 5;
4446

@@ -128,6 +130,28 @@ export class NamespaceResourcesService {
128130
return resources.map((resource) => resource.id);
129131
}
130132

133+
private async hasChildren(
134+
namespaceId: string,
135+
parents: ResourceMetaDto[],
136+
userId: string,
137+
): Promise<boolean> {
138+
const children = await this.resourcesService.getSubResources(
139+
namespaceId,
140+
parents[0].id,
141+
);
142+
for (const child of children) {
143+
const permission = await this.permissionsService.getCurrentPermission(
144+
namespaceId,
145+
[child, ...parents],
146+
userId,
147+
);
148+
if (permission !== ResourcePermission.NO_ACCESS) {
149+
return true;
150+
}
151+
}
152+
return false;
153+
}
154+
131155
async findByIds(namespaceId: string, ids: Array<string>) {
132156
if (ids.length <= 0) {
133157
return [];
@@ -384,13 +408,17 @@ export class NamespaceResourcesService {
384408
};
385409
// Self and child exclusions
386410
if (excludeResourceId) {
387-
const resourceChildren = await this.getAllSubResources(
411+
const resourceChildren = await this.getSubResourcesByUser(
388412
namespaceId,
389413
excludeResourceId,
390-
'',
391-
true,
414+
userId,
415+
);
416+
where.id = Not(
417+
In([
418+
excludeResourceId,
419+
...resourceChildren.map((children) => children.id),
420+
]),
392421
);
393-
where.id = Not(In(resourceChildren.map((children) => children.id)));
394422
}
395423
if (name) {
396424
where.name = Like(`%${name}%`);
@@ -406,90 +434,84 @@ export class NamespaceResourcesService {
406434
userId,
407435
resources,
408436
);
409-
410-
// Load tags for filtered resources
411-
const tagsMap = await this.getTagsForResources(
412-
namespaceId,
413-
filteredResources,
414-
);
415-
416-
return filteredResources.map((res) =>
417-
ResourceMetaDto.fromEntity(res, tagsMap.get(res.id) || []),
418-
);
419-
}
420-
421-
async getAllSubResources(
422-
namespaceId: string,
423-
parentId: string,
424-
userId?: string,
425-
includeRoot: boolean = false,
426-
): Promise<Resource[]> {
427-
let resources: Resource[] = [await this.get(parentId)];
428-
for (const res of resources) {
429-
const subResources: Resource[] = await this.query(namespaceId, res.id);
430-
resources.push(...subResources);
431-
}
432-
resources = includeRoot ? resources : resources.slice(1);
433-
return userId
434-
? await this.permissionFilter(namespaceId, userId, resources)
435-
: resources;
437+
return filteredResources.map((res) => ResourceMetaDto.fromEntity(res));
436438
}
437439

438-
async listChildren(
440+
async getSubResourcesByUser(
439441
namespaceId: string,
440442
resourceId: string,
441443
userId: string,
442444
): Promise<ResourceMetaDto[]> {
443-
const parentResources = await this.resourcesService.getParentResources(
445+
const parents = await this.resourcesService.getParentResources(
444446
namespaceId,
445447
resourceId,
446448
);
449+
const children = await this.getSubResourcesByParents(
450+
namespaceId,
451+
parents,
452+
userId,
453+
);
454+
return children;
455+
}
456+
457+
async getSubResourcesByParents(
458+
namespaceId: string,
459+
parents: ResourceMetaDto[],
460+
userId: string,
461+
): Promise<ResourceMetaDto[]> {
462+
if (!parents) {
463+
return [];
464+
}
447465
const permission = await this.permissionsService.getCurrentPermission(
448466
namespaceId,
449-
parentResources,
467+
parents,
450468
userId,
451469
);
452470
if (permission === ResourcePermission.NO_ACCESS) {
453471
return [];
454472
}
455-
456-
const children = await this.resourceRepository.find({
457-
select: [
458-
'id',
459-
'name',
460-
'parentId',
461-
'resourceType',
462-
'attrs',
463-
'tagIds',
464-
'createdAt',
465-
'updatedAt',
466-
],
467-
where: {
468-
namespaceId,
469-
parentId: resourceId,
470-
},
471-
});
472-
473-
const filteredChildren: Resource[] = [];
473+
const children = await this.resourcesService.getSubResources(
474+
namespaceId,
475+
parents[0].id,
476+
);
477+
const filteredChildren: ResourceMetaDto[] = [];
474478
for (const child of children) {
475479
const permission = await this.permissionsService.getCurrentPermission(
476480
namespaceId,
477-
[child, ...parentResources],
481+
[child, ...parents],
478482
userId,
479483
);
480484
if (permission !== ResourcePermission.NO_ACCESS) {
481485
filteredChildren.push(child);
482486
}
483487
}
484-
// Load tags for filtered children
485-
const tagsMap = await this.getTagsForResources(
488+
return filteredChildren;
489+
}
490+
491+
async listChildren(
492+
namespaceId: string,
493+
resourceId: string,
494+
userId: string,
495+
): Promise<ListChildrenRespDto[]> {
496+
const parents = await this.resourcesService.getParentResources(
486497
namespaceId,
487-
filteredChildren,
498+
resourceId,
488499
);
489-
490-
return filteredChildren.map((r) =>
491-
ResourceMetaDto.fromEntity(r, tagsMap.get(r.id) || []),
500+
const children = await this.getSubResourcesByParents(
501+
namespaceId,
502+
parents,
503+
userId,
492504
);
505+
const resps: ListChildrenRespDto[] = [];
506+
for (const child of children) {
507+
const hasChildren = await this.hasChildren(
508+
namespaceId,
509+
[child, ...parents],
510+
userId,
511+
);
512+
resps.push(new ListChildrenRespDto(child, hasChildren));
513+
}
514+
return resps;
493515
}
494516

495517
async getSpaceType(
@@ -528,24 +550,20 @@ export class NamespaceResourcesService {
528550
: resourceId;
529551
const spaceType = await this.getSpaceType(namespaceId, rootResourceId);
530552

553+
const resourceMeta = ResourceMetaDto.fromEntity(resource);
531554
const curPermission = await this.permissionsService.getCurrentPermission(
532555
namespaceId,
533-
[resource, ...parentResources],
556+
[resourceMeta, ...parentResources],
534557
userId,
535558
);
536559

537560
if (curPermission === ResourcePermission.NO_ACCESS) {
538561
throw new ForbiddenException('Not authorized');
539562
}
540563

541-
// Load tags for resource and path
542-
const allResources = [resource, ...parentResources];
543-
const tagsMap = await this.getTagsForResources(namespaceId, allResources);
544-
545-
const path = [resource, ...parentResources]
546-
.map((r) => ResourceMetaDto.fromEntity(r, tagsMap.get(r.id) || []))
547-
.reverse();
548-
564+
// Load tags of the resource
565+
const tagsMap = await this.getTagsForResources(namespaceId, [resource]);
566+
const path = [resourceMeta, ...parentResources].reverse();
549567
return ResourceDto.fromEntity(
550568
resource,
551569
curPermission,

0 commit comments

Comments
 (0)