Skip to content

Commit cb5dd2d

Browse files
committed
Support one-to-one backward relations
1 parent 2fd57df commit cb5dd2d

File tree

2 files changed

+250
-5
lines changed

2 files changed

+250
-5
lines changed

index.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@ module.exports = function PostGraphileConnectionFilterPlugin(builder, options) {
1616
}
1717

1818
if (connectionFilterRelations) {
19-
require("./src/PgConnectionArgFilterForwardRelationsPlugin.js")(
20-
builder,
21-
options
22-
);
19+
require("./src/PgConnectionArgFilterBackwardRelationsPlugin.js")(
20+
builder,
21+
options
22+
);
23+
require("./src/PgConnectionArgFilterForwardRelationsPlugin.js")(
24+
builder,
25+
options
26+
);
2327
}
24-
28+
2529
require("./src/PgConnectionArgFilterOperatorsPlugin.js")(builder, options);
2630
};
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
module.exports = function PgConnectionArgFilterBackwardRelationsPlugin(
2+
builder,
3+
{ pgSimpleCollections }
4+
) {
5+
const hasConnections = pgSimpleCollections !== "only";
6+
const hasSimpleCollections =
7+
pgSimpleCollections === "only" || pgSimpleCollections === "both";
8+
9+
builder.hook("GraphQLInputObjectType:fields", (fields, build, context) => {
10+
const {
11+
extend,
12+
getTypeByName,
13+
inflection,
14+
pgIntrospectionResultsByKind: introspectionResultsByKind,
15+
backwardRelationFieldInfoFromTable,
16+
} = build;
17+
const {
18+
fieldWithHooks,
19+
scope: { pgIntrospection: foreignTable, isPgConnectionFilter },
20+
} = context;
21+
22+
if (!isPgConnectionFilter) return fields;
23+
24+
const backwardRelationFields = Object.entries(
25+
backwardRelationFieldInfoFromTable(
26+
foreignTable,
27+
introspectionResultsByKind
28+
)
29+
).reduce((memo, curr) => {
30+
const [fieldName, { table }] = curr;
31+
const tableTypeName = inflection.tableType(table);
32+
const type = getTypeByName(inflection.filterType(tableTypeName));
33+
if (type != null) {
34+
memo[fieldName] = fieldWithHooks(
35+
fieldName,
36+
{
37+
description: `Filter by the object’s \`${fieldName}\` field.`,
38+
type,
39+
},
40+
{
41+
isPgConnectionFilterField: true,
42+
}
43+
);
44+
}
45+
return memo;
46+
}, {});
47+
48+
return extend(fields, backwardRelationFields);
49+
});
50+
51+
builder.hook("build", build => {
52+
const {
53+
extend,
54+
inflection,
55+
pgOmit: omit,
56+
pgSql: sql,
57+
connectionFilterFieldResolvers,
58+
} = build;
59+
60+
// *** Much of this code was copied from PgBackwardRelationPlugin.js ***
61+
const backwardRelationFieldInfoFromTable = (
62+
foreignTable,
63+
introspectionResultsByKind
64+
) => {
65+
// This is a relation in which WE are foreign
66+
const foreignKeyConstraints = introspectionResultsByKind.constraint
67+
.filter(con => con.type === "f")
68+
.filter(con => con.foreignClassId === foreignTable.id);
69+
const foreignAttributes = introspectionResultsByKind.attribute
70+
.filter(attr => attr.classId === foreignTable.id)
71+
.sort((a, b) => a.num - b.num);
72+
return foreignKeyConstraints.reduce((memo, constraint) => {
73+
if (omit(constraint, "read")) {
74+
return memo;
75+
}
76+
const table = introspectionResultsByKind.classById[constraint.classId];
77+
const foreignTable =
78+
introspectionResultsByKind.classById[constraint.foreignClassId];
79+
if (!table) {
80+
throw new Error(
81+
`Could not find the table that referenced us (constraint: ${
82+
constraint.name
83+
})`
84+
);
85+
}
86+
87+
const attributes = introspectionResultsByKind.attribute.filter(
88+
attr => attr.classId === table.id
89+
);
90+
91+
const keys = constraint.keyAttributeNums.map(
92+
num => attributes.filter(attr => attr.num === num)[0]
93+
);
94+
const foreignKeys = constraint.foreignKeyAttributeNums.map(
95+
num => foreignAttributes.filter(attr => attr.num === num)[0]
96+
);
97+
if (!keys.every(_ => _) || !foreignKeys.every(_ => _)) {
98+
throw new Error("Could not find key columns!");
99+
}
100+
if (keys.some(key => omit(key, "read"))) {
101+
return memo;
102+
}
103+
if (foreignKeys.some(key => omit(key, "read"))) {
104+
return memo;
105+
}
106+
const isUnique = !!introspectionResultsByKind.constraint.find(
107+
c =>
108+
c.classId === table.id &&
109+
(c.type === "p" || c.type === "u") &&
110+
c.keyAttributeNums.length === keys.length &&
111+
c.keyAttributeNums.every((n, i) => keys[i].num === n)
112+
);
113+
114+
const singleRelationFieldName = isUnique
115+
? inflection.singleRelationByKeysBackwards(
116+
keys,
117+
table,
118+
foreignTable,
119+
constraint
120+
)
121+
: null;
122+
123+
const shouldAddSingleRelation = isUnique;
124+
125+
// Intentionally disabled for now.
126+
// Need to expose `any` and `all` options instead of simply checking for `any`.
127+
const shouldAddManyRelation = false; // !isUnique;
128+
129+
if (
130+
shouldAddSingleRelation &&
131+
!omit(table, "read") &&
132+
singleRelationFieldName
133+
) {
134+
memo = extend(memo, {
135+
[singleRelationFieldName]: {
136+
table,
137+
foreignKeys,
138+
keys,
139+
},
140+
});
141+
}
142+
function makeFields(isConnection) {
143+
if (isUnique && !isConnection) {
144+
// Don't need this, use the singular instead
145+
return;
146+
}
147+
if (shouldAddManyRelation && !omit(table, "many")) {
148+
const manyRelationFieldName = isConnection
149+
? inflection.manyRelationByKeys(
150+
keys,
151+
table,
152+
foreignTable,
153+
constraint
154+
)
155+
: inflection.manyRelationByKeysSimple(
156+
keys,
157+
table,
158+
foreignTable,
159+
constraint
160+
);
161+
162+
memo = extend(memo, {
163+
[manyRelationFieldName]: {
164+
table,
165+
foreignKeys,
166+
keys,
167+
},
168+
});
169+
}
170+
}
171+
if (hasConnections) {
172+
makeFields(true);
173+
}
174+
if (hasSimpleCollections) {
175+
makeFields(false);
176+
}
177+
178+
return memo;
179+
}, {});
180+
};
181+
182+
const resolve = ({
183+
sourceAlias,
184+
source,
185+
fieldName,
186+
fieldValue,
187+
introspectionResultsByKind,
188+
connectionFilterFieldResolvers,
189+
}) => {
190+
const backwardRelationFieldInfo = backwardRelationFieldInfoFromTable(
191+
source,
192+
introspectionResultsByKind
193+
)[fieldName];
194+
195+
if (backwardRelationFieldInfo == null) return null;
196+
197+
const { table, foreignKeys, keys } = backwardRelationFieldInfo;
198+
199+
const tableAlias = sql.identifier(Symbol());
200+
if (table == null) return null;
201+
202+
return sql.query`exists(
203+
select 1 from ${sql.identifier(
204+
table.namespace.name,
205+
table.name
206+
)} as ${tableAlias}
207+
where (${sql.join(
208+
keys.map((key, i) => {
209+
return sql.fragment`${tableAlias}.${sql.identifier(
210+
key.name
211+
)} = ${sourceAlias}.${sql.identifier(foreignKeys[i].name)}`;
212+
}),
213+
") and ("
214+
)})
215+
and (${sql.query`(${sql.join(
216+
Object.entries(fieldValue).map(([fieldName, fieldValue]) => {
217+
// Try to resolve field
218+
for (const resolve of connectionFilterFieldResolvers) {
219+
const resolved = resolve({
220+
sourceAlias: tableAlias,
221+
source: table,
222+
fieldName,
223+
fieldValue,
224+
introspectionResultsByKind,
225+
connectionFilterFieldResolvers,
226+
});
227+
if (resolved != null) return resolved;
228+
}
229+
}),
230+
") and ("
231+
)})`})
232+
)`;
233+
};
234+
235+
connectionFilterFieldResolvers.push(resolve);
236+
237+
return extend(build, {
238+
backwardRelationFieldInfoFromTable,
239+
});
240+
});
241+
};

0 commit comments

Comments
 (0)