Skip to content

Commit f792a02

Browse files
authored
Merge pull request #6029 from darrellwarde/feature/sortable-directive
Add `@sortable` directive
1 parent 33ca95a commit f792a02

File tree

11 files changed

+1575
-3
lines changed

11 files changed

+1575
-3
lines changed

.changeset/ten-walls-grin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@neo4j/graphql": minor
3+
---
4+
5+
Add a new field directive `@sortable` which can be used to configure whether results can be sorted by field values or not.

packages/graphql/src/graphql/directives/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export { relationshipPropertiesDirective } from "./relationship-properties";
4040
export { relayIdDirective } from "./relay-id";
4141
export { selectableDirective } from "./selectable";
4242
export { settableDirective } from "./settable";
43+
export { sortableDirective } from "./sortable";
4344
export { subscriptionDirective } from "./subscription";
4445
export { timestampDirective } from "./timestamp";
4546
export { uniqueDirective } from "./unique";
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import { DirectiveLocation, GraphQLBoolean, GraphQLDirective, GraphQLNonNull } from "graphql";
21+
22+
export const sortableDirective = new GraphQLDirective({
23+
name: "sortable",
24+
description: "Instructs @neo4j/graphql to generate sorting inputs for this field.",
25+
locations: [DirectiveLocation.FIELD_DEFINITION],
26+
args: {
27+
byValue: {
28+
description: "Generates sorting inputs for this field",
29+
type: new GraphQLNonNull(GraphQLBoolean),
30+
defaultValue: true,
31+
},
32+
},
33+
});

