Last active
April 24, 2024 01:47
-
-
Save kasper573/4e82b1e89c7cfef88ee78879715920c4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { pipe, tap } from "wonka"; | |
import { Exchange } from "urql"; | |
import { | |
IntrospectionListTypeRef, | |
IntrospectionNamedTypeRef, | |
IntrospectionQuery, | |
IntrospectionType, | |
IntrospectionTypeRef, | |
Kind, | |
OperationDefinitionNode, | |
TypeNode, | |
} from "graphql"; | |
export const cleanInputExchange = ( | |
introspection: IntrospectionQuery | |
): Exchange => { | |
const introspectionTypes = new Map<string, IntrospectionType>(); | |
for (const type of introspection.__schema.types) { | |
introspectionTypes.set(type.name, type); | |
} | |
const cleanInputData = createInputCleaner(introspectionTypes); | |
return ({ forward }) => | |
(ops$) => { | |
return pipe( | |
ops$, | |
tap(({ query, variables }) => { | |
const operation = query.definitions.find( | |
(def) => def.kind === "OperationDefinition" | |
) as OperationDefinitionNode | undefined; | |
if (operation?.variableDefinitions && variables) { | |
for (const { variable, type } of operation.variableDefinitions) { | |
variables[variable.name.value] = cleanInputData( | |
variables[variable.name.value], | |
selectType(introspectionTypes, type) | |
); | |
} | |
} | |
}), | |
forward | |
); | |
}; | |
}; | |
/** | |
* Retains only the fields that are defined in the input type | |
*/ | |
function createInputCleaner( | |
introspectionTypes: Map<string, IntrospectionType> | |
) { | |
return function cleanInputData( | |
value: unknown, | |
type: IntrospectionTypeRef, | |
isRequired = false | |
): unknown { | |
switch (type.kind) { | |
case "NON_NULL": | |
return cleanInputData(value, type.ofType, true); | |
case "LIST": | |
if (Array.isArray(value)) { | |
return value.map((v) => cleanInputData(v, type.ofType)); | |
} | |
break; | |
case "INPUT_OBJECT": | |
if (isRequired && !value) { | |
break; | |
} | |
const referencedType = introspectionTypes.get(type.name); | |
if (referencedType?.kind !== "INPUT_OBJECT") { | |
break; | |
} | |
const clean: Record<string, unknown> = {}; | |
for (const prop of referencedType.inputFields) { | |
clean[prop.name] = cleanInputData( | |
(value as any)?.[prop.name], | |
prop.type | |
); | |
} | |
return clean; | |
} | |
// Either a primitive that doesn't need cleaning, | |
// or a type we don't know how to clean and we'll leave it up to the server to validate | |
return value; | |
}; | |
} | |
function selectType( | |
introspectionTypes: Map<string, IntrospectionType>, | |
typeNode: TypeNode | |
): IntrospectionTypeRef { | |
switch (typeNode.kind) { | |
case Kind.NON_NULL_TYPE: | |
return { | |
kind: "NON_NULL", | |
ofType: selectType(introspectionTypes, typeNode.type) as | |
| IntrospectionNamedTypeRef | |
| IntrospectionListTypeRef, | |
}; | |
case Kind.LIST_TYPE: | |
return { | |
kind: "LIST", | |
ofType: selectType(introspectionTypes, typeNode.type), | |
}; | |
case Kind.NAMED_TYPE: | |
const type = introspectionTypes.get(typeNode.name.value); | |
if (!type) { | |
throw new Error(`Unknown type ${typeNode.name.value}`); | |
} | |
return type; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment