/
getGraphQLAPIs.ts
169 lines (140 loc) · 5.27 KB
/
getGraphQLAPIs.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import {isMainThread, parentPort, workerData, MessagePort} from 'worker_threads'
import oneline from 'oneline'
import {isPlainObject} from 'lodash'
import type {Schema} from '@sanity/types'
import type {CliV3CommandContext, GraphQLAPIConfig} from '@sanity/cli'
import type {SchemaDefinitionish, TypeResolvedGraphQLAPI} from '../actions/graphql/types'
import {getStudioConfig} from '../util/getStudioConfig'
import {Workspace} from 'sanity'
if (isMainThread || !parentPort) {
throw new Error('This module must be run as a worker thread')
}
getGraphQLAPIsForked(parentPort)
async function getGraphQLAPIsForked(parent: MessagePort): Promise<void> {
const {cliConfig, cliConfigPath, workDir} = workerData
const resolved = await resolveGraphQLApis({cliConfig, cliConfigPath, workDir})
parent.postMessage(resolved)
}
async function resolveGraphQLApis({
cliConfig,
cliConfigPath,
workDir,
}: Pick<CliV3CommandContext, 'cliConfig' | 'cliConfigPath' | 'workDir'>): Promise<
TypeResolvedGraphQLAPI[]
> {
const workspaces = await getStudioConfig({basePath: workDir})
const numSources = workspaces.reduce(
(count, workspace) => count + workspace.unstable_sources.length,
0
)
const multiSource = numSources > 1
const multiWorkspace = workspaces.length > 1
const hasGraphQLConfig = Boolean(cliConfig?.graphql)
if (workspaces.length === 0) {
throw new Error('No studio configuration found')
}
if (numSources === 0) {
throw new Error('No sources (project ID / dataset) configured')
}
// We can only automatically configure if there is a single workspace + source in play
if ((multiWorkspace || multiSource) && !hasGraphQLConfig) {
throw new Error(oneline`
Multiple workspaces/sources configured.
You must define an array of GraphQL APIs in ${cliConfigPath || 'sanity.cli.js'}
and specify which workspace/source to use.
`)
}
// No config is defined, but we have a single workspace + source, so use that
if (!hasGraphQLConfig) {
const {projectId, dataset, schema} = workspaces[0].unstable_sources[0]
return [{schemaTypes: getStrippedSchemaTypes(schema), projectId, dataset}]
}
// Explicity defined config
const apiDefs = validateCliConfig(cliConfig?.graphql || [])
return resolveGraphQLAPIsFromConfig(apiDefs, workspaces)
}
function resolveGraphQLAPIsFromConfig(
apiDefs: GraphQLAPIConfig[],
workspaces: Workspace[]
): TypeResolvedGraphQLAPI[] {
const resolvedApis: TypeResolvedGraphQLAPI[] = []
for (const apiDef of apiDefs) {
const {workspace: workspaceName, source: sourceName} = apiDef
if (!workspaceName && workspaces.length > 1) {
throw new Error(
'Must define `workspace` name in GraphQL API config when multiple workspaces are defined'
)
}
// If we only have a single workspace defined, we can assume that is the intended one,
// even if no `workspace` is defined for the GraphQL API
const workspace =
!workspaceName && workspaces.length === 1
? workspaces[0]
: workspaces.find((space) => space.name === (workspaceName || 'default'))
if (!workspace) {
throw new Error(`Workspace "${workspaceName || 'default'}" not found`)
}
// If we only have a single source defined, we can assume that is the intended one,
// even if no `source` is defined for the GraphQL API
const source =
!sourceName && workspace.unstable_sources.length === 1
? workspace.unstable_sources[0]
: workspace.unstable_sources.find((src) => src.name === (sourceName || 'default'))
if (!source) {
throw new Error(
`Source "${sourceName || 'default'}" not found in workspace "${workspaceName || 'default'}"`
)
}
resolvedApis.push({
...apiDef,
dataset: source.dataset,
projectId: source.projectId,
schemaTypes: getStrippedSchemaTypes(source.schema),
})
}
return resolvedApis
}
function validateCliConfig(
config: GraphQLAPIConfig[],
configPath = 'sanity.cli.js'
): GraphQLAPIConfig[] {
if (!Array.isArray(config)) {
throw new Error(`"graphql" key in "${configPath}" must be an array if defined`)
}
if (config.length === 0) {
throw new Error(`No GraphQL APIs defined in "${configPath}"`)
}
return config
}
function getStrippedSchemaTypes(schema: Schema): SchemaDefinitionish[] {
const schemaDef = schema._original || {types: []}
return schemaDef.types.map((type) => stripType(type))
}
function stripType(input: unknown): SchemaDefinitionish {
return strip(input) as SchemaDefinitionish
}
function strip(input: unknown): unknown {
if (Array.isArray(input)) {
return input.map((item) => strip(item)).filter((item) => typeof item !== 'undefined')
}
if (isPlainishObject(input)) {
return Object.keys(input).reduce((stripped, key) => {
stripped[key] = strip(input[key])
return stripped
}, {} as Record<string, unknown>)
}
return isBasicType(input) ? input : undefined
}
function isPlainishObject(input: unknown): input is Record<string, unknown> {
return isPlainObject(input)
}
function isBasicType(input: unknown): boolean {
const type = typeof input
if (type === 'boolean' || type === 'number' || type === 'string') {
return true
}
if (type !== 'object') {
return false
}
return Array.isArray(input) || input === null || isPlainishObject(input)
}