Skip to content

Commit 9b2fc17

Browse files
committed
Move Resource.tools to loader, so multiple resources can load their tools in one query
1 parent 1120725 commit 9b2fc17

File tree

6 files changed

+84
-22
lines changed

6 files changed

+84
-22
lines changed
Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
2+
import { Loader, type LoaderOf } from '@seedcompany/data-loader';
23
import { Resource } from '~/common';
34
import { ToolUsage } from './dto';
4-
import { ToolUsageService } from './tool-usage.service';
5+
import { ToolUsageByContainerLoader } from './tool-usage-by-container.loader';
56

67
@Resolver(() => Resource)
78
export class ResourceToolsResolver {
8-
constructor(private readonly toolUsageService: ToolUsageService) {}
9-
109
@ResolveField(() => [ToolUsage], {
1110
description: 'Tools used in this resource',
1211
})
13-
async tools(@Parent() resource: Resource): Promise<readonly ToolUsage[]> {
14-
return await this.toolUsageService.readByContainer(resource);
12+
async tools(
13+
@Parent() resource: Resource,
14+
@Loader(() => ToolUsageByContainerLoader)
15+
loader: LoaderOf<ToolUsageByContainerLoader>,
16+
): Promise<readonly ToolUsage[]> {
17+
const { usages } = await loader.load(resource);
18+
return usages;
1519
}
1620
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { type ID, type Resource } from '~/common';
2+
import {
3+
type DataLoaderStrategy,
4+
LoaderFactory,
5+
type LoaderOptionsOf,
6+
} from '~/core/data-loader';
7+
import { type ToolUsage } from './dto';
8+
import { ToolUsageService } from './tool-usage.service';
9+
10+
export interface UsagesByContainer {
11+
container: Resource;
12+
usages: readonly ToolUsage[];
13+
}
14+
15+
@LoaderFactory()
16+
export class ToolUsageByContainerLoader
17+
implements DataLoaderStrategy<UsagesByContainer, Resource, ID>
18+
{
19+
constructor(private readonly usages: ToolUsageService) {}
20+
21+
getOptions() {
22+
return {
23+
propertyKey: ({ container }) => container,
24+
cacheKeyFn: (container) => container.id,
25+
} satisfies LoaderOptionsOf<ToolUsageByContainerLoader>;
26+
}
27+
28+
async loadMany(
29+
ids: readonly Resource[],
30+
): Promise<readonly UsagesByContainer[]> {
31+
return await this.usages.readManyForContainers(ids);
32+
}
33+
}

src/components/tools/tool-usage/tool-usage.gel.repository.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,17 @@ export class ToolUsageRepository
2525
})
2626
implements PublicOf<Neo4jRepository>
2727
{
28-
async listForContainer(container: ID) {
29-
return await this.db.run(this.listForContainerQuery, { container });
28+
async listForContainers(containers: readonly ID[]) {
29+
return await this.db.run(this.listForContainersQuery, { containers });
3030
}
31-
private readonly listForContainerQuery = e.params(
32-
{ container: e.uuid },
31+
private readonly listForContainersQuery = e.params(
32+
{ containers: e.array(e.uuid) },
3333
($) => {
34-
const container = e.cast(e.Resource, $.container);
35-
return e.select(container.tools, this.hydrate);
34+
const containers = e.cast(e.Resource, e.array_unpack($.containers));
35+
return e.select(containers, (container) => ({
36+
container: e.select(container, (c) => ({ id: c.id })),
37+
usages: e.select(container.tools, this.hydrate),
38+
}));
3639
},
3740
);
3841
}

src/components/tools/tool-usage/tool-usage.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
22
import { splitDb } from '~/core/database';
33
import { ToolCoreModule } from '../tool/tool.module';
44
import { ResourceToolsResolver } from './resource-tools.resolver';
5+
import { ToolUsageByContainerLoader } from './tool-usage-by-container.loader';
56
import { ToolUsageRepository as GelRepository } from './tool-usage.gel.repository';
67
import { ToolUsageLoader } from './tool-usage.loader';
78
import { ToolUsageRepository as Neo4jRepository } from './tool-usage.neo4j.repository';
@@ -14,6 +15,7 @@ import { ToolUsageService } from './tool-usage.service';
1415
ToolUsageResolver,
1516
ResourceToolsResolver,
1617
ToolUsageLoader,
18+
ToolUsageByContainerLoader,
1719
ToolUsageService,
1820
splitDb(Neo4jRepository, GelRepository),
1921
],

src/components/tools/tool-usage/tool-usage.neo4j.repository.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { CreationFailed, type ID, type UnsecuredDto } from '~/common';
44
import { DtoRepository } from '~/core/database';
55
import {
66
ACTIVE,
7+
collect,
78
createNode,
89
createRelationships,
910
currentUser,
1011
filter,
1112
matchProps,
1213
merge,
14+
variable,
1315
} from '~/core/database/query';
1416
import { toolFilters } from '../tool/tool.neo4j.repository';
1517
import {
@@ -44,16 +46,25 @@ export class ToolUsageRepository extends DtoRepository(ToolUsage) {
4446
);
4547
}
4648

47-
async listForContainer(containerId: ID) {
49+
async listForContainers(containers: readonly ID[]) {
4850
const result = await this.db
4951
.query()
50-
.match([
51-
node('', 'BaseNode', { id: containerId }),
52-
relation('out', '', 'uses', ACTIVE),
53-
node('node', 'ToolUsage'),
54-
])
55-
.apply(this.hydrate())
56-
.map('dto')
52+
.unwind([...containers], 'containerId')
53+
.match(node('container', 'BaseNode', { id: variable('containerId') }))
54+
.subQuery('container', (sub) =>
55+
sub
56+
.match([
57+
node('container'),
58+
relation('out', '', 'uses', ACTIVE),
59+
node('node', 'ToolUsage'),
60+
])
61+
.subQuery('node', this.hydrate())
62+
.return(collect('dto').as('usages')),
63+
)
64+
.return<{
65+
container: { id: ID };
66+
usages: ReadonlyArray<UnsecuredDto<ToolUsage>>;
67+
}>(['container { .id }', 'usages'])
5768
.run();
5869
return result;
5970
}

src/components/tools/tool-usage/tool-usage.service.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Injectable } from '@nestjs/common';
2+
import { mapKeys } from '@seedcompany/common';
23
import {
34
type ID,
45
InputException,
@@ -13,6 +14,7 @@ import { type BaseNode, isBaseNode } from '~/core/database/results';
1314
import { Privileges } from '../../authorization';
1415
import { Tool } from '../tool/dto';
1516
import { type CreateToolUsage, ToolUsage, type UpdateToolUsage } from './dto';
17+
import { type UsagesByContainer } from './tool-usage-by-container.loader';
1618
import { ToolUsageRepository } from './tool-usage.neo4j.repository';
1719

1820
type TypedResource = Resource & { __typename: string };
@@ -48,9 +50,16 @@ export class ToolUsageService {
4850
return secured.flat();
4951
}
5052

51-
async readByContainer(container: Resource): Promise<readonly ToolUsage[]> {
52-
const dtos = await this.repo.listForContainer(container.id);
53-
return dtos.flatMap((dto) => this.secure(dto, container) ?? []);
53+
async readManyForContainers(containers: readonly Resource[]) {
54+
const containersById = mapKeys.fromList(containers, (r) => r.id).asMap;
55+
const rows = await this.repo.listForContainers(containers.map((r) => r.id));
56+
return rows.map((row): UsagesByContainer => {
57+
const container = containersById.get(row.container.id)!;
58+
return {
59+
container,
60+
usages: row.usages.flatMap((dto) => this.secure(dto, container) ?? []),
61+
};
62+
});
5463
}
5564

5665
private secure(

0 commit comments

Comments
 (0)