From 6c179070eab8e0605f676ffe5af535a3ad50457f Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Mon, 18 Dec 2023 07:44:23 +0100 Subject: [PATCH] add initial datasources design --- .changeset/six-pandas-cry.md | 5 + packages/core/src/builder.ts | 168 +++++++++++++++---------- packages/core/src/datasources/index.ts | 28 +++++ 3 files changed, 137 insertions(+), 64 deletions(-) create mode 100644 .changeset/six-pandas-cry.md create mode 100644 packages/core/src/datasources/index.ts diff --git a/.changeset/six-pandas-cry.md b/.changeset/six-pandas-cry.md new file mode 100644 index 00000000..95addea4 --- /dev/null +++ b/.changeset/six-pandas-cry.md @@ -0,0 +1,5 @@ +--- +'fuse': minor +--- + +Re-introduce the concept of datasources diff --git a/packages/core/src/builder.ts b/packages/core/src/builder.ts index 2d2328c8..82b2ad54 100644 --- a/packages/core/src/builder.ts +++ b/packages/core/src/builder.ts @@ -20,11 +20,8 @@ import DataloaderPlugin, { import { DateResolver, JSONResolver } from 'graphql-scalars' import { GraphQLParams } from 'graphql-yoga' import listPlugin from './pothos-list' -export type { - GetContext, - InitialContext, - StellateOptions, -} from './utils/yoga-helpers' +import { Datasource } from './datasources' +import { NotFoundError } from './errors' const builder = new SchemaBuilder<{ Context: { request: Request; params: GraphQLParams } & Record @@ -69,60 +66,6 @@ builder.mutationType({ builder.addScalarType('JSON', JSONResolver, {}) builder.addScalarType('Date', DateResolver, {}) -type Builder = Omit< - typeof builder, - | 'addScalarType' - | 'loadableInterface' - | 'loadableUnion' - | 'objectType' - | 'loadableInterfaceRef' - | 'loadableObjectRef' - | 'nodeInterfaceRef' - | 'inputRef' - | 'objectRef' - | 'scalarType' - | 'interfaceField' - | 'listObject' - | 'node' - | 'options' - | 'pageInfoRef' - | 'subscriptionType' - | 'queryFields' - | 'queryType' - | 'mutationType' - | 'mutationFields' - | 'connectionObject' - | 'edgeObject' - | 'configStore' - | 'defaultFieldNullability' - | 'defaultInputFieldRequiredness' - | 'globalConnectionField' - | 'globalConnectionFields' - | 'args' - | 'loadableNode' - | 'loadableNodeRef' - | 'interfaceFields' - | 'subscriptionFields' - | 'subscriptionField' - | 'relayMutationField' - | 'enumType' - | 'inputType' - | 'interfaceRef' - | 'interfaceType' - | 'loadableObject' - | 'mutationField' - | 'mutationFields' - | 'objectField' - | 'objectFields' - | 'queryField' - | 'unionType' -> -const reducedBuilder: Builder = builder - -export { reducedBuilder as builder } -export { decodeGlobalID, encodeGlobalID } from '@pothos/plugin-relay' -export * from './errors' - type BuilderTypes = typeof builder extends PothosSchemaTypes.SchemaBuilder< infer T > @@ -172,7 +115,8 @@ export function node< name: string key?: keyof T description?: string - load: ( + datasource?: Datasource + load?: ( ids: Array, ctx: Record, ) => Promise> @@ -204,6 +148,12 @@ export function node< string | number >['interfaces'] }) { + if (!opts.datasource && !opts.load) { + throw new Error( + "A node has to define either 'datasource' or 'load', to be able to load data.", + ) + } + const node = builder.loadableNode(opts.name, { description: opts.description, isTypeOf: opts.isTypeOf, @@ -237,10 +187,40 @@ export function node< return id } }) - const results = await opts.load(translatedIds, ctx) - return results.map((result) => - result instanceof Error ? result : { ...result, __typename: opts.name }, - ) + + if (opts.datasource) { + if (opts.datasource.getMany) { + const results = await opts.datasource.getMany(translatedIds, ctx) + return translatedIds.map((id) => { + const found = results.find( + (result) => result[(opts.key || 'id') as keyof T] === id, + ) + if (!found) { + return new NotFoundError('Could not find node.') + } + return found + }) + } else { + const results = await Promise.allSettled( + translatedIds.map((id) => opts.datasource!.getOne(id, ctx)), + ) + return results.map((result) => + result.status === 'rejected' + ? new NotFoundError(result.reason) + : { ...result.value, __typename: opts.name }, + ) + } + } else if (opts.load) { + const results = await opts.load(translatedIds, ctx) + return results.map((result) => + result instanceof Error + ? result + : { ...result, __typename: opts.name }, + ) + } else { + // Should never happen due to the check when we intialize the node + return [] + } }, }) @@ -496,3 +476,63 @@ export const unionType = ( const { name, ...options } = opts return builder.unionType(name, options) } + +type Builder = Omit< + typeof builder, + | 'addScalarType' + | 'loadableInterface' + | 'loadableUnion' + | 'objectType' + | 'loadableInterfaceRef' + | 'loadableObjectRef' + | 'nodeInterfaceRef' + | 'inputRef' + | 'objectRef' + | 'scalarType' + | 'interfaceField' + | 'listObject' + | 'node' + | 'options' + | 'pageInfoRef' + | 'subscriptionType' + | 'queryFields' + | 'queryType' + | 'mutationType' + | 'mutationFields' + | 'connectionObject' + | 'edgeObject' + | 'configStore' + | 'defaultFieldNullability' + | 'defaultInputFieldRequiredness' + | 'globalConnectionField' + | 'globalConnectionFields' + | 'args' + | 'loadableNode' + | 'loadableNodeRef' + | 'interfaceFields' + | 'subscriptionFields' + | 'subscriptionField' + | 'relayMutationField' + | 'enumType' + | 'inputType' + | 'interfaceRef' + | 'interfaceType' + | 'loadableObject' + | 'mutationField' + | 'mutationFields' + | 'objectField' + | 'objectFields' + | 'queryField' + | 'unionType' +> +const reducedBuilder: Builder = builder + +export { reducedBuilder as builder } +export { decodeGlobalID, encodeGlobalID } from '@pothos/plugin-relay' +export * from './errors' +export { Datasource } from './datasources' +export type { + GetContext, + InitialContext, + StellateOptions, +} from './utils/yoga-helpers' diff --git a/packages/core/src/datasources/index.ts b/packages/core/src/datasources/index.ts new file mode 100644 index 00000000..0202367d --- /dev/null +++ b/packages/core/src/datasources/index.ts @@ -0,0 +1,28 @@ +export type Key = string | number + +export interface Datasource { + getMany?(ids: Key[], ctx: any): Promise> | Array +} + +/** + * A datasource is a way to fetch data from a remote source. It can be a REST API, a database, an + * external third party service, etc. All that's needed is to extend the class and pass it to a node. + * + * @example + * ```ts + * class RestDatasource extends Datasource { + * getMany(ids: Key[], ctx: any): Promise> | Array { + * // getMany is optional, however using it has a performance benefit. + * // a node will be able to resolve all the ids in a single request rather + * // than n (being the amount of ids) requests. + * } + * + * getOne(id: Key, ctx: any): Promise | T { + * return fetch().then(x => x.json()) + * } + * } + * ``` + */ +export abstract class Datasource { + abstract getOne(id: Key, ctx: any): Promise | T +}