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
text/typescript
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
}