Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

fully-typed

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fully-typed - npm Package Compare versions

Comparing version 1.0.0 to 1.1.0

examples/add-numbers.js

53

bin/controllers.js

@@ -18,2 +18,3 @@ /**

'use strict';
const crypto = require('crypto');
const util = require('./util');

@@ -32,2 +33,3 @@

const dependencies = new Map();
const instances = new WeakMap();

@@ -58,2 +60,3 @@ /**

const normalizeFunctions = [];
const firstStringAlias = aliases.filter(a => typeof a === 'string')[0];

@@ -73,6 +76,37 @@ // verify that inherits exist already and build inheritance arrays

/**
* Create a schema instance.
* @param {object} config The configuration for the schema.
* @param {object} schema The schema controller.
* @constructor
*/
function Schema(config, schema) {
const length = ctrls.length;
const extended = config.hasOwnProperty('__') ? config.__ : {};
this.Schema = schema;
// apply controllers to this schema
for (let i = 0; i < length; i++) ctrls[i].call(this, config);
// add additional properties
if (util.isPlainObject(extended.properties)) Object.defineProperties(this, extended.properties);
// create a hash
const protect = {};
const options = getNormalizedSchemaConfiguration(this);
protect.hash = crypto
.createHash('sha256')
.update(Object.keys(options)
.map(key => {
const value = options[key];
if (typeof value === 'function') return value.toString();
if (typeof value === 'object') return JSON.stringify(value);
return value;
})
.join('')
)
.digest('hex');
// store the protected data
instances.set(this, protect);
}

@@ -90,2 +124,6 @@

Schema.prototype.hash = function() {
return instances.has(this) ? instances.get(this).hash : ''
};
Schema.prototype.normalize = function(value) {

@@ -101,2 +139,8 @@ if (typeof value === 'undefined' && this.hasDefault) value = this.default;

Schema.prototype.toJSON = function() {
const options = getNormalizedSchemaConfiguration(this);
if (typeof options.type === 'function') options.type = options.type.name || firstStringAlias || 'anonymous';
return options;
};
Schema.prototype.validate = function(value, prefix) {

@@ -209,2 +253,11 @@ const o = this.error(value, prefix);

return factory;
}
function getNormalizedSchemaConfiguration(obj) {
return Object.getOwnPropertyNames(obj)
.filter(k => k !== 'Schema')
.reduce((prev, key) => {
prev[key] = obj[key];
return prev;
}, {});
}

135

bin/object.js

@@ -35,4 +35,4 @@ /**

if (hasProperties && !util.isPlainObject(config.properties)) {
const message = util.propertyErrorMessage('properties', config.properties, 'Must be a plain object.');
if (hasProperties && !util.isValidSchemaConfiguration(config.properties)) {
const message = util.propertyErrorMessage('properties', config.properties, 'Must be a plain object or an array of plain objects.');
const err = Error(message);

@@ -42,2 +42,11 @@ util.throwWithMeta(err, util.errors.config);

if (config.hasOwnProperty('schema')) {
if (!util.isValidSchemaConfiguration(config.schema)) {
const message = util.propertyErrorMessage('schema', config.schema, 'Must be a plain object or an array of plain objects.');
const err = Error(message);
util.throwWithMeta(err, util.errors.config);
}
validateSchemaConfiguration('schema', config.schema);
}
Object.defineProperties(object, {

@@ -73,2 +82,12 @@

writable: false
},
schema: {
/**
* @property
* @name TypedObject#schema
* @type {object, undefined}
*/
value: config.schema ? Schema(mergeSchemas(config.schema)) : undefined,
writable: false
}

@@ -81,5 +100,7 @@

