├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jsdoc ├── .prettierignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── code_of_conduct.md ├── contributing.md ├── docs └── README.md ├── examples ├── README.md ├── generate.js ├── json-schema-v7.md └── openapi-v3.md ├── lib ├── index.js ├── schema-manager.js ├── strategies │ ├── json-schema-v7.js │ └── openapi-v3.js ├── strategy-interface.js ├── type-mapper.js └── utils │ ├── lodash-natives.js │ └── type-checks.js ├── package.json ├── test ├── models │ ├── company.js │ ├── document.js │ ├── friendship.js │ ├── index.js │ ├── profile.js │ └── user.js ├── schema-manager.test.js ├── strategies │ ├── json-schema-v7-strategy.test.js │ ├── openapi-v3-stragegy.test.js │ └── openapi-v3-validation-wrapper.js ├── strategy-interface.test.js └── utils │ ├── supported-datatype.js │ └── type-checks.test.js └── try-me.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 100 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Define the line ending behavior of the different file extensions 2 | # Set default behavior, in case users don't have core.autocrlf set. 3 | * text=auto 4 | * text eol=lf 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | /node_modules/ 3 | /npm-debug.log 4 | 5 | # docs 6 | /docs/* 7 | !/docs/README.md 8 | 9 | # Editors 10 | .vscode/* 11 | !.vscode/tasks.json 12 | !.vscode/launch.json 13 | !.vscode/extensions.json 14 | 15 | # Lockfiles for apps, but not for packages. 16 | # https://github.com/sindresorhus/ama/issues/479#issuecomment-310661514 17 | /package-lock.json 18 | /yarn.lock 19 | -------------------------------------------------------------------------------- /.jsdoc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [], 3 | "source": { 4 | "include": "./lib/" 5 | }, 6 | "opts": { 7 | "destination": "./docs/", 8 | "recurse": true 9 | }, 10 | "sourceType": "module", 11 | "tags": { 12 | "allowUnknownTags": true, 13 | "dictionaries": ["jsdoc","closure"] 14 | }, 15 | "templates": { 16 | "cleverLinks": false, 17 | "monospaceLinks": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore everything: 2 | /* 3 | 4 | # Except these directories: 5 | !/examples 6 | !/lib 7 | !/test 8 | 9 | # Except these files: 10 | !CONTRIBUTING.md 11 | !README.md 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: npm 3 | 4 | node_js: 5 | - 8 6 | - 10 7 | - 12 8 | - 14 9 | - 15 10 | 11 | sudo: false 12 | 13 | branches: 14 | only: 15 | - master 16 | - /^greenkeeper/.*$/ 17 | 18 | env: 19 | matrix: 20 | - SEQUELIZE_VERSION=4.* 21 | - SEQUELIZE_VERSION=5.* 22 | - SEQUELIZE_VERSION=6.* 23 | 24 | global: 25 | - TEST=1 26 | 27 | matrix: 28 | fast_finish: true 29 | 30 | include: 31 | - node_js: 14 32 | env: TEST=0 LINT=1 33 | 34 | - node_js: 14 35 | env: TEST=0 COVERAGE=1 36 | 37 | - stage: deploy 38 | node_js: 15 39 | env: TEST=0 COVERAGE=0 LINT=0 40 | script: 41 | - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc 42 | - git remote rm origin 43 | - git remote add origin https://alt3:${GITHUB_TOKEN}@github.com/alt3/sequelize-to-json-schemas.git 44 | - git symbolic-ref HEAD refs/heads/master 45 | - npm run release 46 | on: 47 | branch: master 48 | if: "branch = master AND type = push AND commit_message !~ /(?i:no-release)|^(?i:chore: release)/" 49 | 50 | allow_failures: 51 | - env: SEQUELIZE_VERSION=6 52 | 53 | before_script: 54 | - sh -c "if [ '$TEST' = '1' ]; then npm install sequelize@$SEQUELIZE_VERSION; fi" 55 | 56 | script: 57 | - sh -c "if [ '$TEST' = '1' ]; then npm run test; fi" 58 | - sh -c "if [ '$LINT' = '1' ]; then npm run lint; fi" 59 | - sh -c "if [ '$COVERAGE' = '1' ]; then npm run coverage; fi" 60 | 61 | after_success: 62 | - if [[ $COVERAGE == 1 ]]; then bash <(curl -s https://codecov.io/bash); fi 63 | 64 | notifications: 65 | email: 66 | on_success: never 67 | on_failure: always 68 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ALT3 B.V. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM Version](https://img.shields.io/npm/v/@alt3/sequelize-to-json-schemas?style=flat-square)](https://www.npmjs.com/package/@alt3/sequelize-to-json-schemas) 2 | [![Build Status](https://img.shields.io/travis/alt3/sequelize-to-json-schemas/master.svg?style=flat-square)](https://app.travis-ci.com/alt3/sequelize-to-json-schemas) 3 | [![Known Vulnerabilities](https://snyk.io/test/github/alt3/sequelize-to-json-schemas/badge.svg)](https://snyk.io/test/github/alt3/sequelize-to-json-schemas) 4 | ![NPM Total Downloads](https://img.shields.io/npm/dt/@alt3/sequelize-to-json-schemas.svg?style=flat-square) 5 | [![Code Coverage](https://img.shields.io/codecov/c/github/alt3/sequelize-to-json-schemas.svg?style=flat-square)](https://codecov.io/gh/alt3/sequelize-to-json-schemas) 6 | [![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/alt3/sequelize-to-json-schemas?style=flat-square)](https://codeclimate.com/github/alt3/sequelize-to-json-schemas) 7 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) 8 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg?style=flat-square)](https://www.contributor-covenant.org/version/2/0/code_of_conduct) 9 | 10 | # sequelize-to-json-schemas 11 | 12 | Convert Sequelize models into these JSON Schema variants (using the Strategy Pattern): 13 | 14 | - JSON Schema Draft-07 - [sample output](examples/json-schema-v7.md) 15 | - OpenAPI 3.0 - [sample output](examples/openapi-v3.md) 16 | 17 | Compatible with Sequelize versions 4, 5 and 6. 18 | 19 | ## Main Goals 20 | 21 | - understandable code, highly maintainable 22 | - valid schemas (enforced by the [ajv](https://github.com/epoberezkin/ajv) and [Swagger Parser](https://github.com/APIDevTools/swagger-parser) validators) 23 | - JsonSchemaManager for single (rock solid) core functionality shared between all strategies 24 | - StrategyInterface for simplified implementation of new schema variants 25 | 26 | > Feel free to PR strategies for missing schemas 27 | 28 | ## Installation 29 | 30 | ```bash 31 | npm install @alt3/sequelize-to-json-schemas --save 32 | ``` 33 | 34 | ## Usage 35 | 36 | 37 | ```javascript 38 | const { JsonSchemaManager, JsonSchema7Strategy, OpenApi3Strategy } = require('@alt3/sequelize-to-json-schemas'); 39 | const schemaManager = new JsonSchemaManager(); 40 | 41 | // now generate a JSON Schema Draft-07 model schema 42 | let schema = schemaManager.generate(userModel, new JsonSchema7Strategy()); 43 | 44 | // and/or the OpenAPI 3.0 equivalent 45 | schema = schemaManager.generate(userModel, new OpenApi3Strategy()); 46 | ``` 47 | 48 | 49 | ## Configuration Options 50 | 51 | To configure global options use the JsonSchemaManager initialization: 52 | 53 | ```javascript 54 | const schemaManager = new JsonSchemaManager({ 55 | baseUri: '/', 56 | absolutePaths: true, 57 | secureSchemaUri: true, 58 | disableComments: true, 59 | }); 60 | ``` 61 | 62 | To configure (per) model options use the `generate()` method: 63 | 64 | ```javascript 65 | const userSchema = schemaManager.generate(userModel, strategy, { 66 | title: 'Custom model title', 67 | description: 'Custom model description', 68 | exclude: ['someAttribute'], 69 | include: ['someAttribute'], 70 | associations: true, 71 | excludeAssociations: ['someAssociation'], 72 | includeAssociations: ['someAssociation'], 73 | }); 74 | ``` 75 | 76 | The following Sequelize attribute options are automatically converted into 77 | schema properties: 78 | 79 | ```javascript 80 | module.exports = (sequelize) => { 81 | const model = sequelize.define('user', { 82 | userName: { 83 | type: DataTypes.STRING, 84 | allowNull: true, 85 | defaultValue: 'Default Value', 86 | associate: {}, 87 | }, 88 | }); 89 | 90 | return model; 91 | }; 92 | ``` 93 | 94 | To configure additional schema properties add a `jsonSchema` property with 95 | one or more of the following custom options to your Sequelize attribute: 96 | 97 | ```javascript 98 | module.exports = (sequelize) => { 99 | const model = sequelize.define('user', { 100 | userName: { 101 | type: DataTypes.STRING, 102 | jsonSchema: { 103 | description: 'Custom attribute description', 104 | comment: 'Custom attribute comment', 105 | examples: ['Custom example 1', 'Custom example 2'], 106 | readOnly: true, // OR writeOnly: true 107 | }, 108 | }, 109 | }); 110 | 111 | return model; 112 | }; 113 | ``` 114 | 115 | In order to create a valid output for `JSON` columns, or to otherwise override 116 | the schema output for a particular sequelize attribute, add a `schema` attribute: 117 | 118 | ```javascript 119 | module.exports = (sequelize) => { 120 | const model = sequelize.define('user', { 121 | // ... 122 | settings: { 123 | type: DataTypes.JSON, 124 | jsonSchema: { 125 | schema: { type: 'object' }, 126 | }, 127 | }, 128 | }); 129 | 130 | return model; 131 | }; 132 | ``` 133 | 134 | ## Framework Integrations 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 |

Feathers