packages/graphql/src/schema-model/annotation/Annotation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { parsePopulatedByAnnotation } from "../parser/annotations-parser/populat
3535
import { parseQueryAnnotation } from "../parser/annotations-parser/query-annotation";
3636
import { parseSelectableAnnotation } from "../parser/annotations-parser/selectable-annotation";
3737
import { parseSettableAnnotation } from "../parser/annotations-parser/settable-annotation";
38+
import { parseSortableAnnotation } from "../parser/annotations-parser/sortable-annotation";
3839
import { parseSubscriptionAnnotation } from "../parser/annotations-parser/subscription-annotation";
3940
import { parseSubscriptionsAuthorizationAnnotation } from "../parser/annotations-parser/subscriptions-authorization-annotation";
4041
import { parseTimestampAnnotation } from "../parser/annotations-parser/timestamp-annotation";
@@ -61,6 +62,7 @@ import type { QueryAnnotation } from "./QueryAnnotation";
6162
import { RelayIDAnnotation } from "./RelayIDAnnotation";
6263
import type { SelectableAnnotation } from "./SelectableAnnotation";
6364
import type { SettableAnnotation } from "./SettableAnnotation";
65+
import type { SortableAnnotation } from "./SortableAnnotation";
6466
import type { SubscriptionAnnotation } from "./SubscriptionAnnotation";
6567
import type { SubscriptionsAuthorizationAnnotation } from "./SubscriptionsAuthorizationAnnotation";
6668
import type { TimestampAnnotation } from "./TimestampAnnotation";
@@ -99,6 +101,7 @@ export type Annotations = CheckAnnotationName<{
99101
relayId: RelayIDAnnotation;
100102
selectable: SelectableAnnotation;
101103
settable: SettableAnnotation;
104+
sortable: SortableAnnotation;
102105
subscription: SubscriptionAnnotation;
103106
subscriptionsAuthorization: SubscriptionsAuthorizationAnnotation;
104107
timestamp: TimestampAnnotation;
@@ -131,6 +134,7 @@ export const annotationsParsers: { [key in keyof Annotations]: AnnotationParser<
131134
limit: parseLimitAnnotation,
132135
selectable: parseSelectableAnnotation,
133136
settable: parseSettableAnnotation,
137+
sortable: parseSortableAnnotation,
134138
subscription: parseSubscriptionAnnotation,
135139
subscriptionsAuthorization: parseSubscriptionsAuthorizationAnnotation,
136140
timestamp: parseTimestampAnnotation,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import type { Annotation } from "./Annotation";
21+
22+
export class SortableAnnotation implements Annotation {
23+
readonly name = "sortable";
24+
public readonly byValue: boolean;
25+
26+
constructor({ byValue }: { byValue: boolean }) {
27+
this.byValue = byValue;
28+
}
29+
}

packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export class AttributeAdapter {
125125

126126
isSortableField(): boolean {
127127
return (
128+
this.isSortable() &&
128129
!this.typeHelper.isList() &&
129130
!this.isCustomResolvable() &&
130131
(this.typeHelper.isScalar() || this.typeHelper.isEnum() || this.typeHelper.isSpatial()) &&
@@ -193,7 +194,7 @@ export class AttributeAdapter {
193194
isAggregableField(): boolean {
194195
return (
195196
!this.typeHelper.isList() &&
196-
//uncomment me on 7.x !this.typeHelper.isID() &&
197+
//uncomment me on 7.x !this.typeHelper.isID() &&
197198
(this.typeHelper.isScalar() || this.typeHelper.isEnum()) &&
198199
this.isAggregable()
199200
);
@@ -347,6 +348,10 @@ export class AttributeAdapter {
347348
return this.annotations.filterable?.byValue !== false;
348349
}
349350

351+
isSortable(): boolean {
352+
return this.annotations.sortable?.byValue !== false;
353+
}
354+
350355
isCustomResolvable(): boolean {
351356
return !!this.annotations.customResolver;
352357
}

packages/graphql/src/schema-model/library-directives.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
* See the License for the specific language governing permissions and
1717
* limitations under the License.
1818
*/
19-
import type { Annotations } from "./annotation/Annotation";
20-
import { annotationsParsers } from "./annotation/Annotation";
2119
import type { DEPRECATED } from "../constants";
2220
import { SHAREABLE } from "../constants";
2321
import type { ValueOf } from "../utils/value-of";
22+
import type { Annotations } from "./annotation/Annotation";
23+
import { annotationsParsers } from "./annotation/Annotation";
2424

2525
const additionalDirectives = [
2626
"alias",
@@ -43,6 +43,7 @@ export const SCHEMA_CONFIGURATION_FIELD_DIRECTIVES = [
4343
"filterable",
4444
"selectable",
4545
"settable",
46+
"sortable",
4647
] as const satisfies readonly LibraryDirectives[];
4748

4849
export const FIELD_DIRECTIVES = [
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import { makeDirectiveNode } from "@graphql-tools/utils";
21+
import { sortableDirective } from "../../../graphql/directives";
22+
import { parseSortableAnnotation } from "./sortable-annotation";
23+
24+
const tests = [
25+
{
26+
name: "should parse correctly when byValue is true",
27+
directive: makeDirectiveNode(
28+
"sortable",
29+
{
30+
byValue: true,
31+
},
32+
sortableDirective
33+
),
34+
expected: {
35+
byValue: true,
36+
},
37+
},
38+
{
39+
name: "should parse correctly when byValue is false",
40+
directive: makeDirectiveNode(
41+
"sortable",
42+
{
43+
byValue: false,
44+
},
45+
sortableDirective
46+
),
47+
expected: {
48+
byValue: false,
49+
},
50+
},
51+
];
52+
53+
describe("parseSortableAnnotation", () => {
54+
tests.forEach((test) => {
55+
it(`${test.name}`, () => {
56+
const sortableAnnotation = parseSortableAnnotation(test.directive);
57+
expect(sortableAnnotation.byValue).toBe(test.expected.byValue);
58+
});
59+
});
60+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
import type { DirectiveNode } from "graphql";
20+
import { sortableDirective } from "../../../graphql/directives";
21+
import { SortableAnnotation } from "../../annotation/SortableAnnotation";
22+
import { parseArguments } from "../parse-arguments";
23+
24+
export function parseSortableAnnotation(directive: DirectiveNode): SortableAnnotation {
25+
const { byValue } = parseArguments<{
26+
byValue: boolean;
27+
}>(sortableDirective, directive);
28+
29+
return new SortableAnnotation({
30+
byValue,
31+
});
32+
}

packages/graphql/src/schema/validation/utils/invalid-directive-combinations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const invalidFieldCombinations: InvalidFieldCombinations = {
4444
"filterable",
4545
"settable",
4646
"selectable",
47+
"sortable",
4748
],
4849
cypher: ["jwtClaim", "alias", "id", "relationship", "unique"],
4950
default: ["jwtClaim", "populatedBy", "relationship"],
@@ -71,6 +72,7 @@ export const invalidFieldCombinations: InvalidFieldCombinations = {
7172
selectable: ["jwtClaim", "customResolver"],
7273
settable: ["jwtClaim", "customResolver"],
7374
filterable: ["jwtClaim", "customResolver"],
75+
sortable: ["jwtClaim", "customResolver"],
7476
declareRelationship: ["jwtClaim"],
7577
};
7678

0 commit comments

Comments
 (0)