Skip to content

Commit 6c17907

Browse files
committed
add initial datasources design
1 parent 0543d93 commit 6c17907

File tree

3 files changed

+137
-64
lines changed

3 files changed

+137
-64
lines changed

.changeset/six-pandas-cry.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'fuse': minor
3+
---
4+
5+
Re-introduce the concept of datasources

packages/core/src/builder.ts

+104-64
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,8 @@ import DataloaderPlugin, {
2020
import { DateResolver, JSONResolver } from 'graphql-scalars'
2121
import { GraphQLParams } from 'graphql-yoga'
2222
import listPlugin from './pothos-list'
23-
export type {
24-
GetContext,
25-
InitialContext,
26-
StellateOptions,
27-
} from './utils/yoga-helpers'
23+
import { Datasource } from './datasources'
24+
import { NotFoundError } from './errors'
2825

2926
const builder = new SchemaBuilder<{
3027
Context: { request: Request; params: GraphQLParams } & Record<string, unknown>
@@ -69,60 +66,6 @@ builder.mutationType({
6966
builder.addScalarType('JSON', JSONResolver, {})
7067
builder.addScalarType('Date', DateResolver, {})
7168

72-
type Builder = Omit<
73-
typeof builder,
74-
| 'addScalarType'
75-
| 'loadableInterface'
76-
| 'loadableUnion'
77-
| 'objectType'
78-
| 'loadableInterfaceRef'
79-
| 'loadableObjectRef'
80-
| 'nodeInterfaceRef'
81-
| 'inputRef'
82-
| 'objectRef'
83-
| 'scalarType'
84-
| 'interfaceField'
85-
| 'listObject'
86-
| 'node'
87-
| 'options'
88-
| 'pageInfoRef'
89-
| 'subscriptionType'
90-
| 'queryFields'
91-
| 'queryType'
92-
| 'mutationType'
93-
| 'mutationFields'
94-
| 'connectionObject'
95-
| 'edgeObject'
96-
| 'configStore'
97-
| 'defaultFieldNullability'
98-
| 'defaultInputFieldRequiredness'
99-
| 'globalConnectionField'
100-
| 'globalConnectionFields'
101-
| 'args'
102-
| 'loadableNode'
103-
| 'loadableNodeRef'
104-
| 'interfaceFields'
105-
| 'subscriptionFields'
106-
| 'subscriptionField'
107-
| 'relayMutationField'
108-
| 'enumType'
109-
| 'inputType'
110-
| 'interfaceRef'
111-
| 'interfaceType'
112-
| 'loadableObject'
113-
| 'mutationField'
114-
| 'mutationFields'
115-
| 'objectField'
116-
| 'objectFields'
117-
| 'queryField'
118-
| 'unionType'
119-
>
120-
const reducedBuilder: Builder = builder
121-
122-
export { reducedBuilder as builder }
123-
export { decodeGlobalID, encodeGlobalID } from '@pothos/plugin-relay'
124-
export * from './errors'
125-
12669
type BuilderTypes = typeof builder extends PothosSchemaTypes.SchemaBuilder<
12770
infer T
12871
>
@@ -172,7 +115,8 @@ export function node<
172115
name: string
173116
key?: keyof T
174117
description?: string
175-
load: (
118+
datasource?: Datasource<T>
119+
load?: (
176120
ids: Array<string | Key>,
177121
ctx: Record<string, unknown>,
178122
) => Promise<Array<T | Error>>
@@ -204,6 +148,12 @@ export function node<
204148
string | number
205149
>['interfaces']
206150
}) {
151+
if (!opts.datasource && !opts.load) {
152+
throw new Error(
153+
"A node has to define either 'datasource' or 'load', to be able to load data.",
154+
)
155+
}
156+
207157
const node = builder.loadableNode(opts.name, {
208158
description: opts.description,
209159
isTypeOf: opts.isTypeOf,
@@ -237,10 +187,40 @@ export function node<
237187
return id
238188
}
239189
})
240-
const results = await opts.load(translatedIds, ctx)
241-
return results.map((result) =>
242-
result instanceof Error ? result : { ...result, __typename: opts.name },
243-
)
190+
191+
if (opts.datasource) {
192+
if (opts.datasource.getMany) {
193+
const results = await opts.datasource.getMany(translatedIds, ctx)
194+
return translatedIds.map((id) => {
195+
const found = results.find(
196+
(result) => result[(opts.key || 'id') as keyof T] === id,
197+
)
198+
if (!found) {
199+
return new NotFoundError('Could not find node.')
200+
}
201+
return found
202+
})
203+
} else {
204+
const results = await Promise.allSettled(
205+
translatedIds.map((id) => opts.datasource!.getOne(id, ctx)),
206+
)
207+
return results.map((result) =>
208+
result.status === 'rejected'
209+
? new NotFoundError(result.reason)
210+
: { ...result.value, __typename: opts.name },
211+
)
212+
}
213+
} else if (opts.load) {
214+
const results = await opts.load(translatedIds, ctx)
215+
return results.map((result) =>
216+
result instanceof Error
217+
? result
218+
: { ...result, __typename: opts.name },
219+
)
220+
} else {
221+
// Should never happen due to the check when we intialize the node
222+
return []
223+
}
244224
},
245225
})
246226

