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

193 lines (167 loc) • 6.36 kB
import {getNamelessWorkspaceIdentifier, getWorkspaceIdentifier} from './helpers' import {type WorkspaceLike} from './types' import {WorkspaceValidationError} from './WorkspaceValidationError' /** @internal */ export interface ValidateWorkspaceOptions { workspaces: WorkspaceLike[] } /** * Validates workspace configuration, throwing if: * * - Workspaces do not all have base paths and names (if multiple given) * - Base paths or names are invalid * - Base paths or names are not unique * * @internal */ export function validateWorkspaces({workspaces}: ValidateWorkspaceOptions): void { if (workspaces.length === 0) { throw new WorkspaceValidationError('At least one workspace is required.') } validateNames(workspaces) validateBasePaths(workspaces) } /** * Validates the workspace names of every workspace * Only exported for testing purposes * * @param workspaces - An array of workspaces * @internal */ export function validateNames(workspaces: WorkspaceLike[]): void { const isSingleWorkspace = workspaces.length === 1 const names = new Map<string, {index: number; workspace: WorkspaceLike}>() workspaces.forEach((workspace, index) => { const {name: rawName, title} = workspace const thisIdentifier = getNamelessWorkspaceIdentifier(title, index) if (!rawName && !isSingleWorkspace) { throw new WorkspaceValidationError( 'All workspaces must have a `name`, unless only a single workspace is defined. ' + `Workspace ${thisIdentifier} did not define a \`name\`.`, {workspace, index}, ) } const name = isSingleWorkspace && typeof rawName === 'undefined' ? 'default' : rawName if (typeof name !== 'string') { throw new WorkspaceValidationError( `Workspace at index ${index} defined an invalid \`name\` - must be a string.`, {workspace, index}, ) } const normalized = name.toLowerCase() const existingWorkspace = names.get(normalized) if (existingWorkspace) { const prevIdentifier = getNamelessWorkspaceIdentifier( existingWorkspace.workspace.title, existingWorkspace.index, ) throw new WorkspaceValidationError( `\`name\`s must be unique. Workspace ${prevIdentifier} and ` + `workspace ${thisIdentifier} both have the \`name\` \`${name}\``, {workspace, index}, ) } names.set(normalized, {index, workspace}) if (!/^[a-z0-9][a-z0-9_-]*$/i.test(name)) { throw new WorkspaceValidationError( `All workspace \`name\`s must consist of only a-z, 0-9, underscore and dashes, ` + `and cannot begin with an underscore or dash. ` + `Workspace ${thisIdentifier} has the invalid name \`${name}\``, {workspace, index}, ) } }) } /** * Validates the base paths of every workspace * Only exported for testing purposes * * @param workspaces - An array of workspaces * @internal */ export function validateBasePaths(workspaces: WorkspaceLike[]): void { // If we have more than a single workspace, every workspace needs a basepath if (workspaces.length > 1) { workspaces.every(hasBasePath) // Throws on missing basePath } workspaces.every(validateBasePath) const [firstWorkspace, ...restOfWorkspaces] = workspaces const firstWorkspaceSegmentCount = (firstWorkspace.basePath || '/') // remove starting slash before splitting .substring(1) .split('/') .filter(Boolean).length restOfWorkspaces.forEach((workspace, index) => { const workspaceSegmentCount = (workspace.basePath || '/') // remove starting slash before splitting .substring(1) .split('/').length if (firstWorkspaceSegmentCount !== workspaceSegmentCount) { throw new WorkspaceValidationError( `All workspace \`basePath\`s must have the same amount of segments. Workspace \`${getWorkspaceIdentifier( firstWorkspace, index, )}\` had ${firstWorkspaceSegmentCount} segment${ firstWorkspaceSegmentCount === 1 ? '' : 's' } \`${firstWorkspace.basePath}\` but workspace \`${getWorkspaceIdentifier( workspace, index, )}\` had ${workspaceSegmentCount} segment${workspaceSegmentCount === 1 ? '' : 's'} \`${ workspace.basePath }\``, {workspace, index}, ) } }) const basePaths = new Map<string, string>() workspaces.forEach((workspace, index) => { const basePath = (workspace.basePath || '').toLowerCase() const existingWorkspace = basePaths.get(basePath) if (existingWorkspace) { throw new WorkspaceValidationError( `\`basePath\`s must be unique. Workspaces \`${existingWorkspace}\` and ` + `\`${getWorkspaceIdentifier( workspace, index, )}\` both have the \`basePath\` \`${basePath}\``, {workspace, index}, ) } basePaths.set(basePath, getWorkspaceIdentifier(workspace, index)) }) } function hasBasePath(workspace: WorkspaceLike, index: number) { const {name, basePath} = workspace if (basePath && typeof basePath === 'string') { return true } if (typeof basePath === 'undefined') { throw new WorkspaceValidationError( `If more than one workspace is defined, every workspace must have a \`basePath\` defined. ` + `Workspace \`${name}\` is missing a \`basePath\``, {workspace, index}, ) } throw new WorkspaceValidationError( `If more than one workspace is defined, every workspace must have a \`basePath\` defined. ` + `Workspace \`${name}\` has an invalid \`basePath\` (must be a non-empty string)`, {workspace, index}, ) } function validateBasePath(workspace: WorkspaceLike, index: number) { const {name, basePath} = workspace // Empty base paths are okay (we're validating uniqueness and presence on more // than a single workspace in `validateBasePaths`) if (!basePath || basePath === '/') { return } if (!/^\/[a-z0-9/_-]*[a-z0-9_-]+$/i.test(basePath)) { throw new WorkspaceValidationError( `All workspace \`basePath\`s must start with a leading \`/\`, ` + `consist of only URL safe characters, ` + `and cannot end with a trailing \`/\`. ` + `Workspace \`${name}\`'s basePath is \`${basePath}\``, {workspace, index}, ) } }