Skip to content

Commit bcf65d6

Browse files
authored
Merge pull request #6573 from neo4j/6572-changes-of-6488-lead-to-populatedby-directives-within-relationship-properties-not-being-executed-anymore
Fix missing populated by callback in connect
2 parents 2d47bcc + 7760a27 commit bcf65d6

File tree

7 files changed

+230
-15
lines changed

7 files changed

+230
-15
lines changed

.changeset/large-knives-clean.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@neo4j/graphql": patch
3+
---
4+
5+
Fix populatedBy directive used in connect operations

packages/graphql/src/translate/queryAST/factory/OperationFactory.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,12 +229,19 @@ export class OperationsFactory {
229229
entity: ConcreteEntityAdapter | InterfaceEntityAdapter | UnionEntityAdapter,
230230
relationship: RelationshipAdapter,
231231
input: Record<string, any>[],
232-
context: Neo4jGraphQLTranslationContext
232+
context: Neo4jGraphQLTranslationContext,
233+
callbackBucket: CallbackBucket
233234
) {
234235
if (isConcreteEntity(entity)) {
235-
return this.connectFactory.createConnectOperation(entity, relationship, input, context);
236+
return this.connectFactory.createConnectOperation(entity, relationship, input, context, callbackBucket);
236237
} else {
237-
return this.connectFactory.createCompositeConnectOperation(entity, relationship, input, context);
238+
return this.connectFactory.createCompositeConnectOperation(
239+
entity,
240+
relationship,
241+
input,
242+
context,
243+
callbackBucket
244+
);
238245
}
239246
}
240247

packages/graphql/src/translate/queryAST/factory/Operations/ConnectFactory.ts

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* limitations under the License.
1818
*/
1919

20+
import Cypher from "@neo4j/cypher-builder";
2021
import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter";
2122
import type { InterfaceEntityAdapter } from "../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter";
2223
import type { UnionEntityAdapter } from "../../../../schema-model/entity/model-adapters/UnionEntityAdapter";
@@ -30,6 +31,7 @@ import { CompositeConnectOperation } from "../../ast/operations/composite/Compos
3031
import { CompositeConnectPartial } from "../../ast/operations/composite/CompositeConnectPartial";
3132
import { ConnectOperation } from "../../ast/operations/ConnectOperation";
3233
import { NodeSelectionPattern } from "../../ast/selection/SelectionPattern/NodeSelectionPattern";
34+
import type { CallbackBucket } from "../../utils/callback-bucket";
3335
import { isConcreteEntity } from "../../utils/is-concrete-entity";
3436
import { isInterfaceEntity } from "../../utils/is-interface-entity";
3537
import { raiseAttributeAmbiguity } from "../../utils/raise-attribute-ambiguity";
@@ -46,7 +48,8 @@ export class ConnectFactory {
4648
entity: ConcreteEntityAdapter,
4749
relationship: RelationshipAdapter,
4850
input: Record<string, any>[],
49-
context: Neo4jGraphQLTranslationContext
51+
context: Neo4jGraphQLTranslationContext,
52+
callbackBucket: CallbackBucket
5053
): ConnectOperation {
5154
const connectOP = new ConnectOperation({
5255
target: entity,
@@ -62,6 +65,7 @@ export class ConnectFactory {
6265
input,
6366
connect: connectOP,
6467
context,
68+
callbackBucket,
6569
});
6670
return connectOP;
6771
}
@@ -70,11 +74,18 @@ export class ConnectFactory {
7074
entity: InterfaceEntityAdapter | UnionEntityAdapter,
7175
relationship: RelationshipAdapter,
7276
input: Record<string, any>[],
73-
context: Neo4jGraphQLTranslationContext
77+
context: Neo4jGraphQLTranslationContext,
78+
callbackBucket: CallbackBucket
7479
): CompositeConnectOperation {
7580
const partials: CompositeConnectPartial[] = [];
7681
for (const concreteEntity of entity.concreteEntities) {
77-
const partial = this.createCompositeConnectPartial(concreteEntity, relationship, input, context);
82+
const partial = this.createCompositeConnectPartial(
83+
concreteEntity,
84+
relationship,
85+
input,
86+
context,
87+
callbackBucket
88+
);
7889
partials.push(partial);
7990
}
8091

@@ -88,7 +99,8 @@ export class ConnectFactory {
8899
entity: ConcreteEntityAdapter,
89100
relationship: RelationshipAdapter,
90101
input: Record<string, any>[],
91-
context: Neo4jGraphQLTranslationContext
102+
context: Neo4jGraphQLTranslationContext,
103+
callbackBucket: CallbackBucket
92104
): CompositeConnectPartial {
93105
const connectOP = new CompositeConnectPartial({
94106
target: entity,
@@ -104,6 +116,7 @@ export class ConnectFactory {
104116
input,
105117
connect: connectOP,
106118
context,
119+
callbackBucket,
107120
});
108121
return connectOP;
109122
}
@@ -114,12 +127,14 @@ export class ConnectFactory {
114127
input,
115128
connect,
116129
context,
130+
callbackBucket,
117131
}: {
118132
target: ConcreteEntityAdapter;
119133
relationship: RelationshipAdapter;
120134
input: Record<string, any>[];
121135
connect: ConnectOperation;
122136
context: Neo4jGraphQLTranslationContext;
137+
callbackBucket: CallbackBucket;
123138
}) {
124139
this.addEntityAuthorization({
125140
entity: target,
@@ -161,7 +176,8 @@ export class ConnectFactory {
161176
nestedEntity,
162177
nestedRelationship,
163178
nestedConnectInputItem,
164-
context
179+
context,
180+
callbackBucket
165181
);
166182

167183
const mutationOperationField = new MutationOperationField(nestedConnectOperation, key);
@@ -188,6 +204,50 @@ export class ConnectFactory {
188204
}
189205
}
190206
});
207+
208+
this.addPopulatedByFieldToConnect({
209+
connect,
210+
input,
211+
callbackBucket,
212+
relationship,
213+
});
214+
}
215+
216+
private addPopulatedByFieldToConnect({
217+
connect,
218+
input,
219+
callbackBucket,
220+
relationship,
221+
}: {
222+
connect: ConnectOperation;
223+
input: Record<string, any>;
224+
callbackBucket: CallbackBucket;
225+
relationship?: RelationshipAdapter;
226+
}) {
227+
// Using create here because the relationship is created on connect
228+
relationship?.getPopulatedByFields("CREATE").forEach((attribute) => {
229+
const attachedTo = "relationship";
230+
// the param value it's irrelevant as it will be overwritten by the callback function
231+
const relCallbackParam = new Cypher.Param("");
232+
const relField = new ParamInputField({
233+
attribute,
234+
attachedTo,
235+
inputValue: relCallbackParam,
236+
});
237+
connect.addField(relField, "relationship");
238+
239+
const callbackFunctionName = attribute.annotations.populatedBy?.callback;
240+
if (!callbackFunctionName) {
241+
throw new Error(`PopulatedBy callback not found for attribute ${attribute.name}`);
242+
}
243+
244+
callbackBucket.addCallback({
245+
functionName: callbackFunctionName,
246+
param: relCallbackParam,
247+
parent: input.edge,
248+
type: attribute.type,
249+
});
250+
});
191251
}
192252

193253
private addEntityAuthorization({

packages/graphql/src/translate/queryAST/factory/Operations/CreateFactory.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,8 @@ export class CreateFactory {
354354
nestedEntity,
355355
nestedRelationship,
356356
nestedConnectInputItem,
357-
context
357+
context,
358+
callbackBucket
358359
);
359360

360361
const mutationOperationField = new MutationOperationField(nestedConnectOperation, key);
@@ -497,7 +498,7 @@ export class CreateFactory {
497498
relationship?.getPopulatedByFields("CREATE").forEach((attribute) => {
498499
const attachedTo = "relationship";
499500
// the param value it's irrelevant as it will be overwritten by the callback function
500-
const relCallbackParam = new Cypher.Param("5678");
501+
const relCallbackParam = new Cypher.Param("");
501502
const relField = new ParamInputField({
502503
attribute,
503504
attachedTo,
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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 { UniqueType } from "../../utils/graphql-types";
21+
import { TestHelper } from "../../utils/tests-helper";
22+
23+
describe("https://github.com/neo4j/graphql/issues/6572", () => {
24+
const testHelper = new TestHelper();
25+
let typeDefs: string;
26+
27+
let MediaAssetDerivative: UniqueType;
28+
let MediaAssetDerivativeType: UniqueType;
29+
30+
beforeAll(async () => {
31+
MediaAssetDerivative = testHelper.createUniqueType("MediaAssetDerivative");
32+
MediaAssetDerivativeType = testHelper.createUniqueType("MediaAssetDerivativeType");
33+
34+
typeDefs = /* GraphQL */ `
35+
type ${MediaAssetDerivative}
36+
@mutation(operations: [CREATE, UPDATE, DELETE])
37+
@node
38+
@subscription(events: []) {
39+
changedAt: DateTime @timestamp(operations: [CREATE, UPDATE])
40+
createdAt: DateTime @timestamp(operations: [CREATE])
41+
id: String! @settable(onCreate: true, onUpdate: false)
42+
isPublic: Boolean! @default(value: false)
43+
type: [${MediaAssetDerivativeType}!]!
44+
@relationship(
45+
type: "MEDIAASSETDERIVATIVE_IS_OF_MEDIAASSETDERIVATIVETYPE"
46+
properties: "MediaassetderivativeIsOfMediaassetderivativetypeProperties"
47+
direction: OUT
48+
nestedOperations: [CREATE, UPDATE, DELETE, CONNECT, DISCONNECT]
49+
queryDirection: DIRECTED
50+
)
51+
@settable(onCreate: true, onUpdate: true)
52+
}
53+
54+
type ${MediaAssetDerivativeType}
55+
@mutation(operations: [CREATE, UPDATE, DELETE])
56+
@node
57+
@subscription(events: []) {
58+
id: Int! @settable(onCreate: true, onUpdate: false)
59+
value: String! @populatedBy(
60+
callback: "AnotherCallback"
61+
operations: [UPDATE]
62+
)
63+
}
64+
65+
type MediaassetderivativeIsOfMediaassetderivativetypeProperties @relationshipProperties {
66+
_pkFrom: String!
67+
@populatedBy(
68+
callback: "MEDIAASSETDERIVATIVE_IS_OF_MEDIAASSETDERIVATIVETYPE_Callback"
69+
operations: [CREATE, UPDATE]
70+
)
71+
@settable(onCreate: false, onUpdate: false)
72+
}
73+
`;
74+
75+
await testHelper.executeCypher(`
76+
CREATE(:${MediaAssetDerivativeType} { id: 1, value: "ShouldNotChange" })
77+
`);
78+
79+
const callback1 = () => Promise.resolve("FROM_1");
80+
const callback2 = () => Promise.resolve("New Value");
81+
82+
await testHelper.initNeo4jGraphQL({
83+
typeDefs,
84+
features: {
85+
populatedBy: {
86+
callbacks: {
87+
MEDIAASSETDERIVATIVE_IS_OF_MEDIAASSETDERIVATIVETYPE_Callback: callback1,
88+
AnotherCallback: callback2,
89+
},
90+
},
91+
},
92+
});
93+
});
94+
95+
afterAll(async () => {
96+
await testHelper.close();
97+
});
98+
99+
test("calls populatedBy on create -> connect mutation", async () => {
100+
const query = /* GraphQL */ `
101+
mutation {
102+
${MediaAssetDerivative.operations.create}(
103+
input: [{ id: "1", type: { connect: [{ where: { node: { id: { eq: 1 } } } }] } }]
104+
) {
105+
__typename
106+
}
107+
}
108+
`;
109+
110+
const queryResult = await testHelper.executeGraphQL(query);
111+
expect(queryResult.errors).toBeUndefined();
112+
113+
await testHelper
114+
.expectRelationship(
115+
MediaAssetDerivative,
116+
MediaAssetDerivativeType,
117+
"MEDIAASSETDERIVATIVE_IS_OF_MEDIAASSETDERIVATIVETYPE"
118+
)
119+
.toEqual([
120+
{
121+
from: {
122+
id: "1",
123+
changedAt: expect.toBeDate(),
124+
createdAt: expect.toBeDate(),
125+
isPublic: false,
126+
},
127+
to: {
128+
id: 1,
129+
value: "ShouldNotChange",
130+
},
131+
relationship: {
132+
_pkFrom: "FROM_1",
133+
},
134+
},
135+
]);
136+
});
137+
});

packages/graphql/tests/utils/neo-expect/neo-expect.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,11 @@ export abstract class NeoExpect {
8181
return value.toNumber();
8282
}
8383

84+
if (neo4j.isDateTime(value) || neo4j.isLocalDateTime(value) || neo4j.isDate(value)) {
85+
return value.toStandardDate();
86+
}
87+
8488
if (
85-
neo4j.isLocalDateTime(value) ||
86-
neo4j.isDate(value) ||
87-
neo4j.isDateTime(value) ||
8889
neo4j.isDuration(value) ||
8990
neo4j.isLocalTime(value) ||
9091
neo4j.isPoint(value) ||

packages/graphql/tests/utils/tests-helper.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,17 @@ export class TestHelper {
6767
}
6868

6969
/** Expectations over a Node label */
70-
public expectNode(type: UniqueType): NeoExpectNode {
70+
public expectNode(type: UniqueType | string): NeoExpectNode {
7171
const executeFn = this.executeCypher.bind(this);
7272
return new NeoExpectNode(executeFn, type);
7373
}
7474

7575
/** Expectations over a Relationship */
76-
public expectRelationship(from: UniqueType, to: UniqueType, type?: string): NeoExpectRelationship {
76+
public expectRelationship(
77+
from: UniqueType | string,
78+
to: UniqueType | string,
79+
type?: string
80+
): NeoExpectRelationship {
7781
const executeFn = this.executeCypher.bind(this);
7882
return new NeoExpectRelationship(executeFn, from, to, type);
7983
}

0 commit comments

Comments
 (0)