Skip to content

Commit

Permalink
Merge pull request #5437 from MacondoExpress/create-default
Browse files Browse the repository at this point in the history
Support `@default` directive on v6
  • Loading branch information
MacondoExpress authored Aug 6, 2024
2 parents 928335e + f08f22f commit 40d5c5d
Show file tree
Hide file tree
Showing 14 changed files with 840 additions and 591 deletions.
22 changes: 19 additions & 3 deletions packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { SchemaComposer } from "graphql-compose";
import { SchemaBuilderTypes } from "./SchemaBuilderTypes";

export type TypeDefinition = string | WrappedComposer<ObjectTypeComposer | ScalarTypeComposer>;
export type InputTypeDefinition = string | WrappedComposer<InputTypeComposer | ScalarTypeComposer>;

type ObjectOrInputTypeComposer = ObjectTypeComposer | InputTypeComposer;

Expand All @@ -41,7 +42,7 @@ type ListOrNullComposer<T extends ObjectOrInputTypeComposer | ScalarTypeComposer
| NonNullComposer<ListComposer<T>>
| NonNullComposer<ListComposer<NonNullComposer<T>>>;

type WrappedComposer<T extends ObjectOrInputTypeComposer | ScalarTypeComposer> = T | ListOrNullComposer<T>;
export type WrappedComposer<T extends ObjectOrInputTypeComposer | ScalarTypeComposer> = T | ListOrNullComposer<T>;

export type GraphQLResolver = (...args) => any;

Expand All @@ -53,6 +54,14 @@ export type FieldDefinition = {
description?: string | null;
};

export type InputFieldDefinition = {
type: InputTypeDefinition;
args?: Record<string, any>;
deprecationReason?: string | null;
description?: string | null;
defaultValue: any;
};

export class SchemaBuilder {
public readonly types: SchemaBuilderTypes;
private composer: SchemaComposer;
Expand Down Expand Up @@ -108,7 +117,6 @@ export class SchemaBuilder {
if (description) {
tc.setDescription(description);
}

// This is used for global node, not sure if needed for other interfaces
tc.setResolveType((obj) => {
return obj.__resolveType;
Expand All @@ -125,6 +133,7 @@ export class SchemaBuilder {
| GraphQLInputType
| GraphQLNonNull<any>
| WrappedComposer<InputTypeComposer | ScalarTypeComposer>
| InputFieldDefinition
>;
description?: string;
}
Expand All @@ -142,7 +151,14 @@ export class SchemaBuilder {

public createInputObjectType(
name: string,
fields: Record<string, EnumTypeComposer | WrappedComposer<InputTypeComposer>>,
fields: Record<
string,
| EnumTypeComposer
| GraphQLInputType
| GraphQLNonNull<any>
| WrappedComposer<InputTypeComposer | ScalarTypeComposer>
| InputFieldDefinition
>,
description?: string
): InputTypeComposer {
return this.composer.createInputTC({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class TopLevelEntitySchemaTypes {
this.schemaBuilder = schemaBuilder;
this.entityTypeNames = entity.typeNames;
this.schemaTypes = schemaTypes;
this.createSchemaTypes = new TopLevelCreateSchemaTypes({ schemaBuilder, entity, schemaTypes });
this.createSchemaTypes = new TopLevelCreateSchemaTypes({ schemaBuilder, entity });
}

public addTopLevelQueryField(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
* limitations under the License.
*/

import type { GraphQLScalarType } from "graphql";
import type { InputTypeComposer, NonNullComposer, ScalarTypeComposer } from "graphql-compose";
import type { InputTypeComposer, ScalarTypeComposer } from "graphql-compose";
import type { Attribute } from "../../../../schema-model/attribute/Attribute";
import type { AttributeType } from "../../../../schema-model/attribute/AttributeType";
import {
Expand All @@ -34,28 +33,17 @@ import {
import type { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEntity";
import { filterTruthy } from "../../../../utils/utils";
import type { TopLevelEntityTypeNames } from "../../../schema-model/graphql-type-names/TopLevelEntityTypeNames";
import type { SchemaBuilder } from "../../SchemaBuilder";
import type { SchemaTypes } from "../SchemaTypes";
import type { InputFieldDefinition, SchemaBuilder, WrappedComposer } from "../../SchemaBuilder";

export class TopLevelCreateSchemaTypes {
private entityTypeNames: TopLevelEntityTypeNames;
private schemaTypes: SchemaTypes;
private schemaBuilder: SchemaBuilder;
private entity: ConcreteEntity;

constructor({
entity,
schemaBuilder,
schemaTypes,
}: {
entity: ConcreteEntity;
schemaBuilder: SchemaBuilder;
schemaTypes: SchemaTypes;
}) {
constructor({ entity, schemaBuilder }: { entity: ConcreteEntity; schemaBuilder: SchemaBuilder }) {
this.entity = entity;
this.entityTypeNames = entity.typeNames;
this.schemaBuilder = schemaBuilder;
this.schemaTypes = schemaTypes;
}

public get createInput(): InputTypeComposer {
Expand All @@ -79,12 +67,16 @@ export class TopLevelCreateSchemaTypes {
});
}

private getInputFields(attributes: Attribute[]): Record<string, InputTypeComposer> {
const inputFields: Array<[string, InputTypeComposer | GraphQLScalarType] | []> = filterTruthy(
private getInputFields(attributes: Attribute[]): Record<string, InputFieldDefinition> {
const inputFields: Array<[string, InputFieldDefinition] | []> = filterTruthy(
attributes.map((attribute) => {
const inputField = this.attributeToInputField(attribute.type);
const fieldDefinition: InputFieldDefinition = {
type: inputField,
defaultValue: attribute.annotations.default?.value,
};
if (inputField) {
return [attribute.name, inputField];
return [attribute.name, fieldDefinition];
}
})
);
Expand All @@ -109,7 +101,7 @@ export class TopLevelCreateSchemaTypes {
}
}

private createBuiltInFieldInput(type: ScalarType): ScalarTypeComposer | NonNullComposer<ScalarTypeComposer> {
private createBuiltInFieldInput(type: ScalarType): WrappedComposer<ScalarTypeComposer> {
let builtInType: ScalarTypeComposer;
switch (type.name) {
case GraphQLBuiltInScalarType.Boolean: {
Expand Down Expand Up @@ -146,9 +138,7 @@ export class TopLevelCreateSchemaTypes {
return builtInType;
}

private createTemporalFieldInput(
type: Neo4jTemporalType
): ScalarTypeComposer | NonNullComposer<ScalarTypeComposer> {
private createTemporalFieldInput(type: Neo4jTemporalType): WrappedComposer<ScalarTypeComposer> {
let builtInType: ScalarTypeComposer;
switch (type.name) {
case Neo4jGraphQLTemporalType.Date: {
Expand Down Expand Up @@ -185,7 +175,7 @@ export class TopLevelCreateSchemaTypes {
return builtInType;
}

private createSpatialFieldInput(type: Neo4jSpatialType): InputTypeComposer | NonNullComposer<InputTypeComposer> {
private createSpatialFieldInput(type: Neo4jSpatialType): WrappedComposer<InputTypeComposer> {
let builtInType: InputTypeComposer;
switch (type.name) {
case Neo4jGraphQLSpatialType.CartesianPoint: {
Expand Down
100 changes: 100 additions & 0 deletions packages/graphql/src/api-v6/validation/rules/valid-default.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { ASTVisitor, FieldDefinitionNode, StringValueNode } from "graphql";
import type { SDLValidationContext } from "graphql/validation/ValidationContext";
import { isSpatial, isTemporal } from "../../../constants";
import { defaultDirective } from "../../../graphql/directives";
import {
GraphQLBuiltInScalarType,
Neo4jGraphQLNumberType,
Neo4jGraphQLSpatialType,
Neo4jGraphQLTemporalType,
} from "../../../schema-model/attribute/AttributeType";
import {
assertValid,
createGraphQLError,
DocumentValidationError,
} from "../../../schema/validation/custom-rules/utils/document-validation-error";
import { getPathToNode } from "../../../schema/validation/custom-rules/utils/path-parser";
import { assertArgumentHasSameTypeAsField } from "../../../schema/validation/custom-rules/utils/same-type-argument-as-field";
import { getInnerTypeName, isArrayType } from "../../../schema/validation/custom-rules/utils/utils";

export function ValidDefault(context: SDLValidationContext): ASTVisitor {
return {
FieldDefinition(fieldDefinitionNode: FieldDefinitionNode, _key, _parent, path, ancestors) {
const { directives } = fieldDefinitionNode;
if (!directives) {
return;
}
const defaultDirectiveNode = directives.find((directive) => directive.name.value === defaultDirective.name);

if (!defaultDirectiveNode || !defaultDirectiveNode.arguments) {
return;
}
const defaultValue = defaultDirectiveNode.arguments.find((a) => a.name.value === "value");
if (!defaultValue) {
return;
}
const expectedType = getInnerTypeName(fieldDefinitionNode.type);
const { isValid, errorMsg, errorPath } = assertValid(() => {
if (!isArrayType(fieldDefinitionNode)) {
if (isSpatial(expectedType)) {
throw new DocumentValidationError(`@default is not supported by Spatial types.`, ["value"]);
} else if (isTemporal(expectedType)) {
if (Number.isNaN(Date.parse((defaultValue?.value as StringValueNode).value))) {
throw new DocumentValidationError(
`@default.${defaultValue.name.value} is not a valid ${expectedType}`,
["value"]
);
}
} else if (!isTypeABuiltInType(expectedType)) {
//TODO: Add check for user defined enums that are currently not implemented
throw new DocumentValidationError(
`@default directive can only be used on Temporal types and types: Int | Float | String | Boolean | ID | Enum`,
[]
);
}
}
assertArgumentHasSameTypeAsField({
directiveName: "@default",
traversedDef: fieldDefinitionNode,
argument: defaultValue,
enums: [],
});
});
const [pathToNode] = getPathToNode(path, ancestors);
if (!isValid) {
context.reportError(
createGraphQLError({
nodes: [fieldDefinitionNode],
path: [...pathToNode, fieldDefinitionNode.name.value, ...errorPath],
errorMsg,
})
);
}
},
};
}

export function isTypeABuiltInType(expectedType: string): boolean {
return [GraphQLBuiltInScalarType, Neo4jGraphQLNumberType, Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType].some(
(enumValue) => enumValue[expectedType] === expectedType
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { DirectiveCombinationValid } from "../../schema/validation/custom-rules/
import { WarnIfListOfListsFieldDefinition } from "../../schema/validation/custom-rules/warnings/list-of-lists";
import { validateSDL } from "../../schema/validation/validate-sdl";
import type { Neo4jFeaturesSettings } from "../../types";
import { ValidDefault } from "./rules/valid-default";
import { ValidLimit } from "./rules/valid-limit";
import { ValidRelationship } from "./rules/valid-relationship";

Expand All @@ -66,6 +67,7 @@ function runNeo4jGraphQLValidationRules({
...specifiedSDLRules,
ValidRelationship,
ValidLimit,
ValidDefault,
DirectiveCombinationValid,
ValidRelationshipProperties,
ReservedTypeNames,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { EnumTypeDefinitionNode, ArgumentNode, FieldDefinitionNode, ValueNode } from "graphql";
import type { ArgumentNode, EnumTypeDefinitionNode, FieldDefinitionNode, ValueNode } from "graphql";
import { Kind } from "graphql";
import { fromValueKind, getInnerTypeName, isArrayType } from "./utils";
import { isSpatial, isTemporal } from "../../../../constants";
import { DocumentValidationError } from "./document-validation-error";
import { fromValueKind, getInnerTypeName, isArrayType } from "./utils";

export function assertArgumentHasSameTypeAsField({
directiveName,
Expand Down Expand Up @@ -71,7 +71,7 @@ function doTypesMatch(expectedType: string, argumentValueType: ValueNode, enums:
return true;
}
if (expectedType.toLowerCase() === "id") {
return !!(fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase() === "string");
return Boolean(fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase() === "string");
}
return fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase() === expectedType.toLowerCase();
}
Loading

0 comments on commit 40d5c5d

Please sign in to comment.