Skip to content

Commit 928a505

Browse files
Merge branch 'dev' into unique-type-chunk-int
2 parents 3732ac0 + 6b547dd commit 928a505

File tree

57 files changed

+3736
-124
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+3736
-124
lines changed

.changeset/chilly-panthers-love.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+
Adds support for the `@authentication` directive on custom resolved fields of root types Query and Mutation

.changeset/grumpy-lamps-pump.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 simple relationship filter for relationships to interface types

packages/graphql-toolbox/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@
5050
"@dnd-kit/modifiers": "7.0.0",
5151
"@dnd-kit/sortable": "8.0.0",
5252
"@graphiql/react": "0.20.3",
53-
"@neo4j-ndl/base": "2.7.1",
54-
"@neo4j-ndl/react": "2.8.1",
53+
"@neo4j-ndl/base": "2.7.2",
54+
"@neo4j-ndl/react": "2.8.3",
5555
"@neo4j/graphql": "5.1.0",
5656
"@neo4j/introspector": "3.0.0",
5757
"classnames": "2.5.1",

packages/graphql/src/classes/Neo4jGraphQL.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { getDefinitionNodes } from "../schema/get-definition-nodes";
3333
import { makeDocumentToAugment } from "../schema/make-document-to-augment";
3434
import type { WrapResolverArguments } from "../schema/resolvers/composition/wrap-query-and-mutation";
3535
import { wrapQueryAndMutation } from "../schema/resolvers/composition/wrap-query-and-mutation";
36-
import { wrapSubscription } from "../schema/resolvers/composition/wrap-subscription";
36+
import { wrapSubscription, type WrapSubscriptionArgs } from "../schema/resolvers/composition/wrap-subscription";
3737
import { defaultFieldResolver } from "../schema/resolvers/field/defaultField";
3838
import { validateDocument } from "../schema/validation";
3939
import { validateUserDefinition } from "../schema/validation/schema-validation";
@@ -50,6 +50,7 @@ import { Neo4jGraphQLSubscriptionsDefaultEngine } from "./subscription/Neo4jGrap
5050
import type { AssertIndexesAndConstraintsOptions } from "./utils/asserts-indexes-and-constraints";
5151
import { assertIndexesAndConstraints } from "./utils/asserts-indexes-and-constraints";
5252
import checkNeo4jCompat from "./utils/verify-database";
53+
import { generateResolverComposition } from "./utils/generate-resolvers-composition";
5354

5455
type TypeDefinitions = string | DocumentNode | TypeDefinitions[] | (() => TypeDefinitions);
5556

@@ -283,22 +284,28 @@ class Neo4jGraphQL {
283284
authorization: this.authorization,
284285
jwtPayloadFieldsMap: this.jwtFieldsMap,
285286
};
287+
const queryAndMutationWrappers = [wrapQueryAndMutation(wrapResolverArgs)];
286288

