Skip to content

Commit

Permalink
fix(validation): resolve array items without _type (#2715)
Browse files Browse the repository at this point in the history
  • Loading branch information
ricokahler committed Aug 27, 2021
1 parent 7c40692 commit 4bd8082
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 27 deletions.
14 changes: 11 additions & 3 deletions packages/@sanity/validation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,20 @@
"directory": "packages/@sanity/validation"
},
"devDependencies": {
"@sanity/schema": "2.16.0",
"@sanity/client": "^2.16.0",
"@sanity/schema": "^2.16.0",
"jest": "^26.6.3"
},
"peerDependencies": {
"@sanity/client": "^2.0.0"
},
"peerDependenciesMeta": {
"@sanity/client": {
"optional": true
}
},
"dependencies": {
"@sanity/client": "2.16.0",
"@sanity/types": "2.17.0",
"@sanity/types": "^2.17.0",
"date-fns": "^2.16.1",
"lodash": "^4.17.15"
}
Expand Down
27 changes: 27 additions & 0 deletions packages/@sanity/validation/src/util/typeString.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import typeString from './typeString'

describe('typeString', () => {
it('returns the a type string of built in types', () => {
expect(typeString({})).toBe('Object')
expect(
typeString(function () {
// intentionally blank
})
).toBe('Function')
expect(typeString(['hey'])).toBe('Array')
expect(typeString('some string')).toBe('String')
expect(typeString(false)).toBe('Boolean')
expect(typeString(5)).toBe('Number')
expect(typeString(new Date())).toBe('Date')
})

it('returns a type string string using the constructor', () => {
class ExampleClass {}
expect(typeString(new ExampleClass())).toBe('ExampleClass')
})

it('returns a type string for null or undefined', () => {
expect(typeString(null)).toBe('null')
expect(typeString(undefined)).toBe('undefined')
})
})
10 changes: 2 additions & 8 deletions packages/@sanity/validation/src/util/typeString.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,13 @@ function isBuiltIn(_constructor: unknown) {
return false
}

function getConstructorOf(obj: unknown) {
if (obj === null || obj === undefined) return obj

// eslint-disable-next-line @typescript-eslint/ban-types
return (obj as object).constructor
}

export default function typeString(obj: unknown): string {
// [object Blah] -> Blah
const stringType = _toString.call(obj).slice(8, -1)
if (obj === null || obj === undefined) return stringType.toLowerCase()

const constructorType = getConstructorOf(obj)
// eslint-disable-next-line @typescript-eslint/ban-types
const constructorType = (obj as object).constructor
if (constructorType && !isBuiltIn(constructorType)) return constructorType.name
return stringType
}
156 changes: 156 additions & 0 deletions packages/@sanity/validation/src/validateDocument.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/// <reference types="@sanity/types/parts" />

import {Rule, SanityDocument, Schema} from '@sanity/types'
import createSchema from 'part:@sanity/base/schema-creator'
import validateDocument, {resolveTypeForArrayItem} from './validateDocument'

describe('validateDocument', () => {
it('takes in a document + a compiled schema and returns a list of validation markers', async () => {
const schema = createSchema({
types: [
{
name: 'simpleDoc',
type: 'document',
title: 'Simple Document',
fields: [
{
name: 'title',
type: 'string',
validation: (rule: Rule) => rule.required(),
},
],
},
],
})

const document: SanityDocument = {
_id: 'testId',
_createdAt: '2021-08-27T14:48:51.650Z',
_rev: 'exampleRev',
_type: 'simpleDoc',
_updatedAt: '2021-08-27T14:48:51.650Z',
title: null,
}

const result = await validateDocument(document, schema)
expect(result).toMatchObject([
{
type: 'validation',
level: 'error',
item: {
message: 'Expected type "String", got "null"',
paths: [],
},
path: ['title'],
},
{
type: 'validation',
level: 'error',
item: {
message: 'Required',
paths: [],
},
path: ['title'],
},
])
})

it('should be able to resolve an array item type if there is just one type', async () => {
const schema = createSchema({
types: [
{
name: 'testDoc',
type: 'document',
title: 'Test Document',
fields: [
{
name: 'values',
type: 'array',
// note that there is only one type available
of: [{type: 'arrayItem'}],
validation: (rule: Rule) => rule.required(),
},
],
},
{
name: 'arrayItem',
type: 'object',
fields: [{name: 'title', type: 'string'}],
},
],
})

const document: SanityDocument = {
_id: 'testId',
_createdAt: '2021-08-27T14:48:51.650Z',
_rev: 'exampleRev',
_type: 'testDoc',
_updatedAt: '2021-08-27T14:48:51.650Z',
values: [
{
// note how this doesn't have a _type
title: 5,
_key: 'exampleKey',
},
],
}

await expect(validateDocument(document, schema)).resolves.toEqual([
{
type: 'validation',
level: 'error',
item: {
message: 'Expected type "String", got "Number"',
paths: [],
},
path: ['values', {_key: 'exampleKey'}, 'title'],
},
])
})
})

describe('resolveTypeForArrayItem', () => {
const schema: Schema = createSchema({
types: [
{
name: 'foo',
type: 'object',
fields: [{name: 'title', type: 'number'}],
},
{
name: 'bar',
type: 'object',
fields: [{name: 'title', type: 'string'}],
},
],
})

const fooType = schema.get('foo')
const barType = schema.get('bar')

it('finds a matching schema type for an array item value given a list of candidate types', () => {
const resolved = resolveTypeForArrayItem(
{
_type: 'bar',
_key: 'exampleKey',
title: 5,
},
[fooType, barType]
)

expect(resolved).toBe(barType)
})

it('assumes the type if there is only one possible candidate', () => {
const resolved = resolveTypeForArrayItem(
{
// notice no _type
_key: 'exampleKey',
title: 5,
},
[fooType]
)

expect(resolved).toBe(fooType)
})
})
26 changes: 10 additions & 16 deletions packages/@sanity/validation/src/validateDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ import RuleClass from './Rule'

const appendPath = (base: Path, next: Path | PathSegment): Path => base.concat(next)

const resolveTypeForArrayItem = (
export function resolveTypeForArrayItem(
item: unknown,
candidates: SchemaType[]
): SchemaType | undefined => {
): SchemaType | undefined {
// if there is only one type available, assume that it's the correct one
if (candidates.length === 1) {
return candidates[0]
}

const itemType = isTypedObject(item) && item._type

const primitive =
Expand Down Expand Up @@ -69,22 +74,11 @@ export function validateItem(
path: Path,
context: ValidationContext
): Promise<ValidationMarker[]> {
if (!type) {
return Promise.resolve([
{
type: 'validation',
level: 'error',
path,
item: new ValidationErrorClass('Unable to resolve type for item'),
},
])
}

if (Array.isArray(item) && type.jsonType === 'array') {
if (Array.isArray(item) && type?.jsonType === 'array') {
return validateArray(item, type, path, context)
}

if (typeof item === 'object' && item !== null && type.jsonType === 'object') {
if (typeof item === 'object' && item !== null && type?.jsonType === 'object') {
return validateObject(item as Record<string, unknown>, type, path, context)
}

Expand Down Expand Up @@ -212,7 +206,7 @@ async function validateArray(

async function validatePrimitive(
item: unknown,
type: SchemaType,
type: SchemaType | undefined,
path: Path,
context: ValidationContext
): Promise<ValidationMarker[]> {
Expand Down

2 comments on commit 4bd8082

@vercel
Copy link

@vercel vercel bot commented on 4bd8082 Aug 27, 2021

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

@vercel
Copy link

@vercel vercel bot commented on 4bd8082 Aug 27, 2021

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

Please sign in to comment.