UNPKG

sanity

Version:

Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches

176 lines (145 loc) • 5.48 kB
import {type CliV3CommandContext, type GraphQLAPIConfig} from '@sanity/cli' import {type Schema} from '@sanity/types' import {isPlainObject} from 'lodash' import oneline from 'oneline' import {type Workspace} from 'sanity' import {isMainThread, type MessagePort, parentPort, workerData} from 'worker_threads' import {type SchemaDefinitionish, type TypeResolvedGraphQLAPI} from '../actions/graphql/types' import {getStudioWorkspaces} from '../util/getStudioWorkspaces' 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 getStudioWorkspaces({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) }