Skip to content

Commit 258ff53

Browse files
authored
Fix @default so it works for temporal types and BigInt fields (#5865)
* Fix `@default` so it works for temporal types and `BigInt` fields * Fix tests * Address PR comments * Fix tests
1 parent 31f298a commit 258ff53

File tree

6 files changed

+560
-74
lines changed

6 files changed

+560
-74
lines changed

.changeset/gold-days-fly.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@neo4j/graphql": patch
3+
---
4+
5+
`@default` directive fixed to work as expected on fields of temporal type, and `BigInt` fields

packages/graphql/src/schema/get-obj-field-meta.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -432,15 +432,21 @@ function getObjFieldMeta({
432432
}
433433
primitiveField.defaultValue = parseInt(value.value, 10);
434434
break;
435+
case "BigInt":
436+
if (value?.kind !== Kind.INT && value?.kind !== Kind.STRING) {
437+
throw new Error(typeError);
438+
}
439+
primitiveField.defaultValue = parseInt(value.value, 10);
440+
break;
435441
case "Float":
436-
if (value?.kind !== Kind.FLOAT) {
442+
if (value?.kind !== Kind.FLOAT && value?.kind !== Kind.INT) {
437443
throw new Error(typeError);
438444
}
439445
primitiveField.defaultValue = parseFloat(value.value);
440446
break;
441447
default:
442448
throw new Error(
443-
"@default directive can only be used on types: Int | Float | String | Boolean | ID | DateTime | Enum"
449+
"@default directive can only be used on fields of type Int, Float, String, Boolean, ID, BigInt, DateTime, Date, Time, LocalDateTime or LocalTime."
444450
);
445451
}
446452
}

packages/graphql/src/schema/validation/custom-rules/directives/default.ts

+29-9
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@
1616
* See the License for the specific language governing permissions and
1717
* limitations under the License.
1818
*/
19-
import type { DirectiveNode, FieldDefinitionNode, EnumTypeDefinitionNode, StringValueNode } from "graphql";
19+
import type { DirectiveNode, EnumTypeDefinitionNode, FieldDefinitionNode, StringValueNode } from "graphql";
2020
import { Kind } from "graphql";
21-
import { assertArgumentHasSameTypeAsField } from "../utils/same-type-argument-as-field";
22-
import { getInnerTypeName, isArrayType } from "../utils/utils";
21+
import { GRAPHQL_BUILTIN_SCALAR_TYPES } from "../../../../constants";
22+
import { GraphQLDate, GraphQLDateTime, GraphQLLocalDateTime } from "../../../../graphql/scalars";
23+
import { GraphQLLocalTime, parseLocalTime } from "../../../../graphql/scalars/LocalTime";
24+
import { GraphQLTime, parseTime } from "../../../../graphql/scalars/Time";
2325
import { DocumentValidationError } from "../utils/document-validation-error";
24-
import { GRAPHQL_BUILTIN_SCALAR_TYPES, isSpatial, isTemporal } from "../../../../constants";
2526
import type { ObjectOrInterfaceWithExtensions } from "../utils/path-parser";
27+
import { assertArgumentHasSameTypeAsField } from "../utils/same-type-argument-as-field";
28+
import { getInnerTypeName, isArrayType } from "../utils/utils";
2629

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

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

5053
if (!isArrayType(traversedDef)) {
51-
if (isSpatial(expectedType)) {
52-
throw new DocumentValidationError(`@default is not supported by Spatial types.`, ["value"]);
53-
} else if (isTemporal(expectedType)) {
54+
if ([GraphQLDateTime.name, GraphQLLocalDateTime.name, GraphQLDate.name].includes(expectedType)) {
5455
if (Number.isNaN(Date.parse((defaultArg?.value as StringValueNode).value))) {
5556
throw new DocumentValidationError(
5657
`@default.${defaultArg.name.value} is not a valid ${expectedType}`,
5758
["value"]
5859
);
5960
}
61+
} else if (expectedType === GraphQLTime.name) {
62+
try {
63+
parseTime((defaultArg?.value as StringValueNode).value);
64+
} catch {
65+
throw new DocumentValidationError(
66+
`@default.${defaultArg.name.value} is not a valid ${expectedType}`,
67+
["value"]
68+
);
69+
}
70+
} else if (expectedType === GraphQLLocalTime.name) {
71+
try {
72+
parseLocalTime((defaultArg?.value as StringValueNode).value);
73+
} catch {
74+
throw new DocumentValidationError(
75+
`@default.${defaultArg.name.value} is not a valid ${expectedType}`,
76+
["value"]
77+
);
78+
}
6079
} else if (
6180
!GRAPHQL_BUILTIN_SCALAR_TYPES.includes(expectedType) &&
62-
!enums.some((x) => x.name.value === expectedType)
81+
!enums.some((x) => x.name.value === expectedType) &&
82+
expectedType !== "BigInt"
6383
) {
6484
throw new DocumentValidationError(
65-
`@default directive can only be used on Temporal types and types: Int | Float | String | Boolean | ID | Enum`,
85+
`@default directive can only be used on fields of type Int, Float, String, Boolean, ID, BigInt, DateTime, Date, Time, LocalDateTime or LocalTime.`,
6686
[]
6787
);
6888
}

packages/graphql/src/schema/validation/custom-rules/utils/same-type-argument-as-field.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
* See the License for the specific language governing permissions and
1717
* limitations under the License.
1818
*/
19-
import type { EnumTypeDefinitionNode, ArgumentNode, FieldDefinitionNode, ValueNode } from "graphql";
19+
import type { ArgumentNode, EnumTypeDefinitionNode, FieldDefinitionNode, ValueNode } from "graphql";
2020
import { Kind } from "graphql";
21-
import { fromValueKind, getInnerTypeName, isArrayType } from "./utils";
2221
import { isSpatial, isTemporal } from "../../../../constants";
2322
import { DocumentValidationError } from "./document-validation-error";
23+
import { fromValueKind, getInnerTypeName, isArrayType } from "./utils";
2424

2525
export function assertArgumentHasSameTypeAsField({
2626
directiveName,
@@ -73,5 +73,14 @@ function doTypesMatch(expectedType: string, argumentValueType: ValueNode, enums:
7373
if (expectedType.toLowerCase() === "id") {
7474
return !!(fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase() === "string");
7575
}
76+
if (expectedType.toLowerCase() === "bigint") {
77+
const kind = fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase();
78+
return !!(kind == "int" || kind == "string");
79+
}
80+
81+
if (expectedType.toLowerCase() === "float") {
82+
const kind = fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase();
83+
return !!(kind == "int" || kind == "float");
84+
}
7685
return fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase() === expectedType.toLowerCase();
7786
}

packages/graphql/src/schema/validation/validate-document.test.ts

+97-30
Original file line numberDiff line numberDiff line change
@@ -1198,7 +1198,7 @@ describe("validation 2.0", () => {
11981198
expect(errors[0]).toHaveProperty("path", ["User", "updatedAt", "@default", "value"]);
11991199
});
12001200

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

1218+
test("@default on LocalDateTime must be valid, check with valid value", () => {
1219+
const doc = gql`
1220+
type User @node {
1221+
updatedAt: LocalDateTime @default(value: "2023-07-06T09:45:11.336")
1222+
}
1223+
`;
1224+
1225+
const executeValidate = () =>
1226+
validateDocument({
1227+
document: doc,
1228+
additionalDefinitions,
1229+
features: {},
1230+
});
1231+
1232+
expect(executeValidate).not.toThrow();
1233+
});
1234+
1235+
test("@default on Time must be valid, check with valid value", () => {
1236+
const doc = gql`
1237+
type User @node {
1238+
updatedAt: Time @default(value: "09:45:11.336Z")
1239+
}
1240+
`;
1241+
1242+
const executeValidate = () =>
1243+
validateDocument({
1244+
document: doc,
1245+
additionalDefinitions,
1246+
features: {},
1247+
});
1248+
1249+
expect(executeValidate).not.toThrow();
1250+
});
1251+
1252+
test("@default on LocalTime must be valid, check with valid value", () => {
1253+
const doc = gql`
1254+
type User @node {
1255+
updatedAt: LocalTime @default(value: "09:45:11.336")
1256+
}
1257+
`;
1258+
1259+
const executeValidate = () =>
1260+
validateDocument({
1261+
document: doc,
1262+
additionalDefinitions,
1263+
features: {},
1264+
});
1265+
1266+
expect(executeValidate).not.toThrow();
1267+
});
1268+
1269+
test("@default on Date must be valid, check with valid value", () => {
1270+
const doc = gql`
1271+
type User @node {
1272+
updatedAt: Date @default(value: "2023-07-06")
1273+
}
1274+
`;
1275+
1276+
const executeValidate = () =>
1277+
validateDocument({
1278+
document: doc,
1279+
additionalDefinitions,
1280+
features: {},
1281+
});
1282+
1283+
expect(executeValidate).not.toThrow();
1284+
});
1285+
1286+
test("@default on BigInt must be valid, check with valid value", () => {
1287+
const doc = gql`
1288+
type User @node {
1289+
bigintnumber: BigInt @default(value: 0)
1290+
bigintstring: BigInt @default(value: "0")
1291+
}
1292+
`;
1293+
1294+
const executeValidate = () =>
1295+
validateDocument({
1296+
document: doc,
1297+
additionalDefinitions,
1298+
features: {},
1299+
});
1300+
1301+
expect(executeValidate).not.toThrow();
1302+
});
1303+
12181304
test("@default on enum must be enum", () => {
12191305
const enumTypes = gql`
12201306
enum Status {
@@ -1460,11 +1546,7 @@ describe("validation 2.0", () => {
14601546
features: {},
14611547
});
14621548

1463-
const errors = getError(executeValidate);
1464-
expect(errors).toHaveLength(1);
1465-
expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError);
1466-
expect(errors[0]).toHaveProperty("message", "@default.value on Float fields must be of type Float");
1467-
expect(errors[0]).toHaveProperty("path", ["User", "avg", "@default", "value"]);
1549+
expect(executeValidate).not.toThrow();
14681550
});
14691551

14701552
test("@default on float must be float correct", () => {
@@ -1498,14 +1580,7 @@ describe("validation 2.0", () => {
14981580
features: {},
14991581
});
15001582

1501-
const errors = getError(executeValidate);
1502-
expect(errors).toHaveLength(1);
1503-
expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError);
1504-
expect(errors[0]).toHaveProperty(
1505-
"message",
1506-
"@default.value on Float list fields must be a list of Float values"
1507-
);
1508-
expect(errors[0]).toHaveProperty("path", ["User", "avgs", "@default", "value"]);
1583+
expect(executeValidate).not.toThrow();
15091584
});
15101585

15111586
test("@default on float list must be list of float values correct", () => {
@@ -1779,8 +1854,11 @@ describe("validation 2.0", () => {
17791854
const errors = getError(executeValidate);
17801855
expect(errors).toHaveLength(1);
17811856
expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError);
1782-
expect(errors[0]).toHaveProperty("message", "@default is not supported by Spatial types.");
1783-
expect(errors[0]).toHaveProperty("path", ["User", "updatedAt", "@default", "value"]);
1857+
expect(errors[0]).toHaveProperty(
1858+
"message",
1859+
"@default directive can only be used on fields of type Int, Float, String, Boolean, ID, BigInt, DateTime, Date, Time, LocalDateTime or LocalTime."
1860+
);
1861+
expect(errors[0]).toHaveProperty("path", ["User", "updatedAt", "@default"]);
17841862
});
17851863

17861864
test("@default only supported on scalar types", () => {
@@ -1805,7 +1883,7 @@ describe("validation 2.0", () => {
18051883
expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError);
18061884
expect(errors[0]).toHaveProperty(
18071885
"message",
1808-
"@default directive can only be used on Temporal types and types: Int | Float | String | Boolean | ID | Enum"
1886+
"@default directive can only be used on fields of type Int, Float, String, Boolean, ID, BigInt, DateTime, Date, Time, LocalDateTime or LocalTime."
18091887
);
18101888
expect(errors[0]).toHaveProperty("path", ["User", "post", "@default"]);
18111889
});
@@ -2092,11 +2170,7 @@ describe("validation 2.0", () => {
20922170
features: {},
20932171
});
20942172

2095-
const errors = getError(executeValidate);
2096-
expect(errors).toHaveLength(1);
2097-
expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError);
2098-
expect(errors[0]).toHaveProperty("message", "@coalesce.value on Float fields must be of type Float");
2099-
expect(errors[0]).toHaveProperty("path", ["User", "avg", "@coalesce", "value"]);
2173+
expect(executeValidate).not.toThrow();
21002174
});
21012175

21022176
test("@coalesce on float must be float correct", () => {
@@ -2130,14 +2204,7 @@ describe("validation 2.0", () => {
21302204
features: {},
21312205
});
21322206

2133-
const errors = getError(executeValidate);
2134-
expect(errors).toHaveLength(1);
2135-
expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError);
2136-
expect(errors[0]).toHaveProperty(
2137-
"message",
2138-
"@coalesce.value on Float list fields must be a list of Float values"
2139-
);
2140-
expect(errors[0]).toHaveProperty("path", ["User", "avgs", "@coalesce", "value"]);
2207+
expect(executeValidate).not.toThrow();
21412208
});
21422209

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

0 commit comments

Comments
 (0)