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

253 lines (210 loc) 7.55 kB
import {type CliOutputter} from '@sanity/cli' import uniqBy from 'lodash/uniqBy' import {isDefined} from '../../../../manifest/manifestTypeHelpers' import {SANITY_WORKSPACE_SCHEMA_TYPE} from '../../../../manifest/manifestTypes' import {type DeleteSchemaFlags} from '../deleteSchemaAction' import {type DeploySchemasFlags} from '../deploySchemasAction' import {type SchemaListFlags} from '../listSchemasAction' import {resolveManifestDirectory} from './manifestReader' export const validForIdChars = 'a-zA-Z0-9._-' export const validForIdPattern = new RegExp(`^[${validForIdChars}]+$`, 'g') const requiredInId = SANITY_WORKSPACE_SCHEMA_TYPE.replace(/[.]/g, '\\.') const idPattern = new RegExp( `^(?:[${validForIdChars}]+?\\.)?${requiredInId}\\.([${validForIdChars}]+)$`, ) export class FlagValidationError extends Error { constructor(message: string) { super(message) this.name = 'FlagValidationError' } } interface WorkspaceSchemaId { schemaId: string workspace: string } export interface SchemaStoreCommonFlags { 'extract-manifest'?: boolean 'manifest-dir'?: string 'verbose'?: boolean } function parseCommonFlags( flags: SchemaStoreCommonFlags, context: {workDir: string}, errors: string[], ) { const manifestDir = parseManifestDir(flags, errors) const verbose = !!flags.verbose // extract manifest by default: our CLI layer handles both --extract-manifest (true) and --no-extract-manifest (false) const extractManifest = flags['extract-manifest'] ?? true const fullManifestDir = resolveManifestDirectory(context.workDir, manifestDir) return { manifestDir: fullManifestDir, verbose, extractManifest, } } export function parseDeploySchemasConfig(flags: DeploySchemasFlags, context: {workDir: string}) { const errors: string[] = [] const commonFlags = parseCommonFlags(flags, context, errors) const workspaceName = parseWorkspace(flags, errors) const idPrefix = parseIdPrefix(flags, errors) const schemaRequired = !!flags['schema-required'] assertNoErrors(errors) return {...commonFlags, workspaceName, idPrefix, schemaRequired} } export function parseListSchemasConfig(flags: SchemaListFlags, context: {workDir: string}) { const errors: string[] = [] const commonFlags = parseCommonFlags(flags, context, errors) const id = parseId(flags, errors) const json = !!flags.json assertNoErrors(errors) return {...commonFlags, json, id} } export function parseDeleteSchemasConfig(flags: DeleteSchemaFlags, context: {workDir: string}) { const errors: string[] = [] const commonFlags = parseCommonFlags(flags, context, errors) const ids = parseIds(flags, errors) const dataset = parseDataset(flags, errors) assertNoErrors(errors) return {...commonFlags, dataset, ids} } function assertNoErrors(errors: string[]) { if (errors.length) { throw new FlagValidationError( `Invalid arguments:\n${errors.map((error) => ` - ${error}`).join('\n')}`, ) } } export function parseIds(flags: {ids?: unknown}, errors: string[]): WorkspaceSchemaId[] { const parsedIds = parseNonEmptyString(flags, 'ids', errors) if (errors.length) { return [] } const ids = parsedIds .split(',') .map((id) => id.trim()) .filter((id) => !!id) .map((id) => parseWorkspaceSchemaId(id, errors)) .filter(isDefined) const uniqueIds = uniqBy(ids, 'schemaId' satisfies keyof (typeof ids)[number]) if (uniqueIds.length < ids.length) { errors.push(`ids contains duplicates`) } if (!errors.length && !uniqueIds.length) { errors.push(`ids contains no valid id strings`) } return uniqueIds } export function parseId(flags: {id?: unknown}, errors: string[]) { const id = flags.id === undefined ? undefined : parseNonEmptyString(flags, 'id', errors) if (id) { return parseWorkspaceSchemaId(id, errors)?.schemaId } return undefined } export function parseWorkspaceSchemaId(id: string, errors: string[]) { const trimmedId = id.trim() if (!trimmedId.match(validForIdPattern)) { errors.push(`id can only contain characters in [${validForIdChars}] but found: "${trimmedId}"`) return undefined } if (trimmedId.startsWith('-')) { errors.push(`id cannot start with - (dash) but found: "${trimmedId}"`) return undefined } if (trimmedId.match(/\.\./g)) { errors.push(`id cannot have consecutive . (period) characters, but found: "${trimmedId}"`) return undefined } const match = trimmedId.match(idPattern) const workspace = match?.[1] ?? '' if (!workspace) { errors.push( `id must end with ${SANITY_WORKSPACE_SCHEMA_TYPE}.<workspaceName> but found: "${trimmedId}"`, ) return undefined } return { schemaId: trimmedId, workspace, } } function parseDataset(flags: {dataset?: unknown}, errors: string[]) { return flags.dataset === undefined ? undefined : parseNonEmptyString(flags, 'dataset', errors) } function parseWorkspace(flags: {workspace?: unknown}, errors: string[]) { return flags.workspace === undefined ? undefined : parseNonEmptyString(flags, 'workspace', errors) } function parseManifestDir(flags: {'manifest-dir'?: unknown}, errors: string[]) { return flags['manifest-dir'] === undefined ? undefined : parseNonEmptyString(flags, 'manifest-dir', errors) } export function parseIdPrefix(flags: {'id-prefix'?: unknown}, errors: string[]) { if (flags['id-prefix'] === undefined) { return undefined } const idPrefix = parseNonEmptyString(flags, 'id-prefix', errors) if (errors.length) { return undefined } if (idPrefix.endsWith('.')) { errors.push(`id-prefix argument cannot end with . (period), but was: "${idPrefix}"`) return undefined } if (!idPrefix.match(validForIdPattern)) { errors.push( `id-prefix can only contain _id compatible characters [${validForIdChars}], but was: "${idPrefix}"`, ) return undefined } if (idPrefix.startsWith('-')) { errors.push(`id-prefix cannot start with - (dash) but was: "${idPrefix}"`) return undefined } if (idPrefix.match(/\.\./g)) { errors.push(`id-prefix cannot have consecutive . (period) characters, but was: "${idPrefix}"`) return undefined } return idPrefix } function parseNonEmptyString< Flag extends string, Flags extends Partial<Record<Flag, unknown | undefined>>, >(flags: Flags, flagName: Flag, errors: string[]): string { const flag = flags[flagName] if (!isString(flag) || !flag) { errors.push(`${flagName} argument is empty`) return '' } return flag } function isString(flag: unknown): flag is string { return typeof flag === 'string' } function getProjectIdMismatchMessage( workspace: {name: string; projectId: string}, operation: 'read' | 'write', ) { return `No permissions to ${operation} schema for workspace "${workspace.name}" with projectId "${workspace.projectId}"` } /** * At the moment schema store commands does not support studios where workspaces have multiple projects */ export function throwWriteProjectIdMismatch( workspace: {name: string; projectId: string}, projectId: string, ): void { if (workspace.projectId !== projectId) { throw new Error(getProjectIdMismatchMessage(workspace, 'write')) } } export function filterLogReadProjectIdMismatch( workspace: {name: string; projectId: string}, projectId: string, output: CliOutputter, ) { const canRead = workspace.projectId === projectId if (!canRead) output.warn(`${getProjectIdMismatchMessage(workspace, 'read')} – ignoring it.`) return canRead }