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
623 lines (515 loc) • 19.5 kB
text/typescript
import {
type AssetSource,
isSearchStrategy,
type SchemaTypeDefinition,
searchStrategies,
type SearchStrategy,
} from '@sanity/types'
import {type ErrorInfo, type ReactNode} from 'react'
import {type LocaleConfigContext, type LocaleDefinition, type LocaleResourceBundle} from '../i18n'
import {type Template, type TemplateItem} from '../templates'
import {getPrintableType} from '../util/getPrintableType'
import {
type DocumentActionComponent,
type DocumentBadgeComponent,
type DocumentInspector,
} from './document'
import {flattenConfig} from './flattenConfig'
import {
type AsyncConfigPropertyReducer,
type ConfigContext,
type ConfigPropertyReducer,
type DocumentActionsContext,
type DocumentBadgesContext,
type DocumentCommentsEnabledContext,
type DocumentInspectorContext,
type DocumentLanguageFilterComponent,
type DocumentLanguageFilterContext,
type NewDocumentOptionsContext,
type PluginOptions,
type ResolveProductionUrlContext,
type Tool,
} from './types'
export const initialDocumentBadges: DocumentBadgeComponent[] = []
export const initialDocumentActions: DocumentActionComponent[] = []
export const initialLanguageFilter: DocumentLanguageFilterComponent[] = []
export const schemaTypesReducer: ConfigPropertyReducer<
SchemaTypeDefinition[],
Omit<ConfigContext, 'schema' | 'currentUser' | 'client' | 'getClient' | 'i18n'>
> = (prev, {schema}, context) => {
const schemaTypes = schema?.types
if (!schemaTypes) return prev
if (typeof schemaTypes === 'function') return schemaTypes(prev, context)
if (Array.isArray(schemaTypes)) return [...prev, ...schemaTypes]
throw new Error(
`Expected \`schema.types\` to be an array or a function, but received ${getPrintableType(
schemaTypes,
)}`,
)
}
export const resolveProductionUrlReducer: AsyncConfigPropertyReducer<
string | undefined,
ResolveProductionUrlContext
> = async (prev, {document}, context) => {
const resolveProductionUrl = document?.productionUrl
// the redundant await is useful for error logging because the error is caught
// in this stack vs somewhere down stream
// eslint-disable-next-line no-return-await
if (resolveProductionUrl) return await resolveProductionUrl(prev, context)
return prev
}
export const toolsReducer: ConfigPropertyReducer<Tool[], ConfigContext> = (
prev,
{tools},
context,
) => {
if (!tools) return prev
if (typeof tools === 'function') return tools(prev, context)
if (Array.isArray(tools)) return [...prev, ...tools]
throw new Error(
`Expected \`tools\` to be an array or a function, but received ${getPrintableType(tools)}`,
)
}
// we will need this when we ressurect user config for search
/*export const searchFilterReducer: ConfigPropertyReducer<
SearchFilterDefinition<string>[],
ConfigContext
> = (prev, {search}, context) => {
const filters = search?.filters
if (!filters) return prev
if (typeof filters === 'function') return filters(prev, context)
if (Array.isArray(filters)) return [...prev, ...filters]
throw new Error(
`Expected \`search.filters\` to be an array or a function, but received ${typeof filters}`
)
}
export const searchOperatorsReducer: ConfigPropertyReducer<
SearchOperatorDefinition[],
ConfigContext
> = (prev, {search}, context) => {
const operators = search?.operators
if (!operators) return prev
if (typeof operators === 'function') return operators(prev, context)
if (Array.isArray(operators)) return [...prev, ...operators]
throw new Error(
`Expected \`operators\` to be be an array or a function, but received ${getPrintableType(operators)}`
)
}*/
export const schemaTemplatesReducer: ConfigPropertyReducer<Template[], ConfigContext> = (
prev,
{schema},
context,
) => {
const schemaTemplates = schema?.templates
if (!schemaTemplates) return prev
if (typeof schemaTemplates === 'function') return schemaTemplates(prev, context)
if (Array.isArray(schemaTemplates)) return [...prev, ...schemaTemplates]
throw new Error(
`Expected \`schema.templates\` to be an array or a function, but received ${getPrintableType(
schemaTemplates,
)}`,
)
}
export const localeDefReducer: ConfigPropertyReducer<LocaleDefinition[], LocaleConfigContext> = (
prev,
{i18n},
context,
) => {
const locales = i18n?.locales
if (!locales) return prev
if (typeof locales === 'function') return locales(prev, context)
if (Array.isArray(locales)) return [...prev, ...locales]
throw new Error(
`Expected \`i18n.locales\` to be an array or a function, but received ${getPrintableType(
locales,
)}`,
)
}
export const localeBundlesReducer: ConfigPropertyReducer<
LocaleResourceBundle[],
LocaleConfigContext
> = (prev, {i18n}, context) => {
const bundles = i18n?.bundles
if (!bundles) return prev
if (Array.isArray(bundles)) return [...prev, ...bundles]
if (typeof bundles === 'function') return bundles(prev, context)
throw new Error(
`Expected \`i18n.bundles\` to be an array or a function, but received ${typeof bundles}`,
)
}
export const documentBadgesReducer: ConfigPropertyReducer<
DocumentBadgeComponent[],
DocumentBadgesContext
> = (prev, {document}, context) => {
const documentBadges = document?.badges
if (!documentBadges) return prev
if (typeof documentBadges === 'function') return documentBadges(prev, context)
if (Array.isArray(documentBadges)) return [...prev, ...documentBadges]
throw new Error(
`Expected \`document.badges\` to be an array or a function, but received ${getPrintableType(
documentBadges,
)}`,
)
}
export const documentActionsReducer: ConfigPropertyReducer<
DocumentActionComponent[],
DocumentActionsContext
> = (prev, {document}, context) => {
const documentActions = document?.actions
if (!documentActions) return prev
if (typeof documentActions === 'function') return documentActions(prev, context)
if (Array.isArray(documentActions)) return [...prev, ...documentActions]
throw new Error(
`Expected \`document.actions\` to be an array or a function, but received ${getPrintableType(
documentActions,
)}`,
)
}
export const newDocumentOptionsResolver: ConfigPropertyReducer<
TemplateItem[],
NewDocumentOptionsContext
> = (prev, {document}, context) => {
const resolveNewDocumentOptions = document?.newDocumentOptions
if (!resolveNewDocumentOptions) return prev
if (typeof resolveNewDocumentOptions !== 'function') {
throw new Error(
`Expected \`document.resolveNewDocumentOptions\` to be a function, but received ${getPrintableType(
resolveNewDocumentOptions,
)}`,
)
}
return resolveNewDocumentOptions(prev, context)
}
export const fileAssetSourceResolver: ConfigPropertyReducer<AssetSource[], ConfigContext> = (
prev,
{form},
context,
) => {
const assetSources = form?.file?.assetSources
if (!assetSources) return prev
if (typeof assetSources === 'function') return assetSources(prev, context)
if (Array.isArray(assetSources)) return [...prev, ...assetSources]
throw new Error(
`Expected \`form.file.assetSources\` to be an array or a function, but received ${getPrintableType(
assetSources,
)}`,
)
}
export const imageAssetSourceResolver: ConfigPropertyReducer<AssetSource[], ConfigContext> = (
prev,
{form},
context,
) => {
const assetSources = form?.image?.assetSources
if (!assetSources) return prev
if (typeof assetSources === 'function') return assetSources(prev, context)
if (Array.isArray(assetSources)) return [...prev, ...assetSources]
throw new Error(
`Expected \`form.image.assetSources\` to be an array or a function, but received ${getPrintableType(
assetSources,
)}`,
)
}
/**
* @internal
*/
export const documentLanguageFilterReducer: ConfigPropertyReducer<
DocumentLanguageFilterComponent[],
DocumentLanguageFilterContext
> = (prev, {document}, context) => {
const resolveDocumentLanguageFilter = document?.unstable_languageFilter
if (!resolveDocumentLanguageFilter) return prev
if (typeof resolveDocumentLanguageFilter === 'function')
return resolveDocumentLanguageFilter(prev, context)
if (Array.isArray(resolveDocumentLanguageFilter))
return [...prev, ...resolveDocumentLanguageFilter]
throw new Error(
`Expected \`document.unstable_languageFilter\` to be an array or a function, but received ${getPrintableType(
resolveDocumentLanguageFilter,
)}`,
)
}
export const documentInspectorsReducer: ConfigPropertyReducer<
DocumentInspector[],
DocumentInspectorContext
> = (prev, {document}, context) => {
const resolveInspectorsFilter = document?.inspectors
if (!resolveInspectorsFilter) return prev
if (typeof resolveInspectorsFilter === 'function') return resolveInspectorsFilter(prev, context)
if (Array.isArray(resolveInspectorsFilter)) return [...prev, ...resolveInspectorsFilter]
throw new Error(
`Expected \`document.inspectors\` to be an array or a function, but received ${getPrintableType(
resolveInspectorsFilter,
)}`,
)
}
export const documentCommentsEnabledReducer = (opts: {
config: PluginOptions
context: DocumentCommentsEnabledContext
initialValue: boolean
}): boolean => {
const {config, context, initialValue} = opts
const flattenedConfig = flattenConfig(config, [])
// There is no concept of 'previous value' in this API. We only care about the final value.
// That is, if a plugin returns true, but the next plugin returns false, the result will be false.
// The last plugin 'wins'.
const result = flattenedConfig.reduce((acc, {config: innerConfig}) => {
const resolver =
innerConfig.document?.comments?.enabled ?? innerConfig.document?.unstable_comments?.enabled
if (!resolver && typeof resolver !== 'boolean') return acc
if (typeof resolver === 'function') return resolver(context)
if (typeof resolver === 'boolean') return resolver
throw new Error(
`Expected \`document.comments.enabled\` to be a boolean or a function, but received ${getPrintableType(
resolver,
)}`,
)
}, initialValue)
return result
}
export const onUncaughtErrorResolver = (opts: {
config: PluginOptions
context: {error: Error; errorInfo: ErrorInfo}
}) => {
const {config, context} = opts
const flattenedConfig = flattenConfig(config, [])
flattenedConfig.forEach(({config: pluginConfig}) => {
// There is no concept of 'previous value' in this API. We only care about the final value.
// That is, if a plugin returns true, but the next plugin returns false, the result will be false.
// The last plugin 'wins'.
const resolver = pluginConfig.onUncaughtError
if (typeof resolver === 'function') return resolver(context.error, context.errorInfo)
if (!resolver) return undefined
throw new Error(
`Expected \`document.onUncaughtError\` to be a a function, but received ${getPrintableType(
resolver,
)}`,
)
})
}
export const internalTasksReducer = (opts: {
config: PluginOptions
}): {footerAction: ReactNode} | undefined => {
const {config} = opts
const flattenedConfig = flattenConfig(config, [])
const result = flattenedConfig.reduce(
(acc: {footerAction: ReactNode} | undefined, {config: innerConfig}) => {
const resolver = innerConfig.__internal_tasks
if (!resolver) return acc
if (typeof resolver === 'object' && resolver.footerAction) return resolver
throw new Error(
`Expected \`__internal__tasks\` to be an object with footerAction, but received ${getPrintableType(
resolver,
)}`,
)
},
undefined,
)
return result
}
export const eventsAPIReducer = (opts: {
config: PluginOptions
initialValue: boolean
key: 'releases' | 'documents'
}): boolean => {
const {config, initialValue} = opts
const flattenedConfig = flattenConfig(config, [])
const result = flattenedConfig.reduce((acc: boolean, {config: innerConfig}) => {
// @ts-expect-error enabled is a legacy option we want to warn beta testers in case they have enabled it.
if (innerConfig.beta?.eventsAPI?.enabled) {
throw new Error(
`The \`beta.eventsAPI.enabled\` option has been removed. Use \`beta.eventsAPI.${opts.key}\` instead.`,
)
}
const enabled = innerConfig.beta?.eventsAPI?.[opts.key]
if (typeof enabled === 'undefined') return acc
if (typeof enabled === 'boolean') return enabled
throw new Error(
`Expected \`beta.eventsAPI.${opts.key}\` to be a boolean, but received ${getPrintableType(
enabled,
)}`,
)
}, initialValue)
return result
}
export const mediaLibraryEnabledReducer = (opts: {
config: PluginOptions
initialValue: boolean
}): boolean => {
const {config, initialValue} = opts
const flattenedConfig = flattenConfig(config, [])
const result = flattenedConfig.reduce((acc, {config: innerConfig}) => {
const resolver = innerConfig.mediaLibrary?.enabled
if (!resolver && typeof resolver !== 'boolean') return acc
if (typeof resolver === 'boolean') return resolver
throw new Error(
`Expected \`mediaLibrary.enabled\` to be a boolean, but received ${getPrintableType(
resolver,
)}`,
)
}, initialValue)
return result
}
export const mediaLibraryLibraryIdReducer = (opts: {
config: PluginOptions
initialValue: string | undefined
}): string | undefined => {
const {config, initialValue} = opts
const flattenedConfig = flattenConfig(config, [])
const result = flattenedConfig.reduce((acc, {config: innerConfig}) => {
const resolver = innerConfig.mediaLibrary?.libraryId
if (!resolver && typeof resolver !== 'string') return acc
if (typeof resolver === 'string') return resolver
throw new Error(
`Expected \`mediaLibrary.libraryId\` to be a string, but received ${getPrintableType(
resolver,
)}`,
)
}, initialValue)
return result
}
export const serverDocumentActionsReducer = (opts: {
config: PluginOptions
initialValue: boolean | undefined
}): boolean | undefined => {
const {config, initialValue} = opts
const flattenedConfig = flattenConfig(config, [])
const result = flattenedConfig.reduce((acc: boolean | undefined, {config: innerConfig}) => {
const enabled = innerConfig.__internal_serverDocumentActions?.enabled
if (typeof enabled === 'undefined') return acc
if (typeof enabled === 'boolean') return enabled
throw new Error(
`Expected \`__internal_serverDocumentActions\` to be a boolean, but received ${getPrintableType(
enabled,
)}`,
)
}, initialValue)
return result
}
export const partialIndexingEnabledReducer = (opts: {
config: PluginOptions
initialValue: boolean
}): boolean => {
const {config, initialValue} = opts
const flattenedConfig = flattenConfig(config, [])
const result = flattenedConfig.reduce((acc, {config: innerConfig}) => {
const resolver = innerConfig.search?.unstable_partialIndexing?.enabled
if (!resolver && typeof resolver !== 'boolean') return acc
if (typeof resolver === 'boolean') return resolver
throw new Error(
`Expected \`search.unstable_partialIndexing.enabled\` to be a boolean, but received ${getPrintableType(
resolver,
)}`,
)
}, initialValue)
return result
}
export const legacySearchEnabledReducer: ConfigPropertyReducer<boolean, ConfigContext> = (
prev,
{search},
): boolean => {
if (typeof search?.enableLegacySearch !== 'undefined') {
return search.enableLegacySearch
}
return prev
}
/**
* Some projects may already be using the `enableLegacySearch` option. In order to gracefully
* migrate to the `strategy` option, this reducer produces a value that respects any existing
* `enableLegacySearch` option.
*
* If the project currently enables the Text Search API search strategy by setting
* `enableLegacySearch` to `false`, this is mapped to the `groq2024` strategy.
*
* Any explicitly defined `strategy` value will take precedence over the value inferred from
* `enableLegacySearch`.
*/
export const searchStrategyReducer = ({
config,
initialValue,
}: {
config: PluginOptions
initialValue: SearchStrategy
}): SearchStrategy => {
const flattenedConfig = flattenConfig(config, [])
type SearchStrategyReducerState = [
implicit: SearchStrategy | undefined,
explicit: SearchStrategy | undefined,
]
const [implicit, explicit] = flattenedConfig.reduce<SearchStrategyReducerState>(
([currentImplicit, currentExplicit], entry) => {
const {enableLegacySearch, strategy} = entry.config.search ?? {}
// The strategy has been explicitly defined.
if (typeof strategy !== 'undefined') {
if (!isSearchStrategy(strategy)) {
const listFormatter = new Intl.ListFormat('en-US', {type: 'disjunction'})
const options = listFormatter.format(searchStrategies.map((value) => `"${value}"`))
const received =
typeof strategy === 'string' ? `"${strategy}"` : getPrintableType(strategy)
throw new Error(`Expected \`search.strategy\` to be ${options}, but received ${received}`)
}
return [currentImplicit, strategy]
}
// The strategy has been implicitly defined.
if (typeof enableLegacySearch === 'boolean') {
return [enableLegacySearch ? 'groqLegacy' : 'groq2024', currentExplicit]
}
return [currentImplicit, currentExplicit]
},
[undefined, undefined],
)
return explicit ?? implicit ?? initialValue
}
export const startInCreateEnabledReducer = (opts: {
config: PluginOptions
initialValue: boolean
}): boolean => {
const {config, initialValue} = opts
const flattenedConfig = flattenConfig(config, [])
const result = flattenedConfig.reduce((acc, {config: innerConfig}) => {
const resolver = innerConfig.beta?.create?.startInCreateEnabled
if (!resolver && typeof resolver !== 'boolean') return acc
if (typeof resolver === 'boolean') return resolver
throw new Error(
`Expected \`beta.create.startInCreateEnabled\` to be a boolean, but received ${getPrintableType(
resolver,
)}`,
)
}, initialValue)
return result
}
export const createFallbackOriginReducer = (config: PluginOptions): string | undefined => {
const flattenedConfig = flattenConfig(config, [])
const result = flattenedConfig.reduce(
(acc, {config: innerConfig}) => {
const resolver = innerConfig.beta?.create?.fallbackStudioOrigin
if (!resolver) return acc
if (typeof resolver === 'string') return resolver
throw new Error(
`Expected \`beta.create.fallbackStudioOrigin\` to be a string, but received ${getPrintableType(
resolver,
)}`,
)
},
undefined as string | undefined,
)
return result
}
export const announcementsEnabledReducer = (opts: {
config: PluginOptions
initialValue: boolean
}): boolean => {
const {config, initialValue} = opts
const flattenedConfig = flattenConfig(config, [])
const result = flattenedConfig.reduce((acc, {config: innerConfig}) => {
const resolver = innerConfig.announcements?.enabled
if (!resolver && typeof resolver !== 'boolean') return acc
if (typeof resolver === 'boolean') return resolver
throw new Error(
`Expected \`announcements.enabled\` to be a boolean, but received ${getPrintableType(
resolver,
)}`,
)
}, initialValue)
return result
}