Skip to content

Commit 99b1a8e

Browse files
Deckstarbenjie
andauthored
feat(utils): support for NULLS FIRST/LAST in orderByAscDesc (#737)
Co-authored-by: Benjie Gillam <benjie@jemjie.com>
1 parent 16d909a commit 99b1a8e

3 files changed

Lines changed: 403 additions & 5 deletions

File tree

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
import pg from "pg";
2+
import { graphql } from "graphql";
3+
import { createPostGraphileSchema } from "postgraphile-core";
4+
import { makeAddPgTableOrderByPlugin, orderByAscDesc } from "..";
5+
6+
let pgPool;
7+
8+
beforeAll(() => {
9+
pgPool = new pg.Pool({
10+
connectionString: process.env.TEST_DATABASE_URL,
11+
});
12+
});
13+
14+
afterAll(() => {
15+
if (pgPool) {
16+
pgPool.end();
17+
pgPool = null;
18+
}
19+
});
20+
21+
const makePetsPlugin = nullsSortMethod =>
22+
makeAddPgTableOrderByPlugin("graphile_utils", "users", build => {
23+
const { pgSql: sql } = build;
24+
const sqlIdentifier = sql.identifier(Symbol("pet"));
25+
26+
const customOrderBy = orderByAscDesc(
27+
"PET_ID_AVERAGE", // this is a ridiculous and unrealistic column but it will serve for testing purposes
28+
helpers => {
29+
const { queryBuilder } = helpers;
30+
31+
const orderByFrag = sql.fragment`(
32+
select avg(${sqlIdentifier}.id)
33+
from graphile_utils.pets as ${sqlIdentifier}
34+
where ${sqlIdentifier}.user_id = ${queryBuilder.getTableAlias()}.id
35+
)`;
36+
37+
return orderByFrag;
38+
},
39+
{ nulls: nullsSortMethod }
40+
);
41+
42+
return customOrderBy;
43+
});
44+
45+
const getResultingOrderFromUserNodes = userNodes =>
46+
userNodes.map(node => node.name);
47+
48+
const checkArraysAreEqual = (array1, array2) =>
49+
JSON.stringify(array1) === JSON.stringify(array2);
50+
51+
const getSchema = async nullsSortMethod => {
52+
const schema = await createPostGraphileSchema(pgPool, ["graphile_utils"], {
53+
disableDefaultMutations: true,
54+
simpleCollections: "both",
55+
appendPlugins: [makePetsPlugin(nullsSortMethod)],
56+
});
57+
58+
return schema;
59+
};
60+
61+
/**
62+
* We expect the "pet id average" to be the following for each person:
63+
* Alice: null (she has no pets, so no average to make);
64+
* Bob: 1.5 ( = (1 + 2) / 2) -- he gets assigned pets first and has 2
65+
* Caroline: 4 ( = (3 + 4 + 5) / 3) -- she gets assigned pets second and has 3
66+
*
67+
* Note that even if the pet id's increase: the average orders should stay the same
68+
*/
69+
const getAscDescData = async (schema, pgClient) => {
70+
const { data: dataAsc, errors: errorsAsc } = await graphql(
71+
schema,
72+
`
73+
query {
74+
allUsers(orderBy: PET_ID_AVERAGE_ASC) {
75+
nodes {
76+
nodeId
77+
id
78+
name
79+
}
80+
}
81+
}
82+
`,
83+
null,
84+
{ pgClient },
85+
{}
86+
);
87+
88+
const { data: dataDesc, errors: errorsDesc } = await graphql(
89+
schema,
90+
`
91+
query {
92+
allUsers(orderBy: PET_ID_AVERAGE_DESC) {
93+
nodes {
94+
nodeId
95+
id
96+
name
97+
}
98+
}
99+
}
100+
`,
101+
null,
102+
{ pgClient },
103+
{}
104+
);
105+
106+
const userNodesAsc = dataAsc?.allUsers?.nodes;
107+
const userNodesDesc = dataDesc?.allUsers?.nodes;
108+
109+
return {
110+
dataAsc,
111+
dataDesc,
112+
errorsAsc,
113+
errorsDesc,
114+
userNodesAsc,
115+
userNodesDesc,
116+
};
117+
};
118+
119+
it('allows creating a "order by" plugin with DEFAULT asc/desc ordering', async () => {
120+
const schema = await getSchema();
121+
const pgClient = await pgPool.connect();
122+
123+
try {
124+
const {
125+
dataAsc,
126+
dataDesc,
127+
errorsAsc,
128+
errorsDesc,
129+
userNodesAsc,
130+
userNodesDesc,
131+
} = await getAscDescData(schema, pgClient);
132+
133+
// by default, the natural order by puts nulls last when using ascending order
134+
const correctOrderAsc = ["Bob", "Caroline", "Alice"];
135+
const resultingOrderAsc = getResultingOrderFromUserNodes(userNodesAsc);
136+
137+
const ascOrdersAreEqual = checkArraysAreEqual(
138+
correctOrderAsc,
139+
resultingOrderAsc
140+
);
141+
142+
expect(errorsAsc).toBeFalsy();
143+
expect(dataAsc).toBeTruthy();
144+
expect(ascOrdersAreEqual).toBeTruthy();
145+
146+
// by default, the natural order by puts nulls FIRST when using descending order
147+
const correctOrderDesc = ["Alice", "Caroline", "Bob"];
148+
const resultingOrderDesc = getResultingOrderFromUserNodes(userNodesDesc);
149+
150+
const descOrdersAreEqual = checkArraysAreEqual(
151+
correctOrderDesc,
152+
resultingOrderDesc
153+
);
154+
155+
expect(errorsDesc).toBeFalsy();
156+
expect(dataDesc).toBeTruthy();
157+
expect(descOrdersAreEqual).toBeTruthy();
158+
} finally {
159+
await pgClient.release();
160+
}
161+
});
162+
163+
it('allows creating a "order by" plugin with NULLS FIRST asc/desc ordering', async () => {
164+
const schema = await getSchema("first");
165+
const pgClient = await pgPool.connect();
166+
167+
try {
168+
const {
169+
dataAsc,
170+
dataDesc,
171+
errorsAsc,
172+
errorsDesc,
173+
userNodesAsc,
174+
userNodesDesc,
175+
} = await getAscDescData(schema, pgClient);
176+
177+
// nulls first, so Alice, then ascending
178+
const correctOrderAsc = ["Alice", "Bob", "Caroline"];
179+
const resultingOrderAsc = getResultingOrderFromUserNodes(userNodesAsc);
180+
181+
const ascOrdersAreEqual = checkArraysAreEqual(
182+
correctOrderAsc,
183+
resultingOrderAsc
184+
);
185+
186+
expect(errorsAsc).toBeFalsy();
187+
expect(dataAsc).toBeTruthy();
188+
expect(ascOrdersAreEqual).toBeTruthy();
189+
190+
// nulls first, so Alice, then descending
191+
const correctOrderDesc = ["Alice", "Caroline", "Bob"];
192+
const resultingOrderDesc = getResultingOrderFromUserNodes(userNodesDesc);
193+
194+
const descOrdersAreEqual = checkArraysAreEqual(
195+
correctOrderDesc,
196+
resultingOrderDesc
197+
);
198+
199+
expect(errorsDesc).toBeFalsy();
200+
expect(dataDesc).toBeTruthy();
201+
expect(descOrdersAreEqual).toBeTruthy();
202+
} finally {
203+
await pgClient.release();
204+
}
205+
});
206+
207+
it('allows creating a "order by" plugin with NULLS LAST asc/desc ordering', async () => {
208+
const schema = await getSchema("last");
209+
const pgClient = await pgPool.connect();
210+
211+
try {
212+
const {
213+
dataAsc,
214+
dataDesc,
215+
errorsAsc,
216+
errorsDesc,
217+
userNodesAsc,
218+
userNodesDesc,
219+
} = await getAscDescData(schema, pgClient);
220+
221+
// nulls last, so ascending, then Alice
222+
const correctOrderAsc = ["Bob", "Caroline", "Alice"];
223+
const resultingOrderAsc = getResultingOrderFromUserNodes(userNodesAsc);
224+
225+
const ascOrdersAreEqual = checkArraysAreEqual(
226+
correctOrderAsc,
227+
resultingOrderAsc
228+
);
229+
230+
expect(errorsAsc).toBeFalsy();
231+
expect(dataAsc).toBeTruthy();
232+
expect(ascOrdersAreEqual).toBeTruthy();
233+
234+
// nulls last, so descending, then Alice
235+
const correctOrderDesc = ["Caroline", "Bob", "Alice"];
236+
const resultingOrderDesc = getResultingOrderFromUserNodes(userNodesDesc);
237+
238+
const descOrdersAreEqual = checkArraysAreEqual(
239+
correctOrderDesc,
240+
resultingOrderDesc
241+
);
242+
243+
expect(errorsDesc).toBeFalsy();
244+
expect(dataDesc).toBeTruthy();
245+
expect(descOrdersAreEqual).toBeTruthy();
246+
} finally {
247+
await pgClient.release();
248+
}
249+
});
250+
251+
it('allows creating a "order by" plugin with NULLS FIRST IFF ASCENDING asc/desc ordering', async () => {
252+
const schema = await getSchema("first-iff-ascending");
253+
const pgClient = await pgPool.connect();
254+
255+
try {
256+
const {
257+
dataAsc,
258+
dataDesc,
259+
errorsAsc,
260+
errorsDesc,
261+
userNodesAsc,
262+
userNodesDesc,
263+
} = await getAscDescData(schema, pgClient);
264+
265+
// nulls first, so Alice, then ascending
266+
const correctOrderAsc = ["Alice", "Bob", "Caroline"];
267+
const resultingOrderAsc = getResultingOrderFromUserNodes(userNodesAsc);
268+
269+
const ascOrdersAreEqual = checkArraysAreEqual(
270+
correctOrderAsc,
271+
resultingOrderAsc
272+
);
273+
274+
expect(errorsAsc).toBeFalsy();
275+
expect(dataAsc).toBeTruthy();
276+
expect(ascOrdersAreEqual).toBeTruthy();
277+
278+
// nulls last, so descending, then Alice
279+
const correctOrderDesc = ["Caroline", "Bob", "Alice"];
280+
const resultingOrderDesc = getResultingOrderFromUserNodes(userNodesDesc);
281+
282+
const descOrdersAreEqual = checkArraysAreEqual(
283+
correctOrderDesc,
284+
resultingOrderDesc
285+
);
286+
287+
expect(errorsDesc).toBeFalsy();
288+
expect(dataDesc).toBeTruthy();
289+
expect(descOrdersAreEqual).toBeTruthy();
290+
} finally {
291+
await pgClient.release();
292+
}
293+
});
294+
295+
it('allows creating a "order by" plugin with NULLS LAST IFF ASCENDING asc/desc ordering', async () => {
296+
const schema = await getSchema("last-iff-ascending");
297+
const pgClient = await pgPool.connect();
298+
299+
try {
300+
const {
301+
dataAsc,
302+
dataDesc,
303+
errorsAsc,
304+
errorsDesc,
305+
userNodesAsc,
306+
userNodesDesc,
307+
} = await getAscDescData(schema, pgClient);
308+
309+
// nulls last, so ascending, then Alice
310+
const correctOrderAsc = ["Bob", "Caroline", "Alice"];
311+
const resultingOrderAsc = getResultingOrderFromUserNodes(userNodesAsc);
312+
313+
const ascOrdersAreEqual = checkArraysAreEqual(
314+
correctOrderAsc,
315+
resultingOrderAsc
316+
);
317+
318+
expect(errorsAsc).toBeFalsy();
319+
expect(dataAsc).toBeTruthy();
320+
expect(ascOrdersAreEqual).toBeTruthy();
321+
322+
// nulls first, so Alice, then descending
323+
const correctOrderDesc = ["Alice", "Caroline", "Bob"];
324+
const resultingOrderDesc = getResultingOrderFromUserNodes(userNodesDesc);
325+
326+
const descOrdersAreEqual = checkArraysAreEqual(
327+
correctOrderDesc,
328+
resultingOrderDesc
329+
);
330+
331+
expect(errorsDesc).toBeFalsy();
332+
expect(dataDesc).toBeTruthy();
333+
expect(descOrdersAreEqual).toBeTruthy();
334+
} finally {
335+
await pgClient.release();
336+
}
337+
});

packages/graphile-utils/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import makeProcessSchemaPlugin from "./makeProcessSchemaPlugin";
88
import makeAddPgTableConditionPlugin from "./makeAddPgTableConditionPlugin";
99
import makeAddPgTableOrderByPlugin, {
1010
orderByAscDesc,
11+
OrderByAscDescOptions,
1112
MakeAddPgTableOrderByPluginOrders,
1213
} from "./makeAddPgTableOrderByPlugin";
1314

@@ -32,5 +33,6 @@ export {
3233
makeAddPgTableConditionPlugin,
3334
makeAddPgTableOrderByPlugin,
3435
orderByAscDesc,
36+
OrderByAscDescOptions,
3537
MakeAddPgTableOrderByPluginOrders,
3638
};

0 commit comments

Comments
 (0)