From 0edd8933d53d8f533997b52be60bc187d922f4bf Mon Sep 17 00:00:00 2001 From: Eduardo Camara Date: Wed, 6 May 2026 10:29:41 -0300 Subject: [PATCH] feat: override built-in scalars to coerce Int, Float and String inputs graphql-tools v3.x replaces schema._typeMap entries when a GraphQLScalarType instance is provided in the resolver map, safely overriding Int/Float/String without mutating the global graphql package singletons. Co-Authored-By: Claude Sonnet 4.6 --- node/resolvers/index.ts | 4 ++ node/scalars/index.ts | 86 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 node/scalars/index.ts diff --git a/node/resolvers/index.ts b/node/resolvers/index.ts index 0bb75e5a0..721003566 100644 --- a/node/resolvers/index.ts +++ b/node/resolvers/index.ts @@ -1,4 +1,5 @@ import { mutations as authMutations, queries as authQueries } from './auth' +import { CoercibleFloat, CoercibleInt, CoercibleString } from '../scalars' import { fieldResolvers as benefitsFieldResolvers, queries as benefitsQueries, @@ -39,6 +40,9 @@ import { } from './session' export const resolvers = { + Int: CoercibleInt, + Float: CoercibleFloat, + String: CoercibleString, ...catalogFieldResolvers, ...benefitsFieldResolvers, ...profileFieldResolvers, diff --git a/node/scalars/index.ts b/node/scalars/index.ts new file mode 100644 index 000000000..9ab1e70b3 --- /dev/null +++ b/node/scalars/index.ts @@ -0,0 +1,86 @@ +import { + GraphQLFloat, + GraphQLInt, + GraphQLScalarType, + GraphQLString, +} from 'graphql' +import { Kind } from 'graphql/language' + +// Replaces GraphQLInt in schema._typeMap so all Int fields accept "20" as 20. +// parseValue handles variables ({ qty: "20" }), parseLiteral handles inline values. +export const CoercibleInt = new GraphQLScalarType({ + name: 'Int', + description: GraphQLInt.description, + serialize: GraphQLInt.serialize, + parseValue: (value) => { + const parsed = parseInt(String(value), 10) + + if (isNaN(parsed)) { + throw new Error(`Int cannot represent non-integer value: ${value}`) + } + + return parsed + }, + parseLiteral: (ast) => { + if (ast.kind === Kind.INT) { + return parseInt(ast.value, 10) + } + + if (ast.kind === Kind.STRING) { + const parsed = parseInt(ast.value, 10) + + if (!isNaN(parsed)) return parsed + } + + return null + }, +}) + +// Replaces GraphQLFloat in schema._typeMap so all Float fields accept "3.14" as 3.14. +export const CoercibleFloat = new GraphQLScalarType({ + name: 'Float', + description: GraphQLFloat.description, + serialize: GraphQLFloat.serialize, + parseValue: (value) => { + const parsed = parseFloat(String(value)) + + if (isNaN(parsed)) { + throw new Error(`Float cannot represent non-numeric value: ${value}`) + } + + return parsed + }, + parseLiteral: (ast) => { + if (ast.kind === Kind.FLOAT || ast.kind === Kind.INT) { + return parseFloat(ast.value) + } + + if (ast.kind === Kind.STRING) { + const parsed = parseFloat(ast.value) + + if (!isNaN(parsed)) return parsed + } + + return null + }, +}) + +// Replaces GraphQLString in schema._typeMap so all String fields accept numbers as strings. +export const CoercibleString = new GraphQLScalarType({ + name: 'String', + description: GraphQLString.description, + serialize: GraphQLString.serialize, + parseValue: (value) => String(value), + parseLiteral: (ast) => { + if ( + ast.kind === Kind.STRING || + ast.kind === Kind.INT || + ast.kind === Kind.FLOAT || + ast.kind === Kind.BOOLEAN + ) { + return String(ast.value) + } + + return null + }, +})