Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Top level interface connection #4821

Merged
merged 11 commits into from
Mar 6, 2024
40 changes: 40 additions & 0 deletions .changeset/honest-pumpkins-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
"@neo4j/graphql": minor
---

Support for top-level connection query on interfaces. For example:

_Typedefs_

```graphql
interface Show {
title: String!
}

type Movie implements Show {
title: String!
cost: Float
}

type Series implements Show {
title: String!
episodes: Int
}
```

_Query_

```graphql
query {
showsConnection(where: { title_CONTAINS: "The Matrix" }) {
edges {
node {
title
... on Movie {
cost
}
}
}
}
}
```
4 changes: 2 additions & 2 deletions packages/graphql/src/schema-model/entity/ConcreteEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ export class ConcreteEntity implements Entity {
}
}

isConcreteEntity(): this is ConcreteEntity {
public isConcreteEntity(): this is ConcreteEntity {
return true;
}

isCompositeEntity(): this is CompositeEntity {
public isCompositeEntity(): this is CompositeEntity {
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ export class ConcreteEntityOperations extends ImplementingEntityOperations<Concr
public get rootTypeFieldNames(): RootTypeFieldNames {
return {
...super.rootTypeFieldNames,
connection: `${this.entityAdapter.plural}Connection`,
subscribe: {
created: `${this.entityAdapter.singular}Created`,
updated: `${this.entityAdapter.singular}Updated`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
*/

import { upperFirst } from "../../../utils/upper-first";
import type { InterfaceEntityAdapter } from "./InterfaceEntityAdapter";
import type { ConcreteEntityAdapter } from "./ConcreteEntityAdapter";
import type { InterfaceEntityAdapter } from "./InterfaceEntityAdapter";

export type RootTypeFieldNames = {
create: string;
connection: string;
read: string;
update: string;
delete: string;
Expand Down Expand Up @@ -147,6 +148,7 @@ export class ImplementingEntityOperations<T extends InterfaceEntityAdapter | Con

public get rootTypeFieldNames(): RootTypeFieldNames {
return {
connection: `${this.entityAdapter.plural}Connection`,
create: `create${this.pascalCasePlural}`,
read: this.entityAdapter.plural,
update: `update${this.pascalCasePlural}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,44 +53,11 @@ export class InterfaceEntityAdapter {
this.initConcreteEntities(entity.concreteEntities);
}

public findAttribute(name: string): AttributeAdapter | undefined {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bit here is just reordering some methods in InterfaceEntityAdapter to make it more consistent

return this.attributes.get(name);
}

public findRelationshipDeclarations(name: string): RelationshipDeclarationAdapter | undefined {
return this.relationshipDeclarations.get(name);
}

get globalIdField(): AttributeAdapter | undefined {
public get globalIdField(): AttributeAdapter | undefined {
return undefined;
}

private initConcreteEntities(entities: ConcreteEntity[]) {
for (const entity of entities) {
const entityAdapter = new ConcreteEntityAdapter(entity);
this.concreteEntities.push(entityAdapter);
}
}

private initAttributes(attributes: Map<string, Attribute>) {
for (const [attributeName, attribute] of attributes.entries()) {
const attributeAdapter = new AttributeAdapter(attribute);
this.attributes.set(attributeName, attributeAdapter);
if (attributeAdapter.isConstrainable() && attributeAdapter.isUnique()) {
this.uniqueFieldsKeys.push(attribute.name);
}
}
}

private initRelationshipDeclarations(relationshipDeclarations: Map<string, RelationshipDeclaration>) {
for (const [relationshipName, relationshipDeclaration] of relationshipDeclarations.entries()) {
this.relationshipDeclarations.set(
relationshipName,
new RelationshipDeclarationAdapter(relationshipDeclaration, this)
);
}
}
get operations(): InterfaceEntityOperations {
public get operations(): InterfaceEntityOperations {
if (!this._operations) {
return new InterfaceEntityOperations(this);
}
Expand Down Expand Up @@ -119,11 +86,11 @@ export class InterfaceEntityAdapter {
return upperFirst(this.plural);
}

get isReadable(): boolean {
public get isReadable(): boolean {
return this.annotations.query === undefined || this.annotations.query.read === true;
}

get isAggregable(): boolean {
public get isAggregable(): boolean {
return this.annotations.query === undefined || this.annotations.query.aggregate === true;
}

Expand Down Expand Up @@ -156,4 +123,38 @@ export class InterfaceEntityAdapter {
public get subscriptionEventPayloadFields(): AttributeAdapter[] {
return Array.from(this.attributes.values()).filter((attribute) => attribute.isEventPayloadField());
}

public findAttribute(name: string): AttributeAdapter | undefined {
return this.attributes.get(name);
}

public findRelationshipDeclarations(name: string): RelationshipDeclarationAdapter | undefined {
return this.relationshipDeclarations.get(name);
}

private initConcreteEntities(entities: ConcreteEntity[]) {
for (const entity of entities) {
const entityAdapter = new ConcreteEntityAdapter(entity);
this.concreteEntities.push(entityAdapter);
}
}

private initAttributes(attributes: Map<string, Attribute>) {
for (const [attributeName, attribute] of attributes.entries()) {
const attributeAdapter = new AttributeAdapter(attribute);
this.attributes.set(attributeName, attributeAdapter);
if (attributeAdapter.isConstrainable() && attributeAdapter.isUnique()) {
this.uniqueFieldsKeys.push(attribute.name);
}
}
}

private initRelationshipDeclarations(relationshipDeclarations: Map<string, RelationshipDeclaration>) {
for (const [relationshipName, relationshipDeclaration] of relationshipDeclarations.entries()) {
this.relationshipDeclarations.set(
relationshipName,
new RelationshipDeclarationAdapter(relationshipDeclaration, this)
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
* limitations under the License.
*/

import type { InterfaceEntityAdapter } from "./InterfaceEntityAdapter";
import { upperFirst } from "graphql-compose";
import { ImplementingEntityOperations } from "./ImplementingEntityOperations";
import type { InterfaceEntityAdapter } from "./InterfaceEntityAdapter";

export class InterfaceEntityOperations extends ImplementingEntityOperations<InterfaceEntityAdapter> {
constructor(interfaceEntityAdapter: InterfaceEntityAdapter) {
Expand All @@ -32,4 +33,16 @@ export class InterfaceEntityOperations extends ImplementingEntityOperations<Inte
public get implementationsSubscriptionWhereInputTypeName(): string {
return `${this.entityAdapter.name}ImplementationsSubscriptionWhere`;
}

public get fullTextInputTypeName(): string {
return `${this.entityAdapter.name}Fulltext`;
}

public getFullTextIndexInputTypeName(indexName: string): string {
return `${this.entityAdapter.name}${upperFirst(indexName)}Fulltext`;
}

public get connectionFieldTypename(): string {
return `${this.pascalCasePlural}Connection`;
}
}
18 changes: 16 additions & 2 deletions packages/graphql/src/schema/make-augmented-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { UpdateInfo } from "../graphql/objects/UpdateInfo";
import type { Neo4jGraphQLSchemaModel } from "../schema-model/Neo4jGraphQLSchemaModel";
import type { Operation } from "../schema-model/Operation";
import { OperationAdapter } from "../schema-model/OperationAdapter";
import { ConcreteEntity } from "../schema-model/entity/ConcreteEntity";
import { InterfaceEntity } from "../schema-model/entity/InterfaceEntity";
import { UnionEntity } from "../schema-model/entity/UnionEntity";
import { ConcreteEntityAdapter } from "../schema-model/entity/model-adapters/ConcreteEntityAdapter";
Expand All @@ -89,7 +90,6 @@ import { getResolveAndSubscriptionMethods } from "./get-resolve-and-subscription
import { filterInterfaceTypes } from "./make-augmented-schema/filter-interface-types";
import { getUserDefinedDirectives } from "./make-augmented-schema/user-defined-directives";
import { generateSubscriptionTypes } from "./subscriptions/generate-subscription-types";
import { ConcreteEntity } from "../schema-model/entity/ConcreteEntity";

function definitionNodeHasName(x: DefinitionNode): x is DefinitionNode & { name: NameNode } {
return "name" in x;
Expand Down Expand Up @@ -554,10 +554,11 @@ function generateObjectType({
concreteEntityAdapter.operations.rootTypeFieldNames.read,
graphqlDirectivesToCompose(propagatedDirectives)
);

composer.Query.addFields({
[concreteEntityAdapter.operations.rootTypeFieldNames.connection]: rootConnectionResolver({
composer,
concreteEntityAdapter,
entityAdapter: concreteEntityAdapter,
propagatedDirectives,
}),
});
Expand Down Expand Up @@ -687,10 +688,23 @@ function generateInterfaceObjectType({
entityAdapter: interfaceEntityAdapter,
}),
});

composer.Query.setFieldDirectives(
interfaceEntityAdapter.operations.rootTypeFieldNames.read,
graphqlDirectivesToCompose(propagatedDirectives)
);

composer.Query.addFields({
[interfaceEntityAdapter.operations.rootTypeFieldNames.connection]: rootConnectionResolver({
composer,
entityAdapter: interfaceEntityAdapter,
propagatedDirectives,
}),
});
composer.Query.setFieldDirectives(
interfaceEntityAdapter.operations.rootTypeFieldNames.connection,
graphqlDirectivesToCompose(propagatedDirectives)
);
}
if (interfaceEntityAdapter.isAggregable) {
withAggregateSelectionType({
Expand Down
23 changes: 12 additions & 11 deletions packages/graphql/src/schema/resolvers/query/root-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type { InputTypeComposer, SchemaComposer } from "graphql-compose";
import type { PageInfo as PageInfoRelay } from "graphql-relay";
import { PageInfo } from "../../../graphql/objects/PageInfo";
import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter";
import type { InterfaceEntityAdapter } from "../../../schema-model/entity/model-adapters/InterfaceEntityAdapter";
import { translateRead } from "../../../translate";
import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context";
import { execute } from "../../../utils";
Expand All @@ -40,11 +41,11 @@ import type { Neo4jGraphQLComposedContext } from "../composition/wrap-query-and-

export function rootConnectionResolver({
composer,
concreteEntityAdapter,
entityAdapter,
propagatedDirectives,
}: {
composer: SchemaComposer;
concreteEntityAdapter: ConcreteEntityAdapter;
entityAdapter: InterfaceEntityAdapter | ConcreteEntityAdapter;
propagatedDirectives: DirectiveNode[];
}) {
async function resolve(_root: any, args: any, context: Neo4jGraphQLComposedContext, info: GraphQLResolveInfo) {
Expand All @@ -53,7 +54,7 @@ export function rootConnectionResolver({

const { cypher, params } = translateRead({
context: context as Neo4jGraphQLTranslationContext,
entityAdapter: concreteEntityAdapter,
entityAdapter: entityAdapter,
varName: "this",
});

Expand Down Expand Up @@ -98,16 +99,16 @@ export function rootConnectionResolver({
}

const rootEdge = composer.createObjectTC({
name: `${concreteEntityAdapter.name}Edge`,
name: `${entityAdapter.name}Edge`,
fields: {
cursor: new GraphQLNonNull(GraphQLString),
node: `${concreteEntityAdapter.name}!`,
node: `${entityAdapter.name}!`,
},
directives: graphqlDirectivesToCompose(propagatedDirectives),
});

const rootConnection = composer.createObjectTC({
name: `${concreteEntityAdapter.upperFirstPlural}Connection`,
name: `${entityAdapter.upperFirstPlural}Connection`,
fields: {
totalCount: new GraphQLNonNull(GraphQLInt),
pageInfo: new GraphQLNonNull(PageInfo),
Expand All @@ -118,8 +119,8 @@ export function rootConnectionResolver({

// since sort is not created when there is nothing to sort, we check for its existence
let sortArg: InputTypeComposer | undefined;
if (composer.has(concreteEntityAdapter.operations.sortInputTypeName)) {
sortArg = composer.getITC(concreteEntityAdapter.operations.sortInputTypeName);
if (composer.has(entityAdapter.operations.sortInputTypeName)) {
sortArg = composer.getITC(entityAdapter.operations.sortInputTypeName);
}

return {
Expand All @@ -128,12 +129,12 @@ export function rootConnectionResolver({
args: {
first: GraphQLInt,
after: GraphQLString,
where: concreteEntityAdapter.operations.whereInputTypeName,
where: entityAdapter.operations.whereInputTypeName,
...(sortArg ? { sort: sortArg.List } : {}),
...(concreteEntityAdapter.annotations.fulltext
...(entityAdapter.annotations.fulltext
? {
fulltext: {
type: concreteEntityAdapter.operations.fullTextInputTypeName,
type: entityAdapter.operations.fullTextInputTypeName,
description:
"Query a full-text index. Allows for the aggregation of results, but does not return the query score. Use the root full-text query fields if you require the score.",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export class ConnectionReadOperation extends Operation {
});

return filterTruthy([
this.selection,
...this.nodeFields,
...this.edgeFields,
...this.filters,
Expand Down Expand Up @@ -131,7 +132,7 @@ export class ConnectionReadOperation extends Operation {
});

const filtersSubqueries = [...authFilterSubqueries, ...normalFilterSubqueries];

const edgesVar = new Cypher.NamedVariable("edges");
const totalCount = new Cypher.NamedVariable("totalCount");
const edgesProjectionVar = new Cypher.Variable();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ import type { OperationTranspileResult } from "../operations";

export class CompositeConnectionPartial extends ConnectionReadOperation {
public transpile(context: QueryASTContext): OperationTranspileResult {
if (!context.target) throw new Error();
if (!this.relationship) throw new Error("connection fields are not supported on top level interface");

// eslint-disable-next-line prefer-const
let { selection: clause, nestedContext } = this.selection.apply(context);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export class CompositeConnectionReadOperation extends Operation {

constructor(children: CompositeConnectionPartial[]) {
super();

this.children = children;
}

Expand All @@ -48,9 +47,12 @@ export class CompositeConnectionReadOperation extends Operation {
const nestedSubqueries = this.children.flatMap((c) => {
const subQueryContext = new QueryASTContext({ ...context, returnVariable: edgeVar });
const result = c.transpile(subQueryContext);
if (!hasTarget(context)) throw new Error("No parent node found!");
const parentNode = context.target;
return result.clauses.map((sq) => Cypher.concat(new Cypher.With(parentNode), sq));
if (hasTarget(context)) {
const parentNode = context.target;
return result.clauses.map((sq) => Cypher.concat(new Cypher.With(parentNode), sq));
} else {
return result.clauses;
}
});

const union = new Cypher.Union(...nestedSubqueries);
Expand Down
Loading
Loading