.forEach(function(key) {
const value = object.properties[key] || {};
let options = object.properties[key] || {};
const optionsIsPlain = !Array.isArray(options);
const schemaIsPlain = !Array.isArray(config.schema);
if (!util.isPlainObject(value)) {
if (!util.isValidSchemaConfiguration(options)) {
const err = Error('Invalid configuration for property: ' + key + '. Must be a plain object.');

@@ -89,25 +110,35 @@ util.throwWithMeta(err, util.errors.config);

const schema = Schema(value);
// merge generic schema with property specific schemas
if (config.schema) {
if (schemaIsPlain && optionsIsPlain) {
options = mergeSchemas(config.schema, options);
} else if (schemaIsPlain) {
options = options.map(item => mergeSchemas(config.schema, item));
} else if (optionsIsPlain) {
options = config.schema.map(item => mergeSchemas(item, options));
} else {
const array = [];
for (let i = 0; i < options.length; i++) {
for (let j = 0; j < config.schema.length; j++) {
array.push(mergeSchemas(config.schema[j], options[i]));
}
}
options = array;
}
} else if (optionsIsPlain) {
options = mergeSchemas(options);
} else {
options = options.map(o => mergeSchemas(o));
}
// create a schema instance for each property
const schema = Schema(options);
object.properties[key] = schema;
// required
if (value.required && schema.hasDefault) {
const err = Error('Invalid configuration for property: ' + key + '. Cannot make required and provide a default value.');
util.throwWithMeta(err, util.errors.config);
if (Array.isArray(options)) {
schema.schemas.forEach((s, i) => validateSchemaConfiguration(key, s))
} else {
validateSchemaConfiguration(key, schema);
}
// add properties to the schema
Object.defineProperties(schema, {
required: {
/**
* @property
* @name Typed#required
* @type {boolean}
*/
value: value ? !!value.required : false,
writable: false
}
});
});

@@ -130,7 +161,7 @@

const object = this;
// check that all required properties exist
Object.keys(object.properties)
.forEach(function(key) {
const schema = object.properties[key];
// if required then check that it exists on the value
if (schema.required && !value.hasOwnProperty(key)) {

@@ -140,12 +171,18 @@ const err = util.errish('Missing required value for property: ' + key, TypedObject.errors.required);

errors.push(err);
return;
}
});
// run inherited error check if it exists on the value
if (value.hasOwnProperty(key)) {
const err = schema.error(value[key]);
if (err) {
err.property = key;
errors.push(err);
}
// validate each property value
Object.keys(value)
.forEach(key => {
const schema = object.properties.hasOwnProperty(key)
? object.properties[key]
: object.schema;
if (!schema) return;
// run inherited error check on property
const err = schema.error(value[key]);
if (err) {
err.property = key;
errors.push(err);
}

@@ -204,2 +241,32 @@ });

}
};
};
function mergeSchemas(general, specific) {
const merged = Object.assign({}, general, specific || {});
merged.__ = {
properties: {
required: {
value: !!merged.required
}
}/*,
error: function(value, prefix) {
return;
},
normalize: function(value) {
}*/
};
return merged;
}
function validateSchemaConfiguration (key, schema) {
// required
if (schema.required && schema.hasDefault) {
const err = Error('Invalid configuration for property: ' + key + '. Cannot make required and provide a default value.');
util.throwWithMeta(err, util.errors.config);
}
}

@@ -18,2 +18,3 @@ /**

'use strict';
const crypto = require('crypto');
const util = require('./util');

@@ -25,3 +26,3 @@

* Get a typed schema.
* @param {object} [configuration={}]
* @param {object, object[]} [configuration={}]
* @returns {{ error: Function, normalize: Function, validate: Function }}

@@ -36,26 +37,52 @@ */

// multiple configuration tries all schemas
const schemas = configuration.map(createSchema);
const hashes = {};
const schemas = configuration.map((config, i) => createSchema(config))
.filter(schema => {
const hash = schema.hash();
if (hashes[hash]) return false;
hashes[hash] = true;
return true;
});
return {
error: function(value, prefix) {
const data = getPassingSchema(schemas, value);
return data.passing ? null : getMultiError(data.errors, prefix);
},
// generate the hash
const hash = crypto.createHash('sha256')
.update(schemas.map(schema => schema.hash).join(''))
.digest('hex');
normalize: function(value) {
const data = getPassingSchema(schemas, value, '');
if (data.passing) return data.schema.normalize(value);
const meta = getMultiError(data.errors, '');
const err = Error(meta.message);
util.throwWithMeta(err, meta);
},
const result = new MultiSchema();
validate: function(value, prefix) {
const o = this.error(value, prefix);
if (o) {
const err = Error(o.message);
util.throwWithMeta(err, o);
}
result.error = function(value, prefix) {
const data = getPassingSchema(schemas, value);
return data.passing ? null : getMultiError(data.errors, prefix);
};
result.hash = function() {
return hash;
};
result.normalize = function(value) {
const data = getPassingSchema(schemas, value, '');
if (data.passing) return data.schema.normalize(value);
const meta = getMultiError(data.errors, '');
const err = Error(meta.message);
util.throwWithMeta(err, meta);
};
Object.defineProperty(result, 'schemas', {
get: () => schemas.slice(0)
});
result.toJSON = function() {
return schemas.map(schema => schema.toJSON());
};
result.validate = function(value, prefix) {
const o = this.error(value, prefix);
if (o) {
const err = Error(o.message);
util.throwWithMeta(err, o);
}
}
};
return result;
}

@@ -120,2 +147,4 @@

return err;
}
}
function MultiSchema() {}

