Skip to content

Commit 1ddb91d

Browse files
authored
Extend a schema #46 (#47)
* Extend schema #46
1 parent d894f53 commit 1ddb91d

File tree

9 files changed

+162
-12
lines changed

9 files changed

+162
-12
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,31 @@ Output:
274274

275275
{valid: true}
276276

277+
## Extend schema
278+
279+
Normally inheritance with JSON Schema is achieved with `allOf`. However when `.additionalProperties(false)` is used the validator won't
280+
understand which properties come from the base schema. `S.extend` creates a schema merging the base into the new one so
281+
that the validator knows all the properties because it's evaluating only a single schema.
282+
For example in a CRUD API `POST /users` could use the `userBaseSchema` rather than `GET /users` or `PATCH /users` use the `userSchema`
283+
which contains the `id`, `createdAt` and `updatedAt` generated server side.
284+
285+
```js
286+
const S = require('fluent-schema')
287+
const userBaseSchema = S.object()
288+
.additionalProperties(false)
289+
.prop('username', S.string())
290+
.prop('password', S.string())
291+
292+
const userSchema = S.extend(userBaseSchema)
293+
.prop('id', S.string().format('uuid'))
294+
.prop('createdAt', S.string().format('time'))
295+
.prop('updatedAt', S.string().format('time'))
296+
297+
console.log(userSchema)
298+
```
299+
277300
### Detect Fluent Schema objects
301+
278302
Every Fluent Schema objects contains a boolean `isFluentSchema`. In this way you can write your own utilities that understands the Fluent Schema API and improve the user experience of your tool.
279303

280304
```js

src/BaseSchema.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,7 @@ const BaseSchema = (
253253
* @private set a property to a type. Use string number etc.
254254
* @returns {BaseSchema}
255255
*/
256-
as: type => {
257-
return setAttribute({ schema, ...options }, ['type', type])
258-
},
256+
as: type => setAttribute({ schema, ...options }, ['type', type]),
259257

260258
/**
261259
* This validation outcome of this keyword's subschema has no direct effect on the overall validation result.
@@ -363,6 +361,14 @@ const BaseSchema = (
363361
})
364362
},
365363

364+
/**
365+
* @private It returns the internal schema data structure
366+
* @returns {object}
367+
*/
368+
_getState: () => {
369+
return schema
370+
},
371+
366372
/**
367373
* It returns all the schema values
368374
*

src/FluentSchema.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export interface S extends BaseSchema<S> {
145145
array: () => ArraySchema
146146
object: () => ObjectSchema
147147
null: () => NullSchema
148+
extend: (schema: ObjectSchema) => ObjectSchema
148149
//FIXME LS we should return only a MixedSchema
149150
mixed: <T>(types: TYPE[]) => MixedSchema<T> & any
150151
}

src/FluentSchema.integration.test.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ describe('S', () => {
194194
describe('cloning objects retains boolean', () => {
195195
const ajv = new Ajv()
196196
const config = {
197-
schema: S.object().prop('foo', S.string().enum(['foo']))
197+
schema: S.object().prop('foo', S.string().enum(['foo'])),
198198
}
199199
const _config = require('lodash.merge')({}, config)
200200
expect(config.schema[Symbol.for('fluent-schema-object')]).toBeDefined()
@@ -209,14 +209,14 @@ describe('S', () => {
209209
properties: {
210210
foo: {
211211
type: 'string',
212-
enum: ['foo']
213-
}
214-
}
212+
enum: ['foo'],
213+
},
214+
},
215215
})
216216
})
217217

218218
it('valid', () => {
219-
const valid = validate({foo: 'foo'})
219+
const valid = validate({ foo: 'foo' })
220220
expect(validate.errors).toEqual(null)
221221
expect(valid).toBeTruthy()
222222
})

src/FluentSchema.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,10 @@ const S = (
111111
* @returns {ObjectSchema}
112112
*/
113113

114-
object: () =>
114+
object: baseSchema =>
115115
ObjectSchema({
116116
...options,
117-
schema,
117+
schema: baseSchema || schema,
118118
factory: ObjectSchema,
119119
}).as('object'),
120120

@@ -170,6 +170,16 @@ module.exports = {
170170
string: () => S().string(),
171171
mixed: types => S().mixed(types),
172172
object: () => S().object(),
173+
extend: schema => {
174+
if (!schema) {
175+
throw new Error("Schema can't be null or undefined")
176+
}
177+
if (!schema.isFluentSchema) {
178+
throw new Error("Schema isn't FluentSchema type")
179+
}
180+
const state = schema._getState()
181+
return S().object(state)
182+
},
173183
array: () => S().array(),
174184
boolean: () => S().boolean(),
175185
integer: () => S().integer(),

src/ObjectSchema.test.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,4 +550,88 @@ describe('ObjectSchema', () => {
550550
})
551551
})
552552
})
553+
554+
describe('extend', () => {
555+
it('extends a simple schema', () => {
556+
const base = S.object()
557+
.additionalProperties(false)
558+
.prop('foo', S.string().minLength(5))
559+
560+
const extended = S.extend(base).prop('bar', S.number())
561+
562+
expect(extended.valueOf()).toEqual({
563+
$schema: 'http://json-schema.org/draft-07/schema#',
564+
additionalProperties: false,
565+
properties: {
566+
foo: {
567+
type: 'string',
568+
minLength: 5,
569+
},
570+
bar: {
571+
type: 'number',
572+
},
573+
},
574+
type: 'object',
575+
})
576+
})
577+
it('extends a nested schema', () => {
578+
const base = S.object()
579+
.additionalProperties(false)
580+
.prop(
581+
'foo',
582+
S.object().prop(
583+
'id',
584+
S.string()
585+
.format('uuid')
586+
.required()
587+
)
588+
)
589+
.prop('str', S.string().required())
590+
.prop('bol', S.boolean().required())
591+
.prop('num', S.integer().required())
592+
593+
const extended = S.extend(base).prop('bar', S.number())
594+
595+
expect(extended.valueOf()).toEqual({
596+
$schema: 'http://json-schema.org/draft-07/schema#',
597+
additionalProperties: false,
598+
properties: {
599+
foo: {
600+
type: 'object',
601+
properties: {
602+
id: {
603+
type: 'string',
604+
format: 'uuid',
605+
},
606+
},
607+
required: ['id'],
608+
},
609+
bar: {
610+
type: 'number',
611+
},
612+
str: {
613+
type: 'string',
614+
},
615+
bol: {
616+
type: 'boolean',
617+
},
618+
num: {
619+
type: 'integer',
620+
},
621+
},
622+
required: ['str', 'bol', 'num'],
623+
type: 'object',
624+
})
625+
})
626+
it('throws an error if a schema is not provided', () => {
627+
expect(() => {
628+
S.extend()
629+
}).toThrow("Schema can't be null or undefined")
630+
})
631+
it('throws an error if a schema is not provided', () => {
632+
expect(() => {
633+
S.extend('boom!')
634+
}).toThrow("Schema isn't FluentSchema type")
635+
})
636+
})
553637
})

src/example.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,16 @@ console.log(validate.errors)
107107
params: { missingProperty: 'zipcoce' },
108108
message: 'should have required property \'zipcode\'' } ]
109109
*/
110+
111+
const userBaseSchema = S.object()
112+
.additionalProperties(false)
113+
.prop('username', S.string())
114+
.prop('password', S.string())
115+
116+
const userSchema = S.extend(userBaseSchema)
117+
.prop('id', S.string().format('uuid'))
118+
.prop('createdAt', S.string().format('time'))
119+
.prop('updatedAt', S.string().format('time'))
120+
.valueOf()
121+
122+
console.log(userSchema.valueOf())

src/types/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,20 @@ const schema = S.object()
5151
.ifThen(S.object().prop('age', S.string()), S.required(['age']))
5252
.readOnly()
5353
.writeOnly(true)
54-
5554
.valueOf()
5655

5756
console.log(JSON.stringify(schema))
5857
console.log(S.object().isFluentSchema)
58+
59+
const userBaseSchema = S.object()
60+
.additionalProperties(false)
61+
.prop('username', S.string())
62+
.prop('password', S.string())
63+
64+
const userSchema = S.extend(userBaseSchema)
65+
.prop('id', S.string().format('uuid'))
66+
.prop('createdAt', S.string().format('time'))
67+
.prop('updatedAt', S.string().format('time'))
68+
.valueOf()
69+
70+
console.log('\n user:', JSON.stringify(userSchema))

src/utils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,8 @@ const setAttribute = ({ schema, ...options }, attribute) => {
143143
if (currentProp) {
144144
const { name, ...props } = currentProp
145145
return options.factory({ schema, ...options }).prop(name, {
146-
...props,
147146
[key]: value,
147+
...props,
148148
})
149149
}
150150
return options.factory({ schema: { ...schema, [key]: value }, ...options })

0 commit comments

Comments
 (0)