Skip to content

Instantly share code, notes, and snippets.

@kasper573
Last active April 24, 2024 01:47
Show Gist options
  • Save kasper573/4e82b1e89c7cfef88ee78879715920c4 to your computer and use it in GitHub Desktop.
Save kasper573/4e82b1e89c7cfef88ee78879715920c4 to your computer and use it in GitHub Desktop.
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