@@ -496,3 +476,63 @@ export const unionType = (
496476
const { name, ...options } = opts
497477
return builder.unionType(name, options)
498478
}
479+
480+
type Builder = Omit<
481+
typeof builder,
482+
| 'addScalarType'
483+
| 'loadableInterface'
484+
| 'loadableUnion'
485+
| 'objectType'
486+
| 'loadableInterfaceRef'
487+
| 'loadableObjectRef'
488+
| 'nodeInterfaceRef'
489+
| 'inputRef'
490+
| 'objectRef'
491+
| 'scalarType'
492+
| 'interfaceField'
493+
| 'listObject'
494+
| 'node'
495+
| 'options'
496+
| 'pageInfoRef'
497+
| 'subscriptionType'
498+
| 'queryFields'
499+
| 'queryType'
500+
| 'mutationType'
501+
| 'mutationFields'
502+
| 'connectionObject'
503+
| 'edgeObject'
504+
| 'configStore'
505+
| 'defaultFieldNullability'
506+
| 'defaultInputFieldRequiredness'
507+
| 'globalConnectionField'
508+
| 'globalConnectionFields'
509+
| 'args'
510+
| 'loadableNode'
511+
| 'loadableNodeRef'
512+
| 'interfaceFields'
513+
| 'subscriptionFields'
514+
| 'subscriptionField'
515+
| 'relayMutationField'
516+
| 'enumType'
517+
| 'inputType'
518+
| 'interfaceRef'
519+
| 'interfaceType'
520+
| 'loadableObject'
521+
| 'mutationField'
522+
| 'mutationFields'
523+
| 'objectField'
524+
| 'objectFields'
525+
| 'queryField'
526+
| 'unionType'
527+
>
528+
const reducedBuilder: Builder = builder
529+
530+
export { reducedBuilder as builder }
531+
export { decodeGlobalID, encodeGlobalID } from '@pothos/plugin-relay'
532+
export * from './errors'
533+
export { Datasource } from './datasources'
534+
export type {
535+
GetContext,
536+
InitialContext,
537+
StellateOptions,
538+
} from './utils/yoga-helpers'
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export type Key = string | number
2+
3+
export interface Datasource<T extends {}> {
4+
getMany?(ids: Key[], ctx: any): Promise<Array<T>> | Array<T>
5+
}
6+
7+
/**
8+
* A datasource is a way to fetch data from a remote source. It can be a REST API, a database, an
9+
* external third party service, etc. All that's needed is to extend the class and pass it to a node.
10+
*
11+
* @example
12+
* ```ts
13+
* class RestDatasource extends Datasource<T> {
14+
* getMany(ids: Key[], ctx: any): Promise<Array<T>> | Array<T> {
15+
* // getMany is optional, however using it has a performance benefit.
16+
* // a node will be able to resolve all the ids in a single request rather
17+
* // than n (being the amount of ids) requests.
18+
* }
19+
*
20+
* getOne(id: Key, ctx: any): Promise<T> | T {
21+
* return fetch().then(x => x.json())
22+
* }
23+
* }
24+
* ```
25+
*/
26+
export abstract class Datasource<T extends {}> {
27+
abstract getOne(id: Key, ctx: any): Promise<T> | T
28+
}

0 commit comments

Comments
 (0)