-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathGraphRelationBuilder.ts
108 lines (90 loc) · 4.22 KB
/
GraphRelationBuilder.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import { FragmentDefinitionNode, GraphQLResolveInfo, SelectionNode } from 'graphql';
import { findSelectionNode, getNameFromNode, getSelectionSetFromNode } from 'graphql-info-inspector';
import { DataSource, EntityMetadata, EntitySchema, ObjectType } from 'typeorm';
import { RelationMap } from 'typeorm-relations';
import { isEmbeddedMetadata, isRelationMetadata } from './util/metadata';
export class GraphRelationBuilder {
public constructor(private readonly dataSource: DataSource) {}
/*
* Build a map of matching TypeORM relation properties for an entity, based on the `info` given to a GraphQL
* query resolver.
*/
public buildForQuery<Entity extends InstanceType<any>>(
entity: ObjectType<Entity> | EntitySchema<Entity> | string,
info: GraphQLResolveInfo,
path?: string,
): RelationMap<Entity> {
const rootNode = info.fieldNodes.find(fieldNode => fieldNode.name.value === info.fieldName);
if (rootNode == null) {
throw new Error(`Could not locate field named "${info.fieldName}" in query info"`);
}
const baseNode = path != null ? findSelectionNode(path, info) : rootNode;
if (baseNode == null) {
return new RelationMap<Entity>();
}
return this.build(entity, baseNode, info.fragments);
}
/*
* Build a map of matching TypeORM relation properties for an entity, starting at a base SelectionNode.
*/
public build<Entity extends InstanceType<any>>(
entity: ObjectType<Entity> | EntitySchema<Entity> | string,
baseNode: SelectionNode,
fragments?: Record<string, FragmentDefinitionNode>,
basePropertyPath?: string,
currentLevel: number = 0,
): RelationMap<Entity> {
const relationMap = new RelationMap<Entity>();
const selectionSet = getSelectionSetFromNode(baseNode, fragments);
if (selectionSet == null) {
return relationMap;
}
// look for any relation properties among the selected fields inside the base node
selectionSet.selections.forEach((selectionNode: SelectionNode) => {
const currentPropertyPath: string[] = (basePropertyPath ?? '').split('.').filter(path => path !== '');
// eslint-disable-next-line @typescript-eslint/ban-types
let currentTargetEntity = entity;
let nextLevel: number = currentLevel;
const nodeName = getNameFromNode(selectionNode);
// when the node has a name (i.e. is not an inline fragment), we can look for relations to map
if (nodeName != null) {
// remove elements from path up to the level of the current entity
const currentPropertyPathExcludingEntity = [...currentPropertyPath];
currentPropertyPathExcludingEntity.splice(0, currentLevel);
// then add the current node name to the end of the path
const propPath = [...currentPropertyPathExcludingEntity, nodeName].join('.');
// find relation or embedded entity metadata, if field corresponds to such a property on the entity
const propMetadata =
this.getEntityMetadata(currentTargetEntity).findRelationWithPropertyPath(propPath) ||
this.getEntityMetadata(currentTargetEntity).findEmbeddedWithPropertyPath(propPath);
if (propMetadata != null) {
if (isRelationMetadata(propMetadata)) {
currentTargetEntity = propMetadata.inverseEntityMetadata.target;
nextLevel = currentLevel + 1;
currentPropertyPath.push(propMetadata.propertyName);
relationMap.add(currentPropertyPath);
} else if (isEmbeddedMetadata(propMetadata)) {
currentPropertyPath.push(propMetadata.propertyPath);
}
}
}
/*
* Note: if the field is not a mapped property it's still possible that its children contain further
* relation properties, so we continue to recurse as long as there are nested selection sets.
*/
// recursively map nested relations
const nestedRelations = this.build(
currentTargetEntity,
selectionNode,
fragments,
currentPropertyPath.join('.'),
nextLevel,
);
relationMap.add(nestedRelations);
});
return relationMap;
}
private getEntityMetadata(entity: ObjectType<any> | EntitySchema | string): EntityMetadata {
return this.dataSource.getMetadata(entity);
}
}