Skip to content

Commit 1a8a0bc

Browse files
singingwolfboybenjie
authored andcommitted
feat(QueryBuilder): new methods for managing QB children (#537)
1 parent 9771ca5 commit 1a8a0bc

File tree

8 files changed

+301
-2
lines changed

8 files changed

+301
-2
lines changed

packages/graphile-build-pg/src/QueryBuilder.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ export default class QueryBuilder {
9393
addNotDistinctFromNullCase?: boolean;
9494
useAsterisk?: boolean;
9595
}): SQL;
96+
public buildChild(): QueryBuilder;
97+
public buildNamedChildSelecting(
98+
name: RawAlias,
99+
from: SQLGen,
100+
selectExpression: SQLGen,
101+
alias?: SQLAlias
102+
): QueryBuilder;
103+
public getNamedChild(name: RawAlias): QueryBuilder | undefined;
96104

97105
// ----------------------------------------
98106

packages/graphile-build-pg/src/QueryBuilder.js

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class QueryBuilder {
8080
selectedIdentifiers: boolean;
8181
data: {
8282
cursorPrefix: Array<string>,
83+
fixedSelectExpression: ?SQLGen,
8384
select: Array<[SQLGen, RawAlias]>,
8485
selectCursor: ?SQLGen,
8586
from: ?[SQLGen, SQLAlias],
@@ -106,6 +107,7 @@ class QueryBuilder {
106107
};
107108
compiledData: {
108109
cursorPrefix: Array<string>,
110+
fixedSelectExpression: ?SQL,
109111
select: Array<[SQL, RawAlias]>,
110112
selectCursor: ?SQL,
111113
from: ?[SQL, SQLAlias],
@@ -126,6 +128,7 @@ class QueryBuilder {
126128
lockContext: {
127129
queryBuilder: QueryBuilder,
128130
};
131+
_children: Map<RawAlias, QueryBuilder>;
129132

130133
constructor(
131134
options: QueryBuilderOptions = {},
@@ -144,6 +147,7 @@ class QueryBuilder {
144147
// As a performance optimisation, we're going to list a number of lock
145148
// types so that V8 doesn't need to mutate the object too much
146149
cursorComparator: false,
150+
fixedSelectExpression: false,
147151
select: false,
148152
selectCursor: false,
149153
from: false,
@@ -162,6 +166,7 @@ class QueryBuilder {
162166
this.data = {
163167
// TODO: refactor `cursorPrefix`, it shouldn't be here (or should at least have getters/setters)
164168
cursorPrefix: ["natural"],
169+
fixedSelectExpression: null,
165170
select: [],
166171
selectCursor: null,
167172
from: null,
@@ -181,6 +186,7 @@ class QueryBuilder {
181186
// As a performance optimisation, we're going to list a number of lock
182187
// types so that V8 doesn't need to mutate the object too much
183188
cursorComparator: [],
189+
fixedSelectExpression: [],
184190
select: [],
185191
selectCursor: [],
186192
from: [],
@@ -199,6 +205,7 @@ class QueryBuilder {
199205
};
200206
this.compiledData = {
201207
cursorPrefix: ["natural"],
208+
fixedSelectExpression: null,
202209
select: [],
203210
selectCursor: null,
204211
from: null,
@@ -216,6 +223,7 @@ class QueryBuilder {
216223
last: null,
217224
cursorComparator: null,
218225
};
226+
this._children = new Map();
219227
this.beforeLock("select", () => {
220228
this.lock("selectCursor");
221229
if (this.compiledData.selectCursor) {
@@ -363,8 +371,24 @@ ${sql.join(
363371
this.compiledData.cursorComparator(cursorValue, isAfter);
364372
});
365373
}
374+
/** this method is experimental */
375+
fixedSelectExpression(exprGen: SQLGen) {
376+
this.checkLock("fixedSelectExpression");
377+
this.lock("select");
378+
this.lock("selectCursor");
379+
if (this.data.select.length > 0) {
380+
throw new Error("Cannot use .fixedSelectExpression() with .select()");
381+
}
382+
if (this.data.selectCursor) {
383+
throw new Error(
384+
"Cannot use .fixedSelectExpression() with .selectCursor()"
385+
);
386+
}
387+
this.data.fixedSelectExpression = exprGen;
388+
}
366389
select(exprGen: SQLGen, alias: RawAlias) {
367390
this.checkLock("select");
391+
this.lock("fixedSelectExpression");
368392
if (typeof alias === "string") {
369393
// To protect against vulnerabilities such as
370394
//
@@ -403,6 +427,7 @@ ${sql.join(
403427
}
404428
selectCursor(exprGen: SQLGen) {
405429
this.checkLock("selectCursor");
430+
this.lock("fixedSelectExpression");
406431
this.data.selectCursor = exprGen;
407432
}
408433
from(expr: SQLGen, alias?: SQLAlias = sql.identifier(Symbol())) {
@@ -572,6 +597,9 @@ ${sql.join(
572597
}
573598
buildSelectFields() {
574599
this.lockEverything();
600+
if (this.compiledData.fixedSelectExpression) {
601+
return this.compiledData.fixedSelectExpression;
602+
}
575603
return sql.join(
576604
this.compiledData.select.map(
577605
([sqlFragment, alias]) =>
@@ -675,6 +703,15 @@ ${sql.join(
675703
useAsterisk?: boolean,
676704
} = {}
677705
) {
706+
this.lockEverything();
707+
708+
if (this.compiledData.fixedSelectExpression) {
709+
if (Object.keys(options).length > 0) {
710+
throw new Error(
711+
"Do not pass options to QueryBuilder.build() when using `buildNamedChildSelecting`"
712+
);
713+
}
714+
}
678715
const {
679716
asJson = false,
680717
asJsonAggregate = false,
@@ -684,7 +721,6 @@ ${sql.join(
684721
useAsterisk = false,
685722
} = options;
686723

687-
this.lockEverything();
688724
if (onlyJsonField) {
689725
return this.buildSelectJson({ addNullCase, addNotDistinctFromNullCase });
690726
}
@@ -782,6 +818,8 @@ order by (row_number() over (partition by 1)) desc`;
782818
this.data[type].upper,
783819
context
784820
);
821+
} else if (type === "fixedSelectExpression") {
822+
this.compiledData[type] = callIfNecessary(this.data[type], context);
785823
} else if (type === "select") {
786824
/*
787825
* NOTICE: locking select can cause additional selects to be added, so the
@@ -871,9 +909,39 @@ order by (row_number() over (partition by 1)) desc`;
871909
this.lock("first");
872910
this.lock("last");
873911
// We must execute select after orderBy otherwise we cannot generate a cursor
912+
this.lock("fixedSelectExpression");
874913
this.lock("selectCursor");
875914
this.lock("select");
876915
}
916+
/** this method is experimental */
917+
buildChild() {
918+
const options = { supportsJSONB: this.supportsJSONB };
919+
const child = new QueryBuilder(options, this.context, this.rootValue);
920+
child.parentQueryBuilder = this;
921+
return child;
922+
}
923+
/** this method is experimental */
924+
buildNamedChildSelecting(
925+
name: RawAlias,
926+
from: SQLGen,
927+
selectExpression: SQLGen,
928+
alias?: SQLAlias
929+
) {
930+
if (this._children.has(name)) {
931+
throw new Error(
932+
`QueryBuilder already has a child named ${name.toString()}`
933+
);
934+
}
935+
const child = this.buildChild();
936+
child.from(from, alias);
937+
child.fixedSelectExpression(selectExpression);
938+
this._children.set(name, child);
939+
return child;
940+
}
941+
/** this method is experimental */
942+
getNamedChild(name: string) {
943+
return this._children.get(name);
944+
}
877945
}
878946

879947
export default QueryBuilder;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
t1: toyById(id: 1) {
3+
categories {
4+
name
5+
}
6+
}
7+
t2: toyById(id: 1) {
8+
categories(approved: true) {
9+
name
10+
}
11+
}
12+
t3: toyById(id: 1) {
13+
categories(approved: false) {
14+
name
15+
}
16+
}
17+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
module.exports = builder => {
2+
// This hook adds the 'Toy.categories' field
3+
builder.hook("GraphQLObjectType:fields", (fields, build, context) => {
4+
const {
5+
getTypeByName,
6+
graphql: { GraphQLList },
7+
getSafeAliasFromAlias,
8+
getSafeAliasFromResolveInfo,
9+
pgSql: sql,
10+
pgQueryFromResolveData: queryFromResolveData,
11+
} = build;
12+
const { Self, fieldWithHooks } = context;
13+
14+
if (Self.name !== "Toy") {
15+
return fields;
16+
}
17+
18+
const Category = getTypeByName("Category");
19+
return build.extend(fields, {
20+
categories: fieldWithHooks(
21+
"categories",
22+
({ addDataGenerator, getDataFromParsedResolveInfoFragment }) => {
23+
addDataGenerator(parsedResolveInfoFragment => {
24+
return {
25+
pgQuery: queryBuilder => {
26+
queryBuilder.select(() => {
27+
const resolveData = getDataFromParsedResolveInfoFragment(
28+
parsedResolveInfoFragment,
29+
Category
30+
);
31+
const foreignTableAlias = sql.identifier(Symbol());
32+
const query = queryFromResolveData(
33+
sql.fragment`named_query_builder.categories`,
34+
foreignTableAlias,
35+
resolveData,
36+
{
37+
useAsterisk: false,
38+
asJsonAggregate: true,
39+
},
40+
innerQueryBuilder => {
41+
innerQueryBuilder.parentQueryBuilder = queryBuilder;
42+
const alias = Symbol("toyCategoriesSubquery");
43+
const innerInnerQueryBuilder = innerQueryBuilder.buildNamedChildSelecting(
44+
"toyCategoriesSubquery",
45+
sql.identifier("named_query_builder", "toy_categories"),
46+
sql.identifier(alias, "category_id"),
47+
sql.identifier(alias)
48+
);
49+
innerInnerQueryBuilder.where(
50+
sql.fragment`${innerInnerQueryBuilder.getTableAlias()}.toy_id = ${queryBuilder.getTableAlias()}.id`
51+
);
52+
innerQueryBuilder.where(
53+
() =>
54+
sql.fragment`${innerQueryBuilder.getTableAlias()}.id IN (${innerInnerQueryBuilder.build()})`
55+
);
56+
},
57+
queryBuilder.context,
58+
queryBuilder.rootValue
59+
);
60+
return sql.fragment`(${query})`;
61+
}, getSafeAliasFromAlias(parsedResolveInfoFragment.alias));
62+
},
63+
};
64+
});
65+
return {
66+
type: new GraphQLList(Category),
67+
resolve: (data, _args, resolveContext, resolveInfo) => {
68+
if (!data) return null;
69+
const safeAlias = getSafeAliasFromResolveInfo(resolveInfo);
70+
return data[safeAlias];
71+
},
72+
};
73+
},
74+
{
75+
/* w/e */
76+
}
77+
),
78+
});
79+
});
80+
81+
// NOTE: this could be in a completely different plugin
82+
// This hook adds the `approved: Boolean` argument to Toy.categories
83+
builder.hook(
84+
"GraphQLObjectType:fields:field:args",
85+
(args, build, context) => {
86+
const {
87+
graphql: { GraphQLBoolean },
88+
pgSql: sql,
89+
} = build;
90+
const {
91+
Self,
92+
scope: { fieldName },
93+
addArgDataGenerator,
94+
} = context;
95+
if (Self.name !== "Toy" || fieldName !== "categories") {
96+
return args;
97+
}
98+
99+
addArgDataGenerator(({ approved }) => {
100+
return {
101+
pgQuery: queryBuilder => {
102+
if (approved != null) {
103+
const toyCategoriesQueryBuilder = queryBuilder.getNamedChild(
104+
"toyCategoriesSubquery"
105+
);
106+
toyCategoriesQueryBuilder.where(
107+
sql.fragment`${toyCategoriesQueryBuilder.getTableAlias()}.approved = ${sql.value(
108+
approved
109+
)}`
110+
);
111+
}
112+
},
113+
};
114+
});
115+
116+
return build.extend(
117+
args,
118+
{
119+
approved: {
120+
type: GraphQLBoolean,
121+
},
122+
},
123+
"Test"
124+
);
125+
}
126+
);
127+
};

packages/postgraphile-core/__tests__/integration/__snapshots__/queries.test.js.snap

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2004,6 +2004,37 @@ Object {
20042004
}
20052005
`;
20062006
2007+
exports[`named_query_builder.toys.graphql 1`] = `
2008+
Object {
2009+
"data": Object {
2010+
"t1": Object {
2011+
"categories": Array [
2012+
Object {
2013+
"name": "Dinosaurs",
2014+
},
2015+
Object {
2016+
"name": "Military",
2017+
},
2018+
],
2019+
},
2020+
"t2": Object {
2021+
"categories": Array [
2022+
Object {
2023+
"name": "Dinosaurs",
2024+
},
2025+
],
2026+
},
2027+
"t3": Object {
2028+
"categories": Array [
2029+
Object {
2030+
"name": "Military",
2031+
},
2032+
],
2033+
},
2034+
},
2035+
}
2036+
`;
2037+
20072038
exports[`network_types.graphql 1`] = `
20082039
Object {
20092040
"data": Object {

0 commit comments

Comments
 (0)