Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import isPlainObject from 'lodash/isPlainObject';
2import mapValues from 'lodash/mapValues';
3import omit from 'lodash/omit';
4
5/**
6 * In GraphQL, it's often not possible to make output types and input types
7 * totally symmetric, because output types support unions of object types (where
8 * the __typename key is a discriminator) but input types don't. Instead, for
9 * input types, the convention is to use an object type where there's one field
10 * for each possible constituent of the logical input type union, and then only
11 * the applicable field is set on input. Given these conventions, this function
12 * converts between the output type result and the input type.
13 *
14 * E.g., if the schema is:
15 *
16 * ```
17 * union X = A | B
18 * type A { hello: String! }
19 * type B { goodbye: Boolean! }
20 *
21 * input XInput { a: AInput, B: BInput }
22 * input AInput { hello: String! }
23 * input BInput { goodbye: BOolean! }
24 * ```
25 *
26 * Then calling:
27 *
28 * ```
29 * taggedUnionToOneOfInput(
30 * { __typename: 'A', hello: 'World' }, // value of union X, tagged by __typename.
31 * { A: 'a', B: 'b' } // map of the tag values ('A', and 'B') to the input keys.
32 * )
33 * ```
34 *
35 * returns `{ a: { hello: 'World' } }`
36 *
37 * This function looks for the tag key in either `__typename` (which will be the
38 * case w/ GraphQL output unions) or `type` (which some of our old output types
39 * used because they were mirroring typescript).
40 *
41 * @param taggedUnionValue
42 * @param tagValueToInputKeyMap
43 * @returns
44 */
45export function taggedUnionToOneOfInput<U extends string>(
46 taggedUnionValue: ({ type: U } | { __typename: U }) & { [k: string]: any },
47 tagValueToInputKeyMap: { [K in U]: string },
48) {
49 // We could accept this as an argument, but it's convenient to try to infer it
50 // automatically here, given how narrow our use cases are for calling this fn.
51 const tagKey = Object.hasOwn(taggedUnionValue, '__typename')
52 ? ('__typename' as keyof typeof taggedUnionValue)
53 : ('type' as keyof typeof taggedUnionValue);
54
55 const tagValue = taggedUnionValue[tagKey] as U;
56
57 const inputObjectKey = tagValueToInputKeyMap[tagValue];
58
59 return { [inputObjectKey]: omit(taggedUnionValue, tagKey) };
60}
61
62/**
63 * Apollo always adds __typename to the selection set of all queries that it
64 * issues. Sometimes, though, we want to use a query's output, let the end user
65 * modify it, and then pass data back with the same shape as a mutation's input.
66 * But because the corresponding input type for the mutation doesn't have
67 * __typename, the mutation fails. So, this helper function removes __typename
68 * recursively from a query result.
69 */
70export function stripTypename<T extends object>(it: T): WithoutTypename<T> {
71 return (
72 Array.isArray(it)
73 ? it.map(stripTypename)
74 : isPlainObject(it)
75 ? mapValues(omit(it, '__typename'), stripTypename)
76 : it
77 ) as WithoutTypename<T>;
78}
79
80export type WithoutTypename<T> = T extends (infer U)[]
81 ? WithoutTypename<U>[]
82 : T extends (...args: any[]) => any
83 ? T
84 : T extends object
85 ? Omit<{ [K in keyof T]: WithoutTypename<T[K]> }, '__typename'>
86 : T;