From ae4af0042375d1be24eae995bd78207d20678578 Mon Sep 17 00:00:00 2001 From: Lorenzo Sicilia Date: Mon, 25 Nov 2019 12:18:44 +0000 Subject: [PATCH 1/5] write the tests to expose the issue :/ --- src/ObjectSchema.test.js | 60 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/src/ObjectSchema.test.js b/src/ObjectSchema.test.js index b33a9fc..b55231a 100644 --- a/src/ObjectSchema.test.js +++ b/src/ObjectSchema.test.js @@ -554,13 +554,17 @@ describe('ObjectSchema', () => { describe('extend', () => { it('extends a simple schema', () => { const base = S.object() + .id('base') .additionalProperties(false) .prop('foo', S.string().minLength(5)) - const extended = S.extend(base).prop('bar', S.number()) + const extended = S.extend(base) + .id('extended') + .prop('bar', S.number()) expect(extended.valueOf()).toEqual({ $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'extended', additionalProperties: false, properties: { foo: { @@ -576,6 +580,7 @@ describe('ObjectSchema', () => { }) it('extends a nested schema', () => { const base = S.object() + .id('base') .additionalProperties(false) .prop( 'foo', @@ -590,10 +595,13 @@ describe('ObjectSchema', () => { .prop('bol', S.boolean().required()) .prop('num', S.integer().required()) - const extended = S.extend(base).prop('bar', S.number()) + const extended = S.extend(base) + .prop('bar', S.number()) + .id('extended') expect(extended.valueOf()).toEqual({ $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'extended', additionalProperties: false, properties: { foo: { @@ -623,6 +631,54 @@ describe('ObjectSchema', () => { type: 'object', }) }) + it('extends a schema with definitions', () => { + const base = S.object() + .id('base') + .additionalProperties(false) + .definition('def1', S.object().prop('some')) + .definition('def2', S.object().prop('somethingElse')) + .prop( + 'foo', + S.object().prop( + 'id', + S.string() + .format('uuid') + .required() + ) + ) + .prop('str', S.string().required()) + .prop('bol', S.boolean().required()) + .prop('num', S.integer().required()) + + const extended = S.extend(base) + .id('extended') + .definition('def1', S.object().prop('someExtended')) + .prop('bar', S.number()) + + console.log(JSON.stringify(extended.valueOf())) + expect(extended.valueOf()).toEqual({ + $schema: 'http://json-schema.org/draft-07/schema#', + definitions: { + def1: { type: 'object', properties: { someExtended: {} } }, + def2: { type: 'object', properties: { somethingElse: {} } }, + }, + type: 'object', + $id: 'extended', + additionalProperties: false, + properties: { + foo: { + type: 'object', + properties: { id: { type: 'string', format: 'uuid' } }, + required: ['id'], + }, + str: { type: 'string' }, + bol: { type: 'boolean' }, + num: { type: 'integer' }, + bar: { type: 'number' }, + }, + required: ['str', 'bol', 'num'], + }) + }) it('throws an error if a schema is not provided', () => { expect(() => { S.extend() From 5c4be6a349b9cc09dfeec360ece940dfe737d31b Mon Sep 17 00:00:00 2001 From: Lorenzo Sicilia Date: Thu, 28 Nov 2019 13:53:27 +0000 Subject: [PATCH 2/5] Change to S.object().extend(schema) --- README.md | 4 +- package.json | 3 ++ src/BaseSchema.js | 1 + src/FluentSchema.d.ts | 2 +- src/FluentSchema.js | 12 +---- src/ObjectSchema.js | 14 ++++++ src/ObjectSchema.test.js | 20 ++++++--- src/example.js | 3 +- src/types/index.js | 95 ++++++++++++++++++++++++++++++++++++++++ src/types/index.ts | 9 ++-- src/types/tsconfig.json | 2 +- 11 files changed, 139 insertions(+), 26 deletions(-) create mode 100644 src/types/index.js diff --git a/README.md b/README.md index 7cafdc5..64fe9ea 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ A fluent API to generate JSON schemas (draft-07) for Node.js and browser. - Runtime errors for invalid options or keywords misuse - Javascript constants can be used in the JSON schema (e.g. _enum_, _const_, _default_ ) avoiding discrepancies between model and schema - Typescript definitions -- Zero dependencies - Coverage 99% ## Install @@ -289,10 +288,11 @@ const userBaseSchema = S.object() .prop('username', S.string()) .prop('password', S.string()) -const userSchema = S.extend(userBaseSchema) +const userSchema = S.object() .prop('id', S.string().format('uuid')) .prop('createdAt', S.string().format('time')) .prop('updatedAt', S.string().format('time')) + .extend(userBaseSchema) console.log(userSchema) ``` diff --git a/package.json b/package.json index 73f7542..60b0cb2 100644 --- a/package.json +++ b/package.json @@ -62,5 +62,8 @@ "lodash.merge": "^4.6.2", "prettier": "^1.14.3", "typescript": "^3.2.2" + }, + "dependencies": { + "deepmerge": "^4.2.2" } } diff --git a/src/BaseSchema.js b/src/BaseSchema.js index b84d449..6e4f3e5 100644 --- a/src/BaseSchema.js +++ b/src/BaseSchema.js @@ -365,6 +365,7 @@ const BaseSchema = ( * @private It returns the internal schema data structure * @returns {object} */ + // TODO LS if we implement S.raw() we can drop this hack because from a JSON we can rebuild a fluent-schema _getState: () => { return schema }, diff --git a/src/FluentSchema.d.ts b/src/FluentSchema.d.ts index 5c3aa8d..bbe1ff2 100644 --- a/src/FluentSchema.d.ts +++ b/src/FluentSchema.d.ts @@ -114,6 +114,7 @@ export interface ObjectSchema extends BaseSchema { patternProperties: (options: PatternPropertiesOptions) => ObjectSchema dependencies: (options: DependenciesOptions) => ObjectSchema propertyNames: (value: JSONSchema) => ObjectSchema + extend: (schema: ObjectSchema) => ObjectSchema } export interface MixedSchema extends BaseSchema { @@ -145,7 +146,6 @@ export interface S extends BaseSchema { array: () => ArraySchema object: () => ObjectSchema null: () => NullSchema - extend: (schema: ObjectSchema) => ObjectSchema //FIXME LS we should return only a MixedSchema mixed: (types: TYPE[]) => MixedSchema & any } diff --git a/src/FluentSchema.js b/src/FluentSchema.js index 5f89bf1..40b56d0 100644 --- a/src/FluentSchema.js +++ b/src/FluentSchema.js @@ -1,4 +1,6 @@ 'use strict' +const merge = require('deepmerge') + const { FORMATS, TYPES } = require('./utils') const { BaseSchema } = require('./BaseSchema') @@ -170,16 +172,6 @@ module.exports = { string: () => S().string(), mixed: types => S().mixed(types), object: () => S().object(), - extend: schema => { - if (!schema) { - throw new Error("Schema can't be null or undefined") - } - if (!schema.isFluentSchema) { - throw new Error("Schema isn't FluentSchema type") - } - const state = schema._getState() - return S().object(state) - }, array: () => S().array(), boolean: () => S().boolean(), integer: () => S().integer(), diff --git a/src/ObjectSchema.js b/src/ObjectSchema.js index fc3aea5..61ffbb8 100644 --- a/src/ObjectSchema.js +++ b/src/ObjectSchema.js @@ -1,4 +1,5 @@ 'use strict' +const merge = require('deepmerge') const { BaseSchema } = require('./BaseSchema') const { omit, @@ -246,6 +247,19 @@ const ObjectSchema = ({ schema = initialState, ...options } = {}) => { }) }, + extend: base => { + if (!base) { + throw new Error("Schema can't be null or undefined") + } + if (!base.isFluentSchema) { + throw new Error("Schema isn't FluentSchema type") + } + const state = base._getState() + const extended = merge(state, schema) + + return ObjectSchema({ schema: extended, ...options }) + }, + /** * The "definitions" keywords provides a standardized location for schema authors to inline re-usable JSON Schemas into a more general schema. * There are no restrictions placed on the values within the array. diff --git a/src/ObjectSchema.test.js b/src/ObjectSchema.test.js index b55231a..2ed6efe 100644 --- a/src/ObjectSchema.test.js +++ b/src/ObjectSchema.test.js @@ -555,16 +555,20 @@ describe('ObjectSchema', () => { it('extends a simple schema', () => { const base = S.object() .id('base') + .title('base') .additionalProperties(false) .prop('foo', S.string().minLength(5)) - const extended = S.extend(base) + const extended = S.object() .id('extended') + .title('extended') .prop('bar', S.number()) + .extend(base) expect(extended.valueOf()).toEqual({ $schema: 'http://json-schema.org/draft-07/schema#', $id: 'extended', + title: 'extended', additionalProperties: false, properties: { foo: { @@ -595,9 +599,10 @@ describe('ObjectSchema', () => { .prop('bol', S.boolean().required()) .prop('num', S.integer().required()) - const extended = S.extend(base) - .prop('bar', S.number()) + const extended = S.object() .id('extended') + .prop('bar', S.number()) + .extend(base) expect(extended.valueOf()).toEqual({ $schema: 'http://json-schema.org/draft-07/schema#', @@ -650,12 +655,12 @@ describe('ObjectSchema', () => { .prop('bol', S.boolean().required()) .prop('num', S.integer().required()) - const extended = S.extend(base) + const extended = S.object() .id('extended') .definition('def1', S.object().prop('someExtended')) .prop('bar', S.number()) + .extend(base) - console.log(JSON.stringify(extended.valueOf())) expect(extended.valueOf()).toEqual({ $schema: 'http://json-schema.org/draft-07/schema#', definitions: { @@ -679,14 +684,15 @@ describe('ObjectSchema', () => { required: ['str', 'bol', 'num'], }) }) + it('throws an error if a schema is not provided', () => { expect(() => { - S.extend() + S.object().extend() }).toThrow("Schema can't be null or undefined") }) it('throws an error if a schema is not provided', () => { expect(() => { - S.extend('boom!') + S.object().extend('boom!') }).toThrow("Schema isn't FluentSchema type") }) }) diff --git a/src/example.js b/src/example.js index abbc168..330b870 100644 --- a/src/example.js +++ b/src/example.js @@ -113,10 +113,11 @@ const userBaseSchema = S.object() .prop('username', S.string()) .prop('password', S.string()) -const userSchema = S.extend(userBaseSchema) +const userSchema = S.object() .prop('id', S.string().format('uuid')) .prop('createdAt', S.string().format('time')) .prop('updatedAt', S.string().format('time')) + .extend(userBaseSchema) .valueOf() console.log(userSchema.valueOf()) diff --git a/src/types/index.js b/src/types/index.js new file mode 100644 index 0000000..8e69249 --- /dev/null +++ b/src/types/index.js @@ -0,0 +1,95 @@ +'use strict' +// This file will be passed to the TypeScript CLI to verify our typings compile +var __importDefault = + (this && this.__importDefault) || + function(mod) { + return mod && mod.__esModule ? mod : { default: mod } + } +Object.defineProperty(exports, '__esModule', { value: true }) +const FluentSchema_1 = __importDefault(require('../FluentSchema')) +console.log('isFluentSchema:', FluentSchema_1.default.object().isFluentSchema) +const schema = FluentSchema_1.default + .object() + .id('http://foo.com/user') + .title('A User') + .description('A User desc') + .definition( + 'address', + FluentSchema_1.default + .object() + .id('#address') + .prop('line1') + .prop( + 'line2', + FluentSchema_1.default.anyOf([ + FluentSchema_1.default.string(), + FluentSchema_1.default.null(), + ]) + ) + .prop('country') + .allOf([FluentSchema_1.default.string()]) + .prop('city') + .prop('zipcode') + ) + .prop('username', FluentSchema_1.default.string().pattern(/[a-z]*/g)) + .prop('email', FluentSchema_1.default.string().format('email')) + .prop( + 'avatar', + FluentSchema_1.default + .string() + .contentEncoding('base64') + .contentMediaType('image/png') + ) + .required() + .prop( + 'password', + FluentSchema_1.default + .string() + .default('123456') + .minLength(6) + .maxLength(12) + .pattern('.*') + ) + .required() + .prop( + 'addresses', + FluentSchema_1.default + .array() + .items([FluentSchema_1.default.ref('#address')]) + ) + .required() + .prop( + 'role', + FluentSchema_1.default + .object() + .id('http://foo.com/role') + .prop('name') + .enum(['ADMIN', 'USER']) + .prop('permissions') + ) + .required() + .prop('age', FluentSchema_1.default.mixed(['string', 'integer'])) + .ifThen( + FluentSchema_1.default + .object() + .prop('age', FluentSchema_1.default.string()), + FluentSchema_1.default.required(['age']) + ) + .readOnly() + .writeOnly(true) + .valueOf() +console.log('example:\n', JSON.stringify(schema)) +console.log('isFluentSchema:', FluentSchema_1.default.object().isFluentSchema) +const userBaseSchema = FluentSchema_1.default + .object() + .additionalProperties(false) + .prop('username', FluentSchema_1.default.string()) + .prop('password', FluentSchema_1.default.string()) +const userSchema = FluentSchema_1.default + .object() + .prop('id', FluentSchema_1.default.string().format('uuid')) + .prop('createdAt', FluentSchema_1.default.string().format('time')) + .prop('updatedAt', FluentSchema_1.default.string().format('time')) + .extend(userBaseSchema) + .valueOf() +console.log('user:\n', JSON.stringify(userSchema)) diff --git a/src/types/index.ts b/src/types/index.ts index a9932da..3939f9e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -53,18 +53,19 @@ const schema = S.object() .writeOnly(true) .valueOf() -console.log(JSON.stringify(schema)) -console.log(S.object().isFluentSchema) +console.log('example:\n', JSON.stringify(schema)) +console.log('isFluentSchema:', S.object().isFluentSchema) const userBaseSchema = S.object() .additionalProperties(false) .prop('username', S.string()) .prop('password', S.string()) -const userSchema = S.extend(userBaseSchema) +const userSchema = S.object() .prop('id', S.string().format('uuid')) .prop('createdAt', S.string().format('time')) .prop('updatedAt', S.string().format('time')) + .extend(userBaseSchema) .valueOf() -console.log('\n user:', JSON.stringify(userSchema)) +console.log('user:\n', JSON.stringify(userSchema)) diff --git a/src/types/tsconfig.json b/src/types/tsconfig.json index 59f3c7c..160246b 100644 --- a/src/types/tsconfig.json +++ b/src/types/tsconfig.json @@ -3,7 +3,7 @@ "target": "es6", "module": "commonjs", "esModuleInterop": true, - "noEmit": true, + "noEmit": false, "strict": true }, "files": ["./index.ts"] From 9035032dfca0bf6fde54882ae64e259fc83aeb40 Mon Sep 17 00:00:00 2001 From: Lorenzo Sicilia Date: Thu, 28 Nov 2019 14:43:08 +0000 Subject: [PATCH 3/5] cleanup --- src/types/index.js | 95 ---------------------------------------------- 1 file changed, 95 deletions(-) delete mode 100644 src/types/index.js diff --git a/src/types/index.js b/src/types/index.js deleted file mode 100644 index 8e69249..0000000 --- a/src/types/index.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict' -// This file will be passed to the TypeScript CLI to verify our typings compile -var __importDefault = - (this && this.__importDefault) || - function(mod) { - return mod && mod.__esModule ? mod : { default: mod } - } -Object.defineProperty(exports, '__esModule', { value: true }) -const FluentSchema_1 = __importDefault(require('../FluentSchema')) -console.log('isFluentSchema:', FluentSchema_1.default.object().isFluentSchema) -const schema = FluentSchema_1.default - .object() - .id('http://foo.com/user') - .title('A User') - .description('A User desc') - .definition( - 'address', - FluentSchema_1.default - .object() - .id('#address') - .prop('line1') - .prop( - 'line2', - FluentSchema_1.default.anyOf([ - FluentSchema_1.default.string(), - FluentSchema_1.default.null(), - ]) - ) - .prop('country') - .allOf([FluentSchema_1.default.string()]) - .prop('city') - .prop('zipcode') - ) - .prop('username', FluentSchema_1.default.string().pattern(/[a-z]*/g)) - .prop('email', FluentSchema_1.default.string().format('email')) - .prop( - 'avatar', - FluentSchema_1.default - .string() - .contentEncoding('base64') - .contentMediaType('image/png') - ) - .required() - .prop( - 'password', - FluentSchema_1.default - .string() - .default('123456') - .minLength(6) - .maxLength(12) - .pattern('.*') - ) - .required() - .prop( - 'addresses', - FluentSchema_1.default - .array() - .items([FluentSchema_1.default.ref('#address')]) - ) - .required() - .prop( - 'role', - FluentSchema_1.default - .object() - .id('http://foo.com/role') - .prop('name') - .enum(['ADMIN', 'USER']) - .prop('permissions') - ) - .required() - .prop('age', FluentSchema_1.default.mixed(['string', 'integer'])) - .ifThen( - FluentSchema_1.default - .object() - .prop('age', FluentSchema_1.default.string()), - FluentSchema_1.default.required(['age']) - ) - .readOnly() - .writeOnly(true) - .valueOf() -console.log('example:\n', JSON.stringify(schema)) -console.log('isFluentSchema:', FluentSchema_1.default.object().isFluentSchema) -const userBaseSchema = FluentSchema_1.default - .object() - .additionalProperties(false) - .prop('username', FluentSchema_1.default.string()) - .prop('password', FluentSchema_1.default.string()) -const userSchema = FluentSchema_1.default - .object() - .prop('id', FluentSchema_1.default.string().format('uuid')) - .prop('createdAt', FluentSchema_1.default.string().format('time')) - .prop('updatedAt', FluentSchema_1.default.string().format('time')) - .extend(userBaseSchema) - .valueOf() -console.log('user:\n', JSON.stringify(userSchema)) From 38b621144147e994b72862fff0edddc7d2e7b87e Mon Sep 17 00:00:00 2001 From: Lorenzo Sicilia Date: Thu, 28 Nov 2019 14:43:49 +0000 Subject: [PATCH 4/5] Add nodejs 13 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index f5fbf61..078e3eb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: node_js node_js: +- "13" - "11.10.1" # tmp workaround https://stackoverflow.com/questions/55059748/travis-jest-typeerror-cannot-assign-to-read-only-property-symbolsymbol-tostr - "10" - "8" From 9c201cdccfebae69eac95f3fe399ee4bfdaaf7ec Mon Sep 17 00:00:00 2001 From: Lorenzo Sicilia Date: Thu, 28 Nov 2019 14:58:21 +0000 Subject: [PATCH 5/5] back to vanilla node 11 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 078e3eb..322149a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,6 @@ language: node_js node_js: - "13" -- "11.10.1" # tmp workaround https://stackoverflow.com/questions/55059748/travis-jest-typeerror-cannot-assign-to-read-only-property-symbolsymbol-tostr +- "11" - "10" - "8"