143 | 144 | ## License 145 | 146 | This project is released under [MIT LICENSE](LICENSE.txt). 147 | 148 | ## Contributing 149 | 150 | Please refer to the [guidelines for contributing](./contributing.md). 151 | -------------------------------------------------------------------------------- /code_of_conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the project community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | 86 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributions Welcome 2 | 3 | ## Contributor Covenant 4 | 5 | Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. 6 | 7 | You can view the latest version of the Contributor Covenant [here](https://www.contributor-covenant.org/version/2/0/code_of_conduct). 8 | 9 | ## PR Guidelines 10 | 11 | - scoped pull requests (related changes only) 12 | - make sure existing tests pass 13 | - add new tests if new functionality is added 14 | 15 | ## Uses 16 | 17 | - Husky (pre-commit hooks) 18 | - ESlint with the [Airbnb Style Guide](https://github.com/airbnb/javascript) 19 | - Prettier (recommended and unicorn/recommended) 20 | - Jest 21 | - JSDocs 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## sequelize-to-json-schemas -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Automated sample output generated with the most recent version of sequelize-to-json-schemas: 4 | 5 | - [JSON Schema Draft-07](json-schema-v7.md) 6 | - [OpenAPI 3.0](openapi-v3.md) 7 | -------------------------------------------------------------------------------- /examples/generate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script generates markdown files in /docs/sample-output containing fully 3 | * generated schemas for each strategy (so they can be included in the README). 4 | */ 5 | 6 | /* eslint-disable no-console */ 7 | /* eslint-disable import/no-extraneous-dependencies */ 8 | 9 | const fileSystem = require('fs'); 10 | const moment = require('moment'); 11 | const models = require('../test/models'); 12 | const { JsonSchemaManager, JsonSchema7Strategy, OpenApi3Strategy } = require('../lib'); 13 | 14 | const targetFolder = './examples/'; 15 | 16 | const schemaManager = new JsonSchemaManager({ 17 | baseUri: 'https://api.example.com', 18 | absolutePaths: true, 19 | secureSchemaUri: true, 20 | disableComments: false, 21 | }); 22 | 23 | const pageIntro = ` 24 | These schemas were automatically generated on ${moment().format('YYYY-MM-DD')} 25 | using [these Sequelize models](../test/models) and the most recent version of 26 | sequelize-to-json-schemas. To confirm that these are indeed all valid schemas use:`; 27 | 28 | // ---------------------------------------------------------------------------- 29 | // JSON Schema Draft-07 30 | // ---------------------------------------------------------------------------- 31 | let strategy = new JsonSchema7Strategy(); 32 | 33 | let userSchema = schemaManager.generate(models.user, strategy, { 34 | title: 'Custom Title', 35 | description: 'Custom Description', 36 | }); 37 | 38 | let profileSchema = schemaManager.generate(models.profile, strategy); 39 | let documentSchema = schemaManager.generate(models.document, strategy); 40 | let companySchema = schemaManager.generate(models.company, strategy); 41 | let friendshipSchema = schemaManager.generate(models.friendship, strategy); 42 | 43 | let fullSchema = { 44 | $schema: 'https://json-schema.org/draft-07/schema#', 45 | definitions: { 46 | user: userSchema, 47 | profile: profileSchema, 48 | document: documentSchema, 49 | company: companySchema, 50 | friendship: friendshipSchema, 51 | }, 52 | }; 53 | 54 | let markdown = `# JSON Schema Draft-07 55 | ${pageIntro} 56 | 57 | - [JSON Schema Validator](https://www.jsonschemavalidator.net/) 58 | - [ajv](https://github.com/epoberezkin/ajv) 59 | 60 | ## User Model 61 | 62 | 63 | \`\`\`json 64 | ${JSON.stringify(userSchema, null, 2)} 65 | \`\`\` 66 | 67 | 68 | ## Profile Model 69 | 70 | 71 | \`\`\`json 72 | ${JSON.stringify(profileSchema, null, 2)} 73 | \`\`\` 74 | 75 | 76 | ## Document Model 77 | 78 | 79 | \`\`\`json 80 | ${JSON.stringify(documentSchema, null, 2)} 81 | \`\`\` 82 | 83 | 84 | ## Company Model 85 | 86 | 87 | \`\`\`json 88 | ${JSON.stringify(companySchema, null, 2)} 89 | \`\`\` 90 | 91 | 92 | ## Friendship Model 93 | 94 | 95 | \`\`\`json 96 | ${JSON.stringify(friendshipSchema, null, 2)} 97 | \`\`\` 98 | 99 | 100 | ## Full Schema 101 | 102 | Please note that sequelize-to-json-schemas does NOT generate full schemas. This is just an 103 | example of how to integrate the generated model schemas into a full JSON Schema Draft-07 104 | document (by adding model schemas to \`definitions\`). 105 | 106 | 107 | \`\`\`json 108 | ${JSON.stringify(fullSchema, null, 2)} 109 | \`\`\` 110 | 111 | `; 112 | 113 | fileSystem.writeFile(`${targetFolder}json-schema-v7.md`, markdown, function check(error) { 114 | if (error) { 115 | throw error; 116 | } 117 | }); 118 | 119 | console.log('Succesfully generated markdown sample output for JSON Schema Draft-07'); 120 | 121 | // ---------------------------------------------------------------------------- 122 | // OpenAPI 3.0 123 | // ---------------------------------------------------------------------------- 124 | strategy = new OpenApi3Strategy(); 125 | 126 | userSchema = schemaManager.generate(models.user, strategy, { 127 | title: 'Custom User Title', 128 | description: 'Custom User Description', 129 | }); 130 | 131 | profileSchema = schemaManager.generate(models.profile, strategy); 132 | documentSchema = schemaManager.generate(models.document, strategy); 133 | companySchema = schemaManager.generate(models.company, strategy); 134 | friendshipSchema = schemaManager.generate(models.friendship, strategy); 135 | 136 | fullSchema = require('../test/strategies/openapi-v3-validation-wrapper'); 137 | 138 | fullSchema.components.schemas = { 139 | user: userSchema, 140 | profile: profileSchema, 141 | document: documentSchema, 142 | company: companySchema, 143 | friendship: friendshipSchema, 144 | }; 145 | 146 | markdown = `# OpenAPI 3.0 147 | ${pageIntro} 148 | 149 | - [Swagger Editor](https://editor.swagger.io/) 150 | - [Online Swagger & OpenAPI Validator](https://apidevtools.org/swagger-parser/online) 151 | - [Swagger Parser](https://github.com/swagger-api/swagger-parser) 152 | 153 | ## User Model 154 | 155 | 156 | \`\`\`json 157 | ${JSON.stringify(userSchema, null, 2)} 158 | \`\`\` 159 | 160 | 161 | ## Profile Model 162 | 163 | 164 | \`\`\`json 165 | ${JSON.stringify(profileSchema, null, 2)} 166 | \`\`\` 167 | 168 | 169 | ## Document Model 170 | 171 | 172 | \`\`\`json 173 | ${JSON.stringify(documentSchema, null, 2)} 174 | \`\`\` 175 | 176 | 177 | ## Company Model 178 | 179 | 180 | \`\`\`json 181 | ${JSON.stringify(companySchema, null, 2)} 182 | \`\`\` 183 | 184 | 185 | ## Friendship Model 186 | 187 | 188 | \`\`\`json 189 | ${JSON.stringify(friendshipSchema, null, 2)} 190 | \`\`\` 191 | 192 | 193 | ## Full Schema 194 | 195 | Please note that sequelize-to-json-schemas does NOT generate full schemas. This is just an 196 | example of how to integrate the generated model schemas into a full OpenAPI 3.0 document 197 | (by adding model schemas to \`components.schemas\`). 198 | 199 | 200 | \`\`\`json 201 | ${JSON.stringify(fullSchema, null, 2)} 202 | \`\`\` 203 | 204 | `; 205 | 206 | fileSystem.writeFile(`${targetFolder}openapi-v3.md`, markdown, function check(error) { 207 | if (error) { 208 | throw error; 209 | } 210 | }); 211 | 212 | console.log('Succesfully generated markdown sample output for OpenAPI 3.0'); 213 | -------------------------------------------------------------------------------- /examples/json-schema-v7.md: -------------------------------------------------------------------------------- 1 | # JSON Schema Draft-07 2 | 3 | These schemas were automatically generated on 2021-06-14 4 | using [these Sequelize models](../test/models) and the most recent version of 5 | sequelize-to-json-schemas. To confirm that these are indeed all valid schemas use: 6 | 7 | - [JSON Schema Validator](https://www.jsonschemavalidator.net/) 8 | - [ajv](https://github.com/epoberezkin/ajv) 9 | 10 | ## User Model 11 | 12 | 13 | ```json 14 | { 15 | "$schema": "https://json-schema.org/draft-07/schema#", 16 | "$id": "https://api.example.com/user.json", 17 | "title": "Custom Title", 18 | "description": "Custom Description", 19 | "type": "object", 20 | "properties": { 21 | "id": { 22 | "$id": "https://api.example.com/properties/id", 23 | "type": "integer", 24 | "format": "int32" 25 | }, 26 | "createdAt": { 27 | "$id": "https://api.example.com/properties/createdAt", 28 | "type": "string", 29 | "format": "date-time" 30 | }, 31 | "updatedAt": { 32 | "$id": "https://api.example.com/properties/updatedAt", 33 | "type": "string", 34 | "format": "date-time" 35 | }, 36 | "ARRAY_INTEGERS": { 37 | "$id": "https://api.example.com/properties/ARRAY_INTEGERS", 38 | "type": "array", 39 | "items": { 40 | "type": "integer", 41 | "format": "int32" 42 | } 43 | }, 44 | "ARRAY_TEXTS": { 45 | "$id": "https://api.example.com/properties/ARRAY_TEXTS", 46 | "type": "array", 47 | "items": { 48 | "type": "string" 49 | } 50 | }, 51 | "ARRAY_ALLOWNULL_EXPLICIT": { 52 | "$id": "https://api.example.com/properties/ARRAY_ALLOWNULL_EXPLICIT", 53 | "type": [ 54 | "array", 55 | "null" 56 | ], 57 | "items": { 58 | "type": "string" 59 | } 60 | }, 61 | "ARRAY_ALLOWNULL_IMPLICIT": { 62 | "$id": "https://api.example.com/properties/ARRAY_ALLOWNULL_IMPLICIT", 63 | "type": [ 64 | "array", 65 | "null" 66 | ], 67 | "items": { 68 | "type": "string" 69 | } 70 | }, 71 | "ARRAY_ENUM_STRINGS": { 72 | "$id": "https://api.example.com/properties/ARRAY_ENUM_STRINGS", 73 | "type": "array", 74 | "items": { 75 | "type": "string", 76 | "enum": [ 77 | "hello", 78 | "world" 79 | ] 80 | } 81 | }, 82 | "BLOB": { 83 | "$id": "https://api.example.com/properties/BLOB", 84 | "type": "string", 85 | "contentEncoding": "base64" 86 | }, 87 | "CITEXT": { 88 | "$id": "https://api.example.com/properties/CITEXT", 89 | "type": "string" 90 | }, 91 | "INTEGER": { 92 | "$id": "https://api.example.com/properties/INTEGER", 93 | "type": "integer", 94 | "format": "int32", 95 | "default": 0 96 | }, 97 | "STRING": { 98 | "$id": "https://api.example.com/properties/STRING", 99 | "type": "string", 100 | "default": "Default value for STRING" 101 | }, 102 | "STRING_ALLOWNULL_EXPLICIT": { 103 | "$id": "https://api.example.com/properties/STRING_ALLOWNULL_EXPLICIT", 104 | "type": [ 105 | "string", 106 | "null" 107 | ] 108 | }, 109 | "STRING_ALLOWNULL_IMPLICIT": { 110 | "$id": "https://api.example.com/properties/STRING_ALLOWNULL_IMPLICIT", 111 | "type": [ 112 | "string", 113 | "null" 114 | ] 115 | }, 116 | "STRING_1234": { 117 | "$id": "https://api.example.com/properties/STRING_1234", 118 | "type": "string", 119 | "maxLength": 1234 120 | }, 121 | "STRING_DOT_BINARY": { 122 | "$id": "https://api.example.com/properties/STRING_DOT_BINARY", 123 | "type": "string", 124 | "format": "binary" 125 | }, 126 | "TEXT": { 127 | "$id": "https://api.example.com/properties/TEXT", 128 | "type": "string" 129 | }, 130 | "UUIDV4": { 131 | "$id": "https://api.example.com/properties/UUIDV4", 132 | "type": "string", 133 | "format": "uuid" 134 | }, 135 | "JSON": { 136 | "$id": "https://api.example.com/properties/JSON", 137 | "anyOf": [ 138 | { 139 | "type": "object" 140 | }, 141 | { 142 | "type": "array" 143 | }, 144 | { 145 | "type": "boolean" 146 | }, 147 | { 148 | "type": "integer" 149 | }, 150 | { 151 | "type": "number" 152 | }, 153 | { 154 | "type": "string" 155 | } 156 | ], 157 | "type": "object" 158 | }, 159 | "JSONB_ALLOWNULL": { 160 | "$id": "https://api.example.com/properties/JSONB_ALLOWNULL", 161 | "anyOf": [ 162 | { 163 | "type": "object" 164 | }, 165 | { 166 | "type": "array" 167 | }, 168 | { 169 | "type": "boolean" 170 | }, 171 | { 172 | "type": "integer" 173 | }, 174 | { 175 | "type": "number" 176 | }, 177 | { 178 | "type": "string" 179 | }, 180 | { 181 | "type": "null" 182 | } 183 | ] 184 | }, 185 | "VIRTUAL": { 186 | "$id": "https://api.example.com/properties/VIRTUAL", 187 | "type": "boolean" 188 | }, 189 | "VIRTUAL_DEPENDENCY": { 190 | "$id": "https://api.example.com/properties/VIRTUAL_DEPENDENCY", 191 | "type": "integer", 192 | "format": "int32" 193 | }, 194 | "CUSTOM_DESCRIPTION": { 195 | "$id": "https://api.example.com/properties/CUSTOM_DESCRIPTION", 196 | "type": "string", 197 | "description": "Custom attribute description" 198 | }, 199 | "CUSTOM_COMMENT": { 200 | "$id": "https://api.example.com/properties/CUSTOM_COMMENT", 201 | "type": "string", 202 | "$comment": "Custom comment" 203 | }, 204 | "CUSTOM_EXAMPLES": { 205 | "$id": "https://api.example.com/properties/CUSTOM_EXAMPLES", 206 | "type": "string", 207 | "examples": [ 208 | "Custom example 1", 209 | "Custom example 2" 210 | ] 211 | }, 212 | "CUSTOM_READONLY": { 213 | "$id": "https://api.example.com/properties/CUSTOM_READONLY", 214 | "type": "string", 215 | "readOnly": true 216 | }, 217 | "CUSTOM_WRITEONLY": { 218 | "$id": "https://api.example.com/properties/CUSTOM_WRITEONLY", 219 | "type": "string", 220 | "writeOnly": true 221 | }, 222 | "companyId": { 223 | "$id": "https://api.example.com/properties/companyId", 224 | "type": [ 225 | "integer", 226 | "null" 227 | ], 228 | "format": "int32" 229 | }, 230 | "bossId": { 231 | "$id": "https://api.example.com/properties/bossId", 232 | "type": [ 233 | "integer", 234 | "null" 235 | ], 236 | "format": "int32" 237 | }, 238 | "profile": { 239 | "$ref": "#/definitions/profile" 240 | }, 241 | "company": { 242 | "$ref": "#/definitions/company" 243 | }, 244 | "documents": { 245 | "type": "array", 246 | "items": { 247 | "$ref": "#/definitions/document" 248 | } 249 | }, 250 | "boss": { 251 | "$ref": "#/definitions/user" 252 | }, 253 | "friends": { 254 | "type": "array", 255 | "items": { 256 | "allOf": [ 257 | { 258 | "$ref": "#/definitions/user" 259 | }, 260 | { 261 | "type": "object", 262 | "properties": { 263 | "friendships": { 264 | "$ref": "#/definitions/friendship" 265 | } 266 | } 267 | } 268 | ] 269 | } 270 | } 271 | }, 272 | "required": [ 273 | "id", 274 | "createdAt", 275 | "updatedAt", 276 | "ARRAY_INTEGERS", 277 | "ARRAY_TEXTS", 278 | "ARRAY_ENUM_STRINGS", 279 | "BLOB", 280 | "CITEXT", 281 | "INTEGER", 282 | "STRING", 283 | "STRING_1234", 284 | "STRING_DOT_BINARY", 285 | "TEXT", 286 | "UUIDV4", 287 | "JSON", 288 | "VIRTUAL", 289 | "VIRTUAL_DEPENDENCY", 290 | "CUSTOM_DESCRIPTION", 291 | "CUSTOM_COMMENT", 292 | "CUSTOM_EXAMPLES", 293 | "CUSTOM_READONLY", 294 | "CUSTOM_WRITEONLY" 295 | ] 296 | } 297 | ``` 298 | 299 | 300 | ## Profile Model 301 | 302 | 303 | ```json 304 | { 305 | "$schema": "https://json-schema.org/draft-07/schema#", 306 | "$id": "https://api.example.com/profile.json", 307 | "title": "Profile", 308 | "type": "object", 309 | "properties": { 310 | "id": { 311 | "$id": "https://api.example.com/properties/id", 312 | "type": "integer", 313 | "format": "int32" 314 | }, 315 | "name": { 316 | "$id": "https://api.example.com/properties/name", 317 | "type": [ 318 | "string", 319 | "null" 320 | ] 321 | }, 322 | "userId": { 323 | "$id": "https://api.example.com/properties/userId", 324 | "type": [ 325 | "integer", 326 | "null" 327 | ], 328 | "format": "int32" 329 | } 330 | }, 331 | "required": [ 332 | "id" 333 | ] 334 | } 335 | ``` 336 | 337 | 338 | ## Document Model 339 | 340 | 341 | ```json 342 | { 343 | "$schema": "https://json-schema.org/draft-07/schema#", 344 | "$id": "https://api.example.com/document.json", 345 | "title": "Document", 346 | "type": "object", 347 | "properties": { 348 | "id": { 349 | "$id": "https://api.example.com/properties/id", 350 | "type": "integer", 351 | "format": "int32" 352 | }, 353 | "name": { 354 | "$id": "https://api.example.com/properties/name", 355 | "type": [ 356 | "string", 357 | "null" 358 | ] 359 | }, 360 | "userId": { 361 | "$id": "https://api.example.com/properties/userId", 362 | "type": [ 363 | "integer", 364 | "null" 365 | ], 366 | "format": "int32" 367 | } 368 | }, 369 | "required": [ 370 | "id" 371 | ] 372 | } 373 | ``` 374 | 375 | 376 | ## Company Model 377 | 378 | 379 | ```json 380 | { 381 | "$schema": "https://json-schema.org/draft-07/schema#", 382 | "$id": "https://api.example.com/company.json", 383 | "title": "Company", 384 | "type": "object", 385 | "properties": { 386 | "id": { 387 | "$id": "https://api.example.com/properties/id", 388 | "type": "integer", 389 | "format": "int32" 390 | }, 391 | "name": { 392 | "$id": "https://api.example.com/properties/name", 393 | "type": [ 394 | "string", 395 | "null" 396 | ] 397 | } 398 | }, 399 | "required": [ 400 | "id" 401 | ] 402 | } 403 | ``` 404 | 405 | 406 | ## Friendship Model 407 | 408 | 409 | ```json 410 | { 411 | "$schema": "https://json-schema.org/draft-07/schema#", 412 | "$id": "https://api.example.com/friendship.json", 413 | "title": "Friendship", 414 | "type": "object", 415 | "properties": { 416 | "isBestFriend": { 417 | "$id": "https://api.example.com/properties/isBestFriend", 418 | "type": [ 419 | "boolean", 420 | "null" 421 | ], 422 | "default": false 423 | }, 424 | "userId": { 425 | "$id": "https://api.example.com/properties/userId", 426 | "type": [ 427 | "integer", 428 | "null" 429 | ], 430 | "format": "int32" 431 | }, 432 | "friendId": { 433 | "$id": "https://api.example.com/properties/friendId", 434 | "type": [ 435 | "integer", 436 | "null" 437 | ], 438 | "format": "int32" 439 | } 440 | }, 441 | "required": [ 442 | "isBestFriend" 443 | ] 444 | } 445 | ``` 446 | 447 | 448 | ## Full Schema 449 | 450 | Please note that sequelize-to-json-schemas does NOT generate full schemas. This is just an 451 | example of how to integrate the generated model schemas into a full JSON Schema Draft-07 452 | document (by adding model schemas to `definitions`). 453 | 454 | 455 | ```json 456 | { 457 | "$schema": "https://json-schema.org/draft-07/schema#", 458 | "definitions": { 459 | "user": { 460 | "$schema": "https://json-schema.org/draft-07/schema#", 461 | "$id": "https://api.example.com/user.json", 462 | "title": "Custom Title", 463 | "description": "Custom Description", 464 | "type": "object", 465 | "properties": { 466 | "id": { 467 | "$id": "https://api.example.com/properties/id", 468 | "type": "integer", 469 | "format": "int32" 470 | }, 471 | "createdAt": { 472 | "$id": "https://api.example.com/properties/createdAt", 473 | "type": "string", 474 | "format": "date-time" 475 | }, 476 | "updatedAt": { 477 | "$id": "https://api.example.com/properties/updatedAt", 478 | "type": "string", 479 | "format": "date-time" 480 | }, 481 | "ARRAY_INTEGERS": { 482 | "$id": "https://api.example.com/properties/ARRAY_INTEGERS", 483 | "type": "array", 484 | "items": { 485 | "type": "integer", 486 | "format": "int32" 487 | } 488 | }, 489 | "ARRAY_TEXTS": { 490 | "$id": "https://api.example.com/properties/ARRAY_TEXTS", 491 | "type": "array", 492 | "items": { 493 | "type": "string" 494 | } 495 | }, 496 | "ARRAY_ALLOWNULL_EXPLICIT": { 497 | "$id": "https://api.example.com/properties/ARRAY_ALLOWNULL_EXPLICIT", 498 | "type": [ 499 | "array", 500 | "null" 501 | ], 502 | "items": { 503 | "type": "string" 504 | } 505 | }, 506 | "ARRAY_ALLOWNULL_IMPLICIT": { 507 | "$id": "https://api.example.com/properties/ARRAY_ALLOWNULL_IMPLICIT", 508 | "type": [ 509 | "array", 510 | "null" 511 | ], 512 | "items": { 513 | "type": "string" 514 | } 515 | }, 516 | "ARRAY_ENUM_STRINGS": { 517 | "$id": "https://api.example.com/properties/ARRAY_ENUM_STRINGS", 518 | "type": "array", 519 | "items": { 520 | "type": "string", 521 | "enum": [ 522 | "hello", 523 | "world" 524 | ] 525 | } 526 | }, 527 | "BLOB": { 528 | "$id": "https://api.example.com/properties/BLOB", 529 | "type": "string", 530 | "contentEncoding": "base64" 531 | }, 532 | "CITEXT": { 533 | "$id": "https://api.example.com/properties/CITEXT", 534 | "type": "string" 535 | }, 536 | "INTEGER": { 537 | "$id": "https://api.example.com/properties/INTEGER", 538 | "type": "integer", 539 | "format": "int32", 540 | "default": 0 541 | }, 542 | "STRING": { 543 | "$id": "https://api.example.com/properties/STRING", 544 | "type": "string", 545 | "default": "Default value for STRING" 546 | }, 547 | "STRING_ALLOWNULL_EXPLICIT": { 548 | "$id": "https://api.example.com/properties/STRING_ALLOWNULL_EXPLICIT", 549 | "type": [ 550 | "string", 551 | "null" 552 | ] 553 | }, 554 | "STRING_ALLOWNULL_IMPLICIT": { 555 | "$id": "https://api.example.com/properties/STRING_ALLOWNULL_IMPLICIT", 556 | "type": [ 557 | "string", 558 | "null" 559 | ] 560 | }, 561 | "STRING_1234": { 562 | "$id": "https://api.example.com/properties/STRING_1234", 563 | "type": "string", 564 | "maxLength": 1234 565 | }, 566 | "STRING_DOT_BINARY": { 567 | "$id": "https://api.example.com/properties/STRING_DOT_BINARY", 568 | "type": "string", 569 | "format": "binary" 570 | }, 571 | "TEXT": { 572 | "$id": "https://api.example.com/properties/TEXT", 573 | "type": "string" 574 | }, 575 | "UUIDV4": { 576 | "$id": "https://api.example.com/properties/UUIDV4", 577 | "type": "string", 578 | "format": "uuid" 579 | }, 580 | "JSON": { 581 | "$id": "https://api.example.com/properties/JSON", 582 | "anyOf": [ 583 | { 584 | "type": "object" 585 | }, 586 | { 587 | "type": "array" 588 | }, 589 | { 590 | "type": "boolean" 591 | }, 592 | { 593 | "type": "integer" 594 | }, 595 | { 596 | "type": "number" 597 | }, 598 | { 599 | "type": "string" 600 | } 601 | ], 602 | "type": "object" 603 | }, 604 | "JSONB_ALLOWNULL": { 605 | "$id": "https://api.example.com/properties/JSONB_ALLOWNULL", 606 | "anyOf": [ 607 | { 608 | "type": "object" 609 | }, 610 | { 611 | "type": "array" 612 | }, 613 | { 614 | "type": "boolean" 615 | }, 616 | { 617 | "type": "integer" 618 | }, 619 | { 620 | "type": "number" 621 | }, 622 | { 623 | "type": "string" 624 | }, 625 | { 626 | "type": "null" 627 | } 628 | ] 629 | }, 630 | "VIRTUAL": { 631 | "$id": "https://api.example.com/properties/VIRTUAL", 632 | "type": "boolean" 633 | }, 634 | "VIRTUAL_DEPENDENCY": { 635 | "$id": "https://api.example.com/properties/VIRTUAL_DEPENDENCY", 636 | "type": "integer", 637 | "format": "int32" 638 | }, 639 | "CUSTOM_DESCRIPTION": { 640 | "$id": "https://api.example.com/properties/CUSTOM_DESCRIPTION", 641 | "type": "string", 642 | "description": "Custom attribute description" 643 | }, 644 | "CUSTOM_COMMENT": { 645 | "$id": "https://api.example.com/properties/CUSTOM_COMMENT", 646 | "type": "string", 647 | "$comment": "Custom comment" 648 | }, 649 | "CUSTOM_EXAMPLES": { 650 | "$id": "https://api.example.com/properties/CUSTOM_EXAMPLES", 651 | "type": "string", 652 | "examples": [ 653 | "Custom example 1", 654 | "Custom example 2" 655 | ] 656 | }, 657 | "CUSTOM_READONLY": { 658 | "$id": "https://api.example.com/properties/CUSTOM_READONLY", 659 | "type": "string", 660 | "readOnly": true 661 | }, 662 | "CUSTOM_WRITEONLY": { 663 | "$id": "https://api.example.com/properties/CUSTOM_WRITEONLY", 664 | "type": "string", 665 | "writeOnly": true 666 | }, 667 | "companyId": { 668 | "$id": "https://api.example.com/properties/companyId", 669 | "type": [ 670 | "integer", 671 | "null" 672 | ], 673 | "format": "int32" 674 | }, 675 | "bossId": { 676 | "$id": "https://api.example.com/properties/bossId", 677 | "type": [ 678 | "integer", 679 | "null" 680 | ], 681 | "format": "int32" 682 | }, 683 | "profile": { 684 | "$ref": "#/definitions/profile" 685 | }, 686 | "company": { 687 | "$ref": "#/definitions/company" 688 | }, 689 | "documents": { 690 | "type": "array", 691 | "items": { 692 | "$ref": "#/definitions/document" 693 | } 694 | }, 695 | "boss": { 696 | "$ref": "#/definitions/user" 697 | }, 698 | "friends": { 699 | "type": "array", 700 | "items": { 701 | "allOf": [ 702 | { 703 | "$ref": "#/definitions/user" 704 | }, 705 | { 706 | "type": "object", 707 | "properties": { 708 | "friendships": { 709 | "$ref": "#/definitions/friendship" 710 | } 711 | } 712 | } 713 | ] 714 | } 715 | } 716 | }, 717 | "required": [ 718 | "id", 719 | "createdAt", 720 | "updatedAt", 721 | "ARRAY_INTEGERS", 722 | "ARRAY_TEXTS", 723 | "ARRAY_ENUM_STRINGS", 724 | "BLOB", 725 | "CITEXT", 726 | "INTEGER", 727 | "STRING", 728 | "STRING_1234", 729 | "STRING_DOT_BINARY", 730 | "TEXT", 731 | "UUIDV4", 732 | "JSON", 733 | "VIRTUAL", 734 | "VIRTUAL_DEPENDENCY", 735 | "CUSTOM_DESCRIPTION", 736 | "CUSTOM_COMMENT", 737 | "CUSTOM_EXAMPLES", 738 | "CUSTOM_READONLY", 739 | "CUSTOM_WRITEONLY" 740 | ] 741 | }, 742 | "profile": { 743 | "$schema": "https://json-schema.org/draft-07/schema#", 744 | "$id": "https://api.example.com/profile.json", 745 | "title": "Profile", 746 | "type": "object", 747 | "properties": { 748 | "id": { 749 | "$id": "https://api.example.com/properties/id", 750 | "type": "integer", 751 | "format": "int32" 752 | }, 753 | "name": { 754 | "$id": "https://api.example.com/properties/name", 755 | "type": [ 756 | "string", 757 | "null" 758 | ] 759 | }, 760 | "userId": { 761 | "$id": "https://api.example.com/properties/userId", 762 | "type": [ 763 | "integer", 764 | "null" 765 | ], 766 | "format": "int32" 767 | } 768 | }, 769 | "required": [ 770 | "id" 771 | ] 772 | }, 773 | "document": { 774 | "$schema": "https://json-schema.org/draft-07/schema#", 775 | "$id": "https://api.example.com/document.json", 776 | "title": "Document", 777 | "type": "object", 778 | "properties": { 779 | "id": { 780 | "$id": "https://api.example.com/properties/id", 781 | "type": "integer", 782 | "format": "int32" 783 | }, 784 | "name": { 785 | "$id": "https://api.example.com/properties/name", 786 | "type": [ 787 | "string", 788 | "null" 789 | ] 790 | }, 791 | "userId": { 792 | "$id": "https://api.example.com/properties/userId", 793 | "type": [ 794 | "integer", 795 | "null" 796 | ], 797 | "format": "int32" 798 | } 799 | }, 800 | "required": [ 801 | "id" 802 | ] 803 | }, 804 | "company": { 805 | "$schema": "https://json-schema.org/draft-07/schema#", 806 | "$id": "https://api.example.com/company.json", 807 | "title": "Company", 808 | "type": "object", 809 | "properties": { 810 | "id": { 811 | "$id": "https://api.example.com/properties/id", 812 | "type": "integer", 813 | "format": "int32" 814 | }, 815 | "name": { 816 | "$id": "https://api.example.com/properties/name", 817 | "type": [ 818 | "string", 819 | "null" 820 | ] 821 | } 822 | }, 823 | "required": [ 824 | "id" 825 | ] 826 | }, 827 | "friendship": { 828 | "$schema": "https://json-schema.org/draft-07/schema#", 829 | "$id": "https://api.example.com/friendship.json", 830 | "title": "Friendship", 831 | "type": "object", 832 | "properties": { 833 | "isBestFriend": { 834 | "$id": "https://api.example.com/properties/isBestFriend", 835 | "type": [ 836 | "boolean", 837 | "null" 838 | ], 839 | "default": false 840 | }, 841 | "userId": { 842 | "$id": "https://api.example.com/properties/userId", 843 | "type": [ 844 | "integer", 845 | "null" 846 | ], 847 | "format": "int32" 848 | }, 849 | "friendId": { 850 | "$id": "https://api.example.com/properties/friendId", 851 | "type": [ 852 | "integer", 853 | "null" 854 | ], 855 | "format": "int32" 856 | } 857 | }, 858 | "required": [ 859 | "isBestFriend" 860 | ] 861 | } 862 | } 863 | } 864 | ``` 865 | 866 | -------------------------------------------------------------------------------- /examples/openapi-v3.md: -------------------------------------------------------------------------------- 1 | # OpenAPI 3.0 2 | 3 | These schemas were automatically generated on 2021-06-14 4 | using [these Sequelize models](../test/models) and the most recent version of 5 | sequelize-to-json-schemas. To confirm that these are indeed all valid schemas use: 6 | 7 | - [Swagger Editor](https://editor.swagger.io/) 8 | - [Online Swagger & OpenAPI Validator](https://apidevtools.org/swagger-parser/online) 9 | - [Swagger Parser](https://github.com/swagger-api/swagger-parser) 10 | 11 | ## User Model 12 | 13 | 14 | ```json 15 | { 16 | "title": "Custom User Title", 17 | "description": "Custom User Description", 18 | "type": "object", 19 | "properties": { 20 | "id": { 21 | "type": "integer", 22 | "format": "int32" 23 | }, 24 | "createdAt": { 25 | "type": "string", 26 | "format": "date-time" 27 | }, 28 | "updatedAt": { 29 | "type": "string", 30 | "format": "date-time" 31 | }, 32 | "ARRAY_INTEGERS": { 33 | "type": "array", 34 | "items": { 35 | "type": "integer", 36 | "format": "int32" 37 | } 38 | }, 39 | "ARRAY_TEXTS": { 40 | "type": "array", 41 | "items": { 42 | "type": "string" 43 | } 44 | }, 45 | "ARRAY_ALLOWNULL_EXPLICIT": { 46 | "type": "array", 47 | "items": { 48 | "type": "string" 49 | }, 50 | "nullable": true 51 | }, 52 | "ARRAY_ALLOWNULL_IMPLICIT": { 53 | "type": "array", 54 | "items": { 55 | "type": "string" 56 | }, 57 | "nullable": true 58 | }, 59 | "ARRAY_ENUM_STRINGS": { 60 | "type": "array", 61 | "items": { 62 | "type": "string", 63 | "enum": [ 64 | "hello", 65 | "world" 66 | ] 67 | } 68 | }, 69 | "BLOB": { 70 | "type": "string", 71 | "format": "byte" 72 | }, 73 | "CITEXT": { 74 | "type": "string" 75 | }, 76 | "INTEGER": { 77 | "type": "integer", 78 | "format": "int32", 79 | "default": 0 80 | }, 81 | "STRING": { 82 | "type": "string", 83 | "default": "Default value for STRING" 84 | }, 85 | "STRING_ALLOWNULL_EXPLICIT": { 86 | "type": "string", 87 | "nullable": true 88 | }, 89 | "STRING_ALLOWNULL_IMPLICIT": { 90 | "type": "string", 91 | "nullable": true 92 | }, 93 | "STRING_1234": { 94 | "type": "string", 95 | "maxLength": 1234 96 | }, 97 | "STRING_DOT_BINARY": { 98 | "type": "string", 99 | "format": "binary" 100 | }, 101 | "TEXT": { 102 | "type": "string" 103 | }, 104 | "UUIDV4": { 105 | "type": "string", 106 | "format": "uuid" 107 | }, 108 | "JSON": { 109 | "anyOf": [ 110 | { 111 | "type": "object" 112 | }, 113 | { 114 | "type": "array" 115 | }, 116 | { 117 | "type": "boolean" 118 | }, 119 | { 120 | "type": "integer" 121 | }, 122 | { 123 | "type": "number" 124 | }, 125 | { 126 | "type": "string" 127 | } 128 | ], 129 | "type": "object" 130 | }, 131 | "JSONB_ALLOWNULL": { 132 | "anyOf": [ 133 | { 134 | "type": "object" 135 | }, 136 | { 137 | "type": "array" 138 | }, 139 | { 140 | "type": "boolean" 141 | }, 142 | { 143 | "type": "integer" 144 | }, 145 | { 146 | "type": "number" 147 | }, 148 | { 149 | "type": "string" 150 | } 151 | ], 152 | "nullable": true 153 | }, 154 | "VIRTUAL": { 155 | "type": "boolean" 156 | }, 157 | "VIRTUAL_DEPENDENCY": { 158 | "type": "integer", 159 | "format": "int32" 160 | }, 161 | "CUSTOM_DESCRIPTION": { 162 | "type": "string", 163 | "description": "Custom attribute description" 164 | }, 165 | "CUSTOM_COMMENT": { 166 | "type": "string" 167 | }, 168 | "CUSTOM_EXAMPLES": { 169 | "type": "string", 170 | "example": [ 171 | "Custom example 1", 172 | "Custom example 2" 173 | ] 174 | }, 175 | "CUSTOM_READONLY": { 176 | "type": "string", 177 | "readOnly": true 178 | }, 179 | "CUSTOM_WRITEONLY": { 180 | "type": "string", 181 | "writeOnly": true 182 | }, 183 | "companyId": { 184 | "type": "integer", 185 | "format": "int32", 186 | "nullable": true 187 | }, 188 | "bossId": { 189 | "type": "integer", 190 | "format": "int32", 191 | "nullable": true 192 | }, 193 | "profile": { 194 | "$ref": "#/components/schemas/profile" 195 | }, 196 | "company": { 197 | "$ref": "#/components/schemas/company" 198 | }, 199 | "documents": { 200 | "type": "array", 201 | "items": { 202 | "$ref": "#/components/schemas/document" 203 | } 204 | }, 205 | "boss": { 206 | "$ref": "#/components/schemas/user" 207 | }, 208 | "friends": { 209 | "type": "array", 210 | "items": { 211 | "allOf": [ 212 | { 213 | "$ref": "#/components/schemas/user" 214 | }, 215 | { 216 | "type": "object", 217 | "properties": { 218 | "friendships": { 219 | "$ref": "#/components/schemas/friendship" 220 | } 221 | } 222 | } 223 | ] 224 | } 225 | } 226 | }, 227 | "required": [ 228 | "id", 229 | "createdAt", 230 | "updatedAt", 231 | "ARRAY_INTEGERS", 232 | "ARRAY_TEXTS", 233 | "ARRAY_ENUM_STRINGS", 234 | "BLOB", 235 | "CITEXT", 236 | "INTEGER", 237 | "STRING", 238 | "STRING_1234", 239 | "STRING_DOT_BINARY", 240 | "TEXT", 241 | "UUIDV4", 242 | "JSON", 243 | "VIRTUAL", 244 | "VIRTUAL_DEPENDENCY", 245 | "CUSTOM_DESCRIPTION", 246 | "CUSTOM_COMMENT", 247 | "CUSTOM_EXAMPLES", 248 | "CUSTOM_READONLY", 249 | "CUSTOM_WRITEONLY" 250 | ] 251 | } 252 | ``` 253 | 254 | 255 | ## Profile Model 256 | 257 | 258 | ```json 259 | { 260 | "title": "Profile", 261 | "type": "object", 262 | "properties": { 263 | "id": { 264 | "type": "integer", 265 | "format": "int32" 266 | }, 267 | "name": { 268 | "type": "string", 269 | "nullable": true 270 | }, 271 | "userId": { 272 | "type": "integer", 273 | "format": "int32", 274 | "nullable": true 275 | } 276 | }, 277 | "required": [ 278 | "id" 279 | ] 280 | } 281 | ``` 282 | 283 | 284 | ## Document Model 285 | 286 | 287 | ```json 288 | { 289 | "title": "Document", 290 | "type": "object", 291 | "properties": { 292 | "id": { 293 | "type": "integer", 294 | "format": "int32" 295 | }, 296 | "name": { 297 | "type": "string", 298 | "nullable": true 299 | }, 300 | "userId": { 301 | "type": "integer", 302 | "format": "int32", 303 | "nullable": true 304 | } 305 | }, 306 | "required": [ 307 | "id" 308 | ] 309 | } 310 | ``` 311 | 312 | 313 | ## Company Model 314 | 315 | 316 | ```json 317 | { 318 | "title": "Company", 319 | "type": "object", 320 | "properties": { 321 | "id": { 322 | "type": "integer", 323 | "format": "int32" 324 | }, 325 | "name": { 326 | "type": "string", 327 | "nullable": true 328 | } 329 | }, 330 | "required": [ 331 | "id" 332 | ] 333 | } 334 | ``` 335 | 336 | 337 | ## Friendship Model 338 | 339 | 340 | ```json 341 | { 342 | "title": "Friendship", 343 | "type": "object", 344 | "properties": { 345 | "isBestFriend": { 346 | "type": "boolean", 347 | "nullable": true, 348 | "default": false 349 | }, 350 | "userId": { 351 | "type": "integer", 352 | "format": "int32", 353 | "nullable": true 354 | }, 355 | "friendId": { 356 | "type": "integer", 357 | "format": "int32", 358 | "nullable": true 359 | } 360 | }, 361 | "required": [ 362 | "isBestFriend" 363 | ] 364 | } 365 | ``` 366 | 367 | 368 | ## Full Schema 369 | 370 | Please note that sequelize-to-json-schemas does NOT generate full schemas. This is just an 371 | example of how to integrate the generated model schemas into a full OpenAPI 3.0 document 372 | (by adding model schemas to `components.schemas`). 373 | 374 | 375 | ```json 376 | { 377 | "openapi": "3.0.2", 378 | "info": { 379 | "title": "Fake API", 380 | "version": "0.0.1" 381 | }, 382 | "paths": { 383 | "/users": { 384 | "get": { 385 | "parameters": [], 386 | "responses": { 387 | "404": { 388 | "description": "not found" 389 | } 390 | } 391 | } 392 | } 393 | }, 394 | "components": { 395 | "schemas": { 396 | "user": { 397 | "title": "Custom User Title", 398 | "description": "Custom User Description", 399 | "type": "object", 400 | "properties": { 401 | "id": { 402 | "type": "integer", 403 | "format": "int32" 404 | }, 405 | "createdAt": { 406 | "type": "string", 407 | "format": "date-time" 408 | }, 409 | "updatedAt": { 410 | "type": "string", 411 | "format": "date-time" 412 | }, 413 | "ARRAY_INTEGERS": { 414 | "type": "array", 415 | "items": { 416 | "type": "integer", 417 | "format": "int32" 418 | } 419 | }, 420 | "ARRAY_TEXTS": { 421 | "type": "array", 422 | "items": { 423 | "type": "string" 424 | } 425 | }, 426 | "ARRAY_ALLOWNULL_EXPLICIT": { 427 | "type": "array", 428 | "items": { 429 | "type": "string" 430 | }, 431 | "nullable": true 432 | }, 433 | "ARRAY_ALLOWNULL_IMPLICIT": { 434 | "type": "array", 435 | "items": { 436 | "type": "string" 437 | }, 438 | "nullable": true 439 | }, 440 | "ARRAY_ENUM_STRINGS": { 441 | "type": "array", 442 | "items": { 443 | "type": "string", 444 | "enum": [ 445 | "hello", 446 | "world" 447 | ] 448 | } 449 | }, 450 | "BLOB": { 451 | "type": "string", 452 | "format": "byte" 453 | }, 454 | "CITEXT": { 455 | "type": "string" 456 | }, 457 | "INTEGER": { 458 | "type": "integer", 459 | "format": "int32", 460 | "default": 0 461 | }, 462 | "STRING": { 463 | "type": "string", 464 | "default": "Default value for STRING" 465 | }, 466 | "STRING_ALLOWNULL_EXPLICIT": { 467 | "type": "string", 468 | "nullable": true 469 | }, 470 | "STRING_ALLOWNULL_IMPLICIT": { 471 | "type": "string", 472 | "nullable": true 473 | }, 474 | "STRING_1234": { 475 | "type": "string", 476 | "maxLength": 1234 477 | }, 478 | "STRING_DOT_BINARY": { 479 | "type": "string", 480 | "format": "binary" 481 | }, 482 | "TEXT": { 483 | "type": "string" 484 | }, 485 | "UUIDV4": { 486 | "type": "string", 487 | "format": "uuid" 488 | }, 489 | "JSON": { 490 | "anyOf": [ 491 | { 492 | "type": "object" 493 | }, 494 | { 495 | "type": "array" 496 | }, 497 | { 498 | "type": "boolean" 499 | }, 500 | { 501 | "type": "integer" 502 | }, 503 | { 504 | "type": "number" 505 | }, 506 | { 507 | "type": "string" 508 | } 509 | ], 510 | "type": "object" 511 | }, 512 | "JSONB_ALLOWNULL": { 513 | "anyOf": [ 514 | { 515 | "type": "object" 516 | }, 517 | { 518 | "type": "array" 519 | }, 520 | { 521 | "type": "boolean" 522 | }, 523 | { 524 | "type": "integer" 525 | }, 526 | { 527 | "type": "number" 528 | }, 529 | { 530 | "type": "string" 531 | } 532 | ], 533 | "nullable": true 534 | }, 535 | "VIRTUAL": { 536 | "type": "boolean" 537 | }, 538 | "VIRTUAL_DEPENDENCY": { 539 | "type": "integer", 540 | "format": "int32" 541 | }, 542 | "CUSTOM_DESCRIPTION": { 543 | "type": "string", 544 | "description": "Custom attribute description" 545 | }, 546 | "CUSTOM_COMMENT": { 547 | "type": "string" 548 | }, 549 | "CUSTOM_EXAMPLES": { 550 | "type": "string", 551 | "example": [ 552 | "Custom example 1", 553 | "Custom example 2" 554 | ] 555 | }, 556 | "CUSTOM_READONLY": { 557 | "type": "string", 558 | "readOnly": true 559 | }, 560 | "CUSTOM_WRITEONLY": { 561 | "type": "string", 562 | "writeOnly": true 563 | }, 564 | "companyId": { 565 | "type": "integer", 566 | "format": "int32", 567 | "nullable": true 568 | }, 569 | "bossId": { 570 | "type": "integer", 571 | "format": "int32", 572 | "nullable": true 573 | }, 574 | "profile": { 575 | "$ref": "#/components/schemas/profile" 576 | }, 577 | "company": { 578 | "$ref": "#/components/schemas/company" 579 | }, 580 | "documents": { 581 | "type": "array", 582 | "items": { 583 | "$ref": "#/components/schemas/document" 584 | } 585 | }, 586 | "boss": { 587 | "$ref": "#/components/schemas/user" 588 | }, 589 | "friends": { 590 | "type": "array", 591 | "items": { 592 | "allOf": [ 593 | { 594 | "$ref": "#/components/schemas/user" 595 | }, 596 | { 597 | "type": "object", 598 | "properties": { 599 | "friendships": { 600 | "$ref": "#/components/schemas/friendship" 601 | } 602 | } 603 | } 604 | ] 605 | } 606 | } 607 | }, 608 | "required": [ 609 | "id", 610 | "createdAt", 611 | "updatedAt", 612 | "ARRAY_INTEGERS", 613 | "ARRAY_TEXTS", 614 | "ARRAY_ENUM_STRINGS", 615 | "BLOB", 616 | "CITEXT", 617 | "INTEGER", 618 | "STRING", 619 | "STRING_1234", 620 | "STRING_DOT_BINARY", 621 | "TEXT", 622 | "UUIDV4", 623 | "JSON", 624 | "VIRTUAL", 625 | "VIRTUAL_DEPENDENCY", 626 | "CUSTOM_DESCRIPTION", 627 | "CUSTOM_COMMENT", 628 | "CUSTOM_EXAMPLES", 629 | "CUSTOM_READONLY", 630 | "CUSTOM_WRITEONLY" 631 | ] 632 | }, 633 | "profile": { 634 | "title": "Profile", 635 | "type": "object", 636 | "properties": { 637 | "id": { 638 | "type": "integer", 639 | "format": "int32" 640 | }, 641 | "name": { 642 | "type": "string", 643 | "nullable": true 644 | }, 645 | "userId": { 646 | "type": "integer", 647 | "format": "int32", 648 | "nullable": true 649 | } 650 | }, 651 | "required": [ 652 | "id" 653 | ] 654 | }, 655 | "document": { 656 | "title": "Document", 657 | "type": "object", 658 | "properties": { 659 | "id": { 660 | "type": "integer", 661 | "format": "int32" 662 | }, 663 | "name": { 664 | "type": "string", 665 | "nullable": true 666 | }, 667 | "userId": { 668 | "type": "integer", 669 | "format": "int32", 670 | "nullable": true 671 | } 672 | }, 673 | "required": [ 674 | "id" 675 | ] 676 | }, 677 | "company": { 678 | "title": "Company", 679 | "type": "object", 680 | "properties": { 681 | "id": { 682 | "type": "integer", 683 | "format": "int32" 684 | }, 685 | "name": { 686 | "type": "string", 687 | "nullable": true 688 | } 689 | }, 690 | "required": [ 691 | "id" 692 | ] 693 | }, 694 | "friendship": { 695 | "title": "Friendship", 696 | "type": "object", 697 | "properties": { 698 | "isBestFriend": { 699 | "type": "boolean", 700 | "nullable": true, 701 | "default": false 702 | }, 703 | "userId": { 704 | "type": "integer", 705 | "format": "int32", 706 | "nullable": true 707 | }, 708 | "friendId": { 709 | "type": "integer", 710 | "format": "int32", 711 | "nullable": true 712 | } 713 | }, 714 | "required": [ 715 | "isBestFriend" 716 | ] 717 | } 718 | } 719 | } 720 | } 721 | ``` 722 | 723 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const JsonSchemaManager = require('./schema-manager'); 2 | const JsonSchema7Strategy = require('./strategies/json-schema-v7'); 3 | const OpenApi3Strategy = require('./strategies/openapi-v3'); 4 | 5 | module.exports = { 6 | JsonSchemaManager, 7 | JsonSchema7Strategy, 8 | OpenApi3Strategy, 9 | }; 10 | -------------------------------------------------------------------------------- /lib/schema-manager.js: -------------------------------------------------------------------------------- 1 | const { capitalize, omit, pick } = require('./utils/lodash-natives'); 2 | 3 | const StrategyInterface = require('./strategy-interface'); 4 | const TypeMapper = require('./type-mapper'); 5 | const { checkTypeOptional, checkTypeRequired } = require('./utils/type-checks'); 6 | 7 | /** 8 | * Merged constructor options. 9 | * 10 | * @private 11 | */ 12 | const _options = new WeakMap(); 13 | 14 | /** 15 | * Merged model options. 16 | * 17 | * @private 18 | */ 19 | const _modelOptions = new WeakMap(); 20 | 21 | /** 22 | * Strategy instance passed to the `generate()` method. 23 | * 24 | * @private 25 | */ 26 | const _strategy = new WeakMap(); 27 | 28 | /** 29 | * Sequelize model instance passed to the `generate()` method. 30 | * 31 | * @private 32 | */ 33 | const _model = new WeakMap(); 34 | 35 | /** 36 | * Class responsible for generating the various schemas. 37 | * 38 | * @copyright Copyright (c) 2019 ALT3 B.V. 39 | * @license Licensed under the MIT License 40 | */ 41 | class SchemaManager { 42 | /** 43 | * @param {object} options User options. 44 | * @param {string} options.baseUri Base URI prefixed to generated paths, defaults to '/' 45 | * @param {boolean} options.absolutePaths False to generate relative paths, defaults to true 46 | * @param {boolean} options.secureSchemaUri False to render a HTTP link to the strategy-specific schema, defaults to true (HTTPS) 47 | * @param {boolean} options.disableComments False to not render attribute property 'comment', defaults to true 48 | */ 49 | constructor(options) { 50 | const defaultOptions = { 51 | baseUri: '/', 52 | absolutePaths: true, 53 | secureSchemaUri: true, 54 | disableComments: true, 55 | }; 56 | 57 | // eslint-disable-next-line prefer-object-spread 58 | this._verifyOptions(Object.assign({}, defaultOptions, options)); 59 | } 60 | 61 | /** 62 | * Generate json schema for the provided model, using any of the available strategies. 63 | * 64 | * @param {sequelize.Model} model Instance of Sequelize.Model 65 | * @param {strategyInterface} strategy Strategy instance 66 | * @param {object} options User options. 67 | * @param {string} options.title Name to be used as model property 'title' 68 | * @param {string} options.description Text to be used as model property 'description' 69 | * @param {array} options.exclude List of attribute names that will not be included in the generated schema 70 | * @param {array} options.include List of attribute names that will be included in the generated schema 71 | * @param {array} options.associations False to exclude all associations from the generated schema, defaults to true 72 | * @returns {object} Object contaiing the strategy-specific schema 73 | * @param {array} options.excludeAssociations List of association names that will not be included in the generated schema 74 | * @param {array} options.includeAssociations List of association names that will be included in the generated schema 75 | */ 76 | generate(model, strategy, options) { 77 | const defaultOptions = { 78 | title: null, 79 | description: null, 80 | include: [], 81 | exclude: [], 82 | associations: true, 83 | includeAssociations: [], 84 | excludeAssociations: [], 85 | }; 86 | 87 | if (model === undefined) throw new Error('Missing method argument'); 88 | if (strategy === undefined) throw new Error('Mising method argument'); 89 | 90 | // eslint-disable-next-line prefer-object-spread 91 | this._verifyModelOptions(Object.assign({}, defaultOptions, options)); 92 | this._verifyModel(model); 93 | this._verifyStrategy(strategy); 94 | 95 | // import attributes from the sequelize model 96 | const attributes = this._getRawAttributes(model); 97 | 98 | // construct the response 99 | const result = this._getModelContainer(); 100 | const requiredAttributes = []; 101 | 102 | // add property schema (type, nullable, etc) 103 | for (const attributeName of Object.keys(attributes)) { 104 | result.properties[attributeName] = this._getAttributeContainer( 105 | attributeName, 106 | attributes[attributeName], 107 | ); 108 | 109 | if (this.constructor._isRequiredProperty(attributes[attributeName])) { 110 | requiredAttributes.push(attributeName); 111 | } 112 | } 113 | 114 | // merge required 115 | if (requiredAttributes.length > 0) { 116 | result.required = requiredAttributes; 117 | } 118 | 119 | // skip adding associations completely if configured by the user 120 | if (_modelOptions.get(this).associations === false) { 121 | return result; 122 | } 123 | 124 | for (const association of Object.keys(this._getAssociations())) { 125 | Object.assign( 126 | result.properties, 127 | this._getModelPropertyForAssociation(association, model.associations[association]), 128 | ); 129 | } 130 | 131 | return result; 132 | } 133 | 134 | /** 135 | * Ensures constructor options are valid. 136 | * 137 | * @private 138 | * @param {object} options Merged default and user provided options. 139 | * @returns {null} 140 | */ 141 | _verifyOptions(options) { 142 | if (!options.baseUri.endsWith('/')) { 143 | options.baseUri += '/'; // eslint-disable-line no-param-reassign 144 | } 145 | 146 | checkTypeRequired('absolutePaths', options.absolutePaths, 'boolean'); 147 | checkTypeRequired('secureSchemaUri', options.secureSchemaUri, 'boolean'); 148 | checkTypeRequired('disableComments', options.disableComments, 'boolean'); 149 | 150 | _options.set(this, options); 151 | } 152 | 153 | /** 154 | * Ensures model options are valid. 155 | * 156 | * @private 157 | * @param {object} options Merged default and user provided options. 158 | * @returns {null} 159 | */ 160 | _verifyModelOptions(options) { 161 | checkTypeOptional('title', options.title, 'string'); 162 | checkTypeOptional('description', options.description, 'string'); 163 | checkTypeRequired('include', options.include, 'array'); 164 | checkTypeRequired('exclude', options.exclude, 'array'); 165 | 166 | if (options.include.length > 0 && options.exclude.length > 0) { 167 | throw new Error("Model options 'include' and 'exclude' are mutually exclusive"); 168 | } 169 | 170 | checkTypeRequired('associations', options.associations, 'boolean'); 171 | checkTypeRequired('includeAssociations', options.includeAssociations, 'array'); 172 | checkTypeRequired('excludeAssociations', options.excludeAssociations, 'array'); 173 | 174 | if (options.includeAssociations.length > 0 && options.excludeAssociations.length > 0) { 175 | throw new Error( 176 | "Model options 'includeAssociations' and 'excludeAssociations' are mutually exclusive", 177 | ); 178 | } 179 | 180 | _modelOptions.set(this, options); 181 | } 182 | 183 | /** 184 | * Ensures the passed Sequelize model is valid. 185 | * 186 | * @private 187 | * @param {sequelize.Model} model Instance of Sequelize.Model 188 | * @returns {null} 189 | */ 190 | _verifyModel(model) { 191 | if ('rawAttributes' in model) { 192 | _model.set(this, model); 193 | 194 | return null; 195 | } 196 | 197 | if ('attributes' in model) { 198 | _model.set(this, model); 199 | 200 | return null; 201 | } 202 | 203 | throw new TypeError( 204 | 'Provided model does not match expected format. Are you sure this is a Sequelize model?', 205 | ); 206 | } 207 | 208 | /** 209 | * Enusures the passed strategy is valid. 210 | * 211 | * @private 212 | * @param {strategyInterface} strategy Strategy instance 213 | * @returns {null} 214 | */ 215 | _verifyStrategy(strategy) { 216 | if (!(strategy instanceof StrategyInterface)) 217 | throw new TypeError("Strategy must implement the 'StrategyInterface'"); 218 | 219 | _strategy.set(this, strategy); 220 | } 221 | 222 | /** 223 | * Returns the raw properties from a (v4 or v5+) Sequelize model. 224 | * 225 | * @private 226 | * @returns {object} Raw Sequelize attributes 227 | */ 228 | _getRawAttributes() { 229 | const model = _model.get(this); 230 | let attributes = {}; 231 | 232 | if ('rawAttributes' in model) { 233 | attributes = model.rawAttributes; // v5+ 234 | } else { 235 | attributes = model; // v4 236 | } 237 | 238 | if (_modelOptions.get(this).include.length > 0) { 239 | return pick(attributes, _modelOptions.get(this).include); 240 | } 241 | 242 | if (_modelOptions.get(this).exclude.length > 0) { 243 | return omit(attributes, _modelOptions.get(this).exclude); 244 | } 245 | 246 | return attributes; 247 | } 248 | 249 | /** 250 | * Returns the associations for a Sequelize model. 251 | * 252 | * @private 253 | * @returns {object|null} List of associated models or null 254 | */ 255 | _getAssociations() { 256 | const model = _model.get(this); 257 | 258 | if (model.associations.length === 0) { 259 | return null; 260 | } 261 | 262 | return model.associations; 263 | } 264 | 265 | /** 266 | * Returns the strategy-specific schema structure for the model, ready for attribute insertion. 267 | * 268 | * @private 269 | * @see {@link https://json-schema.org/learn/getting-started-step-by-step.html#properties} 270 | * @returns {object} Schema structure 271 | */ 272 | _getModelContainer() { 273 | const result = {}; 274 | 275 | Object.assign(result, this._getPropertySchema()); // some schemas 276 | Object.assign(result, this._getPropertyId(this._getModelFilePath(_model.get(this).name))); // some schemas 277 | Object.assign(result, this._getModelPropertyTitle(_model.get(this))); // all schemas 278 | Object.assign(result, this._getModelPropertyDescription()); // all schemas but only if user passed the model option 279 | 280 | // identical for all models and schemas thus no need to over-engineer 281 | result.type = 'object'; 282 | result.properties = {}; 283 | 284 | return result; 285 | } 286 | 287 | /** 288 | * Returns the strategy-specific schema structure for the attribute. 289 | * 290 | * @private 291 | * @param {string} attributeName Name of the attribute 292 | * @param {object} attributeProperties The raw sequelize attribute properties 293 | * @returns {object} Schema structure 294 | */ 295 | _getAttributeContainer(attributeName, attributeProperties) { 296 | const typeMapper = new TypeMapper(); 297 | const result = {}; 298 | 299 | Object.assign(result, this._getPropertyId(this._getAttributePath(attributeName))); // `id`: some schemas 300 | Object.assign(result, typeMapper.map(attributeName, attributeProperties, _strategy.get(this))); // `type`: all schemas, strategy-specific types 301 | Object.assign( 302 | result, 303 | this._getAttributePropertyTypeOverride(attributeName, attributeProperties), 304 | ); // override the type generated by type-mapper if 'schema' exists 305 | Object.assign( 306 | result, 307 | this._getAttributePropertyDescription(attributeName, attributeProperties), // `description:` requires sequelize property jsonSchema.description 308 | ); 309 | Object.assign( 310 | result, 311 | this._getAttributePropertyComment(attributeName, attributeProperties), // `comment:` requires sequelize property jsonSchema.description 312 | ); 313 | Object.assign(result, this._getPropertyReadOrWriteOnly(attributeName, attributeProperties)); // `readOnly` or `writeOnly`: some schemas 314 | 315 | Object.assign(result, this._getAttributeExamples(attributeName, attributeProperties)); // `examples`: requires sequelize property jsonSchema.examples 316 | 317 | return result; 318 | } 319 | 320 | /** 321 | * Returns the model file name (e.g. `user.json`). 322 | * 323 | * @private 324 | * @returns {string} 325 | */ 326 | static _getModelFileName(modelName) { 327 | return `${modelName}.json`; 328 | } 329 | 330 | /** 331 | * Returns the model path as used by $id and $ref. 332 | * 333 | * @private 334 | * @param {string} modelName Name of the model 335 | * @returns {string} 336 | */ 337 | _getModelFilePath(modelName) { 338 | const path = this.constructor._getModelFileName(modelName); 339 | 340 | if (_options.get(this).absolutePaths) { 341 | return `${_options.get(this).baseUri}${path}`; 342 | } 343 | 344 | return `/${path}`; 345 | } 346 | 347 | /** 348 | * Returns the `schema` property for the model. 349 | * 350 | * @private 351 | * @param {boolean} secureSchemaUri True for HTTPS, false for HTTP 352 | * @returns {object} 353 | */ 354 | _getPropertySchema() { 355 | return _strategy.get(this).getPropertySchema(_options.get(this).secureSchemaUri); 356 | } 357 | 358 | /** 359 | * Returns the `id` property for the model. 360 | * 361 | * @private 362 | * @param {string} Path to the json file 363 | * @returns {object} 364 | */ 365 | _getPropertyId(path) { 366 | return _strategy.get(this).getPropertyId(path); 367 | } 368 | 369 | /** 370 | * Returns the `title` property for the model. Since this property 371 | * is supported by all schemas we do not need a strategy here. 372 | * 373 | * @private 374 | * @param {sequelize.Model} model Instance of Sequelize.Model 375 | * @returns {object} 376 | */ 377 | _getModelPropertyTitle(model) { 378 | let { title } = _modelOptions.get(this); 379 | 380 | if (!title) { 381 | title = capitalize(model.options.name.singular); 382 | } 383 | 384 | return { 385 | title, 386 | }; 387 | } 388 | 389 | /** 390 | * Returns the `description` property for the model. Since this property 391 | * is supported by all schemas we do not need a strategy here. 392 | * 393 | * @private 394 | * @returns {object|null} Null if the user did not pass the option. 395 | */ 396 | _getModelPropertyDescription() { 397 | const { description } = _modelOptions.get(this); 398 | 399 | if (!description) { 400 | return null; 401 | } 402 | 403 | return { 404 | description, 405 | }; 406 | } 407 | 408 | /** 409 | * Checks if the given attribute property is required 410 | * 411 | * @private 412 | * @param {object} attributeProperties The raw sequelize attribute properties 413 | * @returns {boolean} True if attribute is required 414 | */ 415 | static _isRequiredProperty(attributeProperties) { 416 | if (attributeProperties.allowNull === false) { 417 | return true; 418 | } 419 | 420 | if (attributeProperties.defaultValue !== undefined) { 421 | return true; 422 | } 423 | 424 | return false; 425 | } 426 | 427 | /** 428 | * Return the custom Sequelize attribute property as configured in `jsonSchema`. 429 | * 430 | * @private 431 | * @param {string} propertyName Name of the custom attribute property to search for 432 | * @param {object} attributeProperties Raw Sequelize attribute properties 433 | * @returns {*} Null if the custom attribute does not exist 434 | */ 435 | static _getCustomPropertyValue(propertyName, attributeProperties) { 436 | const { jsonSchema } = attributeProperties; 437 | 438 | if (jsonSchema === undefined) { 439 | return null; 440 | } 441 | 442 | if (jsonSchema[propertyName] === undefined) { 443 | return null; 444 | } 445 | 446 | return jsonSchema[propertyName]; 447 | } 448 | 449 | /** 450 | * Returns a user-defined schema object that overrides the type object 451 | * created by the TypeMapper class. This is necessary for any sequelize type 452 | * that is mapped to the ANY array while using the OpenAPI strategy. 453 | * 454 | * @param {string} attributeName Name of the attribute 455 | * @param {object} attributeProperties Raw sequelize attribute properties 456 | * @returns {object|null} 457 | */ 458 | _getAttributePropertyTypeOverride(attributeName, attributeProperties) { 459 | const schema = this.constructor._getCustomPropertyValue('schema', attributeProperties); 460 | 461 | if (!schema) { 462 | return null; 463 | } 464 | 465 | if (typeof schema === 'object' && typeof schema.type === 'string') { 466 | return schema; 467 | } 468 | 469 | throw new TypeError( 470 | `Custom property 'schema' for sequelize attribute '${attributeName}' should be an object with a 'type' key`, 471 | ); 472 | } 473 | 474 | /** 475 | * Returns the attribute path as used by $id and $ref 476 | * 477 | * @private 478 | * @returns {string} 479 | */ 480 | _getAttributePath(attributeName) { 481 | if (_options.get(this).absolutePaths) { 482 | return `${_options.get(this).baseUri}properties/${attributeName}`; 483 | } 484 | 485 | return `/properties/${attributeName}`; 486 | } 487 | 488 | /** 489 | * Returns the user-defined attribute description. Since this property 490 | * is supported by all schemas we do not need a strategy here. 491 | * 492 | * @private 493 | * @param {string} attributeName Name of the attribute 494 | * @param {object} attributeProperties Raw sequelize attribute properties 495 | * @returns {string} 496 | */ 497 | _getAttributePropertyDescription(attributeName, attributeProperties) { 498 | const description = this.constructor._getCustomPropertyValue( 499 | 'description', 500 | attributeProperties, 501 | ); 502 | 503 | if (description === null) { 504 | return null; 505 | } 506 | 507 | checkTypeRequired('description', description, 'string'); 508 | 509 | return { 510 | description, 511 | }; 512 | } 513 | 514 | /** 515 | * Returns the user-defined attribute comment. 516 | * 517 | * @private 518 | * @param {string} attributeName Name of the attribute 519 | * @param {object} attributeProperties Raw sequelize attribute properties 520 | * @returns {string} 521 | */ 522 | _getAttributePropertyComment(attributeName, attributeProperties) { 523 | if (_options.get(this).disableComments === true) { 524 | return null; 525 | } 526 | 527 | const comment = this.constructor._getCustomPropertyValue('comment', attributeProperties); 528 | 529 | if (comment === null) { 530 | return null; 531 | } 532 | 533 | checkTypeRequired('comment', comment, 'string'); 534 | 535 | return _strategy.get(this).getPropertyComment(comment); 536 | } 537 | 538 | /** 539 | * Returns one of the user-defined attribute properties 'readOnly' or 'writeOnly'. 540 | * 541 | * @private 542 | * @param {string} attributeName Name of the attribute 543 | * @param {object} attributeProperties Raw sequelize attribute properties 544 | * @returns {object|null} 545 | */ 546 | _getPropertyReadOrWriteOnly(attributeName, attributeProperties) { 547 | const readOnly = this.constructor._getCustomPropertyValue('readOnly', attributeProperties); 548 | const writeOnly = this.constructor._getCustomPropertyValue('writeOnly', attributeProperties); 549 | 550 | if (readOnly === null && writeOnly === null) { 551 | return null; 552 | } 553 | 554 | if (readOnly && writeOnly) { 555 | throw new TypeError( 556 | `Custom properties 'readOnly' and 'writeOnly' for sequelize attribute '${attributeName}' are mutually exclusive`, 557 | ); 558 | } 559 | 560 | if (readOnly) { 561 | checkTypeRequired('readOnly', readOnly, 'boolean'); 562 | 563 | return { 564 | readOnly: true, 565 | }; 566 | } 567 | 568 | // still here so writeOnly 569 | checkTypeRequired('writeOnly', writeOnly, 'boolean'); 570 | 571 | return { 572 | writeOnly: true, 573 | }; 574 | } 575 | 576 | /** 577 | * Returns the user-defined attribute examples (strategy-specific) 578 | * 579 | * @private 580 | * @param {string} attributeName Name of the attribute 581 | * @param {object} attributeProperties Raw sequelize attribute properties 582 | * @returns {string} 583 | */ 584 | _getAttributeExamples(attributeName, attributeProperties) { 585 | const examples = this.constructor._getCustomPropertyValue('examples', attributeProperties); 586 | 587 | if (examples === null) { 588 | return null; 589 | } 590 | 591 | // see https://json-schema.org/understanding-json-schema/reference/generic.html 592 | if (!Array.isArray(examples)) { 593 | throw new TypeError("The 'examples' property MUST be an array"); 594 | } 595 | 596 | return _strategy.get(this).getPropertyExamples(examples); 597 | } 598 | 599 | /** 600 | * Returns the property for the given association. 601 | * 602 | * @private 603 | * @param {string} association name 604 | * @param {Sequelize.Association} association Sequelize Association 605 | * @returns {object|null} Object if HasOne, BelongsTo or HasMany and not excluded, null otherwise 606 | */ 607 | _getModelPropertyForAssociation(associationName, association) { 608 | const options = _modelOptions.get(this); 609 | 610 | if ( 611 | options.excludeAssociations.length > 0 && 612 | options.excludeAssociations.includes(associationName) 613 | ) { 614 | return null; 615 | } 616 | 617 | if ( 618 | options.includeAssociations.length > 0 && 619 | options.includeAssociations.includes(associationName) === false 620 | ) { 621 | return null; 622 | } 623 | 624 | switch (association.associationType) { 625 | case 'HasOne': 626 | return _strategy.get(this).getPropertyForHasOneAssociation(associationName, association); 627 | case 'BelongsTo': 628 | return _strategy.get(this).getPropertyForBelongsToAssociation(associationName, association); 629 | case 'HasMany': 630 | return _strategy.get(this).getPropertyForHasManyAssociation(associationName, association); 631 | case 'BelongsToMany': 632 | return _strategy 633 | .get(this) 634 | .getPropertyForBelongsToManyAssociation(associationName, association); 635 | default: 636 | return null; 637 | } 638 | } 639 | } 640 | 641 | module.exports = SchemaManager; 642 | -------------------------------------------------------------------------------- /lib/strategies/json-schema-v7.js: -------------------------------------------------------------------------------- 1 | const StrategyInterface = require('../strategy-interface'); 2 | 3 | /** 4 | * Class responsible for converting Sequelize models into "JSON Schema Draft-07" schemas. 5 | * 6 | * @copyright Copyright (c) 2019 ALT3 B.V. 7 | * @license Licensed under the MIT License 8 | * @augments StrategyInterface 9 | */ 10 | class JsonSchema7Strategy extends StrategyInterface { 11 | /** 12 | * Returns the "$schema" property. 13 | * 14 | * @example 15 | * { 16 | * '$schema': 'https://json-schema.org/draft-07/schema#' 17 | * } 18 | * @param {boolean} secureSchemaUri True for HTTPS, false for HTTP 19 | * @returns {object} 20 | */ 21 | getPropertySchema(secureSchemaUri) { 22 | return { 23 | $schema: `${secureSchemaUri ? 'https' : 'http'}://json-schema.org/draft-07/schema#`, 24 | }; 25 | } 26 | 27 | /** 28 | * Returns the "$id" property. 29 | * 30 | * @example 31 | * { 32 | * '$id': '/user.json' 33 | * } 34 | * @param {string} path 35 | * @returns {object} 36 | */ 37 | getPropertyId(path) { 38 | return { 39 | $id: path, 40 | }; 41 | } 42 | 43 | /** 44 | * Returns the "$comment" property (but only if manager option `disableComments` is false). 45 | * 46 | * @example 47 | * { 48 | * '$comment': 'This comment must be a string' 49 | * } 50 | * @param {string} comment 51 | * @returns {object} 52 | */ 53 | getPropertyComment(comment) { 54 | return { 55 | $comment: comment, 56 | }; 57 | } 58 | 59 | /** 60 | * Returns the "examples" property. 61 | * 62 | * @example 63 | * { 64 | * 'examples': [ 65 | * 'example 1', 66 | * 'example 2' 67 | * ] 68 | * } 69 | * @param {array} examples List with one or multiple examples 70 | * @returns {object} 71 | */ 72 | 73 | getPropertyExamples(examples) { 74 | return { examples }; 75 | } 76 | 77 | /** 78 | * Converts a `type` property so it allows null values. 79 | * 80 | * @example 81 | * { 82 | * type: [ 83 | * 'string', 84 | * 'null' 85 | * ] 86 | * } 87 | * 88 | * @param {string} type Value of the `type` property 89 | * @returns {object} 90 | */ 91 | convertTypePropertyToAllowNull(type) { 92 | if (Array.isArray(type)) { 93 | return { 94 | anyOf: [...type, { type: 'null' }], 95 | }; 96 | } 97 | return { 98 | type: [type, 'null'], 99 | }; 100 | } 101 | 102 | /** 103 | * Returns the `contentEncoding` property as used by Json Schema for base64 encoded strings (like BLOB). 104 | * 105 | * @example 106 | * { 107 | * 'contentEncoding': 'base64', 108 | * } 109 | * 110 | * @returns {object} 111 | */ 112 | getPropertyForBase64Encoding() { 113 | return { 114 | contentEncoding: 'base64', 115 | }; 116 | } 117 | 118 | /** 119 | * Returns the property pointing to a HasOne association. 120 | * 121 | * @example 122 | * { 123 | * profile: { 124 | * $ref: '#/definitions/profile' 125 | * } 126 | * } 127 | * @param {string} association name 128 | * @param {Sequelize.association} association Sequelize associaton object 129 | * @returns {object} Null to omit property from the result 130 | */ 131 | getPropertyForHasOneAssociation(associationName, association) { 132 | return { 133 | [associationName]: { 134 | $ref: `#/definitions/${association.target.name}`, // eslint-disable-line unicorn/prevent-abbreviations 135 | }, 136 | }; 137 | } 138 | 139 | /** 140 | * Returns the property pointing to a BelongsTo association. 141 | * 142 | * @example 143 | * { 144 | * company: { 145 | * $ref: '#/definitions/company' 146 | * } 147 | * } 148 | * @param {string} association name 149 | * @param {Sequelize.association} association Sequelize associaton object 150 | * @returns {object} Null to omit property from the result 151 | */ 152 | getPropertyForBelongsToAssociation(associationName, association) { 153 | return { 154 | [associationName]: { 155 | $ref: `#/definitions/${association.target.name}`, // eslint-disable-line unicorn/prevent-abbreviations 156 | }, 157 | }; 158 | } 159 | 160 | /** 161 | * Returns the property pointing to a HasMany association. 162 | * 163 | * @example 164 | * { 165 | * documents: { 166 | * type: "array", 167 | * items: { 168 | * $ref: '#/definitions/document' 169 | * } 170 | * } 171 | * } 172 | * @param {string} association name 173 | * @param {Sequelize.association} association Sequelize associaton object 174 | * @returns {object} Null to omit property from the result 175 | */ 176 | getPropertyForHasManyAssociation(associationName, association) { 177 | return { 178 | [associationName]: { 179 | type: 'array', 180 | items: { 181 | $ref: `#/definitions/${association.target.name}`, // eslint-disable-line unicorn/prevent-abbreviations 182 | }, 183 | }, 184 | }; 185 | } 186 | 187 | /** 188 | * Returns the property pointing to a BelongsToMany association. 189 | * 190 | * @example 191 | * { 192 | * friends: { 193 | * type: "array", 194 | * items: { 195 | * allOf: [ 196 | * { 197 | * $ref: '#/definitions/user' 198 | * }, 199 | * { 200 | * type: 'object', 201 | * properties: { 202 | * friendship: { 203 | * $ref: '#/definitions/friendship' 204 | * } 205 | * } 206 | * } 207 | * ] 208 | * } 209 | * } 210 | * } 211 | * @param {string} association name 212 | * @param {Sequelize.association} association Sequelize associaton object 213 | * @returns {object} Null to omit property from the result 214 | */ 215 | getPropertyForBelongsToManyAssociation(associationName, association) { 216 | return { 217 | [associationName]: { 218 | type: 'array', 219 | items: { 220 | allOf: [ 221 | { 222 | $ref: `#/definitions/${association.target.name}`, // eslint-disable-line unicorn/prevent-abbreviations 223 | }, 224 | { 225 | type: 'object', 226 | properties: { 227 | [association.through.model.options.name.plural]: { 228 | $ref: `#/definitions/${association.through.model.name}`, // eslint-disable-line unicorn/prevent-abbreviations 229 | }, 230 | }, 231 | }, 232 | ], 233 | }, 234 | }, 235 | }; 236 | } 237 | } 238 | 239 | module.exports = JsonSchema7Strategy; 240 | -------------------------------------------------------------------------------- /lib/strategies/openapi-v3.js: -------------------------------------------------------------------------------- 1 | const StrategyInterface = require('../strategy-interface'); 2 | 3 | /** 4 | * Class responsible for converting Sequelize models into "OpenAPI 3.0" schemas. 5 | * 6 | * @copyright Copyright (c) 2019 ALT3 B.V. 7 | * @license Licensed under the MIT License 8 | * @augments StrategyInterface 9 | */ 10 | class OpenApi3Strategy extends StrategyInterface { 11 | /** 12 | * Returns null because OpenAPI 3.0 does not support the "schema" property. 13 | * 14 | * @param {boolean} secureSchemaUri True for HTTPS, false for HTTP 15 | * @returns {null} 16 | */ 17 | getPropertySchema() { 18 | return null; 19 | } 20 | 21 | /** 22 | * Returns null because OpenAPI 3.0 does not support the "id" property. 23 | * 24 | * @example null 25 | * @param {string} path 26 | * @returns {null} 27 | */ 28 | // eslint-disable-next-line no-unused-vars 29 | getPropertyId(path) { 30 | return null; 31 | } 32 | 33 | /** 34 | * Returns null because OpenAPI 3.0 does not support the "comment" property. 35 | * 36 | * @example null 37 | * @param {string} comment 38 | * @returns {null} 39 | */ 40 | // eslint-disable-next-line no-unused-vars 41 | getPropertyComment(comment) { 42 | return null; 43 | } 44 | 45 | /** 46 | * Returns the "example" property. 47 | * 48 | * @example 49 | * { 50 | * 'example': [ 51 | * 'example 1', 52 | * 'example 2' 53 | * ] 54 | * } 55 | * @param {array} examples List with one or multiple examples 56 | * @returns {object} 57 | */ 58 | getPropertyExamples(examples) { 59 | return { 60 | example: examples, 61 | }; 62 | } 63 | 64 | /** 65 | * Returns the `format` property as used by OAS for base64 base64 encoded strings (like BLOB). 66 | * 67 | * @example 68 | * { 69 | * 'format': 'byte', 70 | * } 71 | * 72 | * @returns {object} 73 | */ 74 | getPropertyForBase64Encoding() { 75 | return { 76 | format: 'byte', 77 | }; 78 | } 79 | 80 | /** 81 | * Returns a new `type` property, enriched to allow null values. 82 | * 83 | * @example 84 | * { 85 | * 'type': 'string', 86 | * 'nullable': 'true' 87 | * } 88 | * 89 | * @param {string|array} type Value of the `type` property 90 | * @returns {object} 91 | */ 92 | convertTypePropertyToAllowNull(type) { 93 | if (Array.isArray(type)) { 94 | return { 95 | anyOf: [...type], 96 | nullable: true, 97 | }; 98 | } 99 | return { 100 | type, 101 | nullable: true, 102 | }; 103 | } 104 | 105 | /** 106 | * Returns the property pointing to a HasOne association. 107 | * 108 | * @example 109 | * { 110 | * profile: { 111 | * $ref: '#/components/schemas/profile' 112 | * } 113 | * } 114 | * @param {string} association name 115 | * @param {Sequelize.association} association Sequelize associaton object 116 | * @returns {object} Null to omit property from the result 117 | */ 118 | getPropertyForHasOneAssociation(associationName, association) { 119 | return { 120 | [associationName]: { 121 | $ref: `#/components/schemas/${association.target.name}`, // eslint-disable-line unicorn/prevent-abbreviations 122 | }, 123 | }; 124 | } 125 | 126 | /** 127 | * Returns the property pointing to a BelongsTo association. 128 | * 129 | * @example 130 | * { 131 | * company: { 132 | * $ref: '#/components/schemas/company' 133 | * } 134 | * } 135 | * @param {string} association name 136 | * @param {Sequelize.association} association Sequelize associaton object 137 | * @returns {object} Null to omit property from the result 138 | */ 139 | getPropertyForBelongsToAssociation(associationName, association) { 140 | return { 141 | [associationName]: { 142 | $ref: `#/components/schemas/${association.target.name}`, // eslint-disable-line unicorn/prevent-abbreviations 143 | }, 144 | }; 145 | } 146 | 147 | /** 148 | * Returns the property pointing to a HasMany association. 149 | * 150 | * @example 151 | * { 152 | * documents: { 153 | * type: "array", 154 | * items: { 155 | * $ref: '#/components/schemas/document' 156 | * } 157 | * } 158 | * } 159 | * @param {string} association name 160 | * @param {Sequelize.association} association Sequelize associaton object 161 | * @returns {object} Null to omit property from the result 162 | */ 163 | getPropertyForHasManyAssociation(associationName, association) { 164 | return { 165 | [associationName]: { 166 | type: 'array', 167 | items: { 168 | $ref: `#/components/schemas/${association.target.name}`, // eslint-disable-line unicorn/prevent-abbreviations 169 | }, 170 | }, 171 | }; 172 | } 173 | 174 | /** 175 | * Returns the property pointing to a BelongsToMany association. 176 | * 177 | * @example 178 | * { 179 | * friends: { 180 | * type: "array", 181 | * items: { 182 | * allOf: [ 183 | * { 184 | * $ref: '#/components/schemas/user' 185 | * }, 186 | * { 187 | * type: 'object', 188 | * properties: { 189 | * friendship: { 190 | * $ref: '#/components/schemas/friendship' 191 | * } 192 | * } 193 | * } 194 | * ] 195 | * } 196 | * } 197 | * } 198 | * @param {string} association name 199 | * @param {Sequelize.association} association Sequelize associaton object 200 | * @returns {object} Null to omit property from the result 201 | */ 202 | getPropertyForBelongsToManyAssociation(associationName, association) { 203 | return { 204 | [associationName]: { 205 | type: 'array', 206 | items: { 207 | allOf: [ 208 | { 209 | $ref: `#/components/schemas/${association.target.name}`, // eslint-disable-line unicorn/prevent-abbreviations 210 | }, 211 | { 212 | type: 'object', 213 | properties: { 214 | [association.through.model.options.name.plural]: { 215 | $ref: `#/components/schemas/${association.through.model.name}`, // eslint-disable-line unicorn/prevent-abbreviations 216 | }, 217 | }, 218 | }, 219 | ], 220 | }, 221 | }, 222 | }; 223 | } 224 | } 225 | 226 | module.exports = OpenApi3Strategy; 227 | -------------------------------------------------------------------------------- /lib/strategy-interface.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | /** 4 | * Strategy interface where we define the methods every inheriting strategy MUST implement. 5 | * 6 | * @copyright Copyright (c) 2019 ALT3 B.V. 7 | * @license Licensed under the MIT License 8 | */ 9 | class StrategyInterface { 10 | /** 11 | * Must return the property used as "schema". 12 | * 13 | * @see {@link https://json-schema.org/understanding-json-schema/reference/schema.html#schema} 14 | * @param {boolean} secureSchemaUri True for HTTPS, false for HTTP 15 | * @returns {object|null} Null to omit property from the result 16 | */ 17 | getPropertySchema() { 18 | this.constructor._throwMissingImplementationError(this.constructor.name, 'getPropertySchema'); 19 | } 20 | 21 | /** 22 | * Must return the property used as "id". 23 | * 24 | * @see {@link https://json-schema.org/understanding-json-schema/structuring.html#the-id-property} 25 | * @param {string} path Path to the json file 26 | * @returns {object|null} Null to omit property from the result 27 | */ 28 | getPropertyId(path) { 29 | this.constructor._throwMissingImplementationError(this.constructor.name, 'getPropertyId'); 30 | } 31 | 32 | /** 33 | * Must return the property used as "comment". 34 | * 35 | * Please note that this comment is not intended to be exposed to users of the schema and therefore 36 | * will not be added to the schema unless manager option 'disableComments' is disabled. 37 | * 38 | * @see {@link https://json-schema.org/understanding-json-schema/reference/generic.html#comments} 39 | * @param {string} comment Value to use as the comment 40 | * @returns {object} 41 | */ 42 | 43 | getPropertyComment(comment) { 44 | this.constructor._throwMissingImplementationError(this.constructor.name, 'getPropertyComment'); 45 | } 46 | 47 | /** 48 | * Must return the property used as "examples". 49 | * @param {array} examples List with one or multiple examples 50 | * @returns {object} 51 | */ 52 | 53 | getPropertyExamples(examples) { 54 | this.constructor._throwMissingImplementationError(this.constructor.name, 'getPropertyExamples'); 55 | } 56 | 57 | /** 58 | * Must return the strategy specific property used for base64 string encoding. 59 | * 60 | * @returns {object} 61 | */ 62 | getPropertyForBase64Encoding() { 63 | this.constructor._throwMissingImplementationError( 64 | this.constructor.name, 65 | 'getPropertyForBase64Encoding', 66 | ); 67 | } 68 | 69 | /** 70 | * Must returns a new `type` object, enriched to allow null values. 71 | * 72 | * @param {string} type Name of the type as determined by the Typemapper (e.g. `string`) 73 | * @returns {object} 74 | */ 75 | convertTypePropertyToAllowNull(type) { 76 | this.constructor._throwMissingImplementationError( 77 | this.constructor.name, 78 | 'convertTypePropertyToAllowNull', 79 | ); 80 | } 81 | 82 | /** 83 | * Must return the property pointing to a HasOne association. 84 | * 85 | * @param {Sequelize.association} association Sequelize associaton object 86 | * @returns {object|null} Null to omit property from the result 87 | */ 88 | getPropertyForHasOneAssociation(association) { 89 | this.constructor._throwMissingImplementationError( 90 | this.constructor.name, 91 | 'getPropertyForHasOneAssociation', 92 | ); 93 | } 94 | 95 | /** 96 | * Must return the property pointing to a HasMany association. 97 | * 98 | * @param {Sequelize.association} association Sequelize associaton object 99 | * @returns {object|null} Null to omit property from the result 100 | */ 101 | getPropertyForHasManyAssociation(association) { 102 | this.constructor._throwMissingImplementationError( 103 | this.constructor.name, 104 | 'getPropertyForHasManyAssociation', 105 | ); 106 | } 107 | 108 | /** 109 | * Throws an error if the strategy has not impemented one of the required methods. 110 | * 111 | * @private 112 | * @param {string} strategyName Name of the strategy with missing method 113 | * @param {string} methodName Name of the missing method 114 | */ 115 | static _throwMissingImplementationError(strategyName, methodName) { 116 | throw new Error(`${strategyName} has not implemented the '${methodName}' interface method.`); 117 | } 118 | } 119 | 120 | module.exports = StrategyInterface; 121 | -------------------------------------------------------------------------------- /lib/type-mapper.js: -------------------------------------------------------------------------------- 1 | // Common types. These should never be exposed directly but, rather, get cloned 2 | // before being returned. This avoids cross-contamination if a user modifies 3 | // the their schema. 4 | const OBJECT = { type: 'object' }; 5 | const ARRAY = { type: 'array' }; 6 | const BOOLEAN = { type: 'boolean' }; 7 | const INTEGER = { type: 'integer' }; 8 | const NUMBER = { type: 'number' }; 9 | const STRING = { type: 'string' }; 10 | const ANY = { anyOf: [OBJECT, ARRAY, BOOLEAN, INTEGER, NUMBER, STRING] }; 11 | 12 | /** 13 | * Class responsible for converting Sequelize DataTypes to strategy-compatible `type` properties. 14 | * 15 | * @copyright Copyright (c) 2019 ALT3 B.V. 16 | * @license Licensed under the MIT License 17 | */ 18 | class TypeMapper { 19 | /** 20 | * Returns the strategy-specific `type` property for the given Sequelize DataType 21 | * 22 | * @see {@link https://sequelize.org/master/manual/data-types.html} 23 | * @param {string} attributeName Name of the Sequelize attribute 24 | * @param {object} properties Properties of the Sequelize attribute 25 | * @param {StrategyInterface} strategy Strategy instance 26 | * @returns {object} Object holding the `type` property 27 | * @throws {TypeError} Throws an exception if the resolved DataType is unkown to the Mapper 28 | */ 29 | map(attributeName, properties, strategy) { 30 | let result; 31 | let attributeType = properties && properties.type && properties.type.key; 32 | 33 | // Aliases 34 | switch (attributeType) { 35 | case 'VIRTUAL': { 36 | // Use schema for the return type (if defined) 37 | attributeType = 38 | properties.type && properties.type.returnType && properties.type.returnType.key; 39 | break; 40 | } 41 | 42 | default: 43 | break; 44 | } 45 | 46 | // convert DataType to `type` property 47 | switch (attributeType) { 48 | // ---------------------------------------------------------------------- 49 | // @todo Sequelize.ARRAY(Sequelize.RANGE(Sequelize.DATE)) 50 | // ---------------------------------------------------------------------- 51 | case 'ARRAY': { 52 | result = { 53 | ...ARRAY, 54 | // Sequelize requires attribute.type to be defined for ARRAYs 55 | items: this.map( 56 | attributeName, 57 | { type: properties.type.type, allowNull: false }, 58 | strategy, 59 | ), 60 | }; 61 | break; 62 | } 63 | 64 | // ---------------------------------------------------------------------- 65 | // BIGINT 66 | // BIGINT(11) 67 | // ---------------------------------------------------------------------- 68 | case 'BIGINT': { 69 | result = { ...INTEGER, format: 'int64' }; 70 | break; 71 | } 72 | 73 | // ---------------------------------------------------------------------- 74 | // BLOB 75 | // @todo BLOB('tiny') 76 | // ---------------------------------------------------------------------- 77 | case 'BLOB': { 78 | result = { ...STRING }; 79 | Object.assign(result, strategy.getPropertyForBase64Encoding()); 80 | break; 81 | } 82 | 83 | // ---------------------------------------------------------------------- 84 | // BOOLEAN 85 | // ---------------------------------------------------------------------- 86 | case 'BOOLEAN': { 87 | result = { ...BOOLEAN }; 88 | break; 89 | } 90 | 91 | // ---------------------------------------------------------------------- 92 | // CIDR 93 | // ---------------------------------------------------------------------- 94 | case 'CIDR': { 95 | result = { ...STRING }; 96 | break; 97 | } 98 | 99 | // ---------------------------------------------------------------------- 100 | // CITEXT 101 | // ---------------------------------------------------------------------- 102 | case 'CITEXT': 103 | result = { ...STRING }; 104 | break; 105 | 106 | // ---------------------------------------------------------------------- 107 | // DATE 108 | // @todo DATE(6) 109 | // ---------------------------------------------------------------------- 110 | case 'DATE': { 111 | result = { ...STRING, format: 'date-time' }; 112 | break; 113 | } 114 | 115 | // ---------------------------------------------------------------------- 116 | // DATEONLY 117 | // ---------------------------------------------------------------------- 118 | case 'DATEONLY': { 119 | result = { ...STRING, format: 'date' }; 120 | break; 121 | } 122 | 123 | // ---------------------------------------------------------------------- 124 | // DECIMAL 125 | // @todo DECIMAL(10, 2) 126 | // ---------------------------------------------------------------------- 127 | case 'DECIMAL': { 128 | result = { ...NUMBER }; 129 | break; 130 | } 131 | 132 | // ---------------------------------------------------------------------- 133 | // DOUBLE 134 | // @todo DOUBLE(11) 135 | // @todo DOUBLE(11,10) 136 | // ---------------------------------------------------------------------- 137 | case 'DOUBLE PRECISION': { 138 | result = { ...NUMBER, format: 'double' }; 139 | break; 140 | } 141 | 142 | // ---------------------------------------------------------------------- 143 | // ENUM('value 1', 'value 2') 144 | // ---------------------------------------------------------------------- 145 | case 'ENUM': { 146 | result = { ...STRING, enum: [...(properties.values || properties.type.values)] }; 147 | break; 148 | } 149 | 150 | // ---------------------------------------------------------------------- 151 | // INET 152 | // @todo this one currently breaks json-schema-v7 validation 153 | // @see https://github.com/Julian/jsonschema/issues/171 154 | // ---------------------------------------------------------------------- 155 | case 'INET': { 156 | result = { 157 | type: [ 158 | { ...STRING, format: 'ipv4' }, 159 | { ...STRING, format: 'ipv6' }, 160 | ], 161 | }; 162 | break; 163 | } 164 | 165 | // ---------------------------------------------------------------------- 166 | // INTEGER 167 | // ---------------------------------------------------------------------- 168 | case 'INTEGER': { 169 | result = { ...INTEGER, format: 'int32' }; 170 | break; 171 | } 172 | 173 | // ---------------------------------------------------------------------- 174 | // FLOAT 175 | // @todo FLOAT(11) 176 | // @todo FLOAT(11,10) 177 | // ---------------------------------------------------------------------- 178 | case 'FLOAT': { 179 | result = { ...NUMBER, format: 'float' }; 180 | break; 181 | } 182 | 183 | // ---------------------------------------------------------------------- 184 | // @todo GEOMETRY 185 | // @todo GEOMETRY('POINT') 186 | // @todo GEOMETRY('POINT', 4326) 187 | // ---------------------------------------------------------------------- 188 | case 'GEOMETRY': { 189 | throw new TypeError( 190 | 'sequelize-to-json-schemas has not yet implemented the GEOMETRY DataType', 191 | ); 192 | } 193 | 194 | // ---------------------------------------------------------------------- 195 | // JSON 196 | // ---------------------------------------------------------------------- 197 | case 'JSON': { 198 | result = { ...ANY }; 199 | break; 200 | } 201 | 202 | // ---------------------------------------------------------------------- 203 | // JSONB 204 | // ---------------------------------------------------------------------- 205 | case 'JSONB': { 206 | result = { ...ANY }; 207 | break; 208 | } 209 | 210 | // ---------------------------------------------------------------------- 211 | // MACADDR 212 | // ---------------------------------------------------------------------- 213 | case 'MACADDR': { 214 | result = { ...STRING }; 215 | break; 216 | } 217 | 218 | // ---------------------------------------------------------------------- 219 | // @todo Sequelize.RANGE(Sequelize.INTEGER) 220 | // @todo Sequelize.RANGE(Sequelize.BIGINT) 221 | // @todo Sequelize.RANGE(Sequelize.DATE) 222 | // @todo Sequelize.RANGE(Sequelize.DATEONLY) 223 | // @todo Sequelize.RANGE(Sequelize.DECIMAL) 224 | // ---------------------------------------------------------------------- 225 | case 'RANGE': { 226 | throw new TypeError('sequelize-to-json-schemas has not yet implemented the RANGE DataType'); 227 | } 228 | 229 | // ---------------------------------------------------------------------- 230 | // REAL 231 | // @todo REAL(11) 232 | // @todo REAL(11,12) 233 | // ---------------------------------------------------------------------- 234 | case 'REAL': { 235 | result = { ...NUMBER }; 236 | break; 237 | } 238 | 239 | // ---------------------------------------------------------------------- 240 | // STRING 241 | // STRING(1234) 242 | // STRING.BINARY 243 | // ---------------------------------------------------------------------- 244 | case 'STRING': { 245 | result = { ...STRING }; 246 | 247 | if (properties.type.options.length !== undefined) { 248 | result.maxLength = properties.type.options.length; 249 | } 250 | 251 | if (properties.type.options.binary !== undefined) { 252 | result.format = 'binary'; 253 | } 254 | 255 | break; 256 | } 257 | 258 | // ---------------------------------------------------------------------- 259 | // TEXT 260 | // TEXT('tiny') 261 | // ---------------------------------------------------------------------- 262 | case 'TEXT': 263 | result = { ...STRING }; 264 | break; 265 | 266 | // ---------------------------------------------------------------------- 267 | // UUID @todo: doublecheck the V1/V4 DataTypes 268 | // ---------------------------------------------------------------------- 269 | case 'UUID': { 270 | result = { ...STRING, format: 'uuid' }; 271 | break; 272 | } 273 | case 'UUIDV1': { 274 | result = { ...STRING, format: 'uuid' }; 275 | break; 276 | } 277 | case 'UUIDV4': { 278 | result = { ...STRING, format: 'uuid' }; 279 | break; 280 | } 281 | 282 | // ---------------------------------------------------------------------- 283 | // @todo these require further investigation before relocating 284 | // ---------------------------------------------------------------------- 285 | case 'ABSTRACT': { 286 | throw new TypeError( 287 | 'sequelize-to-json-schemas has not yet implemented the ABSTRACT DataType', 288 | ); 289 | } 290 | 291 | case 'CHAR': { 292 | result = { ...STRING }; 293 | break; 294 | } 295 | 296 | case 'GEOGRAPHY': { 297 | throw new TypeError( 298 | 'sequelize-to-json-schemas has not yet implemented the GEOGRAPHY DataType', 299 | ); 300 | } 301 | 302 | case 'HSTORE': { 303 | throw new TypeError( 304 | 'sequelize-to-json-schemas has not yet implemented the HSTORE DataType', 305 | ); 306 | } 307 | 308 | case 'MEDIUMINT': { 309 | result = { ...INTEGER }; 310 | break; 311 | } 312 | // NOW: null, 313 | case 'NUMBER': { 314 | result = { ...NUMBER }; 315 | break; 316 | } 317 | case 'SMALLINT': { 318 | result = { ...INTEGER }; 319 | break; 320 | } 321 | 322 | case 'TIME': { 323 | result = { ...STRING, format: 'time' }; 324 | break; 325 | } 326 | case 'TINYINT': { 327 | result = { ...NUMBER }; 328 | break; 329 | } 330 | 331 | case 'VIRTUAL': { 332 | // Use result for the return type (if defined) 333 | result = this.map( 334 | attributeName, 335 | { ...properties, type: properties.type && properties.type.returnType }, 336 | strategy, 337 | ); 338 | break; 339 | } 340 | 341 | default: 342 | // ---------------------------------------------------------------------- 343 | // use ANY for anything not matching (for now, needs investigating) 344 | // ---------------------------------------------------------------------- 345 | result = { ...ANY }; 346 | } 347 | 348 | // ---------------------------------------------------------------------- 349 | // Sequelize options applying to all types starting below 350 | // ---------------------------------------------------------------------- 351 | if (properties.allowNull !== false) { 352 | if (result.type) { 353 | Object.assign(result, this.constructor._getNullableType(result.type, strategy)); 354 | } else { 355 | Object.assign(result, this.constructor._getNullableType(result.anyOf, strategy)); 356 | } 357 | } 358 | 359 | if (properties.defaultValue !== undefined) { 360 | result.default = properties.defaultValue; // supported by all strategies so no need for complexity 361 | } 362 | 363 | return result; 364 | } 365 | 366 | /** 367 | * Replaces current `schema.type` with the object returned by the strategy as 368 | * the solution for nullable types can vary strongly between them. 369 | * 370 | * @private 371 | * @param {string} type Name of the type (e.g. 'string') 372 | * @param {StrategyInterface} strategy Strategy instance 373 | * @returns {object} 374 | */ 375 | static _getNullableType(type, strategy) { 376 | const result = strategy.convertTypePropertyToAllowNull(type); 377 | 378 | if (typeof result !== 'object') { 379 | throw new TypeError("convertTypePropertyToAllowNull() return value not of type 'object'"); 380 | } 381 | 382 | if ( 383 | !Object.prototype.hasOwnProperty.call(result, 'type') && 384 | !Object.prototype.hasOwnProperty.call(result, 'anyOf') 385 | ) { 386 | throw new TypeError( 387 | "convertTypePropertyToAllowNull() return value does not have property 'type' or 'anyOf'", 388 | ); 389 | } 390 | 391 | return result; 392 | } 393 | } 394 | 395 | module.exports = TypeMapper; 396 | -------------------------------------------------------------------------------- /lib/utils/lodash-natives.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Native alternatives for lodash. 3 | * 4 | * @comment disabling eslint rules so examples stay identical to github 5 | * 6 | * @see {@link https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore} 7 | * @see {@link https://xebia.com/blog/you-might-not-need-lodash-in-your-es2015-project/} 8 | */ 9 | 10 | /* eslint-disable func-names */ 11 | /* eslint-disable no-prototype-builtins */ 12 | /* eslint-disable no-param-reassign */ 13 | /* eslint-disable unicorn/prevent-abbreviations */ 14 | /* eslint-disable unicorn/no-array-reduce */ 15 | /* eslint-disable unicorn/prefer-object-from-entries */ 16 | 17 | const capitalize = function (string) { 18 | if (typeof string !== 'string') { 19 | throw new TypeError("The 'string' argument for _capitalize() must be a string"); 20 | } 21 | 22 | return string.charAt(0).toUpperCase() + string.slice(1); 23 | }; 24 | 25 | const pick = function (object, keys) { 26 | return keys.reduce((obj, key) => { 27 | if (object && object.hasOwnProperty(key)) { 28 | obj[key] = object[key]; 29 | } 30 | 31 | return obj; 32 | }, {}); 33 | }; 34 | 35 | // --------------------------------------------------- 36 | // omit alternative taken from 37 | // https://dustinpfister.github.io/2019/08/19/lodash_omit/ 38 | // --------------------------------------------------- 39 | const inProps = function (key, props) { 40 | // eslint-disable-next-line unicorn/prefer-includes 41 | return props.some((omitKey) => { 42 | return omitKey === key; 43 | }); 44 | }; 45 | 46 | const omit = function (object, properties) { 47 | const newObject = {}; 48 | for (const key of Object.keys(object)) { 49 | if (!inProps(key, properties)) { 50 | newObject[key] = object[key]; 51 | } 52 | } 53 | 54 | return newObject; 55 | }; 56 | 57 | module.exports = { 58 | capitalize, 59 | omit, 60 | pick, 61 | }; 62 | -------------------------------------------------------------------------------- /lib/utils/type-checks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DRY function for checking if passed value is of expected type. 3 | * 4 | * @param {string} name Name of setting to check 5 | * @param {object} value Value to check 6 | * @param {string} type Type to match 7 | * @param {string} errorPrefix String to prepend thrown error with 8 | * @returns {bool} 9 | * @throws {TypeError} 10 | */ 11 | function checkType(name, value, type, errorPrefix) { 12 | if (type === 'array' && !Array.isArray(value)) { 13 | throw new TypeError(`${errorPrefix} configuration setting '${name}' not of type '${type}'`); 14 | } 15 | 16 | // eslint-disable-next-line valid-typeof 17 | if (type !== 'array' && typeof value !== type) { 18 | throw new TypeError(`${errorPrefix} configuration setting '${name}' not of type '${type}'`); 19 | } 20 | 21 | return true; 22 | } 23 | 24 | /** 25 | * Checks if optional value is of expected type. 26 | * 27 | * @param {string} name Object to check 28 | * @param {object} value Object to check 29 | * @param {string} type Property type 30 | * @returns {bool} 31 | * @throws {TypeError} 32 | */ 33 | const checkTypeOptional = function check(name, value, type) { 34 | if (value === null) { 35 | return true; 36 | } 37 | 38 | return checkType(name, value, type, 'Optional'); 39 | }; 40 | 41 | /** 42 | * Checks if required value is of expected type. 43 | * 44 | * @param {string} name Object to check 45 | * @param {object} value Object to check 46 | * @param {string} type Property type 47 | * @returns {bool} 48 | * @throws {TypeError} 49 | */ 50 | const checkTypeRequired = function check(name, value, type) { 51 | if (value === null) { 52 | throw new TypeError(`Required configuration setting '${name}' is missing`); 53 | } 54 | 55 | return checkType(name, value, type, 'Required'); 56 | }; 57 | 58 | module.exports = { 59 | checkTypeOptional, 60 | checkTypeRequired, 61 | }; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alt3/sequelize-to-json-schemas", 3 | "version": "0.3.56", 4 | "description": "Convert Sequelize models into various JSON Schema variants (using the Strategy Pattern)", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/alt3/sequelize-to-json-schemas.git" 9 | }, 10 | "author": { 11 | "name": "ALT3 B.V.", 12 | "url": "https://github.com/alt3/sequelize-to-json-schemas" 13 | }, 14 | "contributors": [ 15 | "Wolfgang Walther" 16 | ], 17 | "bugs": "https://github.com/alt3/sequelize-to-json-schemas/issues", 18 | "engines": { 19 | "node": ">=8", 20 | "npm": ">=3" 21 | }, 22 | "scripts": { 23 | "coverage": "jest --collect-coverage", 24 | "docs": "del-cli \"docs/*\" \"!docs/README.md\" && npx jsdoc -c .jsdoc docs/README.md", 25 | "examples": "del-cli \"examples/*\" \"!examples/generate.js\" \"!examples/README.md\" && node ./examples/generate.js", 26 | "lint:eslint": "eslint --ignore-path .gitignore .", 27 | "lint:prettier": "prettier --list-different \"**/*.{js,json,md,yml}\"", 28 | "lint": "run-p lint:*", 29 | "start": "npx nodemon index.js", 30 | "test": "jest --verbose", 31 | "test:integration": "jest --testNamePattern, #integration", 32 | "test:unit": "jest --testNamePattern, #unit", 33 | "test:watch": "jest --watch", 34 | "release": "release-it" 35 | }, 36 | "files": [ 37 | "lib" 38 | ], 39 | "main": "lib/index.js", 40 | "keywords": [ 41 | "javascript", 42 | "sequelize", 43 | "json-schema", 44 | "strategy-pattern", 45 | "swagger", 46 | "openapi", 47 | "oas", 48 | "oasv3" 49 | ], 50 | "devDependencies": { 51 | "@release-it/conventional-changelog": "^3.3.0", 52 | "acorn": "^8.6.0", 53 | "ajv": "^8.8.2", 54 | "codecov": "^3.8.3", 55 | "cross-env": "^7.0.3", 56 | "del-cli": "^4.0", 57 | "eslint": "^8.4.1", 58 | "eslint-config-airbnb-base": "^15.0.0", 59 | "eslint-config-prettier": "^8.0.0", 60 | "eslint-plugin-import": "^2.25.3", 61 | "eslint-plugin-jest": "^25.3.0", 62 | "eslint-plugin-jsdoc": "^37.1.0", 63 | "eslint-plugin-prettier": "^4.0.0", 64 | "eslint-plugin-unicorn": "^39.0.0", 65 | "fs": "0.0.1-security", 66 | "husky": "^7.0.4", 67 | "jest": "^25.0.0", 68 | "jsdoc": "^3.6.6", 69 | "lint-staged": "^12.1.2", 70 | "lodash.clonedeep": "^4.5.0", 71 | "moment": "^2.29.1", 72 | "mysql2": "^2.3.3", 73 | "npm-run-all": "^4.1.3", 74 | "prettier": "^2.5.1", 75 | "release-it": "^14.11.8", 76 | "sequelize": "^6.11.0", 77 | "swagger-parser": "^10.0.3" 78 | }, 79 | "esdoc": { 80 | "source": "./src", 81 | "destination": "./esdoc", 82 | "plugins": [ 83 | { 84 | "name": "esdoc-standard-plugin" 85 | } 86 | ] 87 | }, 88 | "eslintConfig": { 89 | "extends": [ 90 | "airbnb-base", 91 | "plugin:prettier/recommended", 92 | "plugin:unicorn/recommended", 93 | "plugin:jest/recommended" 94 | ], 95 | "env": { 96 | "browser": false, 97 | "jest/globals": true 98 | }, 99 | "overrides": [ 100 | { 101 | "files": [ 102 | "*.spec.js" 103 | ], 104 | "env": { 105 | "jest": true 106 | }, 107 | "rules": { 108 | "import/no-extraneous-dependencies": [ 109 | "error", 110 | { 111 | "devDependencies": true 112 | } 113 | ] 114 | } 115 | }, 116 | { 117 | "files": [ 118 | "test/**/*.test.js" 119 | ], 120 | "rules": { 121 | "func-names": "off" 122 | } 123 | }, 124 | { 125 | "files": [ 126 | "lib/strategies/**/*.js" 127 | ], 128 | "rules": { 129 | "class-methods-use-this": "off" 130 | } 131 | } 132 | ], 133 | "plugins": [ 134 | "jest" 135 | ], 136 | "rules": { 137 | "no-restricted-syntax": "off", 138 | "no-underscore-dangle": "off", 139 | "unicorn/no-null": "off", 140 | "unicorn/prefer-module": "off", 141 | "unicorn/prefer-ternary": "off" 142 | } 143 | }, 144 | "husky": { 145 | "hooks": { 146 | "pre-commit": "lint-staged && npm run examples" 147 | } 148 | }, 149 | "jest": { 150 | "testEnvironment": "node", 151 | "coverageReporters": [ 152 | "lcov", 153 | "text", 154 | "html" 155 | ], 156 | "collectCoverageFrom": [ 157 | "./lib/**/*" 158 | ] 159 | }, 160 | "lint-staged": { 161 | "*": "prettier --list-different", 162 | "*.js": [ 163 | "eslint", 164 | "jest --bail --findRelatedTests" 165 | ] 166 | }, 167 | "prettier": { 168 | "singleQuote": true, 169 | "trailingComma": "all" 170 | }, 171 | "release-it": { 172 | "git": { 173 | "commit": true, 174 | "commitMessage": "Chore: Release ${version}", 175 | "requireUpstream": false 176 | }, 177 | "github": { 178 | "release": true 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /test/models/company.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sequelize model definition for testing User belongsTo. 3 | * 4 | * @param sequelize Sequelize Instance 5 | * @param Sequelize Sequelize Class 6 | * @returns {CompanyClass} Returns the Company model 7 | */ 8 | module.exports = (sequelize, { DataTypes }) => { 9 | const Model = sequelize.define( 10 | 'company', 11 | { 12 | id: { 13 | type: DataTypes.INTEGER, 14 | primaryKey: true, 15 | allowNull: false, 16 | }, 17 | name: { 18 | type: DataTypes.STRING, 19 | }, 20 | }, 21 | { 22 | timestamps: false, 23 | underscored: false, 24 | }, 25 | ); 26 | 27 | return Model; 28 | }; 29 | -------------------------------------------------------------------------------- /test/models/document.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sequelize model definition for testing User hasMany. 3 | * 4 | * @param sequelize Sequelize Instance 5 | * @param Sequelize Sequelize Class 6 | * @returns {DocumentClass} Returns the Document model 7 | */ 8 | module.exports = (sequelize, { DataTypes }) => { 9 | const Model = sequelize.define( 10 | 'document', 11 | { 12 | id: { 13 | type: DataTypes.INTEGER, 14 | primaryKey: true, 15 | allowNull: false, 16 | }, 17 | name: { 18 | type: DataTypes.STRING, 19 | }, 20 | }, 21 | { 22 | timestamps: false, 23 | underscored: false, 24 | }, 25 | ); 26 | 27 | return Model; 28 | }; 29 | -------------------------------------------------------------------------------- /test/models/friendship.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sequelize model definition for testing User belongsToMany. 3 | * 4 | * @param sequelize Sequelize Instance 5 | * @param Sequelize Sequelize Class 6 | * @returns {FriendshipClass} Returns the Friendship model 7 | */ 8 | module.exports = (sequelize, { DataTypes }) => { 9 | const Model = sequelize.define( 10 | 'friendship', 11 | { 12 | isBestFriend: { 13 | type: DataTypes.BOOLEAN, 14 | defaultValue: false, 15 | }, 16 | }, 17 | { 18 | timestamps: false, 19 | underscored: false, 20 | }, 21 | ); 22 | 23 | return Model; 24 | }; 25 | -------------------------------------------------------------------------------- /test/models/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper used to connect all models to a database object so everything is accessible 3 | * via a single object. Does not require an active database connection. 4 | */ 5 | 6 | const Sequelize = require('sequelize'); 7 | 8 | const sequelize = new Sequelize({ 9 | dialect: 'mysql', 10 | }); 11 | 12 | const database = {}; 13 | 14 | database.Sequelize = Sequelize; 15 | database.sequelize = sequelize; 16 | 17 | // models 18 | database.user = require('./user')(sequelize, Sequelize); 19 | database.profile = require('./profile')(sequelize, Sequelize); 20 | database.company = require('./company')(sequelize, Sequelize); 21 | database.document = require('./document')(sequelize, Sequelize); 22 | database.friendship = require('./friendship')(sequelize, Sequelize); 23 | 24 | // associations 25 | database.user.hasOne(database.profile); 26 | database.user.belongsTo(database.company); 27 | database.user.hasMany(database.document); 28 | 29 | database.user.hasOne(database.user, { as: 'boss' }); 30 | 31 | database.user.belongsToMany(database.user, { as: 'friends', through: database.friendship }); 32 | 33 | module.exports = database; 34 | -------------------------------------------------------------------------------- /test/models/profile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sequelize model definition for testing User hasOne. 3 | * 4 | * @param sequelize Sequelize Instance 5 | * @param Sequelize Sequelize Class 6 | * @returns {ProfileClass} Returns the Profile model 7 | */ 8 | module.exports = (sequelize, { DataTypes }) => { 9 | const Model = sequelize.define( 10 | 'profile', 11 | { 12 | id: { 13 | type: DataTypes.INTEGER, 14 | primaryKey: true, 15 | allowNull: false, 16 | }, 17 | name: { 18 | type: DataTypes.STRING, 19 | }, 20 | }, 21 | { 22 | timestamps: false, 23 | underscored: false, 24 | }, 25 | ); 26 | 27 | return Model; 28 | }; 29 | -------------------------------------------------------------------------------- /test/models/user.js: -------------------------------------------------------------------------------- 1 | const supportedDataType = require('../utils/supported-datatype'); 2 | 3 | /** 4 | * Sequelize attribute definitions for the `user` model. 5 | * 6 | * This model should contain attribute definitions for all known DataTypes. 7 | * Please note that an attribute definitions will only be included in the 8 | * model if the tested Sequelize version supports the DataType. For example 9 | * Sequelize v4 does not support CITEXT so the _CITEXT_ attribute will not 10 | * be present in the model when testing Sequelize v4. 11 | * 12 | * @see https://sequelize.org/master/manual/data-types.html 13 | * 14 | * @param sequelize Sequelize Instance 15 | * @param Sequelize Sequelize Class 16 | * @returns {UserClass} Returns the User model 17 | */ 18 | module.exports = (sequelize, { DataTypes }) => { 19 | const Model = sequelize.define( 20 | 'user', 21 | { 22 | id: { 23 | type: DataTypes.INTEGER, 24 | primaryKey: true, 25 | allowNull: false, 26 | }, 27 | }, 28 | // sequelize options 29 | { 30 | timestamps: true, 31 | underscored: false, 32 | }, 33 | ); 34 | 35 | // -------------------------------------------------------------------------- 36 | // Define ALL Sequelize DataTypes below, including their variations. Only 37 | // added to the model if supported by this sequelize version. 38 | // -------------------------------------------------------------------------- 39 | if (supportedDataType('ARRAY')) { 40 | Model.rawAttributes.ARRAY_INTEGERS = { 41 | type: DataTypes.ARRAY(DataTypes.INTEGER), 42 | allowNull: false, 43 | }; 44 | 45 | Model.rawAttributes.ARRAY_TEXTS = { 46 | type: DataTypes.ARRAY(DataTypes.TEXT), 47 | allowNull: false, 48 | }; 49 | 50 | Model.rawAttributes.ARRAY_ALLOWNULL_EXPLICIT = { 51 | type: DataTypes.ARRAY(DataTypes.TEXT), 52 | allowNull: true, 53 | }; 54 | 55 | Model.rawAttributes.ARRAY_ALLOWNULL_IMPLICIT = { 56 | type: DataTypes.ARRAY(DataTypes.TEXT), 57 | }; 58 | 59 | if (supportedDataType('ENUM')) { 60 | Model.rawAttributes.ARRAY_ENUM_STRINGS = { 61 | type: DataTypes.ARRAY(DataTypes.ENUM('hello', 'world')), 62 | allowNull: false, 63 | }; 64 | } 65 | } 66 | 67 | if (supportedDataType('BLOB')) { 68 | Model.rawAttributes.BLOB = { 69 | type: DataTypes.BLOB, 70 | allowNull: false, 71 | }; 72 | } 73 | 74 | if (supportedDataType('CITEXT')) { 75 | Model.rawAttributes.CITEXT = { 76 | type: DataTypes.CITEXT, 77 | allowNull: false, 78 | }; 79 | } 80 | 81 | if (supportedDataType('INTEGER')) { 82 | Model.rawAttributes.INTEGER = { 83 | type: DataTypes.INTEGER, 84 | allowNull: false, 85 | defaultValue: 0, 86 | }; 87 | } 88 | 89 | if (supportedDataType('STRING')) { 90 | Model.rawAttributes.STRING = { 91 | type: DataTypes.STRING, 92 | allowNull: false, 93 | defaultValue: 'Default value for STRING', 94 | }; 95 | 96 | Model.rawAttributes.STRING_ALLOWNULL_EXPLICIT = { 97 | type: DataTypes.STRING, 98 | allowNull: true, 99 | }; 100 | 101 | Model.rawAttributes.STRING_ALLOWNULL_IMPLICIT = { 102 | type: DataTypes.STRING, 103 | }; 104 | 105 | Model.rawAttributes.STRING_1234 = { 106 | type: DataTypes.STRING(1234), 107 | allowNull: false, 108 | }; 109 | 110 | Model.rawAttributes.STRING_DOT_BINARY = { 111 | type: DataTypes.STRING.BINARY, 112 | allowNull: false, 113 | }; 114 | } 115 | 116 | if (supportedDataType('TEXT')) { 117 | Model.rawAttributes.TEXT = { 118 | type: DataTypes.TEXT, 119 | allowNull: false, 120 | }; 121 | } 122 | 123 | if (supportedDataType('UUIDV4')) { 124 | Model.rawAttributes.UUIDV4 = { 125 | type: DataTypes.UUID, 126 | allowNull: false, 127 | }; 128 | } 129 | 130 | if (supportedDataType('JSON')) { 131 | Model.rawAttributes.JSON = { 132 | type: DataTypes.JSON, 133 | allowNull: false, 134 | jsonSchema: { 135 | schema: { type: 'object' }, // required for OpenAPI 136 | }, 137 | }; 138 | } 139 | 140 | if (supportedDataType('JSONB')) { 141 | Model.rawAttributes.JSONB_ALLOWNULL = { 142 | type: DataTypes.JSONB, 143 | allowNull: true, 144 | }; 145 | } 146 | 147 | if (supportedDataType('VIRTUAL')) { 148 | Model.rawAttributes.VIRTUAL = { 149 | type: DataTypes.VIRTUAL(DataTypes.BOOLEAN), 150 | allowNull: false, 151 | get: () => true, 152 | }; 153 | 154 | Model.rawAttributes.VIRTUAL_DEPENDENCY = { 155 | type: new DataTypes.VIRTUAL(DataTypes.INTEGER, ['id']), 156 | allowNull: false, 157 | get() { 158 | return this.get('id'); 159 | }, 160 | }; 161 | } 162 | 163 | // -------------------------------------------------------------------------- 164 | // Custom options (as specified through `jsonSchema`) starting below. 165 | // -------------------------------------------------------------------------- 166 | Model.rawAttributes.CUSTOM_DESCRIPTION = { 167 | type: DataTypes.STRING, 168 | allowNull: false, 169 | jsonSchema: { 170 | description: 'Custom attribute description', 171 | }, 172 | }; 173 | 174 | Model.rawAttributes.CUSTOM_COMMENT = { 175 | type: DataTypes.STRING, 176 | allowNull: false, 177 | jsonSchema: { 178 | comment: 'Custom comment', 179 | }, 180 | }; 181 | 182 | Model.rawAttributes.CUSTOM_EXAMPLES = { 183 | type: DataTypes.STRING, 184 | allowNull: false, 185 | jsonSchema: { 186 | examples: ['Custom example 1', 'Custom example 2'], 187 | }, 188 | }; 189 | 190 | Model.rawAttributes.CUSTOM_READONLY = { 191 | type: DataTypes.STRING, 192 | allowNull: false, 193 | jsonSchema: { 194 | readOnly: true, 195 | }, 196 | }; 197 | 198 | Model.rawAttributes.CUSTOM_WRITEONLY = { 199 | allowNull: false, 200 | type: DataTypes.STRING, 201 | jsonSchema: { 202 | writeOnly: true, 203 | }, 204 | }; 205 | 206 | return Model; 207 | }; 208 | -------------------------------------------------------------------------------- /test/schema-manager.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | const models = require('./models'); 4 | const { JsonSchemaManager, JsonSchema7Strategy } = require('../lib'); 5 | 6 | describe('SchemaManager', function () { 7 | describe('Test configuration options for the class constructor', function () { 8 | // ------------------------------------------------------------------------ 9 | // make sure default option values work as expected 10 | // ------------------------------------------------------------------------ 11 | describe('Ensure default options:', function () { 12 | const schemaManager = new JsonSchemaManager(); 13 | const strategy = new JsonSchema7Strategy(); 14 | const schema = schemaManager.generate(models.user, strategy); 15 | 16 | it(`produce a HTTPS schema URI`, function () { 17 | expect(schema.$schema).toEqual('https://json-schema.org/draft-07/schema#'); 18 | }); 19 | 20 | it(`produce relative paths for models`, function () { 21 | expect(schema.$id).toEqual('/user.json'); 22 | }); 23 | 24 | it(`produce relative paths for attribute properties`, function () { 25 | expect(schema.properties.id.$id).toEqual('/properties/id'); 26 | }); 27 | 28 | it(`does not include attribute property '$comment'`, function () { 29 | expect(schema.properties.CUSTOM_COMMENT.$comment).toBeUndefined(); 30 | }); 31 | }); 32 | 33 | // ------------------------------------------------------------------------ 34 | // make sure option 'secureSchemaUri: false` renders a HTTP schema URI 35 | // ------------------------------------------------------------------------ 36 | describe(`Ensure false option 'secureSchemaUri':`, function () { 37 | const schemaManager = new JsonSchemaManager({ 38 | secureSchemaUri: false, 39 | }); 40 | const strategy = new JsonSchema7Strategy(); 41 | const schema = schemaManager.generate(models.user, strategy); 42 | 43 | it(`includes attribute property '$schema' with HTTP link`, function () { 44 | expect(schema.$schema).toEqual('http://json-schema.org/draft-07/schema#'); 45 | }); 46 | }); 47 | 48 | // ------------------------------------------------------------------------ 49 | // make sure option 'baseUri' works as expected 50 | // ------------------------------------------------------------------------ 51 | describe(`Ensure non-default option 'baseUri':`, function () { 52 | const schemaManager = new JsonSchemaManager({ 53 | baseUri: 'https://alt3.io', 54 | }); 55 | const strategy = new JsonSchema7Strategy(); 56 | const schema = schemaManager.generate(models.user, strategy); 57 | 58 | it(`produces absolute paths for models`, function () { 59 | expect(schema.$id).toEqual('https://alt3.io/user.json'); 60 | }); 61 | 62 | it(`produces absolute paths for attribute properties`, function () { 63 | expect(schema.properties.id.$id).toEqual('https://alt3.io/properties/id'); 64 | }); 65 | }); 66 | 67 | // ------------------------------------------------------------------------ 68 | // make sure option 'disableComments' works as expected 69 | // ------------------------------------------------------------------------ 70 | describe(`Ensure false option 'disableComments':`, function () { 71 | const schemaManager = new JsonSchemaManager({ 72 | disableComments: false, 73 | }); 74 | const strategy = new JsonSchema7Strategy(); 75 | const schema = schemaManager.generate(models.user, strategy); 76 | 77 | it(`includes attribute property '$comment'`, function () { 78 | expect(schema.properties.CUSTOM_COMMENT.$comment).toEqual('Custom comment'); 79 | }); 80 | }); 81 | 82 | // ------------------------------------------------------------------------ 83 | // make sure disabling 'absolutePaths' renders relative paths 84 | // ------------------------------------------------------------------------ 85 | describe(`Ensure false option 'absolutePaths':`, function () { 86 | const schemaManager = new JsonSchemaManager({ 87 | baseUri: 'https://alt3.io', 88 | absolutePaths: false, 89 | }); 90 | const strategy = new JsonSchema7Strategy(); 91 | const schema = schemaManager.generate(models.user, strategy); 92 | 93 | it(`ignores baseUri and produces relative paths for models`, function () { 94 | expect(schema.$id).toEqual('/user.json'); 95 | }); 96 | 97 | it(`ignores baseUri and produces relative paths for attribute properties`, function () { 98 | expect(schema.properties.id.$id).toEqual('/properties/id'); 99 | }); 100 | }); 101 | }); 102 | 103 | describe('Test configuration options for the generate() method', function () { 104 | // ------------------------------------------------------------------------ 105 | // make sure default MODEL options render the expected MODEL properties 106 | // ------------------------------------------------------------------------ 107 | describe('Ensure default model options:', function () { 108 | const schemaManager = new JsonSchemaManager(); 109 | const strategy = new JsonSchema7Strategy(); 110 | const schema = schemaManager.generate(models.user, strategy); 111 | 112 | it(`produce auto-generated model.title`, function () { 113 | expect(schema.title).toEqual('User'); 114 | }); 115 | 116 | it(`do not produce model.description`, function () { 117 | expect(schema.description).toBeUndefined(); 118 | }); 119 | }); 120 | 121 | // ------------------------------------------------------------------------ 122 | // make sure non-default options render the expected model properties 123 | // ------------------------------------------------------------------------ 124 | describe('Ensure custom model option:', function () { 125 | const schemaManager = new JsonSchemaManager(); 126 | const strategy = new JsonSchema7Strategy(); 127 | const schema = schemaManager.generate(models.user, strategy, { 128 | title: 'Custom Model Title', 129 | description: 'Custom Model Description', 130 | examples: ['Some Example'], 131 | }); 132 | 133 | it(`'title' overrides value in model.title`, function () { 134 | expect(schema.title).toEqual('Custom Model Title'); 135 | }); 136 | 137 | it(`'description' produces custom property model.description`, function () { 138 | expect(schema.description).toEqual('Custom Model Description'); 139 | }); 140 | }); 141 | 142 | // ------------------------------------------------------------------------ 143 | // make sure excluded attributes do not appear in the resultant schema 144 | // ------------------------------------------------------------------------ 145 | describe('Ensure attribute exclusions:', function () { 146 | const schemaManager = new JsonSchemaManager(); 147 | const strategy = new JsonSchema7Strategy(); 148 | const schema = schemaManager.generate(models.user, strategy, { 149 | exclude: ['STRING', 'STRING_1234'], 150 | }); 151 | 152 | it(`exclude attribute STRING`, function () { 153 | expect(schema.properties.STRING).toBeUndefined(); 154 | }); 155 | 156 | it(`exclude attribute STRING_1234`, function () { 157 | expect(schema.properties.STRING_1234).toBeUndefined(); 158 | }); 159 | 160 | it(`do not exclude other attributes`, function () { 161 | expect(schema.properties).toHaveProperty('id'); 162 | }); 163 | }); 164 | 165 | // ------------------------------------------------------------------------ 166 | // make sure excluded attributes do appear in the resultant schema 167 | // ------------------------------------------------------------------------ 168 | describe('Ensure attribute inclusions:', function () { 169 | const schemaManager = new JsonSchemaManager(); 170 | const strategy = new JsonSchema7Strategy(); 171 | const schema = schemaManager.generate(models.user, strategy, { 172 | include: ['STRING', 'STRING_1234'], 173 | associations: false, 174 | }); 175 | 176 | it(`include attribute STRING`, function () { 177 | expect(schema.properties).toHaveProperty('STRING'); 178 | }); 179 | 180 | it(`include attribute STRING_1234`, function () { 181 | expect(schema.properties).toHaveProperty('STRING_1234'); 182 | }); 183 | 184 | it(`do not include other attributes`, function () { 185 | expect(Object.keys(schema.properties).length).toBe(2); 186 | }); 187 | }); 188 | 189 | // ------------------------------------------------------------------------ 190 | // make sure option 'associations' functions as expected 191 | // ------------------------------------------------------------------------ 192 | describe(`Ensure option 'associations' with default value 'true':`, function () { 193 | const schemaManager = new JsonSchemaManager(); 194 | const strategy = new JsonSchema7Strategy(); 195 | const schema = schemaManager.generate(models.user, strategy); 196 | 197 | it(`generates association property 'profile'`, function () { 198 | expect(schema.properties).toHaveProperty('profile'); 199 | }); 200 | 201 | it(`generates association property 'documents'`, function () { 202 | expect(schema.properties).toHaveProperty('documents'); 203 | }); 204 | }); 205 | 206 | describe(`Ensure option 'associations' with user-specificed value 'false':`, function () { 207 | const schemaManager = new JsonSchemaManager(); 208 | const strategy = new JsonSchema7Strategy(); 209 | const schema = schemaManager.generate(models.user, strategy, { 210 | associations: false, 211 | }); 212 | 213 | it(`does not generate association property 'profile'`, function () { 214 | expect(schema.properties.profile).toBeUndefined(); 215 | }); 216 | 217 | it(`does not generate association property 'documents'`, function () { 218 | expect(schema.properties.documents).toBeUndefined(); 219 | }); 220 | }); 221 | 222 | // ------------------------------------------------------------------------ 223 | // make sure option 'includeAssociations'functions as expected 224 | // ------------------------------------------------------------------------ 225 | describe('Ensure association inclusions:', function () { 226 | const schemaManager = new JsonSchemaManager(); 227 | const strategy = new JsonSchema7Strategy(); 228 | const schema = schemaManager.generate(models.user, strategy, { 229 | includeAssociations: ['profile'], 230 | }); 231 | 232 | it(`include association 'profile'`, function () { 233 | expect(schema.properties).toHaveProperty('profile'); 234 | }); 235 | 236 | it(`do not include association 'documents'`, function () { 237 | expect(schema.properties.documents).toBeUndefined(); 238 | }); 239 | }); 240 | 241 | // ------------------------------------------------------------------------ 242 | // make sure option 'excludeAssociations'functions as expected 243 | // ------------------------------------------------------------------------ 244 | describe('Ensure association exclusions:', function () { 245 | const schemaManager = new JsonSchemaManager(); 246 | const strategy = new JsonSchema7Strategy(); 247 | const schema = schemaManager.generate(models.user, strategy, { 248 | excludeAssociations: ['profile'], 249 | }); 250 | 251 | it(`do not include association 'profile'`, function () { 252 | expect(schema.properties.profile).toBeUndefined(); 253 | }); 254 | 255 | it(`include association 'documents'`, function () { 256 | expect(schema.properties).toHaveProperty('documents'); 257 | }); 258 | }); 259 | }); 260 | }); 261 | -------------------------------------------------------------------------------- /test/strategies/json-schema-v7-strategy.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | /** 4 | * Please note that we are NOT testing: 5 | * - non strategy-specific behavior 6 | * - custom Sequelize attribute options like 'description' and '$comment' 7 | * because these are already tested in the StrategyInterface test case 8 | * which uses JSON Schema Draft-07 as the basis for testing. 9 | */ 10 | const Ajv = require('ajv').default; 11 | const models = require('../models'); 12 | const { JsonSchemaManager, JsonSchema7Strategy } = require('../../lib'); 13 | 14 | describe('JsonSchema7Strategy', function () { 15 | describe('Test output using default options', function () { 16 | // ------------------------------------------------------------------------ 17 | // generate schema 18 | // ------------------------------------------------------------------------ 19 | const schemaManager = new JsonSchemaManager({ 20 | disableComments: false, 21 | }); 22 | const strategy = new JsonSchema7Strategy(); 23 | const schema = schemaManager.generate(models.user, strategy); 24 | 25 | // ------------------------------------------------------------------------ 26 | // make sure sequelize model properties render as expected 27 | // ------------------------------------------------------------------------ 28 | describe('Ensure model properties are rendered as expected and thus schema.model:', function () { 29 | const schemaUri = 'https://json-schema.org/draft-07/schema#'; 30 | it(`has property '$schema' with value '${schemaUri}'`, function () { 31 | expect(schema.$schema).toEqual('https://json-schema.org/draft-07/schema#'); 32 | }); 33 | 34 | it("has property '$id' with value '/user.json'", function () { 35 | expect(schema.$id).toEqual('/user.json'); 36 | }); 37 | }); 38 | 39 | // ------------------------------------------------------------------------ 40 | // make sure sequelize DataTypes render as expected 41 | // ------------------------------------------------------------------------ 42 | describe('Ensure Sequelize DataTypes are properly converted and thus:', function () { 43 | describe('BLOB', function () { 44 | it("has property 'contentEncoding' of type 'base64'", function () { 45 | expect(schema.properties.BLOB.contentEncoding).toEqual('base64'); 46 | }); 47 | }); 48 | 49 | describe('STRING_ALLOWNULL_EXPLICIT', function () { 50 | it("has property 'type' of type 'array'", function () { 51 | expect(Array.isArray(schema.properties.STRING_ALLOWNULL_EXPLICIT.type)).toBe(true); 52 | }); 53 | 54 | it("has property 'type' with two values named 'string' and 'null'", function () { 55 | expect(Object.values(schema.properties.STRING_ALLOWNULL_EXPLICIT.type)).toEqual([ 56 | 'string', 57 | 'null', 58 | ]); 59 | }); 60 | }); 61 | 62 | // Sequelize allows null values by default so we need to make sure rendered schema 63 | // keys allow null by default (even when not explicitely setting `allowNull: true`) 64 | describe('STRING_ALLOWNULL_IMPLICIT', function () { 65 | it("has property 'type' of type 'array'", function () { 66 | expect(Array.isArray(schema.properties.STRING_ALLOWNULL_IMPLICIT.type)).toBe(true); 67 | }); 68 | 69 | it("has property 'type' with two values named 'string' and 'null'", function () { 70 | expect(Object.values(schema.properties.STRING_ALLOWNULL_IMPLICIT.type)).toEqual([ 71 | 'string', 72 | 'null', 73 | ]); 74 | }); 75 | }); 76 | 77 | describe('JSONB_ALLOWNULL', function () { 78 | it("has property 'anyOf' that is an array of types", function () { 79 | expect(Array.isArray(schema.properties.JSONB_ALLOWNULL.anyOf)).toBe(true); 80 | }); 81 | it("has property 'anyOf' with values of type 'object', 'array', 'boolean', 'integer', 'number', 'string' and 'null'", function () { 82 | expect(Object.values(schema.properties.JSONB_ALLOWNULL.anyOf)).toEqual([ 83 | { type: 'object' }, 84 | { type: 'array' }, 85 | { type: 'boolean' }, 86 | { type: 'integer' }, 87 | { type: 'number' }, 88 | { type: 'string' }, 89 | { type: 'null' }, 90 | ]); 91 | }); 92 | }); 93 | 94 | describe('ARRAY_ALLOWNULL_EXPLICIT', function () { 95 | it("has property 'type' of type 'array'", function () { 96 | expect(Array.isArray(schema.properties.ARRAY_ALLOWNULL_EXPLICIT.type)).toBe(true); 97 | }); 98 | 99 | it("has property 'type' with two values named 'array' and 'null'", function () { 100 | expect(Object.values(schema.properties.ARRAY_ALLOWNULL_EXPLICIT.type)).toEqual([ 101 | 'array', 102 | 'null', 103 | ]); 104 | }); 105 | }); 106 | 107 | describe('ARRAY_ALLOWNULL_IMPLICIT', function () { 108 | it("has property 'type' of type 'array'", function () { 109 | expect(Array.isArray(schema.properties.ARRAY_ALLOWNULL_IMPLICIT.type)).toBe(true); 110 | }); 111 | 112 | it("has property 'type' with two values named 'array' and 'null'", function () { 113 | expect(Object.values(schema.properties.ARRAY_ALLOWNULL_IMPLICIT.type)).toEqual([ 114 | 'array', 115 | 'null', 116 | ]); 117 | }); 118 | }); 119 | }); 120 | 121 | // ------------------------------------------------------------------------ 122 | // make sure associations render as expected 123 | // ------------------------------------------------------------------------ 124 | describe('Ensure associations are properly generated and thus:', function () { 125 | describe("user.HasOne(profile) generates singular property 'profile' with:", function () { 126 | it("property '$ref' pointing to '#/definitions/profile'", function () { 127 | expect(schema.properties.profile.$ref).toEqual('#/definitions/profile'); 128 | }); 129 | }); 130 | 131 | describe("user.HasOne(user, as:boss) generates singular property 'boss' with:", function () { 132 | it("property '$ref' pointing to '#/definitions/user'", function () { 133 | expect(schema.properties.boss.$ref).toEqual('#/definitions/user'); 134 | }); 135 | }); 136 | 137 | describe("user.BelongsTo(company) generates singular property 'company' with:", function () { 138 | it("property '$ref' pointing to '#/definitions/company'", function () { 139 | expect(schema.properties.company.$ref).toEqual('#/definitions/company'); 140 | }); 141 | }); 142 | 143 | describe("user.HasMany(document) generates plural property 'documents' with:", function () { 144 | it("property 'type' with value 'array'", function () { 145 | expect(schema.properties.documents.type).toEqual('array'); 146 | }); 147 | 148 | it("property 'items' holding an object with '$ref' pointing at '#/definitions/document'", function () { 149 | expect(schema.properties.documents.items).toEqual({ 150 | $ref: '#/definitions/document', // eslint-disable-line unicorn/prevent-abbreviations 151 | }); 152 | }); 153 | }); 154 | 155 | describe("user.BelongsToMany(user) generates plural property 'friends' with:", function () { 156 | it("property 'type' with value 'array'", function () { 157 | expect(schema.properties.friends.type).toEqual('array'); 158 | }); 159 | 160 | it("property 'items.allOf' of type 'array'", function () { 161 | expect(Array.isArray(schema.properties.friends.items.allOf)).toBe(true); 162 | }); 163 | 164 | it("array 'items.allOf' holding an object with '$ref' pointing at '#/definitions/user'", function () { 165 | expect(schema.properties.friends.items.allOf[0]).toEqual({ 166 | $ref: '#/definitions/user', // eslint-disable-line unicorn/prevent-abbreviations 167 | }); 168 | }); 169 | 170 | it("array 'items.allOf' holding an object with type object and properties.friendships an object with '$ref' pointing at '#/definitions/friendship'", function () { 171 | expect(schema.properties.friends.items.allOf[1]).toEqual({ 172 | type: 'object', 173 | properties: { 174 | friendships: { 175 | $ref: '#/definitions/friendship', // eslint-disable-line unicorn/prevent-abbreviations 176 | }, 177 | }, 178 | }); 179 | }); 180 | }); 181 | }); 182 | 183 | // ------------------------------------------------------------------------ 184 | // confirm the document is valid JSON Schema Draft-07 185 | // ------------------------------------------------------------------------ 186 | describe('Ensure that the resultant document:', function () { 187 | it('successfully validates as JSON Schema Draft-07', async () => { 188 | expect.assertions(1); 189 | 190 | // validate document using ajv 191 | const ajv = new Ajv(); 192 | 193 | const valid = ajv.validate('http://json-schema.org/draft-07/schema#', schema); 194 | if (!valid) { 195 | console.log(ajv.errors); // eslint-disable-line no-console 196 | } 197 | 198 | expect(valid).toBe(true); 199 | }); 200 | }); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /test/strategies/openapi-v3-stragegy.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | /** 4 | * Please note that we are ONLY testing strategy-specific behavior here. All 5 | * non-strategy-specific tests are handled by the StrategyInterface test case. 6 | */ 7 | 8 | const _cloneDeep = require('lodash.clonedeep'); 9 | const SwaggerParser = require('swagger-parser'); 10 | const models = require('../models'); 11 | const { JsonSchemaManager, OpenApi3Strategy } = require('../../lib'); 12 | const schemaWrapper = require('./openapi-v3-validation-wrapper'); 13 | 14 | describe('OpenApi3Strategy', function () { 15 | describe('Test output using default options', function () { 16 | // ------------------------------------------------------------------------ 17 | // generate schema 18 | // ------------------------------------------------------------------------ 19 | const schemaManager = new JsonSchemaManager({ 20 | disableComments: false, 21 | }); 22 | const strategy = new OpenApi3Strategy(); 23 | const schema = schemaManager.generate(models.user, strategy); 24 | 25 | // ------------------------------------------------------------------------ 26 | // make sure sequelize DataTypes render as expected 27 | // ------------------------------------------------------------------------ 28 | describe('Ensure Sequelize DataTypes are properly converted and thus:', function () { 29 | describe('BLOB', function () { 30 | it("has property 'format' of type 'byte'", function () { 31 | expect(schema.properties.BLOB.format).toEqual('byte'); 32 | }); 33 | }); 34 | 35 | describe('STRING_ALLOWNULL_EXPLICIT', function () { 36 | it("has property 'type' of type 'string'", function () { 37 | expect(schema.properties.STRING_ALLOWNULL_EXPLICIT.type).toEqual('string'); 38 | }); 39 | 40 | it("has property 'nullable' of type 'boolean'", function () { 41 | expect(typeof schema.properties.STRING_ALLOWNULL_EXPLICIT.nullable).toEqual('boolean'); 42 | }); 43 | }); 44 | 45 | // Sequelize allows null values by default so we need to make sure rendered schema 46 | // keys allow null by default (even when not explicitely setting `allowNull: true`) 47 | describe('STRING_ALLOWNULL_IMPLICIT', function () { 48 | it("has property 'type' of type 'string'", function () { 49 | expect(schema.properties.STRING_ALLOWNULL_IMPLICIT.type).toEqual('string'); 50 | }); 51 | 52 | it("has property 'nullable' of type 'boolean'", function () { 53 | expect(typeof schema.properties.STRING_ALLOWNULL_IMPLICIT.nullable).toEqual('boolean'); 54 | }); 55 | }); 56 | 57 | describe('JSONB_ALLOWNULL', function () { 58 | it("has property 'anyOf' with values of type 'object', 'array', 'boolean', 'integer', 'number' and 'string'", function () { 59 | expect(schema.properties.JSONB_ALLOWNULL.anyOf).toEqual([ 60 | { type: 'object' }, 61 | { type: 'array' }, 62 | { type: 'boolean' }, 63 | { type: 'integer' }, 64 | { type: 'number' }, 65 | { type: 'string' }, 66 | ]); 67 | }); 68 | 69 | it("has property 'nullable' of type 'boolean'", function () { 70 | expect(typeof schema.properties.JSONB_ALLOWNULL.nullable).toEqual('boolean'); 71 | }); 72 | }); 73 | 74 | describe('ARRAY_ALLOWNULL_EXPLICIT', function () { 75 | it("has property 'type' of type 'string'", function () { 76 | expect(schema.properties.ARRAY_ALLOWNULL_EXPLICIT.type).toEqual('array'); 77 | }); 78 | 79 | it("has property 'nullable' of type 'boolean'", function () { 80 | expect(typeof schema.properties.ARRAY_ALLOWNULL_EXPLICIT.nullable).toEqual('boolean'); 81 | }); 82 | }); 83 | 84 | describe('ARRAY_ALLOWNULL_IMPLICIT', function () { 85 | it("has property 'type' of type 'string'", function () { 86 | expect(schema.properties.ARRAY_ALLOWNULL_IMPLICIT.type).toEqual('array'); 87 | }); 88 | 89 | it("has property 'nullable' of type 'boolean'", function () { 90 | expect(typeof schema.properties.ARRAY_ALLOWNULL_IMPLICIT.nullable).toEqual('boolean'); 91 | }); 92 | }); 93 | }); 94 | 95 | // ------------------------------------------------------------------------ 96 | // make sure custom Sequelize attribute options render as expected 97 | // ------------------------------------------------------------------------ 98 | describe('Ensure custom Sequelize attribute options render as expected and thus:', function () { 99 | describe('CUSTOM_DESCRIPTION', function () { 100 | it(`has property 'description' with the expected string value`, function () { 101 | expect(schema.properties.CUSTOM_DESCRIPTION.description).toEqual( 102 | 'Custom attribute description', 103 | ); 104 | }); 105 | }); 106 | 107 | describe('CUSTOM_EXAMPLES', function () { 108 | it("has property 'example' of type 'array'", function () { 109 | expect(Array.isArray(schema.properties.CUSTOM_EXAMPLES.example)).toBe(true); 110 | }); 111 | 112 | it('with the two expected string values', function () { 113 | expect(schema.properties.CUSTOM_EXAMPLES.example).toEqual([ 114 | 'Custom example 1', 115 | 'Custom example 2', 116 | ]); 117 | }); 118 | }); 119 | 120 | describe('CUSTOM_READONLY', function () { 121 | it(`has property 'readOnly' with value 'true'`, function () { 122 | expect(schema.properties.CUSTOM_READONLY.readOnly).toEqual(true); 123 | }); 124 | }); 125 | 126 | describe('CUSTOM_WRITEONLY', function () { 127 | it(`has property 'writeOnly' with value 'true'`, function () { 128 | expect(schema.properties.CUSTOM_WRITEONLY.writeOnly).toEqual(true); 129 | }); 130 | }); 131 | }); 132 | 133 | // ------------------------------------------------------------------------ 134 | // make sure associations render as expected 135 | // ------------------------------------------------------------------------ 136 | describe('Ensure associations are properly generated and thus:', function () { 137 | describe("user.HasOne(profile) generates singular property 'profile' with:", function () { 138 | it("property '$ref' pointing to '#/components/schemas/profile'", function () { 139 | expect(schema.properties.profile.$ref).toEqual('#/components/schemas/profile'); 140 | }); 141 | }); 142 | 143 | describe("user.HasOne(user, as:boss) generates singular property 'boss' with:", function () { 144 | it("property '$ref' pointing to '#/components/schemas/user'", function () { 145 | expect(schema.properties.boss.$ref).toEqual('#/components/schemas/user'); 146 | }); 147 | }); 148 | 149 | describe("user.BelongsTo(company) generates singular property 'company' with:", function () { 150 | it("property '$ref' pointing to '#/components/schemas/company'", function () { 151 | expect(schema.properties.company.$ref).toEqual('#/components/schemas/company'); 152 | }); 153 | }); 154 | 155 | describe("user.HasMany(document) generates plural property 'documents' with:", function () { 156 | it("property 'type' with value 'array'", function () { 157 | expect(schema.properties.documents.type).toEqual('array'); 158 | }); 159 | 160 | it("array 'items' holding an object with '$ref' pointing to '#/components/schemas/document'", function () { 161 | expect(schema.properties.documents.items).toEqual({ 162 | $ref: '#/components/schemas/document', // eslint-disable-line unicorn/prevent-abbreviations 163 | }); 164 | }); 165 | }); 166 | 167 | describe("user.BelongsToMany(user) generates plural property 'friends' with:", function () { 168 | it("property 'type' with value 'array'", function () { 169 | expect(schema.properties.friends.type).toEqual('array'); 170 | }); 171 | 172 | it("property 'items.allOf' of type 'array'", function () { 173 | expect(Array.isArray(schema.properties.friends.items.allOf)).toBe(true); 174 | }); 175 | 176 | it("array 'items.allOf' holding an object with '$ref' pointing to '#/components/schemas/user'", function () { 177 | expect(schema.properties.friends.items.allOf[0]).toEqual({ 178 | $ref: '#/components/schemas/user', // eslint-disable-line unicorn/prevent-abbreviations 179 | }); 180 | }); 181 | 182 | it("array 'items.allOf' holding an object with type object and properties.friendships an object with '$ref' pointing at '#/components/schemas/friendship'", function () { 183 | expect(schema.properties.friends.items.allOf[1]).toEqual({ 184 | type: 'object', 185 | properties: { 186 | friendships: { 187 | $ref: '#/components/schemas/friendship', // eslint-disable-line unicorn/prevent-abbreviations 188 | }, 189 | }, 190 | }); 191 | }); 192 | }); 193 | }); 194 | 195 | // ------------------------------------------------------------------------ 196 | // make sure the resultant document is valid OpenAPI 3.0 197 | // 198 | // Please note that we MUST include the profiles and documents schemas or 199 | // the $refs will not resolve causing the validation to fail. 200 | // ------------------------------------------------------------------------ 201 | describe('Ensure that the resultant document:', function () { 202 | schemaWrapper.components.schemas.user = schema; 203 | schemaWrapper.components.schemas.profile = schemaManager.generate(models.profile, strategy); 204 | schemaWrapper.components.schemas.document = schemaManager.generate(models.document, strategy); 205 | schemaWrapper.components.schemas.company = schemaManager.generate(models.company, strategy); 206 | schemaWrapper.components.schemas.friendship = schemaManager.generate( 207 | models.friendship, 208 | strategy, 209 | ); 210 | 211 | it("has leaf /openapi with string containing version '3.n.n'", function () { 212 | expect(schemaWrapper.openapi).toMatch(/^3\.\d\.\d/); // 3.n.n 213 | }); 214 | 215 | it('has non-empty container /components/schemas/user', function () { 216 | expect(Object.keys(schemaWrapper.components.schemas.user).length).toBeGreaterThan(0); 217 | }); 218 | 219 | // validate document using Swagger Parser 220 | it('successfully validates as JSON API 3.0', async () => { 221 | expect.assertions(1); 222 | 223 | // https://github.com/APIDevTools/swagger-parser/issues/77 224 | // @todo: enable once fixed, now blocks husky pre-commit hooks 225 | const result = await SwaggerParser.validate(_cloneDeep(schemaWrapper)); 226 | expect(result).toHaveProperty('info'); 227 | }); 228 | }); 229 | 230 | // @todo this should be detected by eslint-plugin-jest no-disabled-tests (but is not) 231 | // test('', function() { 232 | // console.log('Does nothing'); 233 | // }); 234 | 235 | // @todo add this to the StrategyInterface test suite, it should throw an exception 236 | // Model.rawAttributes._FAKE_TYPE_ = { 237 | // type: "FAKETYPE" 238 | // }; 239 | }); 240 | }); 241 | 242 | /* 243 | describe('foo', () => {}); 244 | */ 245 | -------------------------------------------------------------------------------- /test/strategies/openapi-v3-validation-wrapper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unfortunately the generated model schema by itself is not enough to 3 | * validate against the OpenAPI 3.0 standard as that requires additional 4 | * nodes like `info`. Before running Swagger Parser we therefore insert 5 | * the model schema into this skeleton which contains the minimum 6 | * structure required to pass validation. 7 | */ 8 | 9 | module.exports = Object.freeze({ 10 | openapi: '3.0.2', 11 | info: { 12 | title: 'Fake API', 13 | version: '0.0.1', 14 | }, 15 | paths: { 16 | '/users': { 17 | get: { 18 | parameters: [], 19 | responses: { 20 | 404: { 21 | description: 'not found', 22 | }, 23 | }, 24 | }, 25 | }, 26 | }, 27 | components: { 28 | schemas: { 29 | // model schemas will be inserted here 30 | }, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /test/strategy-interface.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test case is used to make sure: 3 | * - child strategies will trigger an excecption if they miss an interface method 4 | * - all non-strategy-specific model properties are rendered as expected 5 | * - all non-strategy-specific attribute properties are rendered as expected 6 | */ 7 | const { JsonSchemaManager, JsonSchema7Strategy } = require('../lib'); 8 | const models = require('./models'); 9 | const StrategyInterface = require('../lib/strategy-interface'); 10 | const supportedDataType = require('./utils/supported-datatype'); 11 | 12 | describe('StrategyInterface', function () { 13 | // ------------------------------------------------------------------------ 14 | // make sure we are testing the expected number of interface methods. If 15 | // this test fails, the exception tests likely need updating as well. 16 | // ------------------------------------------------------------------------ 17 | describe('Ensure we are testing against:', function () { 18 | const methodCount = Object.getOwnPropertyNames(StrategyInterface.prototype).length - 1; // excluding the constructor 19 | 20 | it(`8 interface methods`, function () { 21 | expect(methodCount).toEqual(8); 22 | }); 23 | }); 24 | 25 | // ------------------------------------------------------------------------ 26 | // create a dummy strategy without any implemented methods. 27 | // ------------------------------------------------------------------------ 28 | class DummyStrategy extends StrategyInterface {} 29 | const dummyStrategy = new DummyStrategy(); 30 | 31 | // ------------------------------------------------------------------------ 32 | // make sure exceptions are thrown when an interface method is missing. 33 | // ------------------------------------------------------------------------ 34 | describe('Ensure exceptions are thrown if child class has not implemented:', function () { 35 | it('getPropertySchema()', function () { 36 | expect(() => { 37 | dummyStrategy.getPropertySchema(); 38 | }).toThrow("DummyStrategy has not implemented the 'getPropertySchema' interface method."); 39 | }); 40 | 41 | it('getPropertyId()', function () { 42 | expect(() => { 43 | dummyStrategy.getPropertyId(); 44 | }).toThrow("DummyStrategy has not implemented the 'getPropertyId' interface method."); 45 | }); 46 | 47 | it('getPropertyComment()', function () { 48 | expect(() => { 49 | dummyStrategy.getPropertyComment(); 50 | }).toThrow("DummyStrategy has not implemented the 'getPropertyComment' interface method."); 51 | }); 52 | 53 | it('getPropertyExamples()', function () { 54 | expect(() => { 55 | dummyStrategy.getPropertyExamples(); 56 | }).toThrow("DummyStrategy has not implemented the 'getPropertyExamples' interface method."); 57 | }); 58 | 59 | it('getPropertyForBase64Encoding()', function () { 60 | expect(() => { 61 | dummyStrategy.getPropertyForBase64Encoding(); 62 | }).toThrow( 63 | "DummyStrategy has not implemented the 'getPropertyForBase64Encoding' interface method.", 64 | ); 65 | }); 66 | 67 | it('convertTypePropertyToAllowNull()', function () { 68 | expect(() => { 69 | dummyStrategy.convertTypePropertyToAllowNull(); 70 | }).toThrow( 71 | "DummyStrategy has not implemented the 'convertTypePropertyToAllowNull' interface method.", 72 | ); 73 | }); 74 | 75 | it('getPropertyForHasOneAssociation()', function () { 76 | expect(() => { 77 | dummyStrategy.getPropertyForHasOneAssociation(); 78 | }).toThrow( 79 | "DummyStrategy has not implemented the 'getPropertyForHasOneAssociation' interface method.", 80 | ); 81 | }); 82 | 83 | it('getPropertyForHasManyAssociation()', function () { 84 | expect(() => { 85 | dummyStrategy.getPropertyForHasManyAssociation(); 86 | }).toThrow( 87 | "DummyStrategy has not implemented the 'getPropertyForHasManyAssociation' interface method.", 88 | ); 89 | }); 90 | }); 91 | 92 | // ------------------------------------------------------------------------ 93 | // generate a JSON Draft-07 schema to test against 94 | // ------------------------------------------------------------------------ 95 | const schemaManager = new JsonSchemaManager({ 96 | disableComments: false, 97 | }); 98 | const schema = schemaManager.generate(models.user, new JsonSchema7Strategy()); 99 | 100 | // ------------------------------------------------------------------------ 101 | // make sure non-strategy-specific model properties render as expected 102 | // ------------------------------------------------------------------------ 103 | describe('Ensure model properties are properly generated and thus:', function () { 104 | it("model has property 'title' with value 'users'", function () { 105 | expect(schema.title).toEqual('User'); 106 | }); 107 | 108 | it("model has property 'type' with value 'object'", function () { 109 | expect(schema.type).toEqual('object'); 110 | }); 111 | }); 112 | 113 | // ------------------------------------------------------------------------ 114 | // make sure non-strategy-specific attributes properties render as expected 115 | // ------------------------------------------------------------------------ 116 | describe('Ensure non-strategy-specific Sequelize DataTypes are properly converted and thus:', function () { 117 | if (supportedDataType('ARRAY')) { 118 | describe('ARRAY_INTEGERS', function () { 119 | it("has property 'type' with value 'array'", function () { 120 | expect(schema.properties.ARRAY_INTEGERS.type).toEqual('array'); 121 | }); 122 | it("has property 'items.type' with value 'integer'", function () { 123 | expect(schema.properties.ARRAY_INTEGERS.items.type).toEqual('integer'); 124 | }); 125 | it("has property 'items.format' with value 'int32'", function () { 126 | expect(schema.properties.ARRAY_INTEGERS.items.format).toEqual('int32'); 127 | }); 128 | }); 129 | 130 | describe('ARRAY_TEXTS', function () { 131 | it("has property 'type' of type 'array'", function () { 132 | expect(schema.properties.ARRAY_TEXTS.type).toEqual('array'); 133 | }); 134 | it("has property 'items.type' with value 'string'", function () { 135 | expect(schema.properties.ARRAY_TEXTS.items.type).toEqual('string'); 136 | }); 137 | }); 138 | 139 | if (supportedDataType('ENUM')) { 140 | describe('ARRAY_ENUM_STRINGS', function () { 141 | it("has property 'type' of tyoe 'array", function () { 142 | expect(schema.properties.ARRAY_ENUM_STRINGS.type).toEqual('array'); 143 | }); 144 | it("has property 'items.type' with value 'string'", function () { 145 | expect(schema.properties.ARRAY_ENUM_STRINGS.items.type).toEqual('string'); 146 | }); 147 | it("has property 'items.enum' with length > 0", function () { 148 | expect(schema.properties.ARRAY_ENUM_STRINGS.items.enum.length).toBeGreaterThan(0); 149 | }); 150 | }); 151 | } 152 | } 153 | 154 | if (supportedDataType('CITEXT')) { 155 | describe('CITEXT', function () { 156 | it("has property 'type' of type 'string'", function () { 157 | expect(schema.properties.CITEXT.type).toEqual('string'); 158 | }); 159 | }); 160 | } 161 | 162 | if (supportedDataType('STRING')) { 163 | describe('STRING', function () { 164 | it("has property 'type' of type 'string'", function () { 165 | expect(schema.properties.STRING.type).toEqual('string'); 166 | }); 167 | }); 168 | 169 | describe('STRING_1234', function () { 170 | it("has property 'type' of type 'string'", function () { 171 | expect(schema.properties.STRING_1234.type).toEqual('string'); 172 | }); 173 | 174 | it("has property 'maxLength' with value '1234'", function () { 175 | expect(schema.properties.STRING_1234.maxLength).toEqual(1234); 176 | }); 177 | }); 178 | 179 | describe('STRING_DOT_BINARY', function () { 180 | it("has property 'type' of type 'string'", function () { 181 | expect(schema.properties.STRING_DOT_BINARY.type).toEqual('string'); 182 | }); 183 | 184 | it("has property 'format' of type 'binary'", function () { 185 | expect(schema.properties.STRING_DOT_BINARY.format).toEqual('binary'); 186 | }); 187 | }); 188 | } 189 | 190 | if (supportedDataType('TEXT')) { 191 | describe('TEXT', function () { 192 | it("has property 'type' of type 'string'", function () { 193 | expect(schema.properties.TEXT.type).toEqual('string'); 194 | }); 195 | }); 196 | } 197 | }); 198 | 199 | // ------------------------------------------------------------------------ 200 | // make sure sequelize attribute options render as expected 201 | // ------------------------------------------------------------------------ 202 | describe('Ensure Sequelize attribute options render as expected and thus:', function () { 203 | if (supportedDataType('INTEGER')) { 204 | describe('INTEGER with defaultValue', function () { 205 | it("has property 'default' with integer value 0", function () { 206 | expect(schema.properties.INTEGER.default).toEqual(0); 207 | }); 208 | }); 209 | } 210 | 211 | if (supportedDataType('STRING')) { 212 | describe('STRING with defaultValue', function () { 213 | it("has property 'default' with string value 'Default value for STRING'", function () { 214 | expect(schema.properties.STRING.default).toEqual('Default value for STRING'); 215 | }); 216 | }); 217 | } 218 | }); 219 | 220 | // ------------------------------------------------------------------------ 221 | // make sure custom Sequelize attribute options render as expected 222 | // ------------------------------------------------------------------------ 223 | describe('Ensure custom Sequelize attribute options render as expected and thus:', function () { 224 | describe('CUSTOM_DESCRIPTION', function () { 225 | it(`has property 'description' with the expected string value`, function () { 226 | expect(schema.properties.CUSTOM_DESCRIPTION.description).toEqual( 227 | 'Custom attribute description', 228 | ); 229 | }); 230 | }); 231 | 232 | describe('CUSTOM_COMMENT', function () { 233 | it(`has property '$comment' with the expected string value`, function () { 234 | expect(schema.properties.CUSTOM_COMMENT.$comment).toEqual('Custom comment'); 235 | }); 236 | }); 237 | 238 | describe('CUSTOM_EXAMPLES', function () { 239 | it("has property 'examples' of type 'array'", function () { 240 | expect(Array.isArray(schema.properties.CUSTOM_EXAMPLES.examples)).toBe(true); 241 | }); 242 | 243 | it('with the two expected string values', function () { 244 | expect(schema.properties.CUSTOM_EXAMPLES.examples).toEqual([ 245 | 'Custom example 1', 246 | 'Custom example 2', 247 | ]); 248 | }); 249 | }); 250 | 251 | describe('CUSTOM_READONLY', function () { 252 | it(`has property 'readOnly' with value 'true'`, function () { 253 | expect(schema.properties.CUSTOM_READONLY.readOnly).toEqual(true); 254 | }); 255 | }); 256 | 257 | describe('CUSTOM_WRITEONLY', function () { 258 | it(`has property 'writeOnly' with value 'true'`, function () { 259 | expect(schema.properties.CUSTOM_WRITEONLY.writeOnly).toEqual(true); 260 | }); 261 | }); 262 | }); 263 | }); 264 | -------------------------------------------------------------------------------- /test/utils/supported-datatype.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | 3 | const { DataTypes } = Sequelize; 4 | 5 | /** 6 | * Checks if the given DataType is supported by the current Sequelize version. 7 | * 8 | * @param {string} dataType Name of the DataType, e.g. CITEXT 9 | * @returns {boolean} True if not available 10 | */ 11 | function supportedDataType(dataType) { 12 | if (DataTypes[dataType]) { 13 | return true; 14 | } 15 | 16 | return false; 17 | } 18 | 19 | module.exports = supportedDataType; 20 | -------------------------------------------------------------------------------- /test/utils/type-checks.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Please note that `valiate()` is being tested through the two methods. 3 | */ 4 | const { checkTypeOptional, checkTypeRequired } = require('../../lib/utils/type-checks'); 5 | 6 | describe('input-validation', function () { 7 | // -------------------------------------------------------------------------- 8 | // test checkTypeOptional() 9 | // -------------------------------------------------------------------------- 10 | describe('checkTypeOptional', function () { 11 | it(`returns true for null values`, function () { 12 | expect(checkTypeOptional('name', null, 'string')).toBe(true); 13 | }); 14 | 15 | it(`returns true when passed value matches type string`, function () { 16 | expect(checkTypeOptional('name', 'some-string', 'string')).toBe(true); 17 | }); 18 | 19 | it(`returns true when passed value matches type booelan`, function () { 20 | expect(checkTypeOptional('name', true, 'boolean')).toBe(true); 21 | }); 22 | 23 | it(`returns true when passed value matches type array`, function () { 24 | expect(checkTypeOptional('name', [1, 2, 3], 'array')).toBe(true); 25 | }); 26 | 27 | it('throws an error for mismatching string type', function () { 28 | expect(() => { 29 | checkTypeOptional('name', 123, 'string'); 30 | }).toThrow("Optional configuration setting 'name' not of type 'string'"); 31 | }); 32 | 33 | it('throws an error for mismatching boolean type', function () { 34 | expect(() => { 35 | checkTypeOptional('name', 123, 'boolean'); 36 | }).toThrow("Optional configuration setting 'name' not of type 'boolean'"); 37 | }); 38 | 39 | it('throws an error for mismatching array type', function () { 40 | expect(() => { 41 | checkTypeOptional('name', 123, 'array'); 42 | }).toThrow("Optional configuration setting 'name' not of type 'array'"); 43 | }); 44 | }); 45 | 46 | // -------------------------------------------------------------------------- 47 | // test checkTypeOptional() 48 | // -------------------------------------------------------------------------- 49 | describe('checkTypeRequired', function () { 50 | it('throws an error if value is missing', function () { 51 | expect(() => { 52 | checkTypeRequired('name', null, 'string'); 53 | }).toThrow("Required configuration setting 'name' is missing"); 54 | }); 55 | 56 | it(`returns true when passed value matches type string`, function () { 57 | expect(checkTypeRequired('name', 'some-string', 'string')).toBe(true); 58 | }); 59 | 60 | it(`returns true when passed value matches type booelan`, function () { 61 | expect(checkTypeRequired('name', true, 'boolean')).toBe(true); 62 | }); 63 | 64 | it(`returns true when passed value matches type array`, function () { 65 | expect(checkTypeRequired('name', [1, 2, 3], 'array')).toBe(true); 66 | }); 67 | 68 | it('throws an error for mismatching string type', function () { 69 | expect(() => { 70 | checkTypeRequired('name', 123, 'string'); 71 | }).toThrow("Required configuration setting 'name' not of type 'string'"); 72 | }); 73 | 74 | it('throws an error for mismatching boolean type', function () { 75 | expect(() => { 76 | checkTypeRequired('name', 123, 'boolean'); 77 | }).toThrow("Required configuration setting 'name' not of type 'boolean'"); 78 | }); 79 | 80 | it('throws an error for mismatching array type', function () { 81 | expect(() => { 82 | checkTypeRequired('name', 123, 'array'); 83 | }).toThrow("Required configuration setting 'name' not of type 'array'"); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /try-me.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | /* eslint-disable no-console */ 3 | /* eslint-disable no-unused-vars */ 4 | 5 | /** 6 | * Test runner used for Rapid Development. 7 | */ 8 | const _cloneDeep = require('lodash.clonedeep'); 9 | const SwaggerParser = require('swagger-parser'); 10 | const models = require('./test/models'); 11 | const { JsonSchemaManager, JsonSchema7Strategy, OpenApi3Strategy } = require('./lib'); 12 | 13 | // Initialize the SchemaManager with global configuration options 14 | const schemaManager = new JsonSchemaManager({ 15 | baseUri: 'https://api.example.com', 16 | absolutePaths: true, 17 | disableComments: false, 18 | }); 19 | 20 | // Generate a JSON Schema Draft-07 schema for the user model 21 | const json7strategy = new JsonSchema7Strategy(); 22 | const schema = schemaManager.generate(models.user, json7strategy, { 23 | // title: 'MyUser', 24 | // description: 'My Description', 25 | // include: [ 26 | // '_STRING_', 27 | // '_STRING_50_', 28 | // ], 29 | // exclude: [ 30 | // '_STRING_', 31 | // '_STRING_50_', 32 | // ] 33 | }); 34 | 35 | // schema.definitions.profile = schemaManager.generate(models.profile, json7strategy); 36 | // schema.definitions.document = schemaManager.generate(models.document, json7strategy); 37 | 38 | 39 | console.log('JSON Schema v7:') 40 | // console.log(userSchema); 41 | console.log(JSON.stringify(schema, null, 2)); 42 | 43 | // console.log(models.user.associations); 44 | 45 | // ---------------------------------- 46 | // Generate OpenAPI v3 schema 47 | // ---------------------------------- 48 | const openapi3strategy = new OpenApi3Strategy(); 49 | const userSchema = schemaManager.generate(models.user, openapi3strategy, { 50 | title: 'MyUser', 51 | description: 'My Description', 52 | exclude: [ 53 | '_UUIDV4_', 54 | ] 55 | }); 56 | 57 | console.log('OpenAPI v3:'); 58 | // console.log(userSchema); 59 | 60 | // OpenApi requires more than just the model schema for validation so we insert it into the wrapper 61 | const wrapper = require('./test/strategies/openapi-v3-validation-wrapper'); 62 | 63 | wrapper.components.schemas.user = userSchema; 64 | wrapper.components.schemas.profile = schemaManager.generate(models.profile, openapi3strategy); 65 | wrapper.components.schemas.document = schemaManager.generate(models.document, openapi3strategy); 66 | wrapper.components.schemas.company = schemaManager.generate(models.company, openapi3strategy); 67 | wrapper.components.schemas.friendship = schemaManager.generate( 68 | models.friendship, 69 | openapi3strategy, 70 | ); 71 | 72 | console.log('Validation schema as JSON string:'); 73 | console.log(JSON.stringify(wrapper, null, 2)); 74 | 75 | console.log('Validating generated full schema against swagger-parser:'); 76 | 77 | async function validateSchema () { 78 | try { 79 | const api = await SwaggerParser.validate(_cloneDeep(wrapper)); 80 | console.log("Wrapper passed OpenAPI validation: API name: %s, Version: %s", api.info.title, api.info.version); 81 | } 82 | catch(error) { 83 | console.error(error); 84 | } 85 | } 86 | 87 | validateSchema(); 88 | --------------------------------------------------------------------------------