UNPKG

@alt3/sequelize-to-json-schemas

Version:

Convert Sequelize models into various JSON Schema variants (using the Strategy Pattern)

396 lines (350 loc) 12.9 kB
// 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. const OBJECT = { type: 'object' }; const ARRAY = { type: 'array' }; const BOOLEAN = { type: 'boolean' }; const INTEGER = { type: 'integer' }; const NUMBER = { type: 'number' }; const STRING = { type: 'string' }; const ANY = { anyOf: [OBJECT, ARRAY, BOOLEAN, INTEGER, NUMBER, STRING] }; /** * Class responsible for converting Sequelize DataTypes to strategy-compatible `type` properties. * * @copyright Copyright (c) 2019 ALT3 B.V. * @license Licensed under the MIT License */ class TypeMapper { /** * Returns the strategy-specific `type` property for the given Sequelize DataType * * @see {@link https://sequelize.org/master/manual/data-types.html} * @param {string} attributeName Name of the Sequelize attribute * @param {object} properties Properties of the Sequelize attribute * @param {StrategyInterface} strategy Strategy instance * @returns {object} Object holding the `type` property * @throws {TypeError} Throws an exception if the resolved DataType is unkown to the Mapper */ map(attributeName, properties, strategy) { let result; let attributeType = properties && properties.type && properties.type.key; // Aliases switch (attributeType) { case 'VIRTUAL': { // Use schema for the return type (if defined) attributeType = properties.type && properties.type.returnType && properties.type.returnType.key; break; } default: break; } // convert DataType to `type` property switch (attributeType) { // ---------------------------------------------------------------------- // @todo Sequelize.ARRAY(Sequelize.RANGE(Sequelize.DATE)) // ---------------------------------------------------------------------- case 'ARRAY': { result = { ...ARRAY, // Sequelize requires attribute.type to be defined for ARRAYs items: this.map( attributeName, { type: properties.type.type, allowNull: false }, strategy, ), }; break; } // ---------------------------------------------------------------------- // BIGINT // BIGINT(11) // ---------------------------------------------------------------------- case 'BIGINT': { result = { ...INTEGER, format: 'int64' }; break; } // ---------------------------------------------------------------------- // BLOB // @todo BLOB('tiny') // ---------------------------------------------------------------------- case 'BLOB': { result = { ...STRING }; Object.assign(result, strategy.getPropertyForBase64Encoding()); break; } // ---------------------------------------------------------------------- // BOOLEAN // ---------------------------------------------------------------------- case 'BOOLEAN': { result = { ...BOOLEAN }; break; } // ---------------------------------------------------------------------- // CIDR // ---------------------------------------------------------------------- case 'CIDR': { result = { ...STRING }; break; } // ---------------------------------------------------------------------- // CITEXT // ---------------------------------------------------------------------- case 'CITEXT': result = { ...STRING }; break; // ---------------------------------------------------------------------- // DATE // @todo DATE(6) // ---------------------------------------------------------------------- case 'DATE': { result = { ...STRING, format: 'date-time' }; break; } // ---------------------------------------------------------------------- // DATEONLY // ---------------------------------------------------------------------- case 'DATEONLY': { result = { ...STRING, format: 'date' }; break; } // ---------------------------------------------------------------------- // DECIMAL // @todo DECIMAL(10, 2) // ---------------------------------------------------------------------- case 'DECIMAL': { result = { ...NUMBER }; break; } // ---------------------------------------------------------------------- // DOUBLE // @todo DOUBLE(11) // @todo DOUBLE(11,10) // ---------------------------------------------------------------------- case 'DOUBLE PRECISION': { result = { ...NUMBER, format: 'double' }; break; } // ---------------------------------------------------------------------- // ENUM('value 1', 'value 2') // ---------------------------------------------------------------------- case 'ENUM': { result = { ...STRING, enum: [...(properties.values || properties.type.values)] }; break; } // ---------------------------------------------------------------------- // INET // @todo this one currently breaks json-schema-v7 validation // @see https://github.com/Julian/jsonschema/issues/171 // ---------------------------------------------------------------------- case 'INET': { result = { type: [ { ...STRING, format: 'ipv4' }, { ...STRING, format: 'ipv6' }, ], }; break; } // ---------------------------------------------------------------------- // INTEGER // ---------------------------------------------------------------------- case 'INTEGER': { result = { ...INTEGER, format: 'int32' }; break; } // ---------------------------------------------------------------------- // FLOAT // @todo FLOAT(11) // @todo FLOAT(11,10) // ---------------------------------------------------------------------- case 'FLOAT': { result = { ...NUMBER, format: 'float' }; break; } // ---------------------------------------------------------------------- // @todo GEOMETRY // @todo GEOMETRY('POINT') // @todo GEOMETRY('POINT', 4326) // ---------------------------------------------------------------------- case 'GEOMETRY': { throw new TypeError( 'sequelize-to-json-schemas has not yet implemented the GEOMETRY DataType', ); } // ---------------------------------------------------------------------- // JSON // ---------------------------------------------------------------------- case 'JSON': { result = { ...ANY }; break; } // ---------------------------------------------------------------------- // JSONB // ---------------------------------------------------------------------- case 'JSONB': { result = { ...ANY }; break; } // ---------------------------------------------------------------------- // MACADDR // ---------------------------------------------------------------------- case 'MACADDR': { result = { ...STRING }; break; } // ---------------------------------------------------------------------- // @todo Sequelize.RANGE(Sequelize.INTEGER) // @todo Sequelize.RANGE(Sequelize.BIGINT) // @todo Sequelize.RANGE(Sequelize.DATE) // @todo Sequelize.RANGE(Sequelize.DATEONLY) // @todo Sequelize.RANGE(Sequelize.DECIMAL) // ---------------------------------------------------------------------- case 'RANGE': { throw new TypeError('sequelize-to-json-schemas has not yet implemented the RANGE DataType'); } // ---------------------------------------------------------------------- // REAL // @todo REAL(11) // @todo REAL(11,12) // ---------------------------------------------------------------------- case 'REAL': { result = { ...NUMBER }; break; } // ---------------------------------------------------------------------- // STRING // STRING(1234) // STRING.BINARY // ---------------------------------------------------------------------- case 'STRING': { result = { ...STRING }; if (properties.type.options.length !== undefined) { result.maxLength = properties.type.options.length; } if (properties.type.options.binary !== undefined) { result.format = 'binary'; } break; } // ---------------------------------------------------------------------- // TEXT // TEXT('tiny') // ---------------------------------------------------------------------- case 'TEXT': result = { ...STRING }; break; // ---------------------------------------------------------------------- // UUID @todo: doublecheck the V1/V4 DataTypes // ---------------------------------------------------------------------- case 'UUID': { result = { ...STRING, format: 'uuid' }; break; } case 'UUIDV1': { result = { ...STRING, format: 'uuid' }; break; } case 'UUIDV4': { result = { ...STRING, format: 'uuid' }; break; } // ---------------------------------------------------------------------- // @todo these require further investigation before relocating // ---------------------------------------------------------------------- case 'ABSTRACT': { throw new TypeError( 'sequelize-to-json-schemas has not yet implemented the ABSTRACT DataType', ); } case 'CHAR': { result = { ...STRING }; break; } case 'GEOGRAPHY': { throw new TypeError( 'sequelize-to-json-schemas has not yet implemented the GEOGRAPHY DataType', ); } case 'HSTORE': { throw new TypeError( 'sequelize-to-json-schemas has not yet implemented the HSTORE DataType', ); } case 'MEDIUMINT': { result = { ...INTEGER }; break; } // NOW: null, case 'NUMBER': { result = { ...NUMBER }; break; } case 'SMALLINT': { result = { ...INTEGER }; break; } case 'TIME': { result = { ...STRING, format: 'time' }; break; } case 'TINYINT': { result = { ...NUMBER }; break; } case 'VIRTUAL': { // Use result for the return type (if defined) result = this.map( attributeName, { ...properties, type: properties.type && properties.type.returnType }, strategy, ); break; } default: // ---------------------------------------------------------------------- // use ANY for anything not matching (for now, needs investigating) // ---------------------------------------------------------------------- result = { ...ANY }; } // ---------------------------------------------------------------------- // Sequelize options applying to all types starting below // ---------------------------------------------------------------------- if (properties.allowNull !== false) { if (result.type) { Object.assign(result, this.constructor._getNullableType(result.type, strategy)); } else { Object.assign(result, this.constructor._getNullableType(result.anyOf, strategy)); } } if (properties.defaultValue !== undefined) { result.default = properties.defaultValue; // supported by all strategies so no need for complexity } return result; } /** * Replaces current `schema.type` with the object returned by the strategy as * the solution for nullable types can vary strongly between them. * * @private * @param {string} type Name of the type (e.g. 'string') * @param {StrategyInterface} strategy Strategy instance * @returns {object} */ static _getNullableType(type, strategy) { const result = strategy.convertTypePropertyToAllowNull(type); if (typeof result !== 'object') { throw new TypeError("convertTypePropertyToAllowNull() return value not of type 'object'"); } if ( !Object.prototype.hasOwnProperty.call(result, 'type') && !Object.prototype.hasOwnProperty.call(result, 'anyOf') ) { throw new TypeError( "convertTypePropertyToAllowNull() return value does not have property 'type' or 'anyOf'", ); } return result; } } module.exports = TypeMapper;