|
import { DocumentNode } from 'graphql'; |
|
import gql from 'graphql-tag'; |
|
|
|
import { graphqlClient, withErrorHandlingAsync } from './GraphqlClient'; |
|
|
|
// utility types |
|
type ArrayElement<T> = T extends (infer R)[] ? R : T; |
|
type MakeArray<Test, Elem> = Test extends any[] ? Elem[] : Elem; |
|
|
|
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void |
|
? I |
|
: never; |
|
|
|
// Query fragment |
|
type Fragment<Entity, T> = { |
|
__type: T; //utility, only for holding type, value doesn't matter |
|
name: string; |
|
query: string; |
|
}; |
|
|
|
// Types directly related to QueryBuilder |
|
type BuiltFragments = { |
|
definitions: string; |
|
fields: string; |
|
}; |
|
|
|
type ExecuteFn<Entity, T> = (built: BuiltFragments) => Promise<MakeArray<Entity, T>>; |
|
|
|
interface QueryBuilderAutoConfig { |
|
/** |
|
* A function that should query string for GraphQL. |
|
* |
|
* @param ({ fields }) - generated selection set |
|
* |
|
* @example |
|
* query: ({ fields }) => `{ currentUser { ${fields} } }` |
|
*/ |
|
query: (built: BuiltFragments) => string | DocumentNode; |
|
|
|
/** |
|
* A resolver function for mapping fetched data. |
|
* It's dependant on the query we've written |
|
* |
|
* @example |
|
* query: ({ fields}) => `{ builds { byId(...) { ${fields} } } }` |
|
* resolve: data => data.builds.byId |
|
*/ |
|
resolve: (data: any) => any; |
|
} |
|
|
|
interface QueryBuilderExecuteFnConfig<Entity, T> { |
|
/** |
|
* Instead of specifying `query` and `resolve`, we can provide whole execute function |
|
* This can be used when we want to make a custom query |
|
* |
|
* @example |
|
* executeFn: async ({ definitions, fields}) => { |
|
* const { data } = await graphqlClient.query(gql( |
|
* `${definitions} |
|
* { |
|
* xyz { byId(id: "${id}") { ${fields} } } |
|
* }` |
|
* )).toPromise(); |
|
* return data.xyz.byId; |
|
* } |
|
*/ |
|
executeFn: ExecuteFn<Entity, T>; |
|
} |
|
|
|
type QueryBuilderConfig<Entity, T> = |
|
| QueryBuilderExecuteFnConfig<Entity, T> |
|
| QueryBuilderAutoConfig; |
|
|
|
function configHasExecuteFn<T, U>( |
|
config: QueryBuilderConfig<T, U> |
|
): config is QueryBuilderExecuteFnConfig<T, U> { |
|
return 'executeFn' in config; |
|
} |
|
|
|
/** |
|
* Type based query builder for GraphQL. Besides TypeScript magic, its job is to |
|
* collect desired fields, then build graphql query fragments |
|
* and provide them to execute function provided by user. |
|
* |
|
* All these fancy TypeScript declarations are needed to provide proper |
|
* return type for execute function, based on data user wants to query |
|
*/ |
|
export class QueryBuilder<Entity, T = object> { |
|
private fragments: Fragment<Entity, T>[]; |
|
private executeFn: ExecuteFn<Entity, T>; |
|
|
|
constructor(private entityName: string, config: QueryBuilderConfig<Entity, T>) { |
|
this.fragments = []; |
|
this.executeFn = configHasExecuteFn(config) ? config.executeFn : this.createExecuteFn(config); |
|
} |
|
|
|
withFragments<U extends Fragment<ArrayElement<Entity>, unknown>[]>( |
|
...fragments: U |
|
): QueryBuilder<Entity, T & UnionToIntersection<typeof fragments[number]['__type']>> { |
|
this.fragments = [...this.fragments, ...fragments] as Fragment<ArrayElement<Entity>, T>[]; |
|
return (this as unknown) as QueryBuilder< |
|
Entity, |
|
T & UnionToIntersection<typeof fragments[number]['__type']> |
|
>; |
|
} |
|
|
|
/** |
|
* Alias for `withFragments` |
|
*/ |
|
withEdges<U extends Fragment<ArrayElement<Entity>, unknown>[]>(...edges: U) { |
|
return this.withFragments(...edges); |
|
} |
|
|
|
withFields<U extends keyof ArrayElement<Entity>>( |
|
...keys: U[] |
|
): QueryBuilder<Entity, T & Pick<ArrayElement<Entity>, U>> { |
|
const frag = fragmentFromFields<ArrayElement<Entity>, U>(this.entityName, keys); |
|
|
|
this.fragments = [...this.fragments, frag] as Fragment<ArrayElement<Entity>, T>[]; |
|
return (this as unknown) as QueryBuilder<Entity, T & Pick<ArrayElement<Entity>, U>>; |
|
} |
|
|
|
async executeAsync(): Promise<MakeArray<Entity, T>> { |
|
const builtFragments = { |
|
definitions: this.buildFragmentDefinitions(), |
|
fields: this.buildQueryFields(), |
|
}; |
|
return this.executeFn(builtFragments); |
|
} |
|
|
|
private buildFragmentDefinitions(): string { |
|
return this.fragments.map(f => f.query).join('\n'); |
|
} |
|
|
|
private buildQueryFields(): string { |
|
return this.fragments |
|
.map(f => f.name) |
|
.map(it => `...${it}`) |
|
.join(', '); |
|
} |
|
|
|
private createExecuteFn(config: QueryBuilderAutoConfig): ExecuteFn<Entity, T> { |
|
return async built => { |
|
const fullQuery = gql` |
|
${built.definitions} |
|
${config.query(built)} |
|
`; |
|
const data = await withErrorHandlingAsync(graphqlClient.query(fullQuery).toPromise()); |
|
|
|
return config.resolve(data) as MakeArray<Entity, T>; |
|
}; |
|
} |
|
} |
|
|
|
/** |
|
* Creates query fragment for Entity based on its direct fields |
|
* @param entityName Name of the entity GraphQL type |
|
* @param keys list of fields to fetch from the entity |
|
* |
|
* @example |
|
* const fragment = fragmentFromFields<User, 'name' | 'age'>('User', ['name', 'age']); |
|
*/ |
|
export function fragmentFromFields<Entity, Key extends keyof Entity>( |
|
entityName: string, |
|
keys: Key[] |
|
): Fragment<Entity, Pick<Entity, Key>> { |
|
const __type = (undefined as unknown) as Pick<Entity, typeof keys[number]>; |
|
const name = keys.join('_') + 'Frag'; |
|
const query = `fragment ${name} on ${entityName} { ${keys.join(', ')} }`; |
|
return { __type, name, query }; |
|
} |
|
|
|
/** |
|
* Creates GraphQL query fragment that can be used by QueryBuilder |
|
* @param name Fragment unique name |
|
* @param query Fragment definition string |
|
* |
|
* It's also important to provide resulting TypeScript type: |
|
* @example |
|
* type ResultType = { accounts: { name: string; }[] }; |
|
* const fragment = createFragment<User, ResultType>( |
|
* 'UserAccountNameFrag', |
|
* 'fragment UserAccountNameFrag on User { accounts { name } }' |
|
* ); |
|
*/ |
|
export function createFragment<Entity, T>(name: string, query: string): Fragment<Entity, T> { |
|
return { |
|
__type: (undefined as unknown) as T, |
|
name, |
|
query, |
|
}; |
|
} |