diff --git a/.travis.yml b/.travis.yml index 10405a3..0aed68d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,9 @@ language: node_js cache: npm node_js: - - 8 - - 10 - - 12 - 14 - - 15 + - 16 + - 17 sudo: false @@ -34,21 +32,21 @@ matrix: - node_js: 14 env: TEST=0 COVERAGE=1 - - stage: deploy - node_js: 15 - env: TEST=0 COVERAGE=0 LINT=0 - script: - - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc - - git remote rm origin - - git remote add origin https://alt3:${GITHUB_TOKEN}@github.com/alt3/sequelize-to-json-schemas.git - - git symbolic-ref HEAD refs/heads/master - - npm run release - on: - branch: master - if: "branch = master AND type = push AND commit_message !~ /(?i:no-release)|^(?i:chore: release)/" + # - stage: deploy + # node_js: 17 + # env: TEST=0 COVERAGE=0 LINT=0 + # script: + # - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc + # - git remote rm origin + # - git remote add origin https://alt3:${GITHUB_TOKEN}@github.com/alt3/sequelize-to-json-schemas.git + # - git symbolic-ref HEAD refs/heads/master + # - npm run release + # on: + # branch: master + # if: "branch = master AND type = push AND commit_message !~ /(?i:no-release)|^(?i:chore: release)/" - allow_failures: - - env: SEQUELIZE_VERSION=6 + # allow_failures: + # - env: SEQUELIZE_VERSION=6 before_script: - sh -c "if [ '$TEST' = '1' ]; then npm install sequelize@$SEQUELIZE_VERSION; fi" diff --git a/README.md b/README.md index 3027f89..17b667a 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,13 @@ Convert Sequelize models into these JSON Schema variants (using the Strategy Pattern): - JSON Schema Draft-07 - [sample output](examples/json-schema-v7.md) +- JSON Schema 2019-09 - [sample output](examples/json-schema-v2019-09.md) - OpenAPI 3.0 - [sample output](examples/openapi-v3.md) -Compatible with Sequelize versions 4, 5 and 6. +Compatible with: + +- Sequelize versions 4, 5 and 6 +- NodeJS version 14, 16 and 17 ## Main Goals @@ -38,10 +42,13 @@ npm install @alt3/sequelize-to-json-schemas --save const { JsonSchemaManager, JsonSchema7Strategy, OpenApi3Strategy } = require('@alt3/sequelize-to-json-schemas'); const schemaManager = new JsonSchemaManager(); -// now generate a JSON Schema Draft-07 model schema +// to generate a JSON Schema Draft-07 schema for your model: +let schema = schemaManager.generate(userModel, new JsonSchema7Strategy()); + +// to generate a JSON Schema 2019-09 schema: let schema = schemaManager.generate(userModel, new JsonSchema7Strategy()); -// and/or the OpenAPI 3.0 equivalent +// to generate the OpenAPI 3.0 equivalent schema = schemaManager.generate(userModel, new OpenApi3Strategy()); ``` @@ -67,6 +74,7 @@ const userSchema = schemaManager.generate(userModel, strategy, { description: 'Custom model description', exclude: ['someAttribute'], include: ['someAttribute'], + renderIdProperty: false, // true to render '$id' field for each schema property associations: true, excludeAssociations: ['someAssociation'], includeAssociations: ['someAssociation'], diff --git a/examples/generate.js b/examples/generate.js index 30df45b..76ec962 100644 --- a/examples/generate.js +++ b/examples/generate.js @@ -9,7 +9,12 @@ const fileSystem = require('fs'); const moment = require('moment'); const models = require('../test/models'); -const { JsonSchemaManager, JsonSchema7Strategy, OpenApi3Strategy } = require('../lib'); +const { + JsonSchemaManager, + JsonSchema7Strategy, + JsonSchema201909Strategy, + OpenApi3Strategy, +} = require('../lib'); const targetFolder = './examples/'; @@ -118,6 +123,99 @@ fileSystem.writeFile(`${targetFolder}json-schema-v7.md`, markdown, function chec console.log('Succesfully generated markdown sample output for JSON Schema Draft-07'); +// ---------------------------------------------------------------------------- +// Json Schema 2019-09 +// ---------------------------------------------------------------------------- +strategy = new JsonSchema201909Strategy(); + +userSchema = schemaManager.generate(models.user, strategy, { + title: 'Custom User Title', + description: 'Custom User Description', +}); + +profileSchema = schemaManager.generate(models.profile, strategy); +documentSchema = schemaManager.generate(models.document, strategy); +companySchema = schemaManager.generate(models.company, strategy); +friendshipSchema = schemaManager.generate(models.friendship, strategy); + +fullSchema = { + $schema: 'https://json-schema.org/draft-07/schema#', + definitions: { + user: userSchema, + profile: profileSchema, + document: documentSchema, + company: companySchema, + friendship: friendshipSchema, + }, +}; + +markdown = `# JSON Schema Draft 2019-09 +${pageIntro} + +- [JSON Schema Validator](https://www.jsonschemavalidator.net/) +- [ajv](https://github.com/epoberezkin/ajv) + +## User Model + + +\`\`\`json +${JSON.stringify(userSchema, null, 2)} +\`\`\` + + +## Profile Model + + +\`\`\`json +${JSON.stringify(profileSchema, null, 2)} +\`\`\` + + +## Document Model + + +\`\`\`json +${JSON.stringify(documentSchema, null, 2)} +\`\`\` + + +## Company Model + + +\`\`\`json +${JSON.stringify(companySchema, null, 2)} +\`\`\` + + +## Friendship Model + + +\`\`\`json +${JSON.stringify(friendshipSchema, null, 2)} +\`\`\` + + +## Full Schema + +Please note that sequelize-to-json-schemas does NOT generate full schemas. This is just an +example of how to integrate the generated model schemas into a full JSON Schema Draft 2019-09 +document (by adding model schemas to \`definitions\`). + + +\`\`\`json +${JSON.stringify(fullSchema, null, 2)} +\`\`\` + +`; + +fileSystem.writeFile(`${targetFolder}json-schema-v2019-09.md`, markdown, function check(error) { + if (error) { + throw error; + } +}); + +console.log('Succesfully generated markdown sample output for JSON Schema Draft 2019-09'); + // ---------------------------------------------------------------------------- // OpenAPI 3.0 // ---------------------------------------------------------------------------- diff --git a/examples/json-schema-v2019-09.md b/examples/json-schema-v2019-09.md new file mode 100644 index 0000000..43f2705 --- /dev/null +++ b/examples/json-schema-v2019-09.md @@ -0,0 +1,785 @@ +# JSON Schema Draft 2019-09 + +These schemas were automatically generated on 2021-12-18 +using [these Sequelize models](../test/models) and the most recent version of +sequelize-to-json-schemas. To confirm that these are indeed all valid schemas use: + +- [JSON Schema Validator](https://www.jsonschemavalidator.net/) +- [ajv](https://github.com/epoberezkin/ajv) + +## User Model + + +```json +{ + "$schema": "https://json-schema.org/draft/2019-09/schema#", + "$id": "https://api.example.com/user.json", + "title": "Custom User Title", + "description": "Custom User Description", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "ARRAY_INTEGERS": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "ARRAY_TEXTS": { + "type": "array", + "items": { + "type": "string" + } + }, + "ARRAY_ALLOWNULL_EXPLICIT": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "ARRAY_ALLOWNULL_IMPLICIT": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "ARRAY_ENUM_STRINGS": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "hello", + "world" + ] + } + }, + "BLOB": { + "type": "string", + "contentEncoding": "base64" + }, + "CITEXT": { + "type": "string" + }, + "INTEGER": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "STRING": { + "type": "string", + "default": "Default value for STRING" + }, + "STRING_ALLOWNULL_EXPLICIT": { + "type": [ + "string", + "null" + ] + }, + "STRING_ALLOWNULL_IMPLICIT": { + "type": [ + "string", + "null" + ] + }, + "STRING_1234": { + "type": "string", + "maxLength": 1234 + }, + "STRING_DOT_BINARY": { + "type": "string", + "format": "binary" + }, + "TEXT": { + "type": "string" + }, + "UUIDV4": { + "type": "string", + "format": "uuid" + }, + "JSON": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + }, + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "object" + }, + "JSONB_ALLOWNULL": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + }, + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "VIRTUAL": { + "type": "boolean" + }, + "VIRTUAL_DEPENDENCY": { + "type": "integer", + "format": "int32" + }, + "CUSTOM_DESCRIPTION": { + "type": "string", + "description": "Custom attribute description" + }, + "CUSTOM_COMMENT": { + "type": "string", + "$comment": "Custom comment" + }, + "CUSTOM_EXAMPLES": { + "type": "string", + "examples": [ + "Custom example 1", + "Custom example 2" + ] + }, + "CUSTOM_READONLY": { + "type": "string", + "readOnly": true + }, + "CUSTOM_WRITEONLY": { + "type": "string", + "writeOnly": true + }, + "companyId": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "bossId": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "profile": { + "$ref": "#/definitions/profile" + }, + "company": { + "$ref": "#/definitions/company" + }, + "documents": { + "type": "array", + "items": { + "$ref": "#/definitions/document" + } + }, + "boss": { + "$ref": "#/definitions/user" + }, + "friends": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/definitions/user" + }, + { + "type": "object", + "properties": { + "friendships": { + "$ref": "#/definitions/friendship" + } + } + } + ] + } + } + }, + "required": [ + "id", + "createdAt", + "updatedAt", + "ARRAY_INTEGERS", + "ARRAY_TEXTS", + "ARRAY_ENUM_STRINGS", + "BLOB", + "CITEXT", + "INTEGER", + "STRING", + "STRING_1234", + "STRING_DOT_BINARY", + "TEXT", + "UUIDV4", + "JSON", + "VIRTUAL", + "VIRTUAL_DEPENDENCY", + "CUSTOM_DESCRIPTION", + "CUSTOM_COMMENT", + "CUSTOM_EXAMPLES", + "CUSTOM_READONLY", + "CUSTOM_WRITEONLY" + ] +} +``` + + +## Profile Model + + +```json +{ + "$schema": "https://json-schema.org/draft/2019-09/schema#", + "$id": "https://api.example.com/profile.json", + "title": "Profile", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "userId": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "id" + ] +} +``` + + +## Document Model + + +```json +{ + "$schema": "https://json-schema.org/draft/2019-09/schema#", + "$id": "https://api.example.com/document.json", + "title": "Document", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "userId": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "id" + ] +} +``` + + +## Company Model + + +```json +{ + "$schema": "https://json-schema.org/draft/2019-09/schema#", + "$id": "https://api.example.com/company.json", + "title": "Company", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id" + ] +} +``` + + +## Friendship Model + + +```json +{ + "$schema": "https://json-schema.org/draft/2019-09/schema#", + "$id": "https://api.example.com/friendship.json", + "title": "Friendship", + "type": "object", + "properties": { + "isBestFriend": { + "type": [ + "boolean", + "null" + ], + "default": false + }, + "userId": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "friendId": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "isBestFriend" + ] +} +``` + + +## Full Schema + +Please note that sequelize-to-json-schemas does NOT generate full schemas. This is just an +example of how to integrate the generated model schemas into a full JSON Schema Draft 2019-09 +document (by adding model schemas to `definitions`). + + +```json +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "definitions": { + "user": { + "$schema": "https://json-schema.org/draft/2019-09/schema#", + "$id": "https://api.example.com/user.json", + "title": "Custom User Title", + "description": "Custom User Description", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "ARRAY_INTEGERS": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "ARRAY_TEXTS": { + "type": "array", + "items": { + "type": "string" + } + }, + "ARRAY_ALLOWNULL_EXPLICIT": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "ARRAY_ALLOWNULL_IMPLICIT": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "ARRAY_ENUM_STRINGS": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "hello", + "world" + ] + } + }, + "BLOB": { + "type": "string", + "contentEncoding": "base64" + }, + "CITEXT": { + "type": "string" + }, + "INTEGER": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "STRING": { + "type": "string", + "default": "Default value for STRING" + }, + "STRING_ALLOWNULL_EXPLICIT": { + "type": [ + "string", + "null" + ] + }, + "STRING_ALLOWNULL_IMPLICIT": { + "type": [ + "string", + "null" + ] + }, + "STRING_1234": { + "type": "string", + "maxLength": 1234 + }, + "STRING_DOT_BINARY": { + "type": "string", + "format": "binary" + }, + "TEXT": { + "type": "string" + }, + "UUIDV4": { + "type": "string", + "format": "uuid" + }, + "JSON": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + }, + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "object" + }, + "JSONB_ALLOWNULL": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + }, + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "VIRTUAL": { + "type": "boolean" + }, + "VIRTUAL_DEPENDENCY": { + "type": "integer", + "format": "int32" + }, + "CUSTOM_DESCRIPTION": { + "type": "string", + "description": "Custom attribute description" + }, + "CUSTOM_COMMENT": { + "type": "string", + "$comment": "Custom comment" + }, + "CUSTOM_EXAMPLES": { + "type": "string", + "examples": [ + "Custom example 1", + "Custom example 2" + ] + }, + "CUSTOM_READONLY": { + "type": "string", + "readOnly": true + }, + "CUSTOM_WRITEONLY": { + "type": "string", + "writeOnly": true + }, + "companyId": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "bossId": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "profile": { + "$ref": "#/definitions/profile" + }, + "company": { + "$ref": "#/definitions/company" + }, + "documents": { + "type": "array", + "items": { + "$ref": "#/definitions/document" + } + }, + "boss": { + "$ref": "#/definitions/user" + }, + "friends": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/definitions/user" + }, + { + "type": "object", + "properties": { + "friendships": { + "$ref": "#/definitions/friendship" + } + } + } + ] + } + } + }, + "required": [ + "id", + "createdAt", + "updatedAt", + "ARRAY_INTEGERS", + "ARRAY_TEXTS", + "ARRAY_ENUM_STRINGS", + "BLOB", + "CITEXT", + "INTEGER", + "STRING", + "STRING_1234", + "STRING_DOT_BINARY", + "TEXT", + "UUIDV4", + "JSON", + "VIRTUAL", + "VIRTUAL_DEPENDENCY", + "CUSTOM_DESCRIPTION", + "CUSTOM_COMMENT", + "CUSTOM_EXAMPLES", + "CUSTOM_READONLY", + "CUSTOM_WRITEONLY" + ] + }, + "profile": { + "$schema": "https://json-schema.org/draft/2019-09/schema#", + "$id": "https://api.example.com/profile.json", + "title": "Profile", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "userId": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "id" + ] + }, + "document": { + "$schema": "https://json-schema.org/draft/2019-09/schema#", + "$id": "https://api.example.com/document.json", + "title": "Document", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "userId": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "id" + ] + }, + "company": { + "$schema": "https://json-schema.org/draft/2019-09/schema#", + "$id": "https://api.example.com/company.json", + "title": "Company", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id" + ] + }, + "friendship": { + "$schema": "https://json-schema.org/draft/2019-09/schema#", + "$id": "https://api.example.com/friendship.json", + "title": "Friendship", + "type": "object", + "properties": { + "isBestFriend": { + "type": [ + "boolean", + "null" + ], + "default": false + }, + "userId": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "friendId": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "isBestFriend" + ] + } + } +} +``` + diff --git a/examples/json-schema-v7.md b/examples/json-schema-v7.md index 236956e..92aec2b 100644 --- a/examples/json-schema-v7.md +++ b/examples/json-schema-v7.md @@ -1,6 +1,6 @@ # JSON Schema Draft-07 -These schemas were automatically generated on 2021-06-14 +These schemas were automatically generated on 2021-12-18 using [these Sequelize models](../test/models) and the most recent version of sequelize-to-json-schemas. To confirm that these are indeed all valid schemas use: @@ -19,22 +19,18 @@ sequelize-to-json-schemas. To confirm that these are indeed all valid schemas us "type": "object", "properties": { "id": { - "$id": "https://api.example.com/properties/id", "type": "integer", "format": "int32" }, "createdAt": { - "$id": "https://api.example.com/properties/createdAt", "type": "string", "format": "date-time" }, "updatedAt": { - "$id": "https://api.example.com/properties/updatedAt", "type": "string", "format": "date-time" }, "ARRAY_INTEGERS": { - "$id": "https://api.example.com/properties/ARRAY_INTEGERS", "type": "array", "items": { "type": "integer", @@ -42,14 +38,12 @@ sequelize-to-json-schemas. To confirm that these are indeed all valid schemas us } }, "ARRAY_TEXTS": { - "$id": "https://api.example.com/properties/ARRAY_TEXTS", "type": "array", "items": { "type": "string" } }, "ARRAY_ALLOWNULL_EXPLICIT": { - "$id": "https://api.example.com/properties/ARRAY_ALLOWNULL_EXPLICIT", "type": [ "array", "null" @@ -59,7 +53,6 @@ sequelize-to-json-schemas. To confirm that these are indeed all valid schemas us } }, "ARRAY_ALLOWNULL_IMPLICIT": { - "$id": "https://api.example.com/properties/ARRAY_ALLOWNULL_IMPLICIT", "type": [ "array", "null" @@ -69,7 +62,6 @@ sequelize-to-json-schemas. To confirm that these are indeed all valid schemas us } }, "ARRAY_ENUM_STRINGS": { - "$id": "https://api.example.com/properties/ARRAY_ENUM_STRINGS", "type": "array", "items": { "type": "string", @@ -80,60 +72,49 @@ sequelize-to-json-schemas. To confirm that these are indeed all valid schemas us } }, "BLOB": { - "$id": "https://api.example.com/properties/BLOB", "type": "string", "contentEncoding": "base64" }, "CITEXT": { - "$id": "https://api.example.com/properties/CITEXT", "type": "string" }, "INTEGER": { - "$id": "https://api.example.com/properties/INTEGER", "type": "integer", "format": "int32", "default": 0 }, "STRING": { - "$id": "https://api.example.com/properties/STRING", "type": "string", "default": "Default value for STRING" }, "STRING_ALLOWNULL_EXPLICIT": { - "$id": "https://api.example.com/properties/STRING_ALLOWNULL_EXPLICIT", "type": [ "string", "null" ] }, "STRING_ALLOWNULL_IMPLICIT": { - "$id": "https://api.example.com/properties/STRING_ALLOWNULL_IMPLICIT", "type": [ "string", "null" ] }, "STRING_1234": { - "$id": "https://api.example.com/properties/STRING_1234", "type": "string", "maxLength": 1234 }, "STRING_DOT_BINARY": { - "$id": "https://api.example.com/properties/STRING_DOT_BINARY", "type": "string", "format": "binary" }, "TEXT": { - "$id": "https://api.example.com/properties/TEXT", "type": "string" }, "UUIDV4": { - "$id": "https://api.example.com/properties/UUIDV4", "type": "string", "format": "uuid" }, "JSON": { - "$id": "https://api.example.com/properties/JSON", "anyOf": [ { "type": "object" @@ -157,7 +138,6 @@ sequelize-to-json-schemas. To confirm that these are indeed all valid schemas us "type": "object" }, "JSONB_ALLOWNULL": { - "$id": "https://api.example.com/properties/JSONB_ALLOWNULL", "anyOf": [ { "type": "object" @@ -183,26 +163,21 @@ sequelize-to-json-schemas. To confirm that these are indeed all valid schemas us ] }, "VIRTUAL": { - "$id": "https://api.example.com/properties/VIRTUAL", "type": "boolean" }, "VIRTUAL_DEPENDENCY": { - "$id": "https://api.example.com/properties/VIRTUAL_DEPENDENCY", "type": "integer", "format": "int32" }, "CUSTOM_DESCRIPTION": { - "$id": "https://api.example.com/properties/CUSTOM_DESCRIPTION", "type": "string", "description": "Custom attribute description" }, "CUSTOM_COMMENT": { - "$id": "https://api.example.com/properties/CUSTOM_COMMENT", "type": "string", "$comment": "Custom comment" }, "CUSTOM_EXAMPLES": { - "$id": "https://api.example.com/properties/CUSTOM_EXAMPLES", "type": "string", "examples": [ "Custom example 1", @@ -210,17 +185,14 @@ sequelize-to-json-schemas. To confirm that these are indeed all valid schemas us ] }, "CUSTOM_READONLY": { - "$id": "https://api.example.com/properties/CUSTOM_READONLY", "type": "string", "readOnly": true }, "CUSTOM_WRITEONLY": { - "$id": "https://api.example.com/properties/CUSTOM_WRITEONLY", "type": "string", "writeOnly": true }, "companyId": { - "$id": "https://api.example.com/properties/companyId", "type": [ "integer", "null" @@ -228,7 +200,6 @@ sequelize-to-json-schemas. To confirm that these are indeed all valid schemas us "format": "int32" }, "bossId": { - "$id": "https://api.example.com/properties/bossId", "type": [ "integer", "null" @@ -308,19 +279,16 @@ sequelize-to-json-schemas. To confirm that these are indeed all valid schemas us "type": "object", "properties": { "id": { - "$id": "https://api.example.com/properties/id", "type": "integer", "format": "int32" }, "name": { - "$id": "https://api.example.com/properties/name", "type": [ "string", "null" ] }, "userId": { - "$id": "https://api.example.com/properties/userId", "type": [ "integer", "null" @@ -346,19 +314,16 @@ sequelize-to-json-schemas. To confirm that these are indeed all valid schemas us "type": "object", "properties": { "id": { - "$id": "https://api.example.com/properties/id", "type": "integer", "format": "int32" }, "name": { - "$id": "https://api.example.com/properties/name", "type": [ "string", "null" ] }, "userId": { - "$id": "https://api.example.com/properties/userId", "type": [ "integer", "null" @@ -384,12 +349,10 @@ sequelize-to-json-schemas. To confirm that these are indeed all valid schemas us "type": "object", "properties": { "id": { - "$id": "https://api.example.com/properties/id", "type": "integer", "format": "int32" }, "name": { - "$id": "https://api.example.com/properties/name", "type": [ "string", "null" @@ -414,7 +377,6 @@ sequelize-to-json-schemas. To confirm that these are indeed all valid schemas us "type": "object", "properties": { "isBestFriend": { - "$id": "https://api.example.com/properties/isBestFriend", "type": [ "boolean", "null" @@ -422,7 +384,6 @@ sequelize-to-json-schemas. To confirm that these are indeed all valid schemas us "default": false }, "userId": { - "$id": "https://api.example.com/properties/userId", "type": [ "integer", "null" @@ -430,7 +391,6 @@ sequelize-to-json-schemas. To confirm that these are indeed all valid schemas us "format": "int32" }, "friendId": { - "$id": "https://api.example.com/properties/friendId", "type": [ "integer", "null" @@ -464,22 +424,18 @@ document (by adding model schemas to `definitions`). "type": "object", "properties": { "id": { - "$id": "https://api.example.com/properties/id", "type": "integer", "format": "int32" }, "createdAt": { - "$id": "https://api.example.com/properties/createdAt", "type": "string", "format": "date-time" }, "updatedAt": { - "$id": "https://api.example.com/properties/updatedAt", "type": "string", "format": "date-time" }, "ARRAY_INTEGERS": { - "$id": "https://api.example.com/properties/ARRAY_INTEGERS", "type": "array", "items": { "type": "integer", @@ -487,14 +443,12 @@ document (by adding model schemas to `definitions`). } }, "ARRAY_TEXTS": { - "$id": "https://api.example.com/properties/ARRAY_TEXTS", "type": "array", "items": { "type": "string" } }, "ARRAY_ALLOWNULL_EXPLICIT": { - "$id": "https://api.example.com/properties/ARRAY_ALLOWNULL_EXPLICIT", "type": [ "array", "null" @@ -504,7 +458,6 @@ document (by adding model schemas to `definitions`). } }, "ARRAY_ALLOWNULL_IMPLICIT": { - "$id": "https://api.example.com/properties/ARRAY_ALLOWNULL_IMPLICIT", "type": [ "array", "null" @@ -514,7 +467,6 @@ document (by adding model schemas to `definitions`). } }, "ARRAY_ENUM_STRINGS": { - "$id": "https://api.example.com/properties/ARRAY_ENUM_STRINGS", "type": "array", "items": { "type": "string", @@ -525,60 +477,49 @@ document (by adding model schemas to `definitions`). } }, "BLOB": { - "$id": "https://api.example.com/properties/BLOB", "type": "string", "contentEncoding": "base64" }, "CITEXT": { - "$id": "https://api.example.com/properties/CITEXT", "type": "string" }, "INTEGER": { - "$id": "https://api.example.com/properties/INTEGER", "type": "integer", "format": "int32", "default": 0 }, "STRING": { - "$id": "https://api.example.com/properties/STRING", "type": "string", "default": "Default value for STRING" }, "STRING_ALLOWNULL_EXPLICIT": { - "$id": "https://api.example.com/properties/STRING_ALLOWNULL_EXPLICIT", "type": [ "string", "null" ] }, "STRING_ALLOWNULL_IMPLICIT": { - "$id": "https://api.example.com/properties/STRING_ALLOWNULL_IMPLICIT", "type": [ "string", "null" ] }, "STRING_1234": { - "$id": "https://api.example.com/properties/STRING_1234", "type": "string", "maxLength": 1234 }, "STRING_DOT_BINARY": { - "$id": "https://api.example.com/properties/STRING_DOT_BINARY", "type": "string", "format": "binary" }, "TEXT": { - "$id": "https://api.example.com/properties/TEXT", "type": "string" }, "UUIDV4": { - "$id": "https://api.example.com/properties/UUIDV4", "type": "string", "format": "uuid" }, "JSON": { - "$id": "https://api.example.com/properties/JSON", "anyOf": [ { "type": "object" @@ -602,7 +543,6 @@ document (by adding model schemas to `definitions`). "type": "object" }, "JSONB_ALLOWNULL": { - "$id": "https://api.example.com/properties/JSONB_ALLOWNULL", "anyOf": [ { "type": "object" @@ -628,26 +568,21 @@ document (by adding model schemas to `definitions`). ] }, "VIRTUAL": { - "$id": "https://api.example.com/properties/VIRTUAL", "type": "boolean" }, "VIRTUAL_DEPENDENCY": { - "$id": "https://api.example.com/properties/VIRTUAL_DEPENDENCY", "type": "integer", "format": "int32" }, "CUSTOM_DESCRIPTION": { - "$id": "https://api.example.com/properties/CUSTOM_DESCRIPTION", "type": "string", "description": "Custom attribute description" }, "CUSTOM_COMMENT": { - "$id": "https://api.example.com/properties/CUSTOM_COMMENT", "type": "string", "$comment": "Custom comment" }, "CUSTOM_EXAMPLES": { - "$id": "https://api.example.com/properties/CUSTOM_EXAMPLES", "type": "string", "examples": [ "Custom example 1", @@ -655,17 +590,14 @@ document (by adding model schemas to `definitions`). ] }, "CUSTOM_READONLY": { - "$id": "https://api.example.com/properties/CUSTOM_READONLY", "type": "string", "readOnly": true }, "CUSTOM_WRITEONLY": { - "$id": "https://api.example.com/properties/CUSTOM_WRITEONLY", "type": "string", "writeOnly": true }, "companyId": { - "$id": "https://api.example.com/properties/companyId", "type": [ "integer", "null" @@ -673,7 +605,6 @@ document (by adding model schemas to `definitions`). "format": "int32" }, "bossId": { - "$id": "https://api.example.com/properties/bossId", "type": [ "integer", "null" @@ -746,19 +677,16 @@ document (by adding model schemas to `definitions`). "type": "object", "properties": { "id": { - "$id": "https://api.example.com/properties/id", "type": "integer", "format": "int32" }, "name": { - "$id": "https://api.example.com/properties/name", "type": [ "string", "null" ] }, "userId": { - "$id": "https://api.example.com/properties/userId", "type": [ "integer", "null" @@ -777,19 +705,16 @@ document (by adding model schemas to `definitions`). "type": "object", "properties": { "id": { - "$id": "https://api.example.com/properties/id", "type": "integer", "format": "int32" }, "name": { - "$id": "https://api.example.com/properties/name", "type": [ "string", "null" ] }, "userId": { - "$id": "https://api.example.com/properties/userId", "type": [ "integer", "null" @@ -808,12 +733,10 @@ document (by adding model schemas to `definitions`). "type": "object", "properties": { "id": { - "$id": "https://api.example.com/properties/id", "type": "integer", "format": "int32" }, "name": { - "$id": "https://api.example.com/properties/name", "type": [ "string", "null" @@ -831,7 +754,6 @@ document (by adding model schemas to `definitions`). "type": "object", "properties": { "isBestFriend": { - "$id": "https://api.example.com/properties/isBestFriend", "type": [ "boolean", "null" @@ -839,7 +761,6 @@ document (by adding model schemas to `definitions`). "default": false }, "userId": { - "$id": "https://api.example.com/properties/userId", "type": [ "integer", "null" @@ -847,7 +768,6 @@ document (by adding model schemas to `definitions`). "format": "int32" }, "friendId": { - "$id": "https://api.example.com/properties/friendId", "type": [ "integer", "null" diff --git a/examples/openapi-v3.md b/examples/openapi-v3.md index 5897a04..ba9b5ed 100644 --- a/examples/openapi-v3.md +++ b/examples/openapi-v3.md @@ -1,6 +1,6 @@ # OpenAPI 3.0 -These schemas were automatically generated on 2021-06-14 +These schemas were automatically generated on 2021-12-18 using [these Sequelize models](../test/models) and the most recent version of sequelize-to-json-schemas. To confirm that these are indeed all valid schemas use: diff --git a/lib/index.js b/lib/index.js index f2ff5d7..c48feac 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,9 +1,11 @@ const JsonSchemaManager = require('./schema-manager'); const JsonSchema7Strategy = require('./strategies/json-schema-v7'); +const JsonSchema201909Strategy = require('./strategies/json-schema-v2019-09'); const OpenApi3Strategy = require('./strategies/openapi-v3'); module.exports = { JsonSchemaManager, JsonSchema7Strategy, + JsonSchema201909Strategy, OpenApi3Strategy, }; diff --git a/lib/schema-manager.js b/lib/schema-manager.js index b268367..003fa07 100644 --- a/lib/schema-manager.js +++ b/lib/schema-manager.js @@ -68,10 +68,11 @@ class SchemaManager { * @param {string} options.description Text to be used as model property 'description' * @param {array} options.exclude List of attribute names that will not be included in the generated schema * @param {array} options.include List of attribute names that will be included in the generated schema + * @param {boolean} options.renderIdProperty True will render the '$id' field for properties. Default to false * @param {array} options.associations False to exclude all associations from the generated schema, defaults to true - * @returns {object} Object contaiing the strategy-specific schema * @param {array} options.excludeAssociations List of association names that will not be included in the generated schema * @param {array} options.includeAssociations List of association names that will be included in the generated schema + * @returns {object} Object contaiing the strategy-specific schema */ generate(model, strategy, options) { const defaultOptions = { @@ -79,6 +80,7 @@ class SchemaManager { description: null, include: [], exclude: [], + renderIdProperty: false, associations: true, includeAssociations: [], excludeAssociations: [], @@ -273,7 +275,7 @@ class SchemaManager { const result = {}; Object.assign(result, this._getPropertySchema()); // some schemas - Object.assign(result, this._getPropertyId(this._getModelFilePath(_model.get(this).name))); // some schemas + Object.assign(result, this._getPropertySchemaId(this._getModelFilePath(_model.get(this).name))); // some schemas Object.assign(result, this._getModelPropertyTitle(_model.get(this))); // all schemas Object.assign(result, this._getModelPropertyDescription()); // all schemas but only if user passed the model option @@ -355,6 +357,17 @@ class SchemaManager { return _strategy.get(this).getPropertySchema(_options.get(this).secureSchemaUri); } + /** + * Returns the `id` property for the schema. + * + * @private + * @param {string} Path to the json file + * @returns {object} + */ + _getPropertySchemaId(path) { + return _strategy.get(this).getPropertySchemaId(path); + } + /** * Returns the `id` property for the model. * @@ -363,6 +376,12 @@ class SchemaManager { * @returns {object} */ _getPropertyId(path) { + const { renderIdProperty } = _modelOptions.get(this); + + if (!renderIdProperty) { + return null; + } + return _strategy.get(this).getPropertyId(path); } diff --git a/lib/strategies/json-schema-v2019-09.js b/lib/strategies/json-schema-v2019-09.js new file mode 100644 index 0000000..e550fdf --- /dev/null +++ b/lib/strategies/json-schema-v2019-09.js @@ -0,0 +1,253 @@ +const StrategyInterface = require('../strategy-interface'); + +/** + * Class responsible for converting Sequelize models into "JSON Schema Draft 2019-09" schemas. + * + * @copyright Copyright (c) 2021 ALT3 B.V. + * @license Licensed under the MIT License + * @augments StrategyInterface + */ +class JsonSchema201909Strategy extends StrategyInterface { + /** + * Returns the "$schema" property. + * + * @example + * { + * '$schema': 'https://json-schema.org/draft/20219-09/schema#' + * } + * @param {boolean} secureSchemaUri True for HTTPS, false for HTTP + * @returns {object} + */ + getPropertySchema(secureSchemaUri) { + return { + $schema: `${secureSchemaUri ? 'https' : 'http'}://json-schema.org/draft/2019-09/schema#`, + }; + } + + /** + * Returns the "$id" field for a schema. + * + * @example + * { + * '$id': '/user.json' + * } + * @param {string} path + * @returns {object} + */ + // eslint-disable-next-line no-unused-vars + getPropertySchemaId(path) { + return { + $id: path, + }; + } + + /** + * Returns null because 2019-09 no longer uses the "$id" field for base URI changes. + * + * @see {@link https://json-schema.org/draft/2020-12/release-notes.html#embedded-schemas-and-bundling} + * @example null + * @param {string} path + * @returns {null} + */ + // eslint-disable-next-line no-unused-vars + getPropertyId(path) { + return null; + } + + /** + * Returns the "$comment" property (but only if manager option `disableComments` is false). + * + * @example + * { + * '$comment': 'This comment must be a string' + * } + * @param {string} comment + * @returns {object} + */ + getPropertyComment(comment) { + return { + $comment: comment, + }; + } + + /** + * Returns the "examples" property. + * + * @example + * { + * 'examples': [ + * 'example 1', + * 'example 2' + * ] + * } + * @param {array} examples List with one or multiple examples + * @returns {object} + */ + + getPropertyExamples(examples) { + return { examples }; + } + + /** + * Converts a `type` property so it allows null values. + * + * @example + * { + * type: [ + * 'string', + * 'null' + * ] + * } + * + * @param {string} type Value of the `type` property + * @returns {object} + */ + convertTypePropertyToAllowNull(type) { + if (Array.isArray(type)) { + return { + anyOf: [...type, { type: 'null' }], + }; + } + return { + type: [type, 'null'], + }; + } + + /** + * Returns the `contentEncoding` property as used by Json Schema for base64 encoded strings (like BLOB). + * + * @example + * { + * 'contentEncoding': 'base64', + * } + * + * @returns {object} + */ + getPropertyForBase64Encoding() { + return { + contentEncoding: 'base64', + }; + } + + /** + * Returns the property pointing to a HasOne association. + * + * @example + * { + * profile: { + * $ref: '#/definitions/profile' + * } + * } + * @param {string} association name + * @param {Sequelize.association} association Sequelize associaton object + * @returns {object} Null to omit property from the result + */ + getPropertyForHasOneAssociation(associationName, association) { + return { + [associationName]: { + $ref: `#/definitions/${association.target.name}`, // eslint-disable-line unicorn/prevent-abbreviations + }, + }; + } + + /** + * Returns the property pointing to a BelongsTo association. + * + * @example + * { + * company: { + * $ref: '#/definitions/company' + * } + * } + * @param {string} association name + * @param {Sequelize.association} association Sequelize associaton object + * @returns {object} Null to omit property from the result + */ + getPropertyForBelongsToAssociation(associationName, association) { + return { + [associationName]: { + $ref: `#/definitions/${association.target.name}`, // eslint-disable-line unicorn/prevent-abbreviations + }, + }; + } + + /** + * Returns the property pointing to a HasMany association. + * + * @example + * { + * documents: { + * type: "array", + * items: { + * $ref: '#/definitions/document' + * } + * } + * } + * @param {string} association name + * @param {Sequelize.association} association Sequelize associaton object + * @returns {object} Null to omit property from the result + */ + getPropertyForHasManyAssociation(associationName, association) { + return { + [associationName]: { + type: 'array', + items: { + $ref: `#/definitions/${association.target.name}`, // eslint-disable-line unicorn/prevent-abbreviations + }, + }, + }; + } + + /** + * Returns the property pointing to a BelongsToMany association. + * + * @example + * { + * friends: { + * type: "array", + * items: { + * allOf: [ + * { + * $ref: '#/definitions/user' + * }, + * { + * type: 'object', + * properties: { + * friendship: { + * $ref: '#/definitions/friendship' + * } + * } + * } + * ] + * } + * } + * } + * @param {string} association name + * @param {Sequelize.association} association Sequelize associaton object + * @returns {object} Null to omit property from the result + */ + getPropertyForBelongsToManyAssociation(associationName, association) { + return { + [associationName]: { + type: 'array', + items: { + allOf: [ + { + $ref: `#/definitions/${association.target.name}`, // eslint-disable-line unicorn/prevent-abbreviations + }, + { + type: 'object', + properties: { + [association.through.model.options.name.plural]: { + $ref: `#/definitions/${association.through.model.name}`, // eslint-disable-line unicorn/prevent-abbreviations + }, + }, + }, + ], + }, + }, + }; + } +} + +module.exports = JsonSchema201909Strategy; diff --git a/lib/strategies/json-schema-v7.js b/lib/strategies/json-schema-v7.js index dcc7e83..2d4dcb1 100644 --- a/lib/strategies/json-schema-v7.js +++ b/lib/strategies/json-schema-v7.js @@ -25,7 +25,24 @@ class JsonSchema7Strategy extends StrategyInterface { } /** - * Returns the "$id" property. + * Returns the "$id" field for a schema. + * + * @example + * { + * '$id': '/user.json' + * } + * @param {string} path + * @returns {object} + */ + // eslint-disable-next-line no-unused-vars + getPropertySchemaId(path) { + return { + $id: path, + }; + } + + /** + * Returns the "$id" field for a property. * * @example * { diff --git a/lib/strategies/openapi-v3.js b/lib/strategies/openapi-v3.js index d9b8100..c59150b 100644 --- a/lib/strategies/openapi-v3.js +++ b/lib/strategies/openapi-v3.js @@ -19,7 +19,19 @@ class OpenApi3Strategy extends StrategyInterface { } /** - * Returns null because OpenAPI 3.0 does not support the "id" property. + * Returns null because OpenAPI 3 does not support the "id" field for a schema. + * + * @example null + * @param {string} path + * @returns {null} + */ + // eslint-disable-next-line no-unused-vars + getPropertySchemaId(path) { + return null; + } + + /** + * Returns null because OpenAPI 3.0 does not support the "id" field for a property. * * @example null * @param {string} path diff --git a/lib/strategy-interface.js b/lib/strategy-interface.js index d307041..77b81f3 100644 --- a/lib/strategy-interface.js +++ b/lib/strategy-interface.js @@ -18,6 +18,17 @@ class StrategyInterface { this.constructor._throwMissingImplementationError(this.constructor.name, 'getPropertySchema'); } + /** + * Must return the property used as "schema id". + * + * @see {@link https://json-schema.org/understanding-json-schema/structuring.html#the-id-property} + * @param {string} path Path to the json file + * @returns {object|null} Null to omit property from the result + */ + getPropertySchemaId(path) { + this.constructor._throwMissingImplementationError(this.constructor.name, 'getPropertySchemaId'); + } + /** * Must return the property used as "id". * diff --git a/package.json b/package.json index e7546df..e198dc3 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "oas", "oasv3" ], + "dependencies": {}, "devDependencies": { "@release-it/conventional-changelog": "^3.3.0", "acorn": "^8.6.0", @@ -59,12 +60,12 @@ "eslint-config-prettier": "^8.0.0", "eslint-plugin-import": "^2.25.3", "eslint-plugin-jest": "^25.3.0", - "eslint-plugin-jsdoc": "^37.1.0", + "eslint-plugin-jsdoc": "^37.2.0", "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-unicorn": "^39.0.0", "fs": "0.0.1-security", "husky": "^7.0.4", - "jest": "^25.0.0", + "jest": "^27.4.5", "jsdoc": "^3.6.6", "lint-staged": "^12.1.2", "lodash.clonedeep": "^4.5.0", diff --git a/test/schema-manager.test.js b/test/schema-manager.test.js index 57f9fbe..7c1e111 100644 --- a/test/schema-manager.test.js +++ b/test/schema-manager.test.js @@ -11,7 +11,9 @@ describe('SchemaManager', function () { describe('Ensure default options:', function () { const schemaManager = new JsonSchemaManager(); const strategy = new JsonSchema7Strategy(); - const schema = schemaManager.generate(models.user, strategy); + const schema = schemaManager.generate(models.user, strategy, { + renderIdProperty: true, + }); it(`produce a HTTPS schema URI`, function () { expect(schema.$schema).toEqual('https://json-schema.org/draft-07/schema#'); @@ -53,7 +55,9 @@ describe('SchemaManager', function () { baseUri: 'https://alt3.io', }); const strategy = new JsonSchema7Strategy(); - const schema = schemaManager.generate(models.user, strategy); + const schema = schemaManager.generate(models.user, strategy, { + renderIdProperty: true, + }); it(`produces absolute paths for models`, function () { expect(schema.$id).toEqual('https://alt3.io/user.json'); @@ -64,6 +68,27 @@ describe('SchemaManager', function () { }); }); + // ------------------------------------------------------------------------ + // make sure 'renderIdProperty' works as expected + // ------------------------------------------------------------------------ + describe(`Ensure 'renderIdProperty' :`, function () { + const schemaManager = new JsonSchemaManager(); + const strategy = new JsonSchema7Strategy(); + const schemaWithoutId = schemaManager.generate(models.user, strategy); + + it(`default value false does not render the '$id' field'`, function () { + expect(schemaWithoutId.properties.createdAt).not.toHaveProperty('$id'); + }); + + const schemaWithId = schemaManager.generate(models.user, strategy, { + renderIdProperty: true, + }); + + it(`value true does render the '$id' field'`, function () { + expect(schemaWithId.properties.createdAt).toHaveProperty('$id'); + }); + }); + // ------------------------------------------------------------------------ // make sure option 'disableComments' works as expected // ------------------------------------------------------------------------ @@ -88,7 +113,9 @@ describe('SchemaManager', function () { absolutePaths: false, }); const strategy = new JsonSchema7Strategy(); - const schema = schemaManager.generate(models.user, strategy); + const schema = schemaManager.generate(models.user, strategy, { + renderIdProperty: true, + }); it(`ignores baseUri and produces relative paths for models`, function () { expect(schema.$id).toEqual('/user.json'); diff --git a/test/strategies/json-schema-2019-09-strategy.test.js b/test/strategies/json-schema-2019-09-strategy.test.js new file mode 100644 index 0000000..98b1d55 --- /dev/null +++ b/test/strategies/json-schema-2019-09-strategy.test.js @@ -0,0 +1,204 @@ +/* eslint-disable no-unused-vars */ + +/** + * Please note that we are NOT testing: + * - non strategy-specific behavior + * - custom Sequelize attribute options like 'description' and '$comment' + * because these are already tested in the StrategyInterface test case + * which uses JSON Schema Draft-07 as the basis for testing. + */ +const Ajv2019 = require('ajv/dist/2019'); + +const ajv = new Ajv2019(); +const models = require('../models'); +const { JsonSchemaManager, JsonSchema201909Strategy } = require('../../lib'); + +describe('JsonSchema201909Strategy', function () { + describe('Test output using default options', function () { + // ------------------------------------------------------------------------ + // generate schema + // ------------------------------------------------------------------------ + const schemaManager = new JsonSchemaManager({ + disableComments: false, + }); + const strategy = new JsonSchema201909Strategy(); + const schema = schemaManager.generate(models.user, strategy, { + renderIdProperty: true, + }); + + // ------------------------------------------------------------------------ + // make sure sequelize model properties render as expected + // ------------------------------------------------------------------------ + describe('Ensure model properties are rendered as expected and thus schema.model:', function () { + const schemaUri = 'https://json-schema.org/draft/2019-09/schema#'; + it(`has property '$schema' with value '${schemaUri}'`, function () { + expect(schema.$schema).toEqual('https://json-schema.org/draft/2019-09/schema#'); + }); + + it("schema has property '$id' with value '/user.json'", function () { + expect(schema.$id).toEqual('/user.json'); + }); + }); + + // ------------------------------------------------------------------------ + // make sure sequelize DataTypes render as expected + // ------------------------------------------------------------------------ + describe('Ensure Sequelize DataTypes are properly converted and thus:', function () { + describe('BLOB', function () { + it("has property 'contentEncoding' of type 'base64'", function () { + expect(schema.properties.BLOB.contentEncoding).toEqual('base64'); + }); + }); + + describe('STRING_ALLOWNULL_EXPLICIT', function () { + it("has property 'type' of type 'array'", function () { + expect(Array.isArray(schema.properties.STRING_ALLOWNULL_EXPLICIT.type)).toBe(true); + }); + + it("has property 'type' with two values named 'string' and 'null'", function () { + expect(Object.values(schema.properties.STRING_ALLOWNULL_EXPLICIT.type)).toEqual([ + 'string', + 'null', + ]); + }); + }); + + // Sequelize allows null values by default so we need to make sure rendered schema + // keys allow null by default (even when not explicitely setting `allowNull: true`) + describe('STRING_ALLOWNULL_IMPLICIT', function () { + it("has property 'type' of type 'array'", function () { + expect(Array.isArray(schema.properties.STRING_ALLOWNULL_IMPLICIT.type)).toBe(true); + }); + + it("has property 'type' with two values named 'string' and 'null'", function () { + expect(Object.values(schema.properties.STRING_ALLOWNULL_IMPLICIT.type)).toEqual([ + 'string', + 'null', + ]); + }); + }); + + describe('JSONB_ALLOWNULL', function () { + it("has property 'anyOf' that is an array of types", function () { + expect(Array.isArray(schema.properties.JSONB_ALLOWNULL.anyOf)).toBe(true); + }); + it("has property 'anyOf' with values of type 'object', 'array', 'boolean', 'integer', 'number', 'string' and 'null'", function () { + expect(Object.values(schema.properties.JSONB_ALLOWNULL.anyOf)).toEqual([ + { type: 'object' }, + { type: 'array' }, + { type: 'boolean' }, + { type: 'integer' }, + { type: 'number' }, + { type: 'string' }, + { type: 'null' }, + ]); + }); + }); + + describe('ARRAY_ALLOWNULL_EXPLICIT', function () { + it("has property 'type' of type 'array'", function () { + expect(Array.isArray(schema.properties.ARRAY_ALLOWNULL_EXPLICIT.type)).toBe(true); + }); + + it("has property 'type' with two values named 'array' and 'null'", function () { + expect(Object.values(schema.properties.ARRAY_ALLOWNULL_EXPLICIT.type)).toEqual([ + 'array', + 'null', + ]); + }); + }); + + describe('ARRAY_ALLOWNULL_IMPLICIT', function () { + it("has property 'type' of type 'array'", function () { + expect(Array.isArray(schema.properties.ARRAY_ALLOWNULL_IMPLICIT.type)).toBe(true); + }); + + it("has property 'type' with two values named 'array' and 'null'", function () { + expect(Object.values(schema.properties.ARRAY_ALLOWNULL_IMPLICIT.type)).toEqual([ + 'array', + 'null', + ]); + }); + }); + }); + + // ------------------------------------------------------------------------ + // make sure associations render as expected + // ------------------------------------------------------------------------ + describe('Ensure associations are properly generated and thus:', function () { + describe("user.HasOne(profile) generates singular property 'profile' with:", function () { + it("property '$ref' pointing to '#/definitions/profile'", function () { + expect(schema.properties.profile.$ref).toEqual('#/definitions/profile'); + }); + }); + + describe("user.HasOne(user, as:boss) generates singular property 'boss' with:", function () { + it("property '$ref' pointing to '#/definitions/user'", function () { + expect(schema.properties.boss.$ref).toEqual('#/definitions/user'); + }); + }); + + describe("user.BelongsTo(company) generates singular property 'company' with:", function () { + it("property '$ref' pointing to '#/definitions/company'", function () { + expect(schema.properties.company.$ref).toEqual('#/definitions/company'); + }); + }); + + describe("user.HasMany(document) generates plural property 'documents' with:", function () { + it("property 'type' with value 'array'", function () { + expect(schema.properties.documents.type).toEqual('array'); + }); + + it("property 'items' holding an object with '$ref' pointing at '#/definitions/document'", function () { + expect(schema.properties.documents.items).toEqual({ + $ref: '#/definitions/document', // eslint-disable-line unicorn/prevent-abbreviations + }); + }); + }); + + describe("user.BelongsToMany(user) generates plural property 'friends' with:", function () { + it("property 'type' with value 'array'", function () { + expect(schema.properties.friends.type).toEqual('array'); + }); + + it("property 'items.allOf' of type 'array'", function () { + expect(Array.isArray(schema.properties.friends.items.allOf)).toBe(true); + }); + + it("array 'items.allOf' holding an object with '$ref' pointing at '#/definitions/user'", function () { + expect(schema.properties.friends.items.allOf[0]).toEqual({ + $ref: '#/definitions/user', // eslint-disable-line unicorn/prevent-abbreviations + }); + }); + + it("array 'items.allOf' holding an object with type object and properties.friendships an object with '$ref' pointing at '#/definitions/friendship'", function () { + expect(schema.properties.friends.items.allOf[1]).toEqual({ + type: 'object', + properties: { + friendships: { + $ref: '#/definitions/friendship', // eslint-disable-line unicorn/prevent-abbreviations + }, + }, + }); + }); + }); + }); + + // ------------------------------------------------------------------------ + // confirm the document is valid JSON Schema Draft-07 + // ------------------------------------------------------------------------ + describe('Ensure that the resultant document:', function () { + it('successfully validates as JSON Schema Draft 2019-09', async () => { + expect.assertions(1); + + // validate document using ajv + const valid = ajv.validate('https://json-schema.org/draft/2019-09/schema#', schema); + if (!valid) { + console.log(ajv.errors); // eslint-disable-line no-console + } + + expect(valid).toBe(true); + }); + }); + }); +}); diff --git a/test/strategies/json-schema-v7-strategy.test.js b/test/strategies/json-schema-v7-strategy.test.js index 93cc533..0521423 100644 --- a/test/strategies/json-schema-v7-strategy.test.js +++ b/test/strategies/json-schema-v7-strategy.test.js @@ -20,7 +20,9 @@ describe('JsonSchema7Strategy', function () { disableComments: false, }); const strategy = new JsonSchema7Strategy(); - const schema = schemaManager.generate(models.user, strategy); + const schema = schemaManager.generate(models.user, strategy, { + renderIdProperty: true, + }); // ------------------------------------------------------------------------ // make sure sequelize model properties render as expected @@ -31,7 +33,7 @@ describe('JsonSchema7Strategy', function () { expect(schema.$schema).toEqual('https://json-schema.org/draft-07/schema#'); }); - it("has property '$id' with value '/user.json'", function () { + it("schema has property '$id' with value '/user.json'", function () { expect(schema.$id).toEqual('/user.json'); }); }); diff --git a/test/strategies/openapi-v3-stragegy.test.js b/test/strategies/openapi-v3-stragegy.test.js index 30c3a40..fb19309 100644 --- a/test/strategies/openapi-v3-stragegy.test.js +++ b/test/strategies/openapi-v3-stragegy.test.js @@ -20,7 +20,9 @@ describe('OpenApi3Strategy', function () { disableComments: false, }); const strategy = new OpenApi3Strategy(); - const schema = schemaManager.generate(models.user, strategy); + const schema = schemaManager.generate(models.user, strategy, { + renderIdProperty: true, + }); // ------------------------------------------------------------------------ // make sure sequelize DataTypes render as expected diff --git a/test/strategy-interface.test.js b/test/strategy-interface.test.js index fe8e6da..5cd6339 100644 --- a/test/strategy-interface.test.js +++ b/test/strategy-interface.test.js @@ -17,8 +17,8 @@ describe('StrategyInterface', function () { describe('Ensure we are testing against:', function () { const methodCount = Object.getOwnPropertyNames(StrategyInterface.prototype).length - 1; // excluding the constructor - it(`8 interface methods`, function () { - expect(methodCount).toEqual(8); + it(`9 interface methods`, function () { + expect(methodCount).toEqual(9); }); }); @@ -38,6 +38,12 @@ describe('StrategyInterface', function () { }).toThrow("DummyStrategy has not implemented the 'getPropertySchema' interface method."); }); + it('getPropertySchemaId()', function () { + expect(() => { + dummyStrategy.getPropertySchemaId(); + }).toThrow("DummyStrategy has not implemented the 'getPropertySchemaId' interface method."); + }); + it('getPropertyId()', function () { expect(() => { dummyStrategy.getPropertyId(); diff --git a/try-me.js b/try-me.js index 25249b3..fc4dd84 100644 --- a/try-me.js +++ b/try-me.js @@ -8,7 +8,7 @@ const _cloneDeep = require('lodash.clonedeep'); const SwaggerParser = require('swagger-parser'); const models = require('./test/models'); -const { JsonSchemaManager, JsonSchema7Strategy, OpenApi3Strategy } = require('./lib'); +const { JsonSchemaManager, JsonSchema7Strategy, JsonSchema201909Strategy, OpenApi3Strategy } = require('./lib'); // Initialize the SchemaManager with global configuration options const schemaManager = new JsonSchemaManager({ @@ -17,11 +17,14 @@ const schemaManager = new JsonSchemaManager({ disableComments: false, }); +// ------------------------------------------------------------------------------------------------ // Generate a JSON Schema Draft-07 schema for the user model +// ------------------------------------------------------------------------------------------------ const json7strategy = new JsonSchema7Strategy(); -const schema = schemaManager.generate(models.user, json7strategy, { +const userSchema7 = schemaManager.generate(models.user, json7strategy, { // title: 'MyUser', // description: 'My Description', + // renderIdProperty: true, // include: [ // '_STRING_', // '_STRING_50_', @@ -35,16 +38,33 @@ const schema = schemaManager.generate(models.user, json7strategy, { // schema.definitions.profile = schemaManager.generate(models.profile, json7strategy); // schema.definitions.document = schemaManager.generate(models.document, json7strategy); - -console.log('JSON Schema v7:') -// console.log(userSchema); -console.log(JSON.stringify(schema, null, 2)); +console.log('='.repeat(80)) +console.log('JSON Schema Draft-07') +console.log('='.repeat(80)) +console.log(JSON.stringify(userSchema7, null, 2)); // console.log(models.user.associations); -// ---------------------------------- -// Generate OpenAPI v3 schema -// ---------------------------------- +// ------------------------------------------------------------------------------------------------ +// Generate a JSON Schema Draft-2109-09 schema +// ------------------------------------------------------------------------------------------------ +const json201909strategy = new JsonSchema201909Strategy(); +const userSchema201909 = schemaManager.generate(models.user, json201909strategy, { + title: 'MyUser', + description: 'My Description', + exclude: [ + '_UUIDV4_', + ] +}); + +console.log('='.repeat(80)) +console.log('Json Schema 2019-09'); +console.log('='.repeat(80)) +console.log(JSON.stringify(userSchema201909, null, 2)); + +// ------------------------------------------------------------------------------------------------ +// Generate OpenAPI v3 schema and validate it using SwaggerParser +// ------------------------------------------------------------------------------------------------ const openapi3strategy = new OpenApi3Strategy(); const userSchema = schemaManager.generate(models.user, openapi3strategy, { title: 'MyUser', @@ -54,8 +74,10 @@ const userSchema = schemaManager.generate(models.user, openapi3strategy, { ] }); -console.log('OpenAPI v3:'); -// console.log(userSchema); +console.log('='.repeat(80)) +console.log('OpenAPI v3') +console.log('='.repeat(80)) +console.log(userSchema); // OpenApi requires more than just the model schema for validation so we insert it into the wrapper const wrapper = require('./test/strategies/openapi-v3-validation-wrapper'); @@ -69,7 +91,9 @@ wrapper.components.schemas.friendship = schemaManager.generate( openapi3strategy, ); -console.log('Validation schema as JSON string:'); +console.log('='.repeat(80)) +console.log('OpenAPI validation schema as JSON string:'); +console.log('='.repeat(80)) console.log(JSON.stringify(wrapper, null, 2)); console.log('Validating generated full schema against swagger-parser:');