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

Fix @default so it works for temporal types and BigInt fields (LTS) #5866

Merged
merged 1 commit into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading