Skip to content

Commit

Permalink
Refactors to non-static methods, enabled schema validation test, stra…
Browse files Browse the repository at this point in the history
…tegy based 'allowNull' mapping
  • Loading branch information
bravo-kernel committed Aug 16, 2019
1 parent fa0de7b commit 7a86654
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 51 deletions.
14 changes: 9 additions & 5 deletions lib/schema-manager.js
Expand Up @@ -73,10 +73,14 @@ class SchemaManager {
const attributeProperties = attributes[attributeName];

// set the type property
result.properties[attributeName] = typeMapper.map(attributeName, attributeProperties);
result.properties[attributeName] = typeMapper.map(
attributeName,
attributeProperties,
strategy,
);

// set the id property
const identifierKeyword = _strategy.get(this).constructor.getIdentifierKeyword();
const identifierKeyword = _strategy.get(this).getIdentifierKeyword();
if (identifierKeyword) {
result.properties[attributeName][identifierKeyword] = `/properties/${attributeName}`;
}
Expand Down Expand Up @@ -188,15 +192,15 @@ class SchemaManager {
const result = {};

// some schemas support the identifier property
const identifierKeyword = _strategy.get(this).constructor.getIdentifierKeyword();
const identifierKeyword = _strategy.get(this).getIdentifierKeyword();
if (identifierKeyword) {
result[identifierKeyword] = this._getModelUri();
}

// some schemas support the schema identifier
const schemaKeyword = _strategy.get(this).constructor.getSchemaKeyword();
const schemaKeyword = _strategy.get(this).getSchemaKeyword();
if (schemaKeyword) {
result[schemaKeyword] = _strategy.get(this).constructor.getSchemaUri();
result[schemaKeyword] = _strategy.get(this).getSchemaUri();
}

// all schemas support the title property
Expand Down
19 changes: 16 additions & 3 deletions lib/strategies/json-schema-v6.js
@@ -1,3 +1,5 @@
/* eslint-disable class-methods-use-this */

const StrategyInterface = require('../strategy-interface');

/**
Expand All @@ -13,7 +15,7 @@ class JsonSchema6Strategy extends StrategyInterface {
*
* @returns {string}
*/
static getSchemaUri() {
getSchemaUri() {
return 'https://json-schema.org/draft-06/schema#';
}

Expand All @@ -24,7 +26,7 @@ class JsonSchema6Strategy extends StrategyInterface {
*
* @returns {string}
*/
static getSchemaKeyword() {
getSchemaKeyword() {
return '$schema';
}

Expand All @@ -35,9 +37,20 @@ class JsonSchema6Strategy extends StrategyInterface {
*
* @returns {string}
*/
static getIdentifierKeyword() {
getIdentifierKeyword() {
return '$id';
}

/**
* Returns `null` to be used as direct property of `format`
*
* @example
*
* @returns {object|string}
*/
getNullProperty() {
return 'null';
}
}

module.exports = JsonSchema6Strategy;
21 changes: 18 additions & 3 deletions lib/strategies/openapi-v3.js
@@ -1,3 +1,5 @@
/* eslint-disable class-methods-use-this */

const StrategyInterface = require('../strategy-interface');

/**
Expand All @@ -13,7 +15,7 @@ class OpenApi3Strategy extends StrategyInterface {
* @returns {string}
*/
static getSchemaUri() {
getSchemaUri() {
return 'https://json-schema.org/draft-06/schema#';
}

Expand All @@ -25,7 +27,7 @@ class OpenApi3Strategy extends StrategyInterface {
*
* @returns {null}
*/
static getSchemaKeyword() {
getSchemaKeyword() {
return null;
}

Expand All @@ -37,9 +39,22 @@ class OpenApi3Strategy extends StrategyInterface {
*
* @returns {null}
*/
static getIdentifierKeyword() {
getIdentifierKeyword() {
return null;
}

/**
* Returns new property `nullable: true` used as new property
*
* @example
*
* @returns {object|string}
*/
getNullProperty() {
return {
nullable: true,
};
}
}

module.exports = OpenApi3Strategy;
10 changes: 10 additions & 0 deletions lib/strategy-interface.js
Expand Up @@ -40,6 +40,16 @@ class StrategyInterface {
);
}

/**
* Must return the value used to update the schema if Sequelize attribute
* property `allowNull` has been enabled.
*
* @returns {object|null}
*/
getNullProperty() {
this.constructor._throwMissingImplementationError(this.constructor.name, 'getNullProperty');
}

/**
* Must return the strategy specific 'example' property.
*
Expand Down
57 changes: 39 additions & 18 deletions lib/utils/type-mapper.js
@@ -1,3 +1,5 @@
const _ = require('lodash'); // limit later to `merge`, `capitalize`, etc.

// Common types. These should never be exposed directly but, rather, get cloned
// before being returned. This avoids cross-contamination if a user modifies
// the their schema.
Expand All @@ -15,15 +17,17 @@ const STRING_LENGTHS = { tiny: 255, medium: 16777215, long: 4294967295 };
*/
class TypeMapper {
/**
* Translates the Sequelize attribute `type` property to the correlating JSON schema supported type.
* Translates the `type` properties of a Sequelize attribute `type` property to the correlating JSON schema supported type.
*
* @param {Object} attribute Sequelize attribute
* @returns {Object} property schema
* @param {string} attributeName Name of the Sequelize attribute
* @param {object} properties Properties of the Sequelize attribute
* @param {StrategyInterface} strategy Strategy instance*
* @returns {object} property schema
*/
map(attributeName, attribute) {
map(attributeName, properties, strategy) {
let schema;

let attributeType = attribute && attribute.type && attribute.type.key;
let attributeType = properties && properties.type && properties.type.key;

// NOTE: All known sequelize types should be mentioned in the switch blocks
// below, either under aliases or transforms (but may be commented out if not
Expand All @@ -39,7 +43,7 @@ class TypeMapper {
case 'VIRTUAL': {
// Use schema for the return type (if defined)
attributeType =
attribute.type && attribute.type.returnType && attribute.type.returnType.key;
properties.type && properties.type.returnType && properties.type.returnType.key;
break;
}

Expand All @@ -55,7 +59,7 @@ class TypeMapper {
schema = {
...ARRAY,
// Sequelize requires attribute.type to be defined for ARRAYs
items: this.map({ type: attribute.type.type, allowNull: false }),
items: this.map({ type: properties.type.type, allowNull: false }),
};
break;
}
Expand Down Expand Up @@ -100,7 +104,7 @@ class TypeMapper {
break;
}
case 'ENUM': {
schema = { ...STRING, enum: [...attribute.values] };
schema = { ...STRING, enum: [...properties.values] };
break;
}
case 'FLOAT': {
Expand All @@ -110,6 +114,9 @@ class TypeMapper {
// GEOGRAPHY - needs definition
// GEOMETRY - needs definition
// HSTORE - needs definition

// @todo: fix this one, does not validate against version 7 schema
// @see https://github.com/Julian/jsonschema/issues/171
case 'INET': {
schema = { type: [{ ...STRING, format: 'ipv4' }, { ...STRING, format: 'ipv6' }] };
break;
Expand Down Expand Up @@ -152,14 +159,14 @@ class TypeMapper {

case 'STRING': {
schema = { ...STRING };
let length = attribute.type.options && attribute.type.options.length;
let length = properties.type.options && properties.type.options.length;

// Resolve aliases
length = STRING_LENGTHS[length] || length;
if (length) schema.maxLength = length;

const binary = attribute.type.options;
if (binary) schema.format = 'binary';
// const binary = properties.type.options;
// if (binary) schema.format = 'binary';

break;
}
Expand Down Expand Up @@ -187,24 +194,38 @@ class TypeMapper {

case 'VIRTUAL': {
// Use schema for the return type (if defined)
schema = this.map({ ...attribute, type: attribute.type && attribute.type.returnType });
schema = this.map({ ...properties, type: properties.type && properties.type.returnType });
break;
}

default:
break;
}

// Use ANY for anything that's not recognized. 'Not entirely sure
// this is the right thing to do. File an issue if you think it should behave
// differently.
// throw an exception if we receive unknown (non) Sequelize types.
if (!schema)
throw new TypeError(
`Cannot map attribute '${attributeName}' because of unkown datatype '${attributeName}'`,
`Your Sequelize attribute '${attributeName}' contains unkown datatype '${attributeName}'`,
);

// Add 'null' type?
if (attribute.allowNull !== false) this.constructor.allowNullType(schema, attribute.allowNull);
// if the attribute has enabled Sequelize option `allowNull` use the
// strategy specific return value to update the schema accordingly.
// adjust the schema as required for the current strategy
if (properties.allowNull === true) {
const nullFormat = strategy.getNullProperty();

if (typeof nullFormat === 'string') {
if (!Array.isArray(schema.type)) schema.type = [schema.type];
schema.type.push(nullFormat);
}

if (typeof nullFormat === 'object') {
schema = _.merge(schema, nullFormat);
}
}

// WAS:
// if (properties.allowNull !== false) this.constructor.allowNullType(schema, properties.allowNull);

return schema;
}
Expand Down
1 change: 1 addition & 0 deletions test-the-strategy-pattern.js
Expand Up @@ -41,6 +41,7 @@ let userSchema = schemaManager.generate(userModel, json6strategy);

console.log('JSON Schema v6:')
console.log(userSchema);
console.log(JSON.stringify(userSchema, null, 2));

// ----------------------------------
// Generate OpenAPI v3 schema
Expand Down
29 changes: 16 additions & 13 deletions test/models/user.js
Expand Up @@ -31,18 +31,18 @@ module.exports = sequelize => {
},

// STRING (default)
_STRING: {
_STRING_: {
type: DataTypes.STRING,
allowNull: true, // nice test because this should become `nullable: true` for OpenApi
},

// STRING(1234)
_STRING_LENGTH: {
_STRING_LENGTH_: {
type: DataTypes.STRING(50),
},

// STRING.BINARY
_STRING_BINARY: {
_STRING_BINARY_: {
type: DataTypes.STRING.BINARY,
},

Expand All @@ -53,16 +53,24 @@ module.exports = sequelize => {
},

// UUIDv4
_UUIDv4: {
_UUIDv4_: {
type: DataTypes.UUID, // could be v1 or v4 ??
},

// INET is a good design-driving test as the TypeMapper returned value (array) breaks OpenApi v3.
// The INET type holds an IPv4 or IPv6 host address, and optionally its subnet. Takes 7 or 19 bytes
// @todo disabled until the mapper is fixed, note there
// _INET_: {
// type: Sequelize.INET,
// allowNull: false,
// },

// ----------------------------------------------------------------------
// additions to sequelize datatypes, used to check overrides etc.
// ----------------------------------------------------------------------

// email
email: {
_EMAIL_: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
Expand All @@ -76,17 +84,12 @@ module.exports = sequelize => {
examples: ['Example 1', 'Example 2'],
},
},

// password
password: {
_PASSWORD_: {
type: Sequelize.STRING,
allowNull: false,
},
// hostAddress is a good design-driving test as the TypeMapper returned value (array) breaks OpenApi v3.
// The INET type holds an IPv4 or IPv6 host address, and optionally its subnet. Takes 7 or 19 bytes
hostAddress: {
type: Sequelize.INET,
allowNull: false,
},
},
// sequelize options
{
Expand All @@ -101,7 +104,7 @@ module.exports = sequelize => {
allowNull: false,
};

delete Model.rawAttributes.email;
// delete Model.rawAttributes.email;

Model.refreshAttributes();

Expand Down
18 changes: 9 additions & 9 deletions test/strategies/openapi-v3/stragegy.test.js
Expand Up @@ -30,17 +30,17 @@ describe('OpenAPI v3 strategy (#integration)', function() {
});
});

// describe('Validation', function() {
// it('passes schema validation against the Swagger Parser', async () => {
// expect.assertions(1);
describe('Validation', function() {
it('passes Swagger Parser schema validation', async () => {
expect.assertions(1);

// // https://github.com/APIDevTools/swagger-parser/issues/77
// // @todo: enable once fixed, now blocks husky pre-commit hooks
// const result = await SwaggerParser.validate(_.cloneDeep(schemaWrapper));
// https://github.com/APIDevTools/swagger-parser/issues/77
// @todo: enable once fixed, now blocks husky pre-commit hooks
const result = await SwaggerParser.validate(_.cloneDeep(schemaWrapper));

// expect(result).toHaveProperty('info');
// });
// });
expect(result).toHaveProperty('info');
});
});

// @todo this should be detected by eslint-plugin-jest no-disabled-tests (but is not)
// test('', function() {
Expand Down

0 comments on commit 7a86654

Please sign in to comment.