From 2c81a7df51ca5774567d369d1091d40f4737df82 Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Fri, 11 May 2018 14:25:45 -0400 Subject: [PATCH] fix: can sanitize items in arrays that can be null, close #53 --- README.md | 4 + package-lock.json | 48 +++++------ src/sanitize.ts | 24 +++++- test/sanitize-test.ts | 179 ++++++++++++++++++++++++++++++++++++------ 4 files changed, 202 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index aa72ca9..6a2c6ad 100644 --- a/README.md +++ b/README.md @@ -273,6 +273,10 @@ When asserting an object against a schema a custom error is thrown. It is an ins * `schemaName` is the title of the schema, like `Person` * `schemaVersion` the version like `1.0.0` of the schema violated, if known. +## Debugging + +To see log messages from this module, run with `DEBUG=schema-tools` + ## Testing Uses [ava-ts](https://github.com/andywer/ava-ts#readme) to run Ava test runner directly against TypeScript test files. Use `npm t` to build and test everything in the `test` folder. diff --git a/package-lock.json b/package-lock.json index f52678b..8c43dfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6673,11 +6673,11 @@ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "dev": true, "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "nice-try": "1.0.4", + "path-key": "2.0.1", + "semver": "5.5.0", + "shebang-command": "1.2.0", + "which": "1.3.0" } }, "execa": { @@ -6686,13 +6686,13 @@ "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", "dev": true, "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" + "cross-spawn": "6.0.5", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" } }, "load-json-file": { @@ -6701,10 +6701,10 @@ "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" + "graceful-fs": "4.1.11", + "parse-json": "4.0.0", + "pify": "3.0.0", + "strip-bom": "3.0.0" } }, "parse-json": { @@ -6713,8 +6713,8 @@ "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", "dev": true, "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "error-ex": "1.3.1", + "json-parse-better-errors": "1.0.2" } }, "path-type": { @@ -6723,7 +6723,7 @@ "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", "dev": true, "requires": { - "pify": "^3.0.0" + "pify": "3.0.0" } }, "pify": { @@ -6738,9 +6738,9 @@ "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", "dev": true, "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" + "load-json-file": "4.0.0", + "normalize-package-data": "2.4.0", + "path-type": "3.0.0" } }, "read-pkg-up": { @@ -6749,8 +6749,8 @@ "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", "dev": true, "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" + "find-up": "2.1.0", + "read-pkg": "3.0.0" } }, "resolve-from": { diff --git a/src/sanitize.ts b/src/sanitize.ts index e79427d..66ec392 100644 --- a/src/sanitize.ts +++ b/src/sanitize.ts @@ -1,7 +1,10 @@ -import { PlainObject, JsonSchema, SchemaCollection } from './objects' -import { assertSchema, getObjectSchema } from './api' +import debugApi from 'debug' import { clone } from 'ramda' +import { assertSchema, getObjectSchema } from './api' import { FormatDefaults } from './formats' +import { JsonSchema, PlainObject, SchemaCollection } from './objects' + +const debug = debugApi('schema-tools') export const isDynamicFormat = (formatDefaults: FormatDefaults | undefined) => ( format: string, @@ -12,7 +15,10 @@ const isString = s => typeof s === 'string' const canPropertyBeString = type => type === 'string' || (Array.isArray(type) && type.includes('string')) -const isArrayType = prop => prop.type === 'array' && prop.items +const canPropertyBeArray = type => + type === 'array' || (Array.isArray(type) && type.includes('array')) + +const isArrayType = prop => canPropertyBeArray(prop.type) && prop.items const isStringArray = prop => isArrayType(prop) && canPropertyBeString(prop.items.type) @@ -45,16 +51,26 @@ export const sanitizeBySchema = ( } const prop = props[key] + debug('looking at property %j', prop) if (key in object && Array.isArray(object[key])) { + debug('%s is present as an array', key) + if (isStringArray(prop)) { + debug('%s is a string array', key) // go through the items in the array and if the format is dynamic // set default values const list: string[] = result[key] as string[] if (prop.items && prop.items.format) { const itemFormat = prop.items.format + debug('items format %s', itemFormat) + if (formatDefaults && isDynamic(itemFormat)) { + debug( + 'format %s is dynamic, need to replace with default value', + itemFormat, + ) const defaultValue = formatDefaults[itemFormat] for (let k = 0; k < list.length; k += 1) { list[k] = defaultValue as string @@ -63,6 +79,8 @@ export const sanitizeBySchema = ( } } } else if (isArrayType(prop) && hasPropertiesArray(prop)) { + debug('property %s is array-like and has properties', key) + const list: PlainObject[] = object[key] as PlainObject[] const propSchema: JsonSchema = prop.items as JsonSchema result[key] = list.map(sanitizeBySchema(propSchema, formatDefaults)) diff --git a/test/sanitize-test.ts b/test/sanitize-test.ts index 4f22341..3dc7e51 100644 --- a/test/sanitize-test.ts +++ b/test/sanitize-test.ts @@ -1,9 +1,9 @@ import test from 'ava' -import { sanitize, sanitizeBySchema } from '../src/sanitize' -import { schemas, exampleFormats } from './example-schemas' import stringify from 'json-stable-stringify' -import { JsonSchema } from '../src/objects' import { getDefaults } from '../src/formats' +import { JsonSchema } from '../src/objects' +import { sanitize, sanitizeBySchema } from '../src/sanitize' +import { exampleFormats, schemas } from './example-schemas' const schemaName = 'person' const schemaVersion = '1.0.0' @@ -33,33 +33,160 @@ test('sanitize with default values', t => { }) }) -const schema: JsonSchema = { - title: 'TestSchema', - type: 'object', - properties: { - createdAt: { - type: 'string', - format: 'date-time', - }, - name: { - type: 'string', - }, - hook: { - type: 'string', - format: 'hookId', - }, - ids: { - type: 'array', - items: { +test('sanitize empty object using schema', t => { + const schema: JsonSchema = { + title: 'TestSchema', + type: 'object', + properties: { + createdAt: { + type: 'string', + format: 'date-time', + }, + name: { + type: 'string', + }, + hook: { type: 'string', - format: 'uuid', + format: 'hookId', + }, + ids: { + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, }, }, - }, -} + } -test('sanitize empty object using schema', t => { const o = {} - const result = sanitizeBySchema(schema)(o) + const result = sanitizeBySchema(schema, formatDefaults)(o) t.deepEqual(result, {}) }) + +test('sanitize string array', t => { + t.plan(1) + const schema: JsonSchema = { + title: 'TestSchema', + type: 'object', + properties: { + names: { + type: 'array', + items: { + type: 'string', + format: 'name', + }, + }, + }, + } + + const o = { + names: ['Joe', 'Mary'], + } + const result = sanitizeBySchema(schema, formatDefaults)(o) + t.deepEqual( + result, + { names: ['Buddy', 'Buddy'] }, + 'both names were replaced with default value for the format', + ) +}) + +test('sanitize array', t => { + const schema: JsonSchema = { + title: 'TestSchema', + type: 'object', + properties: { + names: { + type: 'array', + items: { + // requires "title" in order to be considered a schema + title: 'Name', + type: 'object', + properties: { + name: { + type: 'string', + format: 'name', + }, + }, + }, + }, + }, + } + + const o = { + names: [ + { + name: 'Joe', + }, + { + name: 'Mary', + }, + ], + } + const result = sanitizeBySchema(schema, formatDefaults)(o) + t.deepEqual( + result, + { + names: [ + { + name: 'Buddy', + }, + { + name: 'Buddy', + }, + ], + }, + 'name in each object is sanitized', + ) +}) + +test('sanitize array that can be null', t => { + const schema: JsonSchema = { + title: 'TestSchema', + type: 'object', + properties: { + names: { + // notice that names can be "null" + // https://github.com/cypress-io/schema-tools/issues/53 + type: ['array', 'null'], + items: { + // requires "title" in order to be considered a schema + title: 'Name', + type: 'object', + properties: { + name: { + type: 'string', + format: 'name', + }, + }, + }, + }, + }, + } + + const o = { + names: [ + { + name: 'Joe', + }, + { + name: 'Mary', + }, + ], + } + const result = sanitizeBySchema(schema, formatDefaults)(o) + t.deepEqual( + result, + { + names: [ + { + name: 'Buddy', + }, + { + name: 'Buddy', + }, + ], + }, + 'name in each object is sanitized', + ) +})