├── .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 | [](https://www.npmjs.com/package/@alt3/sequelize-to-json-schemas)
2 | [](https://app.travis-ci.com/alt3/sequelize-to-json-schemas)
3 | [](https://snyk.io/test/github/alt3/sequelize-to-json-schemas)
4 | 
5 | [](https://codecov.io/gh/alt3/sequelize-to-json-schemas)
6 | [](https://codeclimate.com/github/alt3/sequelize-to-json-schemas)
7 | [](https://conventionalcommits.org)
8 | [](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 |
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 |
--------------------------------------------------------------------------------