Skip to content

Commit 553d404

Browse files
authored
Merge branch 'dev' into update-perf-tests
2 parents d7a8e55 + 9b6e65d commit 553d404

File tree

13 files changed

+445
-151
lines changed

13 files changed

+445
-151
lines changed

.changeset/tiny-tips-cross.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
"@neo4j/graphql": patch
3+
---
4+
5+
Allow enabling/disabling of connection query fields on type by type basis as well as for the whole schema via a new `@query` directive argument `connection`. Default value of `connection` is the same as `read`, inheriting its default value of `true` if not provided.
6+
7+
Usage example:
8+
9+
The following type definitions will create the GraphQL Query fields:
10+
11+
```gql
12+
type Actor @query(read: false, connection: true) @node {
13+
name: String
14+
}
15+
```
16+
17+
```graphql
18+
type Query {
19+
# only connection field
20+
actorsConnection(after: String, first: Int, sort: [ActorSort!], where: ActorWhere): ActorsConnection!
21+
}
22+
```
23+
24+
Inheriting the value of the read argument as default:
25+
26+
```gql
27+
type Actor @query(read: false) @node {
28+
name: String
29+
}
30+
```
31+
32+
```graphql
33+
type Query {
34+
# no reads
35+
}
36+
```

.github/workflows/reusable-codeql-analysis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ jobs:
1515
node-version: lts/*
1616
package-manager-cache: false
1717
- name: Initialize CodeQL
18-
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4
18+
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4
1919
with:
2020
config-file: ./.github/codeql/codeql-config.yml
2121
languages: javascript
2222
- name: Perform CodeQL Analysis
23-
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4
23+
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
},
2626
"devDependencies": {
2727
"@tsconfig/node24": "24.0.4",
28-
"@typescript-eslint/eslint-plugin": "8.53.0",
29-
"@typescript-eslint/parser": "8.53.0",
28+
"@typescript-eslint/eslint-plugin": "8.53.1",
29+
"@typescript-eslint/parser": "8.53.1",
3030
"concurrently": "9.2.1",
3131
"dotenv": "17.2.3",
3232
"eslint": "8.57.1",

packages/graphql/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
},
4747
"author": "Neo4j Inc.",
4848
"devDependencies": {
49-
"@apollo/gateway": "2.12.2",
49+
"@apollo/gateway": "2.13.0",
5050
"@apollo/server": "5.2.0",
5151
"@types/is-uuid": "1.0.2",
5252
"@types/jest": "30.0.0",
@@ -65,7 +65,7 @@
6565
"jest-extended": "7.0.0",
6666
"jsonwebtoken": "9.0.3",
6767
"jwks-rsa": "3.2.1",
68-
"knip": "5.82.0",
68+
"knip": "5.82.1",
6969
"koa": "3.1.1",
7070
"koa-jwt": "4.0.4",
7171
"koa-router": "14.0.0",

packages/graphql/src/graphql/directives/query.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,21 @@ import { DirectiveLocation, GraphQLBoolean, GraphQLDirective, GraphQLNonNull } f
2121

2222
export const queryDirective = new GraphQLDirective({
2323
name: "query",
24-
description: "Instructs @neo4j/graphql to exclude read or aggregate operations from the query root type.",
24+
description:
25+
"Instructs @neo4j/graphql to exclude read, connection, or aggregate operations from the query root type.",
2526
args: {
2627
read: {
27-
description: "Disable/Enabled read operations from query root type",
28+
description: "Disable/Enabled all read operations from query root type (this means connections too).",
2829
type: new GraphQLNonNull(GraphQLBoolean),
2930
defaultValue: true,
3031
},
32+
connection: {
33+
description:
34+
"Disable/Enabled connection operations from query root type. Default value matches value of read argument, or true if not provided.",
35+
type: GraphQLBoolean,
36+
},
3137
aggregate: {
32-
description: "Disable/Enabled aggregate operations from query root type",
38+
description: "Disable/Enabled aggregate operations from the connection read operations.",
3339
type: new GraphQLNonNull(GraphQLBoolean),
3440
defaultValue: false,
3541
},

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ export class QueryAnnotation implements Annotation {
2323
readonly name = "query";
2424
public readonly read: boolean;
2525
public readonly aggregate: boolean;
26+
public readonly connection: boolean;
2627

27-
constructor({ read, aggregate }: { read: boolean; aggregate: boolean }) {
28+
constructor({ read, aggregate, connection }: { read: boolean; aggregate: boolean; connection: boolean }) {
2829
this.read = read;
2930
this.aggregate = aggregate;
31+
this.connection = connection;
3032
}
3133
}

packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ export class ConcreteEntityAdapter {
101101
return schemaModel.annotations.query === undefined || schemaModel.annotations.query.read === true;
102102
}
103103

104+
public isReadableFromConnectionRootQuery(schemaModel: Neo4jGraphQLSchemaModel): boolean {
105+
if (this.annotations.query) {
106+
if (this.annotations.query.connection !== undefined) {
107+
return this.annotations.query.connection;
108+
}
109+
return this.annotations.query.read;
110+
}
111+
112+
return schemaModel.annotations.query === undefined || schemaModel.annotations.query.connection === true;
113+
}
114+
104115
public isAggregable(schemaModel: Neo4jGraphQLSchemaModel): boolean {
105116
if (this.annotations.query) {
106117
return this.annotations.query.aggregate;

packages/graphql/src/schema-model/entity/model-adapters/InterfaceEntityAdapter.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@ export class InterfaceEntityAdapter {
104104

105105
return schemaModel.annotations.query === undefined || schemaModel.annotations.query.read === true;
106106
}
107+
public isReadableFromConnectionRootQuery(schemaModel: Neo4jGraphQLSchemaModel): boolean {
108+
if (this.annotations.query) {
109+
if (this.annotations.query.connection !== undefined) {
110+
return this.annotations.query.connection;
111+
}
112+
return this.annotations.query.read;
113+
}
114+
return schemaModel.annotations.query === undefined || schemaModel.annotations.query.connection === true;
115+
}
107116

108117
public isAggregable(schemaModel: Neo4jGraphQLSchemaModel): boolean {
109118
if (this.annotations.query) {

packages/graphql/src/schema-model/parser/annotations-parser/query-annotation.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ import { QueryAnnotation } from "../../annotation/QueryAnnotation";
2323
import { parseArguments } from "../parse-arguments";
2424

2525
export function parseQueryAnnotation(directive: DirectiveNode): QueryAnnotation {
26-
const { read, aggregate } = parseArguments<{ read: boolean; aggregate: boolean }>(queryDirective, directive);
26+
const { read, aggregate, connection } = parseArguments<{ read: boolean; aggregate: boolean; connection: boolean }>(
27+
queryDirective,
28+
directive
29+
);
2730

2831
return new QueryAnnotation({
2932
read,
3033
aggregate,
34+
connection,
3135
});
3236
}

packages/graphql/src/schema/make-augmented-schema.ts

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,10 @@ function generateObjectType({
594594

595595
ensureNonEmptyInput(composer, concreteEntityAdapter.operations.updateInputTypeName);
596596
ensureNonEmptyInput(composer, concreteEntityAdapter.operations.createInputTypeName);
597-
if (concreteEntityAdapter.isReadable(schemaModel) || concreteEntityAdapter.isAggregable(schemaModel)) {
597+
if (
598+
concreteEntityAdapter.isReadableFromConnectionRootQuery(schemaModel) ||
599+
concreteEntityAdapter.isAggregable(schemaModel)
600+
) {
598601
complexityEstimatorHelper.registerField(
599602
"Query",
600603
concreteEntityAdapter.operations.rootTypeFieldNames.connection
@@ -608,6 +611,10 @@ function generateObjectType({
608611
schemaModel,
609612
}),
610613
});
614+
composer.Query.setFieldDirectives(
615+
concreteEntityAdapter.operations.rootTypeFieldNames.connection,
616+
graphqlDirectivesToCompose(propagatedDirectives)
617+
);
611618
}
612619
if (concreteEntityAdapter.isReadable(schemaModel)) {
613620
complexityEstimatorHelper.registerField("Query", concreteEntityAdapter.operations.rootTypeFieldNames.read);
@@ -622,11 +629,6 @@ function generateObjectType({
622629
concreteEntityAdapter.operations.rootTypeFieldNames.read,
623630
graphqlDirectivesToCompose(propagatedDirectives)
624631
);
625-
626-
composer.Query.setFieldDirectives(
627-
concreteEntityAdapter.operations.rootTypeFieldNames.connection,
628-
graphqlDirectivesToCompose(propagatedDirectives)
629-
);
630632
}
631633
if (concreteEntityAdapter.isAggregable(schemaModel)) {
632634
withAggregateSelectionType({
@@ -736,7 +738,10 @@ function generateInterfaceObjectType({
736738
const hasImplementedEntities = interfaceEntityAdapter.concreteEntities.length > 0;
737739
if (hasImplementedEntities) {
738740
const propagatedDirectives = propagatedDirectivesForNode.get(interfaceEntityAdapter.name) || [];
739-
if (interfaceEntityAdapter.isReadable(schemaModel) || interfaceEntityAdapter.isAggregable(schemaModel)) {
741+
if (
742+
interfaceEntityAdapter.isReadableFromConnectionRootQuery(schemaModel) ||
743+
interfaceEntityAdapter.isAggregable(schemaModel)
744+
) {
740745
composer.Query.addFields({
741746
[interfaceEntityAdapter.operations.rootTypeFieldNames.connection]: rootConnectionResolver({
742747
composer,
@@ -746,6 +751,15 @@ function generateInterfaceObjectType({
746751
schemaModel,
747752
}),
748753
});
754+
complexityEstimatorHelper.registerField(
755+
"Query",
756+
interfaceEntityAdapter.operations.rootTypeFieldNames.connection
757+
);
758+
759+
composer.Query.setFieldDirectives(
760+
interfaceEntityAdapter.operations.rootTypeFieldNames.connection,
761+
graphqlDirectivesToCompose(propagatedDirectives)
762+
);
749763
}
750764
if (interfaceEntityAdapter.isReadable(schemaModel)) {
751765
complexityEstimatorHelper.registerField("Query", interfaceEntityAdapter.operations.rootTypeFieldNames.read);
@@ -761,25 +775,6 @@ function generateInterfaceObjectType({
761775
interfaceEntityAdapter.operations.rootTypeFieldNames.read,
762776
graphqlDirectivesToCompose(propagatedDirectives)
763777
);
764-
765-
complexityEstimatorHelper.registerField(
766-
"Query",
767-
interfaceEntityAdapter.operations.rootTypeFieldNames.connection
768-
);
769-
770-
composer.Query.addFields({
771-
[interfaceEntityAdapter.operations.rootTypeFieldNames.connection]: rootConnectionResolver({
772-
composer,
773-
entityAdapter: interfaceEntityAdapter,
774-
propagatedDirectives,
775-
isLimitRequired: features?.limitRequired,
776-
schemaModel,
777-
}),
778-
});
779-
composer.Query.setFieldDirectives(
780-
interfaceEntityAdapter.operations.rootTypeFieldNames.connection,
781-
graphqlDirectivesToCompose(propagatedDirectives)
782-
);
783778
}
784779

785780
if (interfaceEntityAdapter.isAggregable(schemaModel)) {

0 commit comments

Comments
 (0)