Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import _ from 'lodash';
2
3const { omit } = _;
4
5/**
6 * In GraphQL, input types don't support unions in the same way output types do.
7 * Instead, for input types, the convention for representing what is logically
8 * still a tagged union is to use an object type where there's one field for
9 * each possible constituent of the union, and then only the applicable field is
10 * given a value on input data. This way of representing a union is inconsistent
11 * with how we tend to represent unions on the backend/in TS, which involves
12 * putting the discriminator as a `type` key's value within the union type's
13 * object value.
14 *
15 * Given these conventions, this function converts between the input type and
16 * our backend representations.
17 *
18 * E.g., if the schema is:
19 *
20 * ```gql
21 * input XInput { a: AInput, B: BInput }
22 * input AInput { hello: String! }
23 * input BInput { _: true }
24 * ```
25 *
26 * Then calling:
27 *
28 * ```
29 * oneOfInputToTaggedUnion(
30 * { a: { hello: 'World' } }, // value of union XInput
31 * { a: 'A', b: 'B' } // map of input keys to the final `type` values.
32 * )
33 * ```
34 *
35 * returns `{ type: 'A', hello: 'World' }`
36 *
37 * The use of an optional `_` property in the `BInput` type is a graphql
38 * convention for when one constituent of the union doesn't need any extra fields.
39 * (See https://github.com/graphql/graphql-spec/pull/825#issuecomment-1182979316).
40 * Accordingly, this function also strips off any input value field called `_`.
41 *
42 * This function uses `type` as the key for holding the tag, which we might
43 * support customizing later (though that gets tricky to express in TS).
44 *
45 * @param gqlInputValue
46 * @param inputKeyToTypeValueMap
47 * @returns
48 */
49export function oneOfInputToTaggedUnion<
50 InputValue extends Record<string, object | undefined | null>,
51 TypeValue extends string,
52 Mapping extends { [K in keyof InputValue]: TypeValue },
53>(gqlInputValue: InputValue, inputKeyToTypeValueMap: Mapping) {
54 const inputFilledEntries = Object.entries(gqlInputValue).filter(
55 ([_k, v]) => v != null,
56 ) as [keyof InputValue, InputValue][];
57
58 if (inputFilledEntries.length !== 1) {
59 throw new Error(
60 'Input object must have exactly one key with a (non-null) value.',
61 );
62 }
63
64 const [inputKey, inputValue] = inputFilledEntries[0];
65
66 return {
67 type: inputKeyToTypeValueMap[inputKey],
68 ...omit(inputValue, '_'),
69 } as unknown as Exclude<
70 {
71 [K in keyof InputValue]: { type: Mapping[K] } & Omit<
72 Exclude<InputValue[K], null | undefined>,
73 '_'
74 >;
75 }[keyof InputValue],
76 undefined
77 >;
78}