287-
const resolversComposition = {
288-
"Query.*": [wrapQueryAndMutation(wrapResolverArgs)],
289-
"Mutation.*": [wrapQueryAndMutation(wrapResolverArgs)],
289+
const isSubscriptionEnabled = !!this.features.subscriptions;
290+
const wrapSubscriptionResolverArgs = {
291+
subscriptionsEngine: this.features.subscriptions,
292+
schemaModel: this.schemaModel,
293+
authorization: this.authorization,
294+
jwtPayloadFieldsMap: this.jwtFieldsMap,
290295
};
296+
const subscriptionWrappers = isSubscriptionEnabled
297+
? [wrapSubscription(wrapSubscriptionResolverArgs as WrapSubscriptionArgs)]
298+
: [];
291299

292-
if (this.features.subscriptions) {
293-
resolversComposition["Subscription.*"] = wrapSubscription({
294-
subscriptionsEngine: this.features.subscriptions,
295-
schemaModel: this.schemaModel,
296-
authorization: this.authorization,
297-
jwtPayloadFieldsMap: this.jwtFieldsMap,
298-
});
299-
}
300+
const resolversComposition = generateResolverComposition({
301+
schemaModel: this.schemaModel,
302+
isSubscriptionEnabled,
303+
queryAndMutationWrappers,
304+
subscriptionWrappers,
305+
});
300306

301307
// Merge generated and custom resolvers
308+
// Merging must be done before composing because wrapper won't run otherwise
302309
const mergedResolvers = mergeResolvers([...asArray(resolvers), ...asArray(this.resolvers)]);
303310
return composeResolvers(mergedResolvers, resolversComposition);
304311
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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 { ResolversComposerMapping } from "@graphql-tools/resolvers-composition";
21+
import type { IResolvers } from "@graphql-tools/utils";
22+
import type { GraphQLResolveInfo } from "graphql";
23+
import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel";
24+
import { isAuthenticated } from "../../translate/authorization/check-authentication";
25+
26+
export function generateResolverComposition({
27+
schemaModel,
28+
isSubscriptionEnabled,
29+
queryAndMutationWrappers,
30+
subscriptionWrappers,
31+
}: {
32+
schemaModel: Neo4jGraphQLSchemaModel;
33+
isSubscriptionEnabled: boolean;
34+
queryAndMutationWrappers: ((next: any) => (root: any, args: any, context: any, info: any) => any)[];
35+
subscriptionWrappers: ((next: any) => (root: any, args: any, context: any, info: any) => any)[];
36+
}): ResolversComposerMapping<IResolvers<any, GraphQLResolveInfo, Record<string, any>, any>> {
37+
const resolverComposition = {};
38+
const {
39+
userCustomResolverPattern: customResolverQueryPattern,
40+
generatedResolverPattern: generatedResolverQueryPattern,
41+
} = getPathMatcherForRootType("Query", schemaModel);
42+
resolverComposition[`Query.${customResolverQueryPattern}`] = [
43+
...queryAndMutationWrappers,
44+
isAuthenticated(["READ"], schemaModel.operations.Query),
45+
];
46+
resolverComposition[`Query.${generatedResolverQueryPattern}`] = queryAndMutationWrappers;
47+
48+
const {
49+
userCustomResolverPattern: customResolverMutationPattern,
50+
generatedResolverPattern: generatedResolverMutationPattern,
51+
} = getPathMatcherForRootType("Mutation", schemaModel);
52+
resolverComposition[`Mutation.${customResolverMutationPattern}`] = [
53+
...queryAndMutationWrappers,
54+
isAuthenticated(["CREATE", "UPDATE", "DELETE"], schemaModel.operations.Mutation),
55+
];
56+
resolverComposition[`Mutation.${generatedResolverMutationPattern}`] = queryAndMutationWrappers;
57+
58+
if (isSubscriptionEnabled) {
59+
resolverComposition["Subscription.*"] = subscriptionWrappers;
60+
}
61+
return resolverComposition;
62+
}
63+
64+
function getPathMatcherForRootType(
65+
rootType: "Query" | "Mutation",
66+
schemaModel: Neo4jGraphQLSchemaModel
67+
): {
68+
userCustomResolverPattern: string;
69+
generatedResolverPattern: string;
70+
} {
71+
const operation = schemaModel.operations[rootType];
72+
if (!operation) {
73+
return { userCustomResolverPattern: "*", generatedResolverPattern: "*" };
74+
}
75+
const userDefinedFields = Array.from(operation.userResolvedAttributes.keys());
76+
if (!userDefinedFields.length) {
77+
return { userCustomResolverPattern: "*", generatedResolverPattern: "*" };
78+
}
79+
const userCustomResolverPattern = `{${userDefinedFields.join(", ")}}`;
80+
return { userCustomResolverPattern, generatedResolverPattern: `!${userCustomResolverPattern}` };
81+
}

packages/graphql/src/schema-model/Operation.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,18 @@ export class Operation {
2626
public readonly name: string;
2727
// only includes custom Cypher fields
2828
public readonly attributes: Map<string, Attribute> = new Map();
29+
public readonly userResolvedAttributes: Map<string, Attribute> = new Map();
2930
public readonly annotations: Partial<Annotations>;
3031

3132
constructor({
3233
name,
3334
attributes = [],
35+
userResolvedAttributes = [],
3436
annotations = {},
3537
}: {
3638
name: string;
3739
attributes?: Attribute[];
40+
userResolvedAttributes?: Attribute[];
3841
annotations?: Partial<Annotations>;
3942
}) {
4043
this.name = name;
@@ -43,16 +46,32 @@ export class Operation {
4346
for (const attribute of attributes) {
4447
this.addAttribute(attribute);
4548
}
49+
for (const attribute of userResolvedAttributes) {
50+
this.addUserResolvedAttributes(attribute);
51+
}
4652
}
4753

4854
public findAttribute(name: string): Attribute | undefined {
4955
return this.attributes.get(name);
5056
}
5157

58+
public findUserResolvedAttributes(name: string): Attribute | undefined {
59+
return this.userResolvedAttributes.get(name);
60+
}
61+
5262
private addAttribute(attribute: Attribute): void {
5363
if (this.attributes.has(attribute.name)) {
5464
throw new Neo4jGraphQLSchemaValidationError(`Attribute ${attribute.name} already exists in ${this.name}`);
5565
}
5666
this.attributes.set(attribute.name, attribute);
5767
}
68+
69+
private addUserResolvedAttributes(attribute: Attribute): void {
70+
if (this.userResolvedAttributes.has(attribute.name)) {
71+
throw new Neo4jGraphQLSchemaValidationError(
72+
`User Resolved Attribute ${attribute.name} already exists in ${this.name}`
73+
);
74+
}
75+
this.userResolvedAttributes.set(attribute.name, attribute);
76+
}
5877
}

packages/graphql/src/schema-model/OperationAdapter.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ import type { Operation } from "./Operation";
2525
export class OperationAdapter {
2626
public readonly name: string;
2727
public readonly attributes: Map<string, AttributeAdapter> = new Map();
28+
public readonly userResolvedAttributes: Map<string, AttributeAdapter> = new Map();
2829
public readonly annotations: Partial<Annotations>;
2930

3031
constructor(entity: Operation) {
3132
this.name = entity.name;
3233
this.initAttributes(entity.attributes);
34+
this.initUserResolvedAttributes(entity.userResolvedAttributes);
3335
this.annotations = entity.annotations;
3436
}
3537

@@ -39,6 +41,12 @@ export class OperationAdapter {
3941
this.attributes.set(attributeName, attributeAdapter);
4042
}
4143
}
44+
private initUserResolvedAttributes(attributes: Map<string, Attribute>) {
45+
for (const [attributeName, attribute] of attributes.entries()) {
46+
const attributeAdapter = new AttributeAdapter(attribute);
47+
this.userResolvedAttributes.set(attributeName, attributeAdapter);
48+
}
49+
}
4250

4351
public get objectFields(): AttributeAdapter[] {
4452
return Array.from(this.attributes.values()).filter((attribute) => attribute.isObjectField());

packages/graphql/src/schema-model/generate-model.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -543,13 +543,23 @@ function generateOperation(
543543
definition: ObjectTypeDefinitionNode,
544544
definitionCollection: DefinitionCollection
545545
): Operation {
546-
const attributes = (definition.fields || [])
546+
const { attributes, userResolvedAttributes } = (definition.fields || [])
547547
.map((fieldDefinition) => parseAttribute(fieldDefinition, definitionCollection))
548-
.filter((attribute) => attribute.annotations.cypher);
549-
548+
.reduce<{ attributes: Attribute[]; userResolvedAttributes: Attribute[] }>(
549+
(acc, attribute) => {
550+
if (attribute.annotations.cypher) {
551+
acc.attributes.push(attribute);
552+
} else {
553+
acc.userResolvedAttributes.push(attribute);
554+
}
555+
return acc;
556+
},
557+
{ attributes: [], userResolvedAttributes: [] }
558+
);
550559
return new Operation({
551560
name: definition.name.value,
552561
attributes,
562+
userResolvedAttributes,
553563
annotations: parseAnnotations(definition.directives || []),
554564
});
555565
}

packages/graphql/src/schema/generation/where-input.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,6 @@ export function withSourceWhereInputType({
163163
const relationshipTarget = relationshipAdapter.target;
164164
const relationshipSource = relationshipAdapter.source;
165165
const whereInput = composer.getITC(relationshipSource.operations.whereInputTypeName);
166-
// TODO: relationship simple filters were not supported on Interface target, only connection filters
167-
// when implementing translation, simply remove this if-case
168-
if (relationshipTarget instanceof InterfaceEntityAdapter) {
169-
const connectionFields = augmentWhereInputTypeWithConnectionFields(relationshipAdapter, deprecatedDirectives);
170-
whereInput.addFields(connectionFields);
171-
return whereInput;
172-
}
173166
const fields = augmentWhereInputTypeWithRelationshipFields(relationshipAdapter, deprecatedDirectives);
174167
whereInput.addFields(fields);
175168

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,16 @@ function makeAugmentedSchema({
347347

348348
objectComposer.addFields({ [attributeAdapter.name]: { ...composedField, ...customResolver } });
349349
}
350+
351+
// this is to remove library directives from custom resolvers on root type fields in augmented schema
352+
for (const attributeAdapter of operationAdapter.userResolvedAttributes.values()) {
353+
const composedField = attributeAdapterToComposeFields([attributeAdapter], userDefinedFieldDirectives)[
354+
attributeAdapter.name
355+
];
356+
if (composedField) {
357+
objectComposer.addFields({ [attributeAdapter.name]: composedField });
358+
}
359+
}
350360
});
351361

352362
if (!Object.values(composer.Mutation.getFields()).length) {

0 commit comments

Comments
 (0)