Skip to content

Commit 834e5bf

Browse files
authored
feat: use @link directive instead of custom resolvers (#242)
We would like to add Sanity support to Netlify Connect, but the requirements for schemas are stricter than for Gatsby. This PR changes the schema to use `@link` directives instead of custom resolvers. As well as supporting Netlify Connect, having a stricter schema allows Gatsby to use performance optimisations that aren't possible with custom resolvers. As well as switching reference resolvers to `@link`, this changes union resolvers to instead add `internal.type` values to union fields. Doing this means we can remove all of the custom resolvers, apart from those used for raw fields.
1 parent 8eea726 commit 834e5bf

File tree

4 files changed

+102
-80
lines changed

4 files changed

+102
-80
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ coverage
99
public
1010
.vercel
1111
.vscode
12+
.DS_Store

src/util/getGraphQLResolverMap.ts

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import {camelCase} from 'lodash'
22
import {CreateResolversArgs} from 'gatsby'
33
import {GraphQLFieldResolver} from 'gatsby/graphql'
4-
import {SanityRef} from '../types/sanity'
5-
import {GatsbyResolverMap, GatsbyNodeModel, GatsbyGraphQLContext} from '../types/gatsby'
4+
import {GatsbyResolverMap, GatsbyGraphQLContext} from '../types/gatsby'
65
import {TypeMap, FieldDef} from './remoteGraphQLSchema'
7-
import {getTypeName, getConflictFreeFieldName} from './normalize'
86
import {resolveReferences} from './resolveReferences'
97
import {PluginConfig} from './validateConfig'
108

@@ -26,24 +24,6 @@ export function getGraphQLResolverMap(
2624
fields[field.fieldName] = {resolve: getRawResolver(field, pluginConfig, context)}
2725
return fields
2826
}, resolvers[objectType.name] || {})
29-
30-
// Add resolvers for lists, referenes and unions
31-
resolvers[objectType.name] = fieldNames
32-
.map((fieldName) => ({fieldName, ...objectType.fields[fieldName]}))
33-
.filter(
34-
(field) =>
35-
field.isList ||
36-
field.isReference ||
37-
typeMap.unions[getTypeName(field.namedType.name.value)],
38-
)
39-
.reduce((fields, field) => {
40-
const targetField = objectType.isDocument
41-
? getConflictFreeFieldName(field.fieldName)
42-
: field.fieldName
43-
44-
fields[targetField] = {resolve: getResolver(field)}
45-
return fields
46-
}, resolvers[objectType.name] || {})
4727
})
4828

4929
return resolvers
@@ -67,34 +47,3 @@ function getRawResolver(
6747
: value
6848
}
6949
}
70-
71-
function getResolver(
72-
field: FieldDef & {fieldName: string},
73-
): GraphQLFieldResolver<{[key: string]: any}, GatsbyGraphQLContext> {
74-
return (source, args, context) => {
75-
if (field.isList) {
76-
const items: SanityRef[] = source[field.fieldName] || []
77-
return items && Array.isArray(items)
78-
? items.map((item) => maybeResolveReference(item, context.nodeModel))
79-
: []
80-
}
81-
82-
const item: SanityRef | undefined = source[field.fieldName]
83-
return maybeResolveReference(item, context.nodeModel)
84-
}
85-
}
86-
87-
function maybeResolveReference(
88-
item: {_ref?: string; _type?: string; internal?: {}} | undefined,
89-
nodeModel: GatsbyNodeModel,
90-
) {
91-
if (item && typeof item._ref === 'string') {
92-
return nodeModel.getNodeById({id: item._ref})
93-
}
94-
95-
if (item && typeof item._type === 'string' && !item.internal) {
96-
return {...item, internal: {type: getTypeName(item._type)}}
97-
}
98-
99-
return item
100-
}

src/util/normalize.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Actions, NodePluginArgs} from 'gatsby'
22
import {extractWithPath} from '@sanity/mutator'
33
import {specifiedScalarTypes} from 'gatsby/graphql'
4-
import {set, startCase, camelCase, cloneDeep, upperFirst} from 'lodash'
4+
import {get, set, startCase, camelCase, cloneDeep, upperFirst} from 'lodash'
55
import {SanityDocument} from '../types/sanity'
66
import {safeId, unprefixId} from './documentIds'
77
import {TypeMap} from './remoteGraphQLSchema'
@@ -35,6 +35,9 @@ export function toGatsbyNode(doc: SanityDocument, options: ProcessingOptions): S
3535
const rawAliases = getRawAliases(doc, options)
3636
const safe = prefixConflictingKeys(doc)
3737
const withRefs = rewriteNodeReferences(safe, options)
38+
39+
addInternalTypesToUnionFields(withRefs, options)
40+
3841
const type = getTypeName(doc._type)
3942
const urlBuilder = imageUrlBuilder(options.client)
4043

@@ -149,3 +152,54 @@ function rewriteNodeReferences(doc: SanityDocument, options: ProcessingOptions)
149152

150153
return newDoc
151154
}
155+
// Adds `internal: { type: 'TheTypeName' }` to union fields nodes, to allow runtime
156+
// type resolution.
157+
function addInternalTypesToUnionFields(doc: SanityDocument, options: ProcessingOptions) {
158+
const {typeMap} = options
159+
const types = extractWithPath('..[_type]', doc)
160+
161+
const typeName = getTypeName(doc._type)
162+
const thisType = typeMap.objects[typeName]
163+
if (!thisType) {
164+
return
165+
}
166+
167+
for (const type of types) {
168+
// Not needed for references or root objects
169+
if (type.value === 'reference' || type.path.length < 2) {
170+
continue
171+
}
172+
173+
// extractWithPath returns integers to indicate array indices for list types
174+
const isListType = Number.isInteger(type.path[type.path.length - 2])
175+
176+
// For list types we need to go up an extra level to get the actual field name
177+
const parentOffset = isListType ? 3 : 2
178+
179+
const parentNode =
180+
type.path.length === parentOffset ? doc : get(doc, type.path.slice(0, -parentOffset))
181+
const parentTypeName = getTypeName(parentNode._type)
182+
const parentType = typeMap.objects[parentTypeName]
183+
184+
if (!parentType) {
185+
continue
186+
}
187+
188+
const field = parentType.fields[type.path[type.path.length - parentOffset]]
189+
190+
if (!field) {
191+
continue
192+
}
193+
194+
const fieldTypeName = getTypeName(field.namedType.name.value)
195+
196+
// All this was just to check if we're dealing with a union field
197+
if (!typeMap.unions[fieldTypeName]) {
198+
continue
199+
}
200+
const typeName = getTypeName(type.value)
201+
202+
// Add the internal type to the field
203+
set(doc, type.path.slice(0, -1).concat('internal'), {type: typeName})
204+
}
205+
}

src/util/rewriteGraphQLSchema.ts

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
DirectiveNode,
2020
ScalarTypeDefinitionNode,
2121
specifiedScalarTypes,
22+
Kind,
2223
} from 'gatsby/graphql'
2324
import {camelCase} from 'lodash'
2425
import {RESTRICTED_NODE_FIELDS, getConflictFreeFieldName} from './normalize'
@@ -92,14 +93,14 @@ function transformObjectTypeDefinition(
9293

9394
// Implement Gatsby node interface if it is a document
9495
if (isDocumentType(node)) {
95-
interfaces.push({kind: 'NamedType' as any, name: {kind: 'Name' as any, value: 'Node'}})
96+
interfaces.push({kind: Kind.NAMED_TYPE, name: {kind: Kind.NAME, value: 'Node'}})
9697
}
9798

9899
return {
99100
...node,
100101
name: {...node.name, value: getTypeName(node.name.value)},
101102
interfaces,
102-
directives: [{kind: 'Directive' as any, name: {kind: 'Name' as any, value: 'dontInfer'}}],
103+
directives: [{kind: Kind.DIRECTIVE, name: {kind: Kind.NAME, value: 'dontInfer'}}],
103104
fields: [
104105
...fields
105106
.filter((field) => !isJsonAlias(field))
@@ -122,15 +123,15 @@ function getRawFields(
122123

123124
acc.push({
124125
kind: field.kind,
125-
name: {kind: 'Name' as any, value: '_' + camelCase(`raw ${name}`)},
126-
type: {kind: 'NamedType' as any, name: {kind: 'Name' as any, value: 'JSON'}},
126+
name: {kind: Kind.NAME, value: '_' + camelCase(`raw ${name}`)},
127+
type: {kind: Kind.NAMED_TYPE, name: {kind: Kind.NAME, value: 'JSON'}},
127128
arguments: [
128129
{
129-
kind: 'InputValueDefinition' as any,
130-
name: {kind: 'Name' as any, value: 'resolveReferences'},
130+
kind: Kind.INPUT_VALUE_DEFINITION,
131+
name: {kind: Kind.NAME, value: 'resolveReferences'},
131132
type: {
132-
kind: 'NamedType' as any,
133-
name: {kind: 'Name' as any, value: 'SanityResolveReferencesConfiguration'},
133+
kind: Kind.NAMED_TYPE,
134+
name: {kind: Kind.NAME, value: 'SanityResolveReferencesConfiguration'},
134135
},
135136
},
136137
],
@@ -196,19 +197,19 @@ function isJsonAlias(field: FieldDefinitionNode): boolean {
196197

197198
function makeBlockField(name: string): FieldDefinitionNode {
198199
return {
199-
kind: 'FieldDefinition' as any,
200+
kind: Kind.FIELD_DEFINITION,
200201
name: {
201-
kind: 'Name' as any,
202+
kind: Kind.NAME,
202203
value: name,
203204
},
204205
arguments: [],
205206
directives: [],
206207
type: {
207-
kind: 'ListType' as any,
208+
kind: Kind.LIST_TYPE,
208209
type: {
209-
kind: 'NamedType' as any,
210+
kind: Kind.NAMED_TYPE,
210211
name: {
211-
kind: 'Name' as any,
212+
kind: Kind.NAME,
212213
value: 'SanityBlock',
213214
},
214215
},
@@ -224,14 +225,18 @@ function makeNullable(nodeType: TypeNode): TypeNode {
224225
if (nodeType.kind === 'ListType') {
225226
const unwrapped = maybeRewriteType(unwrapType(nodeType))
226227
return {
227-
kind: 'ListType' as any,
228+
kind: Kind.LIST_TYPE,
228229
type: makeNullable(unwrapped),
229230
}
230231
}
231232

232233
return maybeRewriteType(nodeType.type) as NamedTypeNode
233234
}
234235

236+
function isReferenceField(field: FieldDefinitionNode): boolean {
237+
return (field.directives || []).some((dir) => dir.name.value === 'reference')
238+
}
239+
235240
function transformFieldNodeAst(
236241
node: FieldDefinitionNode,
237242
parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode,
@@ -247,8 +252,22 @@ function transformFieldNodeAst(
247252

248253
if (field.type.kind === 'NamedType' && field.type.name.value === 'Date') {
249254
field.directives.push({
250-
kind: 'Directive' as any,
251-
name: {kind: 'Name' as any, value: 'dateformat'},
255+
kind: Kind.DIRECTIVE,
256+
name: {kind: Kind.NAME, value: 'dateformat'},
257+
})
258+
}
259+
260+
if (isReferenceField(node)) {
261+
field.directives.push({
262+
kind: Kind.DIRECTIVE,
263+
name: {kind: Kind.NAME, value: 'link'},
264+
arguments: [
265+
{
266+
kind: Kind.ARGUMENT,
267+
name: {kind: Kind.NAME, value: 'from'},
268+
value: {kind: Kind.STRING, value: `${field.name.value}._ref`},
269+
},
270+
],
252271
})
253272
}
254273

@@ -257,12 +276,11 @@ function transformFieldNodeAst(
257276

258277
function rewireIdType(nodeType: TypeNode): TypeNode {
259278
if (nodeType.kind === 'NamedType' && nodeType.name.value === 'ID') {
260-
return {...nodeType, name: {kind: 'Name' as any, value: 'String'}}
279+
return {...nodeType, name: {kind: Kind.NAME, value: 'String'}}
261280
}
262281

263282
return nodeType
264283
}
265-
266284
function maybeRewriteType(nodeType: TypeNode): TypeNode {
267285
const type = nodeType as NamedTypeNode
268286
if (typeof type.name === 'undefined') {
@@ -271,14 +289,14 @@ function maybeRewriteType(nodeType: TypeNode): TypeNode {
271289

272290
// Gatsby has a date type, but not a datetime, so rewire it
273291
if (type.name.value === 'DateTime') {
274-
return {...type, name: {kind: 'Name' as any, value: 'Date'}}
292+
return {...type, name: {kind: Kind.NAME, value: 'Date'}}
275293
}
276294

277295
if (builtins.includes(type.name.value)) {
278296
return type
279297
}
280298

281-
return {...type, name: {kind: 'Name' as any, value: getTypeName(type.name.value)}}
299+
return {...type, name: {kind: Kind.NAME, value: getTypeName(type.name.value)}}
282300
}
283301

284302
function maybeRewriteFieldName(
@@ -321,17 +339,17 @@ function getTypeName(name: string) {
321339

322340
function getResolveReferencesConfigType(): DefinitionNode {
323341
return {
324-
kind: 'InputObjectTypeDefinition' as any,
325-
name: {kind: 'Name' as any, value: 'SanityResolveReferencesConfiguration'},
342+
kind: Kind.INPUT_OBJECT_TYPE_DEFINITION,
343+
name: {kind: Kind.NAME, value: 'SanityResolveReferencesConfiguration'},
326344
fields: [
327345
{
328-
kind: 'InputValueDefinition' as any,
329-
name: {kind: 'Name' as any, value: 'maxDepth'},
346+
kind: Kind.INPUT_VALUE_DEFINITION,
347+
name: {kind: Kind.NAME, value: 'maxDepth'},
330348
type: {
331-
kind: 'NonNullType' as any,
332-
type: {kind: 'NamedType' as any, name: {kind: 'Name' as any, value: 'Int'}},
349+
kind: Kind.NON_NULL_TYPE,
350+
type: {kind: Kind.NAMED_TYPE, name: {kind: Kind.NAME, value: 'Int'}},
333351
},
334-
description: {kind: 'StringValue' as any, value: 'Max depth to resolve references to'},
352+
description: {kind: Kind.STRING, value: 'Max depth to resolve references to'},
335353
},
336354
],
337355
}

0 commit comments

Comments
 (0)