-
Notifications
You must be signed in to change notification settings - Fork 429
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(validation): pass path and parent correctly to validateItem
- Loading branch information
1 parent
6629482
commit 060c9a2
Showing
8 changed files
with
756 additions
and
278 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
154 changes: 28 additions & 126 deletions
154
packages/@sanity/validation/src/inferFromSchemaType.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,148 +1,50 @@ | ||
import {Schema, SchemaType, Rule as IRule} from '@sanity/types' | ||
import RuleClass from './Rule' | ||
import {slugValidator} from './validators/slugValidator' | ||
import {blockValidator} from './validators/blockValidator' | ||
import {Schema, SchemaType} from '@sanity/types' | ||
import normalizeValidationRules from './util/normalizeValidationRules' | ||
|
||
function inferFromSchemaType( | ||
typeDef: SchemaType, | ||
schema: Schema, | ||
visited = new Set<SchemaType>() | ||
): SchemaType { | ||
function traverse(typeDef: SchemaType, visited: Set<SchemaType>) { | ||
if (visited.has(typeDef)) { | ||
return typeDef | ||
return | ||
} | ||
|
||
visited.add(typeDef) | ||
|
||
if (typeDef.validation === false) { | ||
typeDef.validation = [] | ||
return typeDef | ||
} | ||
|
||
const isInitialized = | ||
Array.isArray(typeDef.validation) && | ||
typeDef.validation.every((item) => typeof item?.validate === 'function') | ||
|
||
if (isInitialized) { | ||
inferForFields(typeDef, schema, visited) | ||
inferForMemberTypes(typeDef, schema, visited) | ||
return typeDef | ||
} | ||
|
||
const type = typeDef.type | ||
const typed = RuleClass[typeDef.jsonType] | ||
let base = typed ? typed(typeDef) : new RuleClass(typeDef) | ||
|
||
if (type && type.name === 'datetime') { | ||
base = base.type('Date') | ||
} | ||
|
||
if (type && type.name === 'date') { | ||
base = base.type('Date') | ||
} | ||
|
||
if (type && type.name === 'url') { | ||
base = base.uri() | ||
} | ||
|
||
if (type && type.name === 'slug') { | ||
base = base.custom(slugValidator) | ||
} | ||
typeDef.validation = normalizeValidationRules(typeDef) | ||
|
||
if (type && type.name === 'reference') { | ||
base = base.reference() | ||
if ('fields' in typeDef) { | ||
for (const field of typeDef.fields) { | ||
traverse(field.type, visited) | ||
} | ||
} | ||
|
||
if (type && type.name === 'email') { | ||
base = base.email() | ||
} | ||
|
||
if (type && type.name === 'block') { | ||
base = base.block(blockValidator) | ||
if ('of' in typeDef) { | ||
for (const candidate of typeDef.of) { | ||
traverse(candidate, visited) | ||
} | ||
} | ||
|
||
// eslint-disable-next-line no-warning-comments | ||
// @ts-expect-error TODO (eventually): `annotations` does not exist on the SchemaType yet | ||
if (typeDef.annotations) { | ||
// eslint-disable-next-line no-warning-comments | ||
// @ts-expect-error TODO (eventually): `annotations` does not exist on the SchemaType yet | ||
typeDef.annotations.forEach((annotation) => inferFromSchemaType(annotation)) | ||
for (const annotation of typeDef.annotations) { | ||
traverse(annotation, visited) | ||
} | ||
} | ||
|
||
// eslint-disable-next-line no-warning-comments | ||
// @ts-expect-error TODO (eventually): fix options list grabbing | ||
if (typeDef.options && typeDef.options.list && Array.isArray(typeDef.options.list)) { | ||
base = base.valid( | ||
// eslint-disable-next-line no-warning-comments | ||
// @ts-expect-error TODO (eventually): fix options list grabbing | ||
typeDef.options.list.map((option) => extractValueFromListOption(option, typeDef)) | ||
) | ||
} | ||
|
||
typeDef.validation = inferValidation(typeDef, base) | ||
inferForFields(typeDef, schema, visited) | ||
inferForMemberTypes(typeDef, schema, visited) | ||
|
||
return typeDef | ||
} | ||
|
||
function inferForFields(typeDef: SchemaType, schema: Schema, visited: Set<SchemaType>): void { | ||
if (typeDef.jsonType !== 'object' || !typeDef.fields) { | ||
return | ||
} | ||
|
||
typeDef.fields.forEach((field) => { | ||
inferFromSchemaType(field.type, schema, visited) | ||
}) | ||
} | ||
|
||
function inferForMemberTypes(typeDef: SchemaType, schema: Schema, visited: Set<SchemaType>): void { | ||
if (typeDef.jsonType === 'array' && typeDef.of) { | ||
typeDef.of.forEach((candidate) => inferFromSchemaType(candidate, schema, visited)) | ||
} | ||
} | ||
|
||
function extractValueFromListOption(option: unknown, typeDef: SchemaType): unknown { | ||
// If you define a `list` option with object items, where the item has a `value` field, | ||
// we don't want to treat that as the value but rather the surrounding object | ||
// This differs from the case where you have a title/value pair setup for a string/number, for instance | ||
if (typeDef.jsonType === 'object' && hasValueField(typeDef)) { | ||
return option | ||
} | ||
|
||
return (option as Record<string, unknown>).value === undefined | ||
? option | ||
: (option as Record<string, unknown>).value | ||
} | ||
|
||
function hasValueField(typeDef: SchemaType): boolean { | ||
if (!('fields' in typeDef) && typeDef.type) { | ||
return hasValueField(typeDef.type) | ||
} | ||
|
||
if (!typeDef || !('fields' in typeDef)) { | ||
return false | ||
} | ||
|
||
if (!Array.isArray(typeDef.fields)) { | ||
return false | ||
} | ||
|
||
if (typeDef.fields.some((field) => field.name === 'value')) { | ||
return true | ||
} | ||
|
||
return false | ||
} | ||
|
||
function inferValidation(field: SchemaType, baseRule: IRule): IRule[] { | ||
if (!field.validation) { | ||
return [baseRule] | ||
} | ||
|
||
const validation = | ||
typeof field.validation === 'function' ? field.validation(baseRule) : field.validation | ||
return Array.isArray(validation) ? validation : [validation] | ||
// NOTE: this overload is for TS API compatibility with a previous implementation | ||
function inferFromSchemaType( | ||
typeDef: SchemaType, | ||
// these are intentionally unused | ||
_schema: Schema, | ||
_visited?: Set<SchemaType> | ||
): SchemaType | ||
// note: this seemingly redundant overload is required | ||
function inferFromSchemaType(typeDef: SchemaType): SchemaType | ||
function inferFromSchemaType(typeDef: SchemaType): SchemaType { | ||
traverse(typeDef, new Set()) | ||
return typeDef | ||
} | ||
|
||
export default inferFromSchemaType |
170 changes: 170 additions & 0 deletions
170
packages/@sanity/validation/src/util/normalizeValidationRules.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
import {NumberSchemaType, SchemaType, StringSchemaType} from '@sanity/types' | ||
import RuleClass from '../Rule' | ||
import normalizeValidationRules from './normalizeValidationRules' | ||
|
||
describe('normalizeValidationRules', () => { | ||
// see `infer.test.ts` for more related tests. | ||
// note the Schema.compile runs this function indirectly via `inferFromSchema` | ||
it('utilizes schema types to infer base rules', () => { | ||
const coolNumberType: NumberSchemaType = { | ||
jsonType: 'number', | ||
name: 'coolNumber', | ||
} | ||
|
||
const rules = normalizeValidationRules(coolNumberType) | ||
expect(rules).toHaveLength(1) | ||
const [rule] = rules | ||
|
||
expect(rule).toBeInstanceOf(RuleClass) | ||
expect(rule._rules).toMatchObject([ | ||
{ | ||
constraint: 'Number', | ||
flag: 'type', | ||
}, | ||
]) | ||
}) | ||
|
||
it('follows the type chain to determine the base rule', () => { | ||
const sickDatetime = { | ||
type: { | ||
type: { | ||
jsonType: 'string', | ||
}, | ||
name: 'datetime', | ||
}, | ||
name: 'sickDatetime', | ||
} | ||
|
||
const rules = normalizeValidationRules(sickDatetime as SchemaType) | ||
expect(rules).toHaveLength(1) | ||
const [rule] = rules | ||
|
||
// type chain is applied from inner to outer so the resulting type should be | ||
// date instead of string | ||
expect(rule._rules).toMatchObject([ | ||
{ | ||
constraint: 'Date', | ||
flag: 'type', | ||
}, | ||
]) | ||
}) | ||
|
||
it('converts a validation function to a rule instance', () => { | ||
const coolStringType: StringSchemaType = { | ||
jsonType: 'string', | ||
name: 'coolString', | ||
validation: (rule) => rule.uppercase(), | ||
} | ||
|
||
const rules = normalizeValidationRules(coolStringType) | ||
expect(rules).toHaveLength(1) | ||
const [rule] = rules | ||
|
||
expect(rule).toBeInstanceOf(RuleClass) | ||
expect(rule._rules).toMatchObject([ | ||
{ | ||
constraint: 'String', | ||
flag: 'type', | ||
}, | ||
{ | ||
constraint: 'uppercase', | ||
flag: 'stringCasing', | ||
}, | ||
]) | ||
}) | ||
|
||
it('converts falsy values to an empty array', () => { | ||
expect(normalizeValidationRules(undefined)).toEqual([]) | ||
}) | ||
|
||
it('converts schema list options with titles to `rule.valid` constraints', () => { | ||
const stringTypeWithOptions: StringSchemaType = { | ||
jsonType: 'string', | ||
name: 'stringTypeWithOptions', | ||
options: { | ||
list: [ | ||
{title: 'Blue', value: 'blue'}, | ||
{title: 'Red', value: 'red'}, | ||
], | ||
}, | ||
} | ||
|
||
const rules = normalizeValidationRules(stringTypeWithOptions) | ||
expect(rules).toHaveLength(1) | ||
const [rule] = rules | ||
|
||
expect(rule).toBeInstanceOf(RuleClass) | ||
expect(rule._rules).toMatchObject([ | ||
{ | ||
constraint: 'String', | ||
flag: 'type', | ||
}, | ||
{ | ||
constraint: ['blue', 'red'], | ||
flag: 'valid', | ||
}, | ||
]) | ||
}) | ||
|
||
it('converts schema list options with strings only to `rule.valid` constraints', () => { | ||
const stringTypeWithOptions: StringSchemaType = { | ||
jsonType: 'string', | ||
name: 'stringTypeWithOptions', | ||
options: { | ||
list: ['blue', 'red'], | ||
}, | ||
} | ||
|
||
const rules = normalizeValidationRules(stringTypeWithOptions) | ||
expect(rules).toHaveLength(1) | ||
const [rule] = rules | ||
|
||
expect(rule).toBeInstanceOf(RuleClass) | ||
expect(rule._rules).toMatchObject([ | ||
{ | ||
constraint: 'String', | ||
flag: 'type', | ||
}, | ||
{ | ||
constraint: ['blue', 'red'], | ||
flag: 'valid', | ||
}, | ||
]) | ||
}) | ||
|
||
it('converts arrays of validation', () => { | ||
const coolNumberType: NumberSchemaType = { | ||
jsonType: 'number', | ||
name: 'coolNumber', | ||
validation: [(rule) => rule.greaterThan(3), RuleClass.number().lessThan(5)], | ||
} | ||
|
||
const rules = normalizeValidationRules(coolNumberType) | ||
expect(rules).toHaveLength(2) | ||
const [first, second] = rules | ||
|
||
expect(first).toBeInstanceOf(RuleClass) | ||
expect(first._rules).toMatchObject([ | ||
{ | ||
constraint: 'Number', | ||
flag: 'type', | ||
}, | ||
{ | ||
constraint: 3, | ||
flag: 'greaterThan', | ||
}, | ||
]) | ||
|
||
expect(second).toBeInstanceOf(RuleClass) | ||
expect(second._rules).toMatchObject([ | ||
{ | ||
constraint: 'Number', | ||
flag: 'type', | ||
}, | ||
{ | ||
constraint: 5, | ||
flag: 'lessThan', | ||
}, | ||
]) | ||
}) | ||
}) |
Oops, something went wrong.
060c9a2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
test-studio – ./
test-studio-git-next.sanity.build
test-studio.sanity.build
060c9a2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
perf-studio – ./
perf-studio.sanity.build
perf-studio-git-next.sanity.build