Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add initial datasources design #103

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/six-pandas-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'fuse': minor
---

Re-introduce the concept of datasources
168 changes: 104 additions & 64 deletions packages/core/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>
Expand Down Expand Up @@ -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
>
Expand Down Expand Up @@ -172,7 +115,8 @@ export function node<
name: string
key?: keyof T
description?: string
load: (
datasource?: Datasource<T>
load?: (
ids: Array<string | Key>,
ctx: Record<string, unknown>,
) => Promise<Array<T | Error>>
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 []
}
},
})

Expand Down Expand Up @@ -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'
28 changes: 28 additions & 0 deletions packages/core/src/datasources/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export type Key = string | number

export interface Datasource<T extends {}> {
getMany?(ids: Key[], ctx: any): Promise<Array<T>> | Array<T>
}

/**
* 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<T> {
* getMany(ids: Key[], ctx: any): Promise<Array<T>> | Array<T> {
* // 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> | T {
* return fetch().then(x => x.json())
* }
* }
* ```
*/
export abstract class Datasource<T extends {}> {
abstract getOne(id: Key, ctx: any): Promise<T> | T
}