Skip to content

Commit 040f22f

Browse files
committed
Merge remote-tracking branch 'origin/dev' into group-by
2 parents c31a5a7 + d7cd76f commit 040f22f

File tree

39 files changed

+16021
-185
lines changed

39 files changed

+16021
-185
lines changed

.changeset/chilly-grapes-rest.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@neo4j/graphql": minor
3+
---
4+
5+
Add support for single element relationships. For example:
6+
7+
```graphql
8+
type Movie @node {
9+
title: String!
10+
actor: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn")
11+
director: Director! @relationship(type: "DIRECTED", direction: IN, properties: "Directed")
12+
}
13+
```
14+
15+
It makes possible to model and query the data of single element relationships, with the following constraints:
16+
17+
- If multiple relationships exists, the first one will be returned. The relationship that will be returned will not be guaranteed
18+
- Connections will maintain the many-to-many API, even if it is a single relationship. This is to maintain the relay spec
19+
- Delete mutations will be available for nullable fields
20+
- Create mutations will be available for both, nullable and non-nullable

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ export class ConcreteEntityAdapter {
153153

154154
public get relatedEntities(): EntityAdapter[] {
155155
if (!this._relatedEntities) {
156+
// TODO: remove array destructuring with Node 20
156157
this._relatedEntities = [...this.relationships.values()].map((relationship) => relationship.target);
157158
}
158159
return this._relatedEntities;
@@ -268,6 +269,11 @@ export class ConcreteEntityAdapter {
268269
return this.relationships.get(name);
269270
}
270271

272+
public hasListRelationship(): boolean {
273+
// TODO: remove array destructuring with Node 20
274+
return !![...this.relationships.values()].find((r) => r.isList);
275+
}
276+
271277
// TODO: identify usage of old Node.[getLabels | getLabelsString] and migrate them if needed
272278
public getLabels(): string[] {
273279
return Array.from(this.labels);

packages/graphql/src/schema/create-relationship-fields/create-relationship-fields.ts

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,6 @@ export function createRelationshipFields({
192192
return;
193193
}
194194

195-
if (!relationshipAdapter.isList) {
196-
throw new Error(
197-
`@relationship on non-list field [${relationshipAdapter.source.name}.${relationshipAdapter.name}] not supported`
198-
);
199-
}
200-
201195
// TODO: find a way to merge these 2 into 1 RelationshipProperties generation function
202196
if (relationshipAdapter instanceof RelationshipDeclarationAdapter) {
203197
doForRelationshipDeclaration({
@@ -334,13 +328,6 @@ function createRelationshipFieldsForTarget({
334328
)
335329
);
336330

337-
withRelationInputType({
338-
relationshipAdapter,
339-
composer,
340-
deprecatedDirectives,
341-
userDefinedFieldDirectives,
342-
});
343-
344331
augmentCreateInputTypeWithRelationshipsInput({
345332
relationshipAdapter,
346333
composer,
@@ -349,31 +336,40 @@ function createRelationshipFieldsForTarget({
349336
features,
350337
});
351338

352-
augmentConnectInputTypeWithConnectFieldInput({
353-
relationshipAdapter,
354-
composer,
355-
deprecatedDirectives,
356-
});
357-
358339
augmentDeleteInputTypeWithDeleteFieldInput({
359340
relationshipAdapter,
360341
composer,
361342
deprecatedDirectives,
362343
features,
363344
});
364345

365-
augmentDisconnectInputTypeWithDisconnectFieldInput({
366-
relationshipAdapter,
367-
composer,
368-
deprecatedDirectives,
369-
features,
370-
});
346+
if (relationshipAdapter.isList) {
347+
withRelationInputType({
348+
relationshipAdapter,
349+
composer,
350+
deprecatedDirectives,
351+
userDefinedFieldDirectives,
352+
});
371353

372-
augmentUpdateInputTypeWithUpdateFieldInput({
373-
relationshipAdapter,
374-
composer,
375-
deprecatedDirectives,
376-
userDefinedFieldDirectives,
377-
features,
378-
});
354+
augmentConnectInputTypeWithConnectFieldInput({
355+
relationshipAdapter,
356+
composer,
357+
deprecatedDirectives,
358+
});
359+
360+
augmentDisconnectInputTypeWithDisconnectFieldInput({
361+
relationshipAdapter,
362+
composer,
363+
deprecatedDirectives,
364+
features,
365+
});
366+
367+
augmentUpdateInputTypeWithUpdateFieldInput({
368+
relationshipAdapter,
369+
composer,
370+
deprecatedDirectives,
371+
userDefinedFieldDirectives,
372+
features,
373+
});
374+
}
379375
}

packages/graphql/src/schema/generation/augment-object-or-interface.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ export function augmentObjectOrInterfaceTypeWithRelationshipField({
6161
}
6262
}
6363

64+
if (!relationshipAdapter.isList) {
65+
generateRelFieldArgs = false;
66+
}
67+
6468
if (generateRelFieldArgs) {
6569
const relationshipTarget =
6670
relationshipAdapter instanceof RelationshipAdapter && relationshipAdapter.originalTarget
@@ -71,9 +75,11 @@ export function augmentObjectOrInterfaceTypeWithRelationshipField({
7175

7276
const nodeFieldsArgs = {
7377
where: whereTypeName,
74-
limit: features?.limitRequired ? new GraphQLNonNull(GraphQLInt) : GraphQLInt,
75-
offset: GraphQLInt,
7678
};
79+
80+
nodeFieldsArgs["limit"] = features?.limitRequired ? new GraphQLNonNull(GraphQLInt) : GraphQLInt;
81+
nodeFieldsArgs["offset"] = GraphQLInt;
82+
7783
if (!(relationshipTarget instanceof UnionEntityAdapter)) {
7884
const sortConfig = makeSortInput({
7985
entityAdapter: relationshipTarget,
@@ -107,24 +113,30 @@ export function augmentObjectOrInterfaceTypeWithConnectionField(
107113
)
108114
);
109115

110-
const composeNodeArgs: ObjectTypeComposerArgumentConfigMapDefinition = {
111-
where: makeConnectionWhereInputType({
112-
relationshipAdapter,
113-
composer: schemaComposer,
114-
}),
115-
first: {
116-
type: features?.limitRequired ? new GraphQLNonNull(GraphQLInt) : GraphQLInt,
117-
},
118-
after: {
119-
type: GraphQLString,
120-
},
121-
};
122-
const connectionSortITC = withConnectionSortInputType({
116+
const composeNodeArgs: ObjectTypeComposerArgumentConfigMapDefinition = {};
117+
118+
// we want this type to be created for single relationships, but don't want to set the argument
119+
const connectionWhereInput = makeConnectionWhereInputType({
123120
relationshipAdapter,
124121
composer: schemaComposer,
125122
});
126-
if (connectionSortITC) {
127-
composeNodeArgs.sort = connectionSortITC.NonNull.List;
123+
124+
if (relationshipAdapter.isList) {
125+
composeNodeArgs.where = connectionWhereInput;
126+
composeNodeArgs.first = {
127+
type: features?.limitRequired ? new GraphQLNonNull(GraphQLInt) : GraphQLInt,
128+
};
129+
composeNodeArgs.after = {
130+
type: GraphQLString,
131+
};
132+
133+
const connectionSortITC = withConnectionSortInputType({
134+
relationshipAdapter,
135+
composer: schemaComposer,
136+
});
137+
if (connectionSortITC) {
138+
composeNodeArgs.sort = connectionSortITC.NonNull.List;
139+
}
128140
}
129141
const isTargetUnion = relationshipAdapter.target instanceof UnionEntityAdapter;
130142
const isSourceInterface = relationshipAdapter.source instanceof InterfaceEntityAdapter;

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@ export function augmentWhereInputWithRelationshipFilters({
5050
deprecatedDirectives: Directive[];
5151
features?: Neo4jFeaturesSettings;
5252
}) {
53+
if (!relationshipAdapter.isList) {
54+
whereInput.addFields({
55+
[relationshipAdapter.name]: {
56+
type: relationshipAdapter.target.operations.whereInputTypeName,
57+
},
58+
});
59+
whereInput.addFields({
60+
[relationshipAdapter.operations.connectionFieldName]: {
61+
type: relationshipAdapter.operations.getConnectionWhereTypename(),
62+
},
63+
});
64+
return {};
65+
}
5366
if (!relationshipAdapter.isFilterableByAggregate() && !relationshipAdapter.isFilterableByValue()) {
5467
return {};
5568
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ function withConnectInputType({
4141
composer: SchemaComposer;
4242
}): InputTypeComposer | undefined {
4343
if (entityAdapter instanceof ConcreteEntityAdapter) {
44+
if (!entityAdapter.hasListRelationship()) {
45+
return undefined;
46+
}
4447
return composer.getOrCreateITC(entityAdapter.operations.connectInputTypeName);
4548
}
4649

@@ -194,9 +197,14 @@ export function withConnectFieldInputType({
194197
return composer.getITC(typeName);
195198
}
196199

200+
const fields = makeConnectFieldInputTypeFields({ relationshipAdapter, composer, ifUnionMemberEntity });
201+
if (!relationshipAdapter.isList) {
202+
return undefined;
203+
}
204+
197205
const connectFieldInput = composer.createInputTC({
198206
name: typeName,
199-
fields: makeConnectFieldInputTypeFields({ relationshipAdapter, composer, ifUnionMemberEntity }),
207+
fields,
200208
});
201209
return connectFieldInput;
202210
}

packages/graphql/src/schema/generation/connection-object-type.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function withConnectionObjectType({
5151
const isTargetUnion = relationshipAdapter.target instanceof UnionEntityAdapter;
5252
const isSourceInterface = relationshipAdapter.source instanceof InterfaceEntityAdapter;
5353

54-
if (relationshipAdapter.aggregate && !isTargetUnion && !isSourceInterface) {
54+
if (relationshipAdapter.isList && relationshipAdapter.aggregate && !isTargetUnion && !isSourceInterface) {
5555
const connectionObjectType = composer.getOrCreateOTC(typeName);
5656
connectionObjectType.addFields({
5757
aggregate: composer.getOTC(relationshipAdapter.operations.getAggregateFieldTypename()).NonNull,

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ function withDisconnectInputType({
4242
composer: SchemaComposer;
4343
}): InputTypeComposer | undefined {
4444
if (entityAdapter instanceof ConcreteEntityAdapter) {
45+
if (!entityAdapter.hasListRelationship()) {
46+
return undefined;
47+
}
4548
return composer.getOrCreateITC(entityAdapter.operations.updateMutationArgumentNames.disconnect);
4649
}
4750

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,18 @@ export function withCreateFieldInputType({
202202
if (composer.has(createName)) {
203203
return composer.getITC(createName);
204204
}
205+
const fields = makeCreateFieldInputTypeFields({
206+
relationshipAdapter,
207+
composer,
208+
ifUnionMemberEntity,
209+
userDefinedFieldDirectives,
210+
});
211+
if (!Object.keys(fields).length) {
212+
return undefined;
213+
}
205214
const createFieldInput = composer.createInputTC({
206215
name: createName,
207-
fields: makeCreateFieldInputTypeFields({
208-
relationshipAdapter,
209-
composer,
210-
ifUnionMemberEntity,
211-
userDefinedFieldDirectives,
212-
}),
216+
fields,
213217
});
214218
return createFieldInput;
215219
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ export function withSourceWhereInputType({
195195
return;
196196
}
197197

198-
if (relationshipAdapter.isFilterableByAggregate()) {
198+
if (relationshipAdapter.isList && relationshipAdapter.isFilterableByAggregate()) {
199199
withConnectionAggregateInputType({
200200
relationshipAdapter,
201201
entityAdapter: relationshipTarget,

0 commit comments

Comments
 (0)