Skip to content

Commit

Permalink
Fix @default so it works for temporal types and BigInt fields (#5865
Browse files Browse the repository at this point in the history
) (#5866)

* Fix `@default` so it works for temporal types and `BigInt` fields

* Fix tests

* Address PR comments

* Fix tests
  • Loading branch information
darrellwarde authored Dec 3, 2024
2 parents 0f16065 + 67df449 commit e4f3d9e
Show file tree
Hide file tree
Showing 6 changed files with 560 additions and 71 deletions.
5 changes: 5 additions & 0 deletions .changeset/gold-days-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/graphql": patch
---

`@default` directive fixed to work as expected on fields of temporal type, and `BigInt` fields
10 changes: 8 additions & 2 deletions packages/graphql/src/schema/get-obj-field-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,15 +432,21 @@ function getObjFieldMeta({
}
primitiveField.defaultValue = parseInt(value.value, 10);
break;
case "BigInt":
if (value?.kind !== Kind.INT && value?.kind !== Kind.STRING) {
throw new Error(typeError);
}
primitiveField.defaultValue = parseInt(value.value, 10);
break;
case "Float":
if (value?.kind !== Kind.FLOAT) {
if (value?.kind !== Kind.FLOAT && value?.kind !== Kind.INT) {
throw new Error(typeError);
}
primitiveField.defaultValue = parseFloat(value.value);
break;
default:
throw new Error(
"@default directive can only be used on types: Int | Float | String | Boolean | ID | DateTime | Enum"
"@default directive can only be used on fields of type Int, Float, String, Boolean, ID, BigInt, DateTime, Date, Time, LocalDateTime or LocalTime."
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { DirectiveNode, FieldDefinitionNode, EnumTypeDefinitionNode, StringValueNode } from "graphql";
import type { DirectiveNode, EnumTypeDefinitionNode, FieldDefinitionNode, StringValueNode } from "graphql";
import { Kind } from "graphql";
import { assertArgumentHasSameTypeAsField } from "../utils/same-type-argument-as-field";
import { getInnerTypeName, isArrayType } from "../utils/utils";
import { GRAPHQL_BUILTIN_SCALAR_TYPES } from "../../../../constants";
import { GraphQLDate, GraphQLDateTime, GraphQLLocalDateTime } from "../../../../graphql/scalars";
import { GraphQLLocalTime, parseLocalTime } from "../../../../graphql/scalars/LocalTime";
import { GraphQLTime, parseTime } from "../../../../graphql/scalars/Time";
import { DocumentValidationError } from "../utils/document-validation-error";
import { GRAPHQL_BUILTIN_SCALAR_TYPES, isSpatial, isTemporal } from "../../../../constants";
import type { ObjectOrInterfaceWithExtensions } from "../utils/path-parser";
import { assertArgumentHasSameTypeAsField } from "../utils/same-type-argument-as-field";
import { getInnerTypeName, isArrayType } from "../utils/utils";

// TODO: schema-generation: save enums as map

Expand All @@ -48,21 +51,38 @@ export function verifyDefault(enums: EnumTypeDefinitionNode[]) {
}

if (!isArrayType(traversedDef)) {
if (isSpatial(expectedType)) {
throw new DocumentValidationError(`@default is not supported by Spatial types.`, ["value"]);
} else if (isTemporal(expectedType)) {
if ([GraphQLDateTime.name, GraphQLLocalDateTime.name, GraphQLDate.name].includes(expectedType)) {
if (Number.isNaN(Date.parse((defaultArg?.value as StringValueNode).value))) {
throw new DocumentValidationError(
`@default.${defaultArg.name.value} is not a valid ${expectedType}`,
["value"]
);
}
} else if (expectedType === GraphQLTime.name) {
try {
parseTime((defaultArg?.value as StringValueNode).value);
} catch {
throw new DocumentValidationError(
`@default.${defaultArg.name.value} is not a valid ${expectedType}`,
["value"]
);
}
} else if (expectedType === GraphQLLocalTime.name) {
try {
parseLocalTime((defaultArg?.value as StringValueNode).value);
} catch {
throw new DocumentValidationError(
`@default.${defaultArg.name.value} is not a valid ${expectedType}`,
["value"]
);
}
} else if (
!GRAPHQL_BUILTIN_SCALAR_TYPES.includes(expectedType) &&
!enums.some((x) => x.name.value === expectedType)
!enums.some((x) => x.name.value === expectedType) &&
expectedType !== "BigInt"
) {
throw new DocumentValidationError(
`@default directive can only be used on Temporal types and types: Int | Float | String | Boolean | ID | Enum`,
`@default directive can only be used on fields of type Int, Float, String, Boolean, ID, BigInt, DateTime, Date, Time, LocalDateTime or LocalTime.`,
[]
);
}
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 @@ -73,5 +73,14 @@ function doTypesMatch(expectedType: string, argumentValueType: ValueNode, enums:
if (expectedType.toLowerCase() === "id") {
return !!(fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase() === "string");
}
if (expectedType.toLowerCase() === "bigint") {
const kind = fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase();
return !!(kind == "int" || kind == "string");
}

if (expectedType.toLowerCase() === "float") {
const kind = fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase();
return !!(kind == "int" || kind == "float");
}
return fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase() === expectedType.toLowerCase();
}
127 changes: 97 additions & 30 deletions packages/graphql/src/schema/validation/validate-document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1045,7 +1045,7 @@ describe("validation 2.0", () => {
expect(errors[0]).toHaveProperty("path", ["User", "updatedAt", "@default", "value"]);
});

test("@default on datetime must be valid datetime correct", () => {
test("@default on DateTime must be valid, check with valid value", () => {
const doc = gql`
type User {
updatedAt: DateTime @default(value: "2023-07-06T09:45:11.336Z")
Expand All @@ -1062,6 +1062,92 @@ describe("validation 2.0", () => {
expect(executeValidate).not.toThrow();
});

test("@default on LocalDateTime must be valid, check with valid value", () => {
const doc = gql`
type User @node {
updatedAt: LocalDateTime @default(value: "2023-07-06T09:45:11.336")
}
`;

const executeValidate = () =>
validateDocument({
document: doc,
additionalDefinitions,
features: {},
});

expect(executeValidate).not.toThrow();
});

test("@default on Time must be valid, check with valid value", () => {
const doc = gql`
type User @node {
updatedAt: Time @default(value: "09:45:11.336Z")
}
`;

const executeValidate = () =>
validateDocument({
document: doc,
additionalDefinitions,
features: {},
});

expect(executeValidate).not.toThrow();
});

test("@default on LocalTime must be valid, check with valid value", () => {
const doc = gql`
type User @node {
updatedAt: LocalTime @default(value: "09:45:11.336")
}
`;

const executeValidate = () =>
validateDocument({
document: doc,
additionalDefinitions,
features: {},
});

expect(executeValidate).not.toThrow();
});

test("@default on Date must be valid, check with valid value", () => {
const doc = gql`
type User @node {
updatedAt: Date @default(value: "2023-07-06")
}
`;

const executeValidate = () =>
validateDocument({
document: doc,
additionalDefinitions,
features: {},
});

expect(executeValidate).not.toThrow();
});

test("@default on BigInt must be valid, check with valid value", () => {
const doc = gql`
type User @node {
bigintnumber: BigInt @default(value: 0)
bigintstring: BigInt @default(value: "0")
}
`;

const executeValidate = () =>
validateDocument({
document: doc,
additionalDefinitions,
features: {},
});

expect(executeValidate).not.toThrow();
});

test("@default on enum must be enum", () => {
const enumTypes = gql`
enum Status {
Expand Down Expand Up @@ -1307,11 +1393,7 @@ describe("validation 2.0", () => {
features: {},
});

const errors = getError(executeValidate);
expect(errors).toHaveLength(1);
expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError);
expect(errors[0]).toHaveProperty("message", "@default.value on Float fields must be of type Float");
expect(errors[0]).toHaveProperty("path", ["User", "avg", "@default", "value"]);
expect(executeValidate).not.toThrow();
});

test("@default on float must be float correct", () => {
Expand Down Expand Up @@ -1345,14 +1427,7 @@ describe("validation 2.0", () => {
features: {},
});

const errors = getError(executeValidate);
expect(errors).toHaveLength(1);
expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError);
expect(errors[0]).toHaveProperty(
"message",
"@default.value on Float list fields must be a list of Float values"
);
expect(errors[0]).toHaveProperty("path", ["User", "avgs", "@default", "value"]);
expect(executeValidate).not.toThrow();
});

test("@default on float list must be list of float values correct", () => {
Expand Down Expand Up @@ -1626,8 +1701,11 @@ describe("validation 2.0", () => {
const errors = getError(executeValidate);
expect(errors).toHaveLength(1);
expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError);
expect(errors[0]).toHaveProperty("message", "@default is not supported by Spatial types.");
expect(errors[0]).toHaveProperty("path", ["User", "updatedAt", "@default", "value"]);
expect(errors[0]).toHaveProperty(
"message",
"@default directive can only be used on fields of type Int, Float, String, Boolean, ID, BigInt, DateTime, Date, Time, LocalDateTime or LocalTime."
);
expect(errors[0]).toHaveProperty("path", ["User", "updatedAt", "@default"]);
});

test("@default only supported on scalar types", () => {
Expand All @@ -1652,7 +1730,7 @@ describe("validation 2.0", () => {
expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError);
expect(errors[0]).toHaveProperty(
"message",
"@default directive can only be used on Temporal types and types: Int | Float | String | Boolean | ID | Enum"
"@default directive can only be used on fields of type Int, Float, String, Boolean, ID, BigInt, DateTime, Date, Time, LocalDateTime or LocalTime."
);
expect(errors[0]).toHaveProperty("path", ["User", "post", "@default"]);
});
Expand Down Expand Up @@ -1939,11 +2017,7 @@ describe("validation 2.0", () => {
features: {},
});

const errors = getError(executeValidate);
expect(errors).toHaveLength(1);
expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError);
expect(errors[0]).toHaveProperty("message", "@coalesce.value on Float fields must be of type Float");
expect(errors[0]).toHaveProperty("path", ["User", "avg", "@coalesce", "value"]);
expect(executeValidate).not.toThrow();
});

test("@coalesce on float must be float correct", () => {
Expand Down Expand Up @@ -1977,14 +2051,7 @@ describe("validation 2.0", () => {
features: {},
});

const errors = getError(executeValidate);
expect(errors).toHaveLength(1);
expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError);
expect(errors[0]).toHaveProperty(
"message",
"@coalesce.value on Float list fields must be a list of Float values"
);
expect(errors[0]).toHaveProperty("path", ["User", "avgs", "@coalesce", "value"]);
expect(executeValidate).not.toThrow();
});

test("@coalesce on float list must be list of float values correct", () => {
Expand Down
Loading

0 comments on commit e4f3d9e

Please sign in to comment.