Skip to content

Commit

Permalink
Merge pull request #5944 from neo4j/aggregation-connections
Browse files Browse the repository at this point in the history
Top Level aggregations in connections
  • Loading branch information
angrykoala authored Jan 23, 2025
2 parents fd3f015 + 5d36415 commit 2cb4148
Show file tree
Hide file tree
Showing 142 changed files with 10,858 additions and 990 deletions.
20 changes: 20 additions & 0 deletions .changeset/hot-windows-whisper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@neo4j/graphql": minor
---

Add aggregate fieldd in connection:

```graphql
query {
moviesConnection {
aggregate {
node {
count
int {
longest
}
}
}
}
}
```
25 changes: 25 additions & 0 deletions .changeset/thin-goats-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
"@neo4j/graphql": patch
---

Deprecated old aggregate operations:

```graphql
query {
moviesAggregate {
count
rating {
min
}
}
}
```

These fields can be completely removed from the schema with the new flag `deprecatedAggregateOperations`:

```js
const neoSchema = new Neo4jGraphQL({
typeDefs,
features: { excludeDeprecatedFields: { deprecatedAggregateOperations: true } },
});
```
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export type RootTypeFieldNames = {
type AggregateTypeNames = {
selection: string;
input: string;
connection: string;
node: string;
};

type MutationResponseTypeNames = {
Expand Down Expand Up @@ -161,6 +163,8 @@ export class ImplementingEntityOperations<T extends InterfaceEntityAdapter | Con
return {
selection: `${this.entityAdapter.name}AggregateSelection`,
input: `${this.entityAdapter.name}AggregateSelectionInput`,
connection: `${this.entityAdapter.name}Aggregate`,
node: `${this.entityAdapter.name}AggregateNode`,
};
}

Expand Down
12 changes: 11 additions & 1 deletion packages/graphql/src/schema/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/

import { DEPRECATED } from "../constants";
import type { ConcreteEntityAdapter } from "../schema-model/entity/model-adapters/ConcreteEntityAdapter";
import type { InterfaceEntityAdapter } from "../schema-model/entity/model-adapters/InterfaceEntityAdapter";

export const DEPRECATE_IMPLICIT_EQUAL_FILTERS = {
name: DEPRECATED,
Expand Down Expand Up @@ -61,7 +63,6 @@ export const DEPRECATE_OVERWRITE = {
},
};


export const DEPRECATE_ID_AGGREGATION = {
name: DEPRECATED,
args: {
Expand All @@ -75,3 +76,12 @@ export const DEPRECATE_TYPENAME_IN = {
reason: "The typename_IN filter is deprecated, please use the typename filter instead",
},
};

export function DEPRECATE_AGGREGATION(entity: ConcreteEntityAdapter | InterfaceEntityAdapter) {
return {
name: DEPRECATED,
args: {
reason: `Please use the explicit the field "aggregate" inside "${entity.operations.rootTypeFieldNames.connection}"`,
},
};
}
44 changes: 44 additions & 0 deletions packages/graphql/src/schema/generation/aggregate-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,53 @@ export function withAggregateSelectionType({
directives: graphqlDirectivesToCompose(propagatedDirectives),
});
aggregateSelection.addFields(makeAggregableFields({ entityAdapter, aggregationTypesMapper, features }));

createConnectionAggregate({
entityAdapter,
aggregationTypesMapper,
propagatedDirectives,
composer,
features,
});
return aggregateSelection;
}

/** Create aggregate field inside connections */
function createConnectionAggregate({
entityAdapter,
aggregationTypesMapper,
propagatedDirectives,
composer,
features,
}: {
entityAdapter: ConcreteEntityAdapter | InterfaceEntityAdapter;
aggregationTypesMapper: AggregationTypesMapper;
propagatedDirectives: DirectiveNode[];
composer: SchemaComposer;
features: Neo4jFeaturesSettings | undefined;
}): ObjectTypeComposer {
const aggregateNode = composer.createObjectTC({
name: entityAdapter.operations.aggregateTypeNames.node,
fields: {
count: {
type: new GraphQLNonNull(GraphQLInt),
resolve: numericalResolver,
args: {},
},
},
directives: graphqlDirectivesToCompose(propagatedDirectives),
});
aggregateNode.addFields(makeAggregableFields({ entityAdapter, aggregationTypesMapper, features }));

return composer.createObjectTC({
name: entityAdapter.operations.aggregateTypeNames.connection,
fields: {
node: aggregateNode.NonNull,
},
directives: graphqlDirectivesToCompose(propagatedDirectives),
});
}

function makeAggregableFields({
entityAdapter,
aggregationTypesMapper,
Expand Down
43 changes: 25 additions & 18 deletions packages/graphql/src/schema/make-augmented-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import { RelationshipDeclarationAdapter } from "../schema-model/relationship/mod
import type { CypherField, Neo4jFeaturesSettings } from "../types";
import { filterTruthy } from "../utils/utils";
import { augmentVectorSchema } from "./augment/vector";
import { DEPRECATE_AGGREGATION } from "./constants";
import { createConnectionFields } from "./create-connection-fields";
import { addGlobalNodeFields } from "./create-global-nodes";
import { createRelationshipFields } from "./create-relationship-fields/create-relationship-fields";
Expand All @@ -83,6 +84,7 @@ import { withObjectType } from "./generation/object-type";
import { withMutationResponseTypes } from "./generation/response-types";
import { withOptionsInputType } from "./generation/sort-and-options-input";
import { withUpdateInputType } from "./generation/update-input";
import { shouldAddDeprecatedFields } from "./generation/utils";
import { withUniqueWhereInputType, withWhereInputType } from "./generation/where-input";
import getNodes from "./get-nodes";
import { getResolveAndSubscriptionMethods } from "./get-resolve-and-subscription-methods";
Expand Down Expand Up @@ -580,15 +582,17 @@ function generateObjectType({
features,
});

composer.Query.addFields({
[concreteEntityAdapter.operations.rootTypeFieldNames.aggregate]: aggregateResolver({
entityAdapter: concreteEntityAdapter,
}),
});
composer.Query.setFieldDirectives(
concreteEntityAdapter.operations.rootTypeFieldNames.aggregate,
graphqlDirectivesToCompose(propagatedDirectives)
);
if (shouldAddDeprecatedFields(features, "deprecatedAggregateOperations")) {
composer.Query.addFields({
[concreteEntityAdapter.operations.rootTypeFieldNames.aggregate]: aggregateResolver({
entityAdapter: concreteEntityAdapter,
}),
});
composer.Query.setFieldDirectives(concreteEntityAdapter.operations.rootTypeFieldNames.aggregate, [
...graphqlDirectivesToCompose(propagatedDirectives),
DEPRECATE_AGGREGATION(concreteEntityAdapter),
]);
}
}

if (concreteEntityAdapter.isCreatable) {
Expand Down Expand Up @@ -720,14 +724,17 @@ function generateInterfaceObjectType({
features,
});

composer.Query.addFields({
[interfaceEntityAdapter.operations.rootTypeFieldNames.aggregate]: aggregateResolver({
entityAdapter: interfaceEntityAdapter,
}),
});
composer.Query.setFieldDirectives(
interfaceEntityAdapter.operations.rootTypeFieldNames.aggregate,
graphqlDirectivesToCompose(propagatedDirectives)
);
if (shouldAddDeprecatedFields(features, "deprecatedAggregateOperations")) {
composer.Query.addFields({
[interfaceEntityAdapter.operations.rootTypeFieldNames.aggregate]: aggregateResolver({
entityAdapter: interfaceEntityAdapter,
}),
});

composer.Query.setFieldDirectives(interfaceEntityAdapter.operations.rootTypeFieldNames.aggregate, [
...graphqlDirectivesToCompose(propagatedDirectives),
DEPRECATE_AGGREGATION(interfaceEntityAdapter),
]);
}
}
}
3 changes: 3 additions & 0 deletions packages/graphql/src/schema/pagination.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe("cursor-pagination", () => {
selectionSet: undefined,
});
expect(result).toStrictEqual({
aggregate: {},
edges: arraySlice.map((edge, index) => ({ ...edge, cursor: offsetToCursor(index) })),
pageInfo: {
hasNextPage: false,
Expand All @@ -68,6 +69,7 @@ describe("cursor-pagination", () => {
selectionSet: undefined,
});
expect(result).toStrictEqual({
aggregate: {},
edges: arraySlice.map((edge, index) => ({ ...edge, cursor: offsetToCursor(index) })),
pageInfo: {
hasNextPage: false,
Expand All @@ -88,6 +90,7 @@ describe("cursor-pagination", () => {
selectionSet: undefined,
});
expect(result).toStrictEqual({
aggregate: {},
edges: arraySlice.map((edge, index) => ({ ...edge, cursor: offsetToCursor(index + 11) })),
pageInfo: {
hasNextPage: false,
Expand Down
3 changes: 3 additions & 0 deletions packages/graphql/src/schema/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export function createConnectionWithEdgeProperties({
const sliceStart = lastEdgeCursor + 1;

const edges: any[] = source?.edges || [];
const aggregate: any = source?.aggregate || {};

const selections = selectionSet?.selections || [];

Expand All @@ -114,6 +115,7 @@ export function createConnectionWithEdgeProperties({

const pageInfoKey = getAliasKey({ selectionSet, key: "pageInfo" });
const edgesKey = getAliasKey({ selectionSet, key: "edges" });
const aggregateKey = getAliasKey({ selectionSet, key: "aggregate" });
const pageInfoField = selections.find((x): x is FieldNode => x.kind === Kind.FIELD && x.name.value === "pageInfo");
const pageInfoSelectionSet = pageInfoField?.selectionSet;
const startCursorKey = getAliasKey({ selectionSet: pageInfoSelectionSet, key: "startCursor" });
Expand All @@ -123,6 +125,7 @@ export function createConnectionWithEdgeProperties({

return {
[edgesKey]: mappedEdges,
[aggregateKey]: aggregate,
[pageInfoKey]: {
[startCursorKey]: startCursor,
[endCursorKey]: endCursor,
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql/src/schema/resolvers/query/aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import type { GraphQLResolveInfo } from "graphql";
import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter";
import type { InterfaceEntityAdapter } from "../../../schema-model/entity/model-adapters/InterfaceEntityAdapter";
import { translateAggregate } from "../../../translate";
import { isConcreteEntity } from "../../../translate/queryAST/utils/is-concrete-entity";
import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context";
import { execute } from "../../../utils";
import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree";
import type { Neo4jGraphQLComposedContext } from "../composition/wrap-query-and-mutation";
import { isConcreteEntity } from "../../../translate/queryAST/utils/is-concrete-entity";

export function aggregateResolver({
entityAdapter,
Expand Down
21 changes: 14 additions & 7 deletions packages/graphql/src/schema/resolvers/query/root-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
type GraphQLResolveInfo,
type SelectionSetNode,
} from "graphql";
import type { InputTypeComposer, SchemaComposer } from "graphql-compose";
import type { InputTypeComposer, ObjectTypeComposerFieldConfigMapDefinition, SchemaComposer } from "graphql-compose";
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";
Expand Down Expand Up @@ -72,7 +72,7 @@ export function rootConnectionResolver({

const connection = createConnectionWithEdgeProperties({
selectionSet: resolveTree as unknown as SelectionSetNode,
source: { edges: record.edges },
source: { edges: record.edges, aggregate: record.aggregate },
args: { first: args.first, after: args.after },
totalCount,
});
Expand All @@ -81,6 +81,7 @@ export function rootConnectionResolver({
totalCount,
edges: connection.edges,
pageInfo: connection.pageInfo,
aggregate: connection.aggregate,
};
}

Expand All @@ -93,13 +94,19 @@ export function rootConnectionResolver({
directives: graphqlDirectivesToCompose(propagatedDirectives),
});

const rootFields: ObjectTypeComposerFieldConfigMapDefinition<any, any> = {
totalCount: new GraphQLNonNull(GraphQLInt),
pageInfo: new GraphQLNonNull(PageInfo),
edges: rootEdge.NonNull.List.NonNull,
};

if (entityAdapter.isAggregable) {
rootFields["aggregate"] = `${entityAdapter.operations.aggregateTypeNames.connection}!`;
}

const rootConnection = composer.createObjectTC({
name: `${entityAdapter.upperFirstPlural}Connection`,
fields: {
totalCount: new GraphQLNonNull(GraphQLInt),
pageInfo: new GraphQLNonNull(PageInfo),
edges: rootEdge.NonNull.List.NonNull,
},
fields: rootFields,
directives: graphqlDirectivesToCompose(propagatedDirectives),
});

Expand Down
Loading

0 comments on commit 2cb4148

Please sign in to comment.