@@ -98,2 +98,14 @@ /**

exports.isValidSchemaConfiguration = function(value) {
if (Array.isArray(value)) {
const length = value.length;
for (let i = 0; i < length; i++) {
if (!exports.isPlainObject(value[i])) return false;
}
return true;
} else {
return exports.isPlainObject(value);
}
};
exports.propertyErrorMessage = function (property, actual, expected) {

@@ -100,0 +112,0 @@ return 'Invalid configuration value for property: ' + property + '. ' + expected + ' Received: ' + quoteWrap(actual);

{
"name": "fully-typed",
"version": "1.0.0",
"version": "1.1.0",
"description": "Run time type validation, transformation, and error generator that works out of the box on primitives, objects, arrays, and nested objects. Also extensible for custom types.",

@@ -5,0 +5,0 @@ "main": "index.js",

@@ -1,2 +0,1 @@

[![npm module downloads](http://img.shields.io/npm/dt/fully-typed.svg)](https://www.npmjs.org/package/fully-typed)
[![Build status](https://img.shields.io/travis/byu-oit-appdev/fully-typed.svg?style=flat)](https://travis-ci.org/byu-oit-appdev/fully-typed)

@@ -11,3 +10,3 @@

- Create schemas to validate values against.
- Build in support for arrays, booleans, functions, numbers, objects, strings, and symbols.
- Built in support for arrays, booleans, functions, numbers, objects, strings, and symbols.
- Extensible - use plugins or create your own to integrate more types.

@@ -22,2 +21,52 @@ - Get detailed error messages when a wrong value is run against a schema.

const schema = Typed({
type: Number,
default: 0
});
// will only add numbers, throws errors otherwise
exports.add = function (a, b) {
a = schema.normalize(a); // throw an error if not a number or if undefined defaults to 0
b = schema.normalize(b);
return a + b;
};
```
**Example**
```js
const Typed = require('fully-typed');
const schema = Typed({
type: Object,
properties: {
name: {
required: true,
type: String,
minLength: 1
},
age: {
type: Number,
min: 0
},
employed: {
type: Boolean,
default: true
}
}
});
function addPerson(configuration) {
const config = schema.normalize(configuration); // If the input is invalid an error is thrown
// with specifics as to why it failed
// ... do more stuff
}
```
**Example**
```js
const Typed = require('fully-typed');
// define a schema

@@ -299,8 +348,8 @@ const positiveIntegerSchema = Typed({

type: Number,
max: 1
min: 1
});
schema.error(-1); // no errors
schema.error(-1); // error
schema.error(1); // no errors
schema.error(2); // error
schema.error(2); // no errors
```

@@ -386,2 +435,82 @@

- *schema* - (Object) A configuration schema to apply to each property on the object. This is useful for allowing objects to have any property but requiring that each property adhere to a schema. If specific properties are defined then the schema defined here will be extended by and superseded by the specific property's schema.
```js
const schema = Typed({
type: Object,
// schema specifics for a single property extend the general schema
properties: {
name: {
// the type is inherited as String
minLength: 1, // min length of 1 overwrites general min length of 10
required: true // name is required
},
age: {
// because this property is of type Number the non-number properties
// in the general schema definition are ignored
type: Number
}
},
// a generic schema to apply to all properties within the object
schema: {
type: String,
minLength: 10
}
});
schema.error({ name: 'Bob' }); // no errors
schema.error({}); // error
```
The following example shows a [one-of](#one-of) general schema definition. All variations of the general schema are possible extensions across the property specific schemas.
```js
const schema = Typed({
type: Object,
// schema specifics for a single property extend the general schema
properties: {
name: {
type: String, // because this is a string it will extend
// the String specific general schema
minLength: 1
},
age: {
// because this property is of type Number it extends one of the generic number schemas
type: Number
}
foo: {
// the type might be a String or Number
min: 5, // this property will only apply if the type is a number and
// it will supersede the general min value
minLength: 1, // this property will only apply if the type is a string
required: true // name is required
}
},
// a generic schema to apply to all properties within the object
schema: [
{
type: String,
maxLength: 10
},
{
type: Number,
min: 0,
max: 10
},
{
type: Number,
min: 20,
max: 30
}
]
});
schema.error({ name: 'Bob' }); // no errors
schema.error({}); // error
```
### One-Of

@@ -542,2 +671,101 @@

schema.validate('a'); // throws an error
```
## Plugins
### Write the Plugin
To write a plugin you need to define and export the controller for a type.
**truthy-controller.js**
```js
module.exports = Truthy;
function Truthy (config) {
// process the user's schema configuration
const additonalNotTruthyValues = Array.isArray(config.notTruthy)
? config.notTruthy
: [];
const allNotTruthyValues = [false, 0, null, '', undefined].concat(additionalNotTruthyValues);
// define properties that the Foo type keeps
Object.defineProperties(this, {
notTruthy: {
value: allNotTruthyValues,
writable: false
}
});
}
Foo.prototype.error = function (value, prefix) {
const falsy = this.notTruthy.indexOf(value) !== -1;
return falsy
? prefix + 'Value is not truthy: ' + value
: null;
};
TypedBoolean.prototype.normalize = function (value) {
return !!value; // make the value true (not just truthy)
};
```
### Add a Plugin
You can add an existing plugin to any project by telling fully-typed about the new type controller.
```js
const Typed = require('fully-typed');
const Truthy = require('./truthy-controller');
Typed.controllers.define(['truthy'], Truthy, ['typed']);
```
#### Schema.controllers.define
**Parameters:**
- *aliases* - An array of aliases that are used when defining schemas to identify the controller to use. Schemas can be any value and any type. For example, the predefined `String` schema has two aliases: `['string', String]'`.
- *controller* - The controller to define.
- *inherits* - An array of aliases whose configuration properties, validations, and normalizations should be inherited for this controller. For example, all type definitions for fully-typed inherit from `'typed'`.
#### An Idea
You may not want to ask the user's of your plugin to specify it's aliases and dependencies, but you do need to ask the user of your plugin to supply the Schema library to be used. Here's an alternative.
Your module:
```js
const Truthy = require('./truthy-controller');
module.exports = function (Typed) {
Typed.controllers.define(['truthy', Truthy], Truthy, ['typed']);
return Truthy;
}
```
Their Code:
```js
const Typed = require('fully-typed');
const Truthy = require('truthy')(Typed);
const schema = Typed({
type: Truthy, // or 'truthy' since that is an alias too
notTruthy: ['false'] // string of 'false' added as non-truthy value
});
function foo(param) {
schema.validate(param);
// do stuff
};
foo('hello'); // runs successfully
foo('false'); // throws an error stating: 'Value is not truthy: false'
```

@@ -28,7 +28,24 @@ /**

it('properties must be plain object', () => {
const e = util.extractError(() => Schema({ type: Object, properties: null }));
it('properties cannot be null', () => {
const properties = null;
const e = util.extractError(() => Schema({ type: Object, properties: properties }));
expect(e.code).to.equal(util.errors.config.code);
});
it('properties can be a plain object', () => {
const properties = {};
expect(() => Schema({ type: Object, properties: properties })).not.to.throw(Error);
});
it('properties can be an array of plain objects', () => {
const properties = [{}];
expect(() => Schema({ type: Object, properties: properties })).not.to.throw(Error);
});
it('properties cannot be an array with a non plain objects', () => {
const properties = [null];
const e = util.extractError(() => Schema({ type: Object, properties: properties }));
expect(e.code).to.equal(util.errors.config.code);
});
it('property cannot be number', () => {

@@ -103,2 +120,235 @@ const e = util.extractError(() => Schema({ type: Object, properties: { a: 123 } }));

describe('general schema', () => {
it('can have schema property', () => {
const o = Schema({ type: Object, schema: { type: String } });
expect(o).to.have.ownProperty('schema');
});
it('cannot be a null object', () => {
const config = { type: Object, schema: null };
expect(() => Schema(config)).to.throw(Error);
});
it('can be a non null object', () => {
const config = { type: Object, schema: { type: String } };
expect(() => Schema(config)).to.not.throw(Error);
});
it('can be an array of objects', () => {
const config = { type: Object, schema: [{ type: String }, { type: Number }] };
expect(() => Schema(config)).to.not.throw(Error);
});
describe('extends across properties', () => {
let o;
describe('plain specific and plain general', () => {
before(() => {
o = Schema({
type: Object,
properties: {
a: {
type: String
},
b: {
type: Number
},
c: {
required: false
}
},
schema: {
type: Boolean,
minLength: 10,
min: 10,
required: true
}
});
});
it('a', () => {
expect(o.properties.a.type).to.equal(String);
expect(o.properties.a.minLength).to.equal(10);
expect(o.properties.a).not.to.have.ownProperty('min');
expect(o.properties.a.required).to.be.true;
});
it('b', () => {
expect(o.properties.b.type).to.equal(Number);
expect(o.properties.b.min).to.equal(10);
expect(o.properties.b).not.to.have.ownProperty('minLength');
expect(o.properties.b.required).to.be.true;
});
it('c', () => {
expect(o.properties.c.type).to.equal(Boolean);
expect(o.properties.c).not.to.have.ownProperty('min');
expect(o.properties.c).not.to.have.ownProperty('minLength');
expect(o.properties.c.required).to.be.false;
});
});
describe('plain specific and array general', () => {
let o;
before(() => {
o = Schema({
type: Object,
properties: {
a: {
type: String
},
b: {
type: Number
},
c: {
required: false
}
},
schema: [{
type: Boolean,
required: true
}]
});
});
it('a', () => {
expect(o.properties.a.schemas[0].type).to.equal(String);
expect(o.properties.a.schemas[0].required).to.be.true;
expect(o.properties.a.schemas.length).to.equal(1);
});
it('b', () => {
expect(o.properties.b.schemas[0].type).to.equal(Number);
expect(o.properties.b.schemas[0].required).to.be.true;
expect(o.properties.b.schemas.length).to.equal(1);
});
it('c', () => {
expect(o.properties.c.schemas[0].type).to.equal(Boolean);
expect(o.properties.c.schemas[0].required).to.be.false;
expect(o.properties.c.schemas.length).to.equal(1);
});
});
describe('array specific and plain general', () => {
let o;
before(() => {
o = Schema({
type: Object,
properties: {
a: [
{
type: String
},
{
type: Number,
required: false
}
]
},
schema: {
type: Boolean,
required: true
}
});
});
it('has 2 distinct configurations', () => {
expect(o.properties.a.schemas.length).to.equal(2);
});
it('a[0]', () => {
expect(o.properties.a.schemas[0].type).to.equal(String);
expect(o.properties.a.schemas[0].required).to.be.true;
});
it('a[1]', () => {
expect(o.properties.a.schemas[1].type).to.equal(Number);
expect(o.properties.a.schemas[1].required).to.be.false;
});
});
describe('array specific and array general', () => {
let o;
before(() => {
o = Schema({
type: Object,
properties: {
a: [
{
type: String
},
{
type: Number
}
]
},
schema: [
{
type: String,
required: true
},
{
type: Number
},
{
min: 0
}
]
});
});
// some combinations create the same configuration, there are 5 distinct configurations here
it('has 5 distinct configurations', () => {
expect(o.properties.a.schemas.length).to.equal(5);
});
it('a[0]', () => {
const s = o.properties.a.schemas[0];
expect(s.type).to.equal(String);
expect(s.required).to.be.true;
});
it('a[1]', () => {
const s = o.properties.a.schemas[1];
expect(s.type).to.equal(String);
expect(s.required).to.be.false;
});
it('a[2]', () => {
const s = o.properties.a.schemas[2];
expect(s.type).to.equal(Number);
expect(s.required).to.be.true;
expect(s.min).to.be.NaN;
});
it('a[3]', () => {
const s = o.properties.a.schemas[3];
expect(s.type).to.equal(Number);
expect(s.required).to.be.false;
expect(s.min).to.be.NaN;
});
it('a[4]', () => {
const s = o.properties.a.schemas[4];
expect(s.type).to.equal(Number);
expect(s.required).to.be.false;
expect(s.min).to.equal(0);
});
});
});
});
describe('#error', () => {

@@ -140,2 +390,16 @@

it('checks for array of schemas', () => {
const o = Schema({ type: Object, properties: { x: [{ type: Number }, {type: String}] } });
expect(o.error({ x: 'hello' })).to.be.null;
expect(o.error({ x: 123 })).to.be.null;
expect(o.error({ x: true })).not.to.be.null;
});
it('can validate against non-defined parameters using schema', () => {
const o = Schema({ type: Object, schema: {type: Number} });
expect(o.error({ foo: 123 })).to.be.null;
expect(o.error({ foo: 'abc' })).not.to.be.null;
expect(o.error({ foo: true })).not.to.be.null;
});
});

@@ -142,0 +406,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc