├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── .npmignore ├── .npmrc.config ├── LICENSE ├── README.md ├── config.js ├── index.js ├── jest.config.js ├── jest.setup.js ├── lib ├── __test__ │ └── helpers.test.js ├── config.js ├── helpers.js ├── model.js ├── options │ ├── __test__ │ │ └── fieldOptionsMapping.test.js │ ├── fieldOptionsMapping.js │ ├── forceRebuild.js │ └── validateFieldOptMap.js ├── query.js ├── schema.js └── types.js ├── package.json ├── samples ├── 1.js ├── 2.js ├── 3.js ├── 4.js ├── 5.js └── 6.js ├── test.sh ├── test ├── models │ ├── book.js │ ├── index.js │ ├── person.js │ └── ugly.js └── suites │ ├── ajv-validation.test.js │ ├── array-of-array.test.js │ ├── circular-refs.test.js │ ├── custom-field-options-mapping.test.js │ ├── description.test.js │ ├── discriminators.test.js │ ├── force-rebuild.test.js │ ├── nullable-types.test.js │ ├── population.test.js │ ├── queries.test.js │ ├── readonly.test.js │ ├── required-issue.test.js │ ├── required.test.js │ ├── selection.test.js │ ├── translation.test.js │ ├── uploading-mongoose-implicitly.test.js │ └── validation.test.js └── version.sh /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es6: true, 5 | node: true, 6 | 'jest/globals': true, 7 | }, 8 | extends: [ 9 | 'eslint:recommended', 10 | 'airbnb-base', 11 | ], 12 | plugins: ['jest'], 13 | globals: { 14 | Atomics: 'readonly', 15 | SharedArrayBuffer: 'readonly', 16 | }, 17 | parserOptions: { 18 | ecmaVersion: 2018, 19 | }, 20 | rules: { 21 | indent: [ 22 | 'error', 23 | 2, 24 | ], 25 | 'linebreak-style': [ 26 | 'error', 27 | 'unix', 28 | ], 29 | quotes: [ 30 | 'error', 31 | 'single', 32 | ], 33 | semi: [ 34 | 'error', 35 | 'always', 36 | ], 37 | 'global-require': 0, 38 | 'no-underscore-dangle': 0, 39 | 'import/order': 0, 40 | camelcase: 0, 41 | 'no-param-reassign': 0, 42 | 'no-use-before-define': 0, 43 | 'no-plusplus': 0, 44 | 'no-restricted-syntax': 0, 45 | 'no-continue': 0, 46 | 'arrow-parens': ['error', 'as-needed'], 47 | 'no-mixed-operators': 0, 48 | 'jest/no-disabled-tests': 'warn', 49 | 'jest/no-focused-tests': 'error', 50 | 'jest/no-identical-title': 'error', 51 | 'jest/prefer-to-have-length': 'warn', 52 | 'jest/valid-expect': 'error', 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Test on Node ${{ matrix.node }} on ubuntu-latest with Mongoose@${{ matrix.mongoose }} 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | node: [16, 18, 20] 14 | mongoose: [5, 6, 7, 8] 15 | 16 | steps: 17 | - name: Clone repo 18 | uses: actions/checkout@v2 19 | 20 | - name: Set Node.js version 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node }} 24 | 25 | - name: Install dependencies 26 | run: | 27 | npm install 28 | npm install mongoose@${{ matrix.mongoose }} 29 | 30 | - name: Lint 31 | run: npm run lint 32 | 33 | - name: Test with Coverage 34 | run: npm run test 35 | 36 | - name: Publish on Coveralls 37 | uses: coverallsapp/github-action@master 38 | with: 39 | github-token: ${{ secrets.GITHUB_TOKEN }} 40 | flag-name: node-${{ matrix.node }}-mongoose-${{ matrix.mongoose }} 41 | parallel: true 42 | 43 | report-coverage: 44 | needs: test 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Coveralls Finished 48 | uses: coverallsapp/github-action@master 49 | with: 50 | github-token: ${{ secrets.github_token }} 51 | parallel-finished: true 52 | 53 | publish: 54 | needs: report-coverage 55 | runs-on: ubuntu-latest 56 | if: startsWith(github.ref, 'refs/tags/v') 57 | steps: 58 | - name: Clone repo 59 | uses: actions/checkout@v2 60 | 61 | - name: Set Node.js version 62 | uses: actions/setup-node@v1 63 | with: 64 | node-version: 20 65 | 66 | - name: Check Version 67 | run: ./version.sh 68 | 69 | - name: Publish to npm 70 | run: | 71 | cp .npmrc.config .npmrc 72 | npm publish 73 | env: 74 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | locals 3 | coverage 4 | out 5 | .vscode 6 | .npmrc -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | ** 2 | 3 | !lib/** 4 | !index.js 5 | !config.js -------------------------------------------------------------------------------- /.npmrc.config: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} 2 | registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 - 2020 Dmitry Scheglov 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 | ![Build Status](https://github.com/DScheglov/mongoose-schema-jsonschema/actions/workflows/run-tests.yml/badge.svg) 2 | [![Coverage Status](https://coveralls.io/repos/github/DScheglov/mongoose-schema-jsonschema/badge.svg?branch=master)](https://coveralls.io/github/DScheglov/) 3 | [![npm downloads](https://img.shields.io/npm/dm/mongoose-schema-jsonschema)](https://www.npmjs.com/package/mongoose-schema-jsonschema) 4 | [![NPM](https://img.shields.io/npm/l/mongoose-schema-jsonschema)](https://github.com/DScheglov/mongoose-schema-jsonschema/blob/master/LICENSE) 5 | 6 | # mongoose-schema-jsonschema 7 | 8 | The module allows to create json schema from Mongoose schema by adding 9 | `jsonSchema` method to `mongoose.Schema`, `mongoose.Model` and `mongoose.Query` 10 | classes 11 | 12 | ## Contents 13 | 14 | - [mongoose-schema-jsonschema](#mongoose-schema-jsonschema) 15 | - [Contents](#contents) 16 | - [Installation](#installation) 17 | - [Schema Build Configuration](#schema-build-configuration) 18 | - [Samples](#samples) 19 | - [Validation tools](#validation-tools) 20 | - [Specifications](#specifications) 21 | - [mongoose.Schema.prototype.jsonSchema](#mongooseschemaprototypejsonschema) 22 | - [mongoose.Model.jsonSchema](#mongoosemodeljsonschema) 23 | - [mongoose.Query.prototype.jsonSchema](#mongoosequeryprototypejsonschema) 24 | - [Custom Schema Types Support](#custom-schema-types-support) 25 | - [Releases](#releases) 26 | - [Supported versions](#supported-versions) 27 | 28 | ----------------- 29 | 30 | ## Installation 31 | 32 | ```shell 33 | npm install mongoose-schema-jsonschema 34 | ``` 35 | 36 | ## Schema Build Configuration 37 | 38 | Since v1.4.0 it is able to configure how `jsonSchema()` works. 39 | 40 | To do that package was extended with `config` function. 41 | 42 | ```js 43 | const config = require('mongoose-schema-jsonschema/config'); 44 | 45 | config({ 46 | // ... options go here 47 | }); 48 | ``` 49 | 50 | Currently there are two options that affects build process: 51 | 52 | - **forceRebuild**: `boolean` -- **mongoose-schema-jsonschema** caches json schemas built for mongoose schemas. 53 | That means we cannot built updated jsonSchema after some updates were made in the mongoose schema 54 | that already has jsonSchema. 55 | To resolve this issue the `forceRebuild` was added (see sample bellow) 56 | 57 | - **fieldOptionsMapping**: `{ [key: string]: string } | Array` - allows to specify how to convert some custom options specified in the mongoose field definition. 58 | 59 | ```js 60 | const mongoose = require('mongoose-schema-jsonschema')(); 61 | const config = require('mongoose-schema-jsonschema/config'); 62 | 63 | const { Schema } = mongoose; 64 | 65 | const BookSchema = new Schema({ 66 | title: { type: String, required: true, notes: 'Book Title' }, 67 | year: Number, 68 | author: { type: String, required: true }, 69 | }); 70 | 71 | const fieldOptionsMapping = { 72 | notes: 'x-notes', 73 | }; 74 | 75 | config({ fieldOptionsMapping }); 76 | console.dir(BookSchema.jsonSchema(), { depth: null }); 77 | 78 | config({ fieldOptionsMapping: [], forceRebuild: true }); // reset 79 | console.dir(BookSchema.jsonSchema(), { depth: null }); 80 | ``` 81 | 82 | **Output**: 83 | 84 | ```js 85 | { 86 | type: 'object', 87 | properties: { 88 | title: { type: 'string', 'x-notes': 'Book Title' }, 89 | year: { type: 'number' }, 90 | author: { type: 'string' }, 91 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' } 92 | }, 93 | required: [ 'title', 'author' ] 94 | } 95 | { 96 | type: 'object', 97 | properties: { 98 | title: { type: 'string' }, 99 | year: { type: 'number' }, 100 | author: { type: 'string' }, 101 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' } 102 | }, 103 | required: [ 'title', 'author' ] 104 | } 105 | ``` 106 | 107 | ## Samples 108 | 109 | Let's build json schema from simple mongoose schema 110 | 111 | ```javascript 112 | const mongoose = require('mongoose'); 113 | require('mongoose-schema-jsonschema')(mongoose); 114 | 115 | const Schema = mongoose.Schema; 116 | 117 | const BookSchema = new Schema({ 118 | title: { type: String, required: true }, 119 | year: Number, 120 | author: { type: String, required: true }, 121 | }); 122 | 123 | const jsonSchema = BookSchema.jsonSchema(); 124 | 125 | console.dir(jsonSchema, { depth: null }); 126 | 127 | ``` 128 | 129 | **Output**: 130 | 131 | ```javascript 132 | { 133 | type: 'object', 134 | properties: { 135 | title: { type: 'string' }, 136 | year: { type: 'number' }, 137 | author: { type: 'string' }, 138 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' } 139 | }, 140 | required: [ 'title', 'author' ] 141 | } 142 | ``` 143 | 144 | The mongoose.Model.jsonSchema method allows to build json schema considering 145 | the field selection and population 146 | 147 | ```javascript 148 | const mongoose = require('mongoose'); 149 | require('mongoose-schema-jsonschema')(mongoose); 150 | 151 | const Schema = mongoose.Schema; 152 | 153 | const BookSchema = new Schema({ 154 | title: { type: String, required: true }, 155 | year: Number, 156 | author: { type: Schema.Types.ObjectId, required: true, ref: 'Person' } 157 | }); 158 | 159 | const PersonSchema = new Schema({ 160 | firstName: { type: String, required: true }, 161 | lastName: { type: String, required: true }, 162 | dateOfBirth: Date 163 | }); 164 | 165 | const Book = mongoose.model('Book', BookSchema); 166 | const Person = mongoose.model('Person', PersonSchema) 167 | 168 | console.dir(Book.jsonSchema('title year'), { depth: null }); 169 | console.dir(Book.jsonSchema('', 'author'), { depth: null }); 170 | 171 | ``` 172 | 173 | **Output**: 174 | 175 | ```javascript 176 | { 177 | title: 'Book', 178 | type: 'object', 179 | properties: { 180 | title: { type: 'string' }, 181 | year: { type: 'number' }, 182 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' } 183 | } 184 | } 185 | { 186 | title: 'Book', 187 | type: 'object', 188 | properties: { 189 | title: { type: 'string' }, 190 | year: { type: 'number' }, 191 | author: { 192 | title: 'Person', 193 | type: 'object', 194 | properties: { 195 | firstName: { type: 'string' }, 196 | lastName: { type: 'string' }, 197 | dateOfBirth: { type: 'string', format: 'date-time' }, 198 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 199 | __v: { type: 'number' } 200 | }, 201 | required: [ 'firstName', 'lastName' ], 202 | 'x-ref': 'Person', 203 | description: 'Refers to Person' 204 | }, 205 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 206 | __v: { type: 'number' } 207 | }, 208 | required: [ 'title', 'author' ] 209 | } 210 | ``` 211 | 212 | ```javascript 213 | const mongoose = require('mongoose'); 214 | const extendMongoose = require('mongoose-schema-jsonschema'); 215 | 216 | extendMongoose(mongoose); 217 | 218 | const { Schema } = mongoose; 219 | 220 | const BookSchema = new Schema({ 221 | title: { type: String, required: true }, 222 | year: Number, 223 | author: { type: Schema.Types.ObjectId, required: true, ref: 'Person' } 224 | }); 225 | 226 | const Book = mongoose.model('Book', BookSchema); 227 | const Q = Book.find().select('title').limit(5); 228 | 229 | 230 | console.dir(Q.jsonSchema(), { depth: null }); 231 | ``` 232 | 233 | **Output**: 234 | 235 | ```javascript 236 | { 237 | title: 'List of books', 238 | type: 'array', 239 | items: { 240 | type: 'object', 241 | properties: { 242 | title: { type: 'string' }, 243 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' } 244 | } 245 | }, 246 | maxItems: 5 247 | } 248 | ``` 249 | 250 | ## Validation tools 251 | 252 | Created by **mongoose-schema-jsonschema** json-schema's could be used for 253 | document validation with: 254 | 255 | - [`ajv`](https://www.npmjs.com/package/ajv) 256 | - [`jsonschema`](https://www.npmjs.com/package/jsonschema) 257 | 258 | ## Specifications 259 | 260 | ### mongoose.Schema.prototype.jsonSchema 261 | 262 | Builds the json schema based on the Mongoose schema. 263 | if schema has been already built the method returns new deep copy 264 | 265 | Method considers the `schema.options.toJSON.virtuals` to included 266 | the virtual paths (without detailed description) 267 | 268 | Declaration: 269 | 270 | ```javascript 271 | function schema_jsonSchema(name) { ... } 272 | ``` 273 | 274 | Parameters: 275 | 276 | - **name**: `String` - Name of the object 277 | - *Returns* `Object` - json schema 278 | 279 | ### mongoose.Model.jsonSchema 280 | 281 | Builds json schema for model considering the selection and population 282 | 283 | if `fields` specified the method removes `required` constraints 284 | 285 | Declaration: 286 | 287 | ```javascript 288 | function model_jsonSchema(fields, populate) { ... } 289 | ``` 290 | 291 | Parameters: 292 | 293 | - **fields**: `String`|`Array`|`Object` - mongoose selection object 294 | - **populate**: `String`|`Object` - mongoose population options 295 | - *Returns* `Object` - json schema 296 | 297 | ### mongoose.Query.prototype.jsonSchema 298 | 299 | Builds json schema considering the query type and query options. 300 | The method returns the schema for array if query type is `find` and 301 | the schema for single document if query type is `findOne` or `findOneAnd*`. 302 | 303 | In case when the method returns schema for array the collection name is used to 304 | form title of the resulting schema. In `findOne*` case the title is the name 305 | of the appropriate model. 306 | 307 | Declaration: 308 | 309 | ```javascript 310 | function query_jsonSchema() { ... } 311 | ``` 312 | 313 | Parameters: 314 | 315 | - *Returns* `Object` - json schema 316 | 317 | ## Custom Schema Types Support 318 | 319 | If you use custom Schema Types you should define the jsonSchema method 320 | for your type-class(es). 321 | 322 | The base functionality is accessible from your code by calling base-class methods: 323 | 324 | ```javascript 325 | newSchemaType.prototype.jsonSchema = function() { 326 | // Simple types (strings, numbers, bools): 327 | const jsonSchema = mongoose.SchemaType.prototype.jsonSchema.call(this); 328 | 329 | // Date: 330 | const jsonSchema = Types.Date.prototype.jsonSchema.call(this); 331 | 332 | // ObjectId 333 | const jsonSchema = Types.ObjectId.prototype.jsonSchema.call(this); 334 | 335 | // for Array (or DocumentArray) 336 | const jsonSchema = Types.Array.prototype.jsonSchema.call(this); 337 | 338 | // for Embedded documents 339 | const jsonSchema = Types.Embedded.prototype.jsonSchema.call(this); 340 | 341 | // for Mixed documents: 342 | const jsonSchema = Types.Mixed.prototype.jsonSchema.call(this); 343 | 344 | /* 345 | * 346 | * Place your code instead of this comment 347 | * 348 | */ 349 | 350 | return jsonSchema; 351 | } 352 | ``` 353 | 354 | ## Releases 355 | 356 | - version 1.0 - Basic functionality 357 | - version 1.1 - Mongoose.Query support implemented 358 | - version 1.1.5 - uuid issue fixed, ajv compliance verified 359 | - version 1.1.8 - Schema.Types.Mixed issue fixed 360 | - version 1.1.9 - readonly settings support added 361 | - version 1.1.11 - required issue fixed [issue#2](https://github.com/DScheglov/mongoose-schema-jsonschema/issues/2) 362 | - version 1.1.12 - mixed-type fields description and title support added (fix for issue: [issue#3](https://github.com/DScheglov/mongoose-schema-jsonschema/issues/3)) 363 | - version 1.1.15 - support for ensured [issue#8](https://github.com/DScheglov/mongoose-schema-jsonschema/issues/8) 364 | - version 1.3.0 365 | - nullable types support (as union: `[type, 'null']`) 366 | - `examples` option support [issue#14](https://github.com/DScheglov/mongoose-schema-jsonschema/issues/14) 367 | - support for fields dynamicly marked as `required` [issue#16](https://github.com/DScheglov/mongoose-schema-jsonschema/issues/16) 368 | - Node support restricted to 8.x, 9.x, 10.x, 12.x 369 | - Mongoose support restricted to 5.x 370 | - *Development*: 371 | - migrated from `mocha` + `istanbul` to `jest` 372 | - added `eslint` 373 | - version 1.3.1 - support `minlenght` and `maxlength` [issue#21](https://github.com/DScheglov/mongoose-schema-jsonschema/issues/21) 374 | - version 1.4.0 - **broken** - schema build configurations (`forceRebuild` and `fieldOptionsMapping`) 375 | - version 1.4.2 - fix for broken version 1.4.0 [issue#22](https://github.com/DScheglov/mongoose-schema-jsonschema/issues/22) 376 | - version 1.4.4 - fix for field constraints [issue#25](https://github.com/DScheglov/mongoose-schema-jsonschema/issues/25) 377 | - version 2.0.0 - Support for . Node v8.x.x, v9.x.x are no longer supported (use v1.4.7 of the lib) 378 | - version 2.1.0 - Support for and Node v14.x, v16.x, v18.x 379 | - version 2.2.0 - Support for and Node v20.x. Node v14.x is no longer supported (use v2.1.0 of the lib) 380 | - version 2.2.1 - fix for `required` fields: if `required` is a function, it is not considered as required field 381 | - version 3.0.0 - breaking changes on Array with `required`: the `minItems` constraint is removed from JSON schema 382 | 383 | ## Supported versions 384 | 385 | - node.js: 16.x, 18.x, 20.x 386 | - mongoose: 5.x, 6.x, 7.x, 8.x 387 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/config'); 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const schema_jsonSchema = require('./lib/schema'); 2 | const model_jsonSchema = require('./lib/model'); 3 | const query_jsonSchema = require('./lib/query'); 4 | const types = require('./lib/types'); 5 | 6 | module.exports = function moduleFactory(mongoose) { 7 | // eslint-disable-next-line global-require 8 | mongoose = mongoose || require('mongoose'); 9 | const { Types } = mongoose.Schema; 10 | 11 | mongoose.SchemaType.prototype.jsonSchema = types.simpleType_jsonSchema; 12 | 13 | Types.Date.prototype.jsonSchema = types.date_jsonSchema; 14 | Types.ObjectId.prototype.jsonSchema = types.objectId_jsonSchema; 15 | 16 | Types.Array.prototype.jsonSchema = types.array_jsonSchema; 17 | Types.DocumentArray.prototype.jsonSchema = types.array_jsonSchema; 18 | 19 | if (Types.Embedded) { 20 | Types.Embedded.prototype.jsonSchema = types.mixed_jsonSchema; 21 | } 22 | 23 | if (Types.Subdocument) { 24 | Types.Subdocument.prototype.jsonSchema = types.mixed_jsonSchema; 25 | } 26 | 27 | Types.Mixed.prototype.jsonSchema = types.mixed_jsonSchema; 28 | 29 | Types.Map.prototype.jsonSchema = types.map_jsonSchema; 30 | 31 | mongoose.Schema.prototype.jsonSchema = schema_jsonSchema; 32 | mongoose.Model.jsonSchema = model_jsonSchema; 33 | mongoose.Query.prototype.jsonSchema = query_jsonSchema; 34 | 35 | return mongoose; 36 | }; 37 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | collectCoverageFrom: ['lib/**'], 4 | coverageDirectory: 'coverage', 5 | testEnvironment: 'node', 6 | transformIgnorePatterns: ['/node_modules/'], 7 | setupFiles: ['./jest.setup.js'], 8 | }; 9 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | if (global.TextEncoder == null) { 2 | // Support for node v.10 3 | const util = require('util'); 4 | global.TextEncoder = util.TextEncoder; 5 | global.TextDecoder = util.TextDecoder; 6 | } 7 | -------------------------------------------------------------------------------- /lib/__test__/helpers.test.js: -------------------------------------------------------------------------------- 1 | const { compose, hasProperty, idX } = require('../helpers'); 2 | 3 | describe('compose', () => { 4 | it('should return an idX function', () => { 5 | expect(compose()).toBe(idX); 6 | }); 7 | 8 | it('should return the same function if only one funtion passed', () => { 9 | const fn = () => {}; 10 | expect(compose(fn)).toBe(fn); 11 | }); 12 | 13 | it('should compose 2 functions', () => { 14 | const sum = (a, b) => a + b; 15 | const double = x => 2 * x; 16 | const doubleSum = compose(double, sum); 17 | 18 | expect(doubleSum(1, 2)).toBe(6); 19 | }); 20 | 21 | it('should compose 3 functions', () => { 22 | const argsList = (...args) => args; 23 | const packToArray = value => [value]; 24 | const packToObj = field => value => ({ [field]: value }); 25 | 26 | const packArgsList = compose(packToArray, packToObj('args'), argsList); 27 | 28 | expect( 29 | packArgsList(1, 2, 3), 30 | ).toEqual([{ args: [1, 2, 3] }]); 31 | }); 32 | }); 33 | 34 | describe('hasProperty', () => { 35 | it('should return true if field is in the object', () => { 36 | expect( 37 | hasProperty({ field: 1 }, 'field'), 38 | ).toBe(true); 39 | }); 40 | 41 | it('should return false if field is not in the object', () => { 42 | expect( 43 | hasProperty({ field: 1 }, 'field2'), 44 | ).toBe(false); 45 | }); 46 | 47 | it('should not consider prototype', () => { 48 | const obj = Object.create({ protoProp: true }); 49 | expect(obj.protoProp).toBe(true); 50 | expect(hasProperty(obj, 'protoProp')).toBe(false); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | const [ 2 | FIELD_OPTIONS_MAPPING, 3 | fieldOptionsMapping, 4 | defFieldOptionsMapping, 5 | ] = require('./options/fieldOptionsMapping'); 6 | 7 | const [ 8 | FORCE_REBUILD, 9 | defForceRebuild, 10 | ] = require('./options/forceRebuild'); 11 | 12 | const mappers = new Map([ 13 | [FIELD_OPTIONS_MAPPING, fieldOptionsMapping], 14 | ]); 15 | 16 | const options = new Map([ 17 | [FIELD_OPTIONS_MAPPING, defFieldOptionsMapping], 18 | [FORCE_REBUILD, defForceRebuild], 19 | ]); 20 | 21 | const setOption = optionsPatch => option => { 22 | const valueMapper = mappers.get(option); 23 | const value = optionsPatch[option]; 24 | options.set( 25 | option, 26 | typeof valueMapper === 'function' ? valueMapper(value) : value, 27 | ); 28 | }; 29 | 30 | const config = optionsPatch => Object 31 | .keys(optionsPatch) 32 | .forEach(setOption(optionsPatch)); 33 | 34 | config.get = option => options.get(option); 35 | 36 | module.exports = config; 37 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | function findPath(obj, path) { 4 | let spec; 5 | let isArray; 6 | let cont; 7 | const jssPath = path.split('.'); 8 | const prop = jssPath.pop(); 9 | 10 | if (jssPath.length) { 11 | cont = jssPath.reduce((o, p) => { 12 | while (o && o.type === 'array') o = o.items; 13 | return o.properties[p]; 14 | }, obj); 15 | while (cont && cont.type === 'array') cont = cont.items; 16 | } else { 17 | cont = obj; 18 | } 19 | const parent = cont; 20 | cont = cont.properties; 21 | const baseCont = cont; 22 | 23 | spec = cont[prop]; 24 | isArray = false; 25 | while (spec && spec.type === 'array') { 26 | isArray = true; 27 | cont = spec; 28 | spec = spec.items; 29 | } 30 | 31 | return { 32 | spec, 33 | cont, 34 | baseCont, 35 | prop, 36 | isArray, 37 | parent, 38 | }; 39 | } 40 | 41 | function ensurePath(obj, path) { 42 | assert.ok(obj); 43 | 44 | let cont; 45 | 46 | const jssPath = path.split('.'); 47 | const first = jssPath.shift(); 48 | 49 | cont = obj; 50 | 51 | while (cont.type === 'array') cont = cont.items; 52 | 53 | if (!cont.properties) cont.properties = {}; 54 | 55 | if (jssPath.length === 0) { 56 | return { 57 | cont, 58 | prop: first, 59 | }; 60 | } 61 | 62 | cont.properties[first] = cont.properties[first] || { 63 | title: first, 64 | type: 'object', 65 | }; 66 | 67 | const spec = cont.properties[first]; 68 | 69 | return ensurePath(spec, jssPath.join('.')); 70 | } 71 | 72 | const compose2 = (f, g) => (...args) => f(g(...args)); 73 | 74 | const idX = x => x; 75 | 76 | const compose = (...fns) => ( 77 | fns.length > 0 ? fns.reduce(compose2) : idX 78 | ); 79 | 80 | const hasProperty = Function.prototype.call.bind( 81 | Object.prototype.hasOwnProperty, 82 | ); 83 | 84 | const readConstraint = constraint => (Array.isArray(constraint) ? constraint[0] : constraint); 85 | 86 | const normalizeSelection = fields => Object.keys(fields).reduce( 87 | (target, key) => Object.defineProperty( 88 | target, 89 | key.replace(/^-/, ''), 90 | { 91 | value: fields[key], 92 | enumerable: true, 93 | writable: true, 94 | configurable: true, 95 | }, 96 | ), 97 | {}, 98 | ); 99 | 100 | module.exports = { 101 | findPath, 102 | ensurePath, 103 | compose, 104 | idX, 105 | hasProperty, 106 | readConstraint, 107 | normalizeSelection, 108 | }; 109 | -------------------------------------------------------------------------------- /lib/model.js: -------------------------------------------------------------------------------- 1 | const { findPath, normalizeSelection } = require('./helpers'); 2 | 3 | module.exports = model_jsonSchema; 4 | 5 | /** 6 | * model_jsonSchema - builds json schema for model considering 7 | * the selection and population 8 | * 9 | * if `fields` specified the method removes `required` contraints 10 | * 11 | * @memberof mongoose.Model 12 | * 13 | * @param {String|Array|Object} fields mongoose selection object 14 | * @param {String|Object} populate mongoose population options 15 | * @return {object} json schema 16 | */ 17 | function model_jsonSchema(fields, populate, readonly) { 18 | let jsonSchema = this.schema.jsonSchema(this.modelName); 19 | 20 | if (populate != null) { 21 | jsonSchema = __populate.call(this, jsonSchema, populate); 22 | } 23 | 24 | __excludedPaths(this.schema, fields).forEach( 25 | __delPath.bind(null, jsonSchema), 26 | ); 27 | 28 | if (readonly) { 29 | __excludedReadonlyPaths(jsonSchema, readonly); 30 | } 31 | 32 | if (fields) __removeRequired(jsonSchema); 33 | 34 | return jsonSchema; 35 | } 36 | 37 | /** 38 | * __populate - enreaches jsonSchema with a sub-document schemas 39 | * 40 | * @param {Object} jsonSchema jsonSchema object 41 | * @param {String|Array|Object} populate mongoose populate object 42 | * @return {Object} enreached json-schema 43 | */ 44 | function __populate(jsonSchema, populate) { 45 | const pTree = normailizePopulationTree(populate); 46 | let path; let model; let subDoc; let 47 | jss; 48 | for (path of pTree) { 49 | jss = findPath(jsonSchema, path.path); 50 | 51 | if (!jss.spec) continue; 52 | 53 | model = jss.spec['x-ref'] || path.model; 54 | if (!model) continue; 55 | try { 56 | if (typeof model === 'string') model = this.base.model(model); 57 | } catch (e) { 58 | continue; 59 | } 60 | subDoc = model.jsonSchema(path.select, path.populate); 61 | subDoc['x-ref'] = jss.spec['x-ref']; 62 | if (jss.spec.description) subDoc.description = jss.spec.description; 63 | if (jss.isArray) { 64 | jss.cont.items = subDoc; 65 | } else { 66 | jss.cont[jss.prop] = subDoc; 67 | } 68 | } 69 | return jsonSchema; 70 | } 71 | 72 | /** 73 | * __delPath - removes path specified from the json-schema as also as 74 | * empty parent paths 75 | * 76 | * @param {object} jsonSchema description 77 | * @param {String} path description 78 | */ 79 | function __delPath(jsonSchema, path) { 80 | const jss = findPath(jsonSchema, path); 81 | delete jss.baseCont[jss.prop]; 82 | if (jss.parent.required && jss.parent.required.length) { 83 | jss.parent.required = jss.parent.required.filter(f => f !== jss.prop); 84 | } 85 | if (Object.keys(jss.baseCont).length === 0) { 86 | __delPath(jsonSchema, path.split('.').slice(0, -1).join('.')); 87 | } 88 | } 89 | 90 | /** 91 | * __removeRequired - removes required fields specification 92 | * 93 | * @param {Object} jsonSchema schema 94 | */ 95 | function __removeRequired(obj) { 96 | while (obj && obj.type === 'array') obj = obj.items; 97 | delete obj.required; 98 | if (obj.properties) { 99 | Object.keys(obj.properties).forEach(p => { 100 | __removeRequired(obj.properties[p]); 101 | }); 102 | } 103 | } 104 | 105 | function normailizePopulationTree(populate) { 106 | if (typeof (populate) === 'string') populate = populate.split(' '); 107 | if (populate.path) populate = [populate]; 108 | return populate.map(p => { 109 | if (!p.path) return { path: p }; 110 | return p; 111 | }); 112 | } 113 | 114 | function __excludedPaths(schema, selection) { 115 | const virtuals = Object.keys(schema.virtuals); 116 | let paths = __allPaths(schema); 117 | let exclude = paths.reduce((excl, p) => { 118 | const path = __getPath(schema, p); 119 | if (path && path.options && path.options.select === false) excl[p] = 0; 120 | return excl; 121 | }, {}); 122 | 123 | selection = selection || {}; 124 | 125 | if (typeof (selection) === 'string') { 126 | selection = selection.split(/\s+/); 127 | } 128 | 129 | if (selection instanceof Array) { 130 | selection = selection.reduce((sel, p) => { 131 | if (p[0] === '+') { 132 | p = p.substr(1); 133 | delete exclude[p]; 134 | } 135 | if (p[0] === '-') { 136 | sel[p.substr(1)] = 0; 137 | } else sel[p] = 1; 138 | return sel; 139 | }, {}); 140 | } 141 | 142 | selection = normalizeSelection(selection); 143 | 144 | const needToAddVirtuals = schema.options && schema.options.toJSON 145 | && (schema.options.toJSON.virtuals || schema.options.toJSON.getters); 146 | 147 | paths = paths.concat(virtuals); 148 | if (!needToAddVirtuals) { 149 | exclude = virtuals.reduce((excl, p) => { 150 | excl[p] = 0; 151 | return excl; 152 | }, exclude); 153 | } 154 | 155 | const explicitlyInclude = Object.keys(selection).some(p => selection[p] === 1); 156 | Object.assign(selection, exclude); 157 | 158 | if (explicitlyInclude) { 159 | selection._id = (typeof selection._id === 'number') ? selection._id : 1; 160 | if (needToAddVirtuals) { 161 | selection.id = (typeof selection.id === 'number') 162 | ? selection.id 163 | : selection._id; 164 | } 165 | return paths.filter(p => { 166 | if (selection[p] != null) return !selection[p]; 167 | return !Object.keys(selection).some( 168 | s => p.indexOf(`${s}.`) === 0 && selection[s], 169 | ); 170 | }); 171 | } 172 | 173 | return Object.keys(selection); 174 | } 175 | 176 | function __allPaths(schema) { 177 | return Object.keys(schema.paths).reduce((paths, p) => { 178 | const path = schema.paths[p]; 179 | if (path.instance !== 'Array' || !path.schema) { 180 | paths.push(p); 181 | return paths; 182 | } 183 | __allPaths(path.schema).forEach(subPath => { 184 | paths.push(`${p}.${subPath}`); 185 | }); 186 | return paths; 187 | }, []); 188 | } 189 | 190 | function __getPath(schema, path) { 191 | let p = schema.paths[path]; 192 | if (p) return p; 193 | 194 | path = path.split('.'); 195 | const l = path.length; 196 | p = ''; 197 | for (let i = 0; i < l; i++) { 198 | p += path[i]; 199 | if (schema.paths[p] && schema.paths[p].schema) { 200 | return __getPath(schema.paths[p].schema, path.slice(i + 1).join('.')); 201 | } 202 | p += '.'; 203 | } 204 | return null; 205 | } 206 | 207 | function __excludedReadonlyPaths(schema, rules, prefix) { 208 | prefix = prefix || ''; 209 | 210 | while (schema.type === 'array') schema = schema.items; 211 | const props = schema.properties; 212 | 213 | if (!props) return; 214 | 215 | Object.keys(props).forEach(f => { 216 | for (const rule of rules) { 217 | if (rule.path.test(prefix + f)) { 218 | delete props[f]; 219 | if (schema.required && schema.required.length) { 220 | schema.required = schema.required.filter(r => r !== f); 221 | if (!schema.required.length) delete schema.required; 222 | } 223 | return; 224 | } 225 | } 226 | if (props[f].type === 'object' || props[f].type === 'array') { 227 | __excludedReadonlyPaths(props[f], rules, `${prefix + f}.`); 228 | } 229 | }); 230 | } 231 | -------------------------------------------------------------------------------- /lib/options/__test__/fieldOptionsMapping.test.js: -------------------------------------------------------------------------------- 1 | const [OPTION_NAME, optionParse, optionDefaultValue] = require('../fieldOptionsMapping'); 2 | 3 | describe('option fieldOptionsMapping', () => { 4 | test('option name should be specified', () => { 5 | expect(typeof OPTION_NAME).toBe('string'); 6 | expect(OPTION_NAME).not.toBe(''); 7 | }); 8 | 9 | test('optionsDefaultValue should be a function', () => { 10 | expect(typeof optionDefaultValue).toBe('function'); 11 | }); 12 | 13 | test('optionParse should parse an object', () => { 14 | const fieldsMapper = optionParse({ x: 'x-x', y: 'x-y' }); 15 | expect(fieldsMapper({ x: 1, y: 2, z: 3 })).toEqual({ 16 | 'x-x': 1, 17 | 'x-y': 2, 18 | }); 19 | }); 20 | 21 | test('optionsParse should parse an array of strings', () => { 22 | const fieldsMapper = optionParse(['x', 'y']); 23 | expect(fieldsMapper({ x: 1, y: 2, z: 3 })).toEqual({ 24 | x: 1, 25 | y: 2, 26 | }); 27 | }); 28 | 29 | test('optionsParse should parse an array of strings and not map field if it doesn\'t present in the src', () => { 30 | const fieldsMapper = optionParse(['x', 'y']); 31 | expect(fieldsMapper({ x: 1, z: 3 })).toEqual({ 32 | x: 1, 33 | }); 34 | }); 35 | 36 | test('optionsParse should parse an array of string-tuples', () => { 37 | const fieldsMapper = optionParse([ 38 | ['x', 'x-x'], 39 | ['y', 'x-y'], 40 | ]); 41 | expect(fieldsMapper({ x: 1, y: 2, z: 3 })).toEqual({ 42 | 'x-x': 1, 43 | 'x-y': 2, 44 | }); 45 | }); 46 | 47 | test('should raise an error if specified not an object or an array', () => { 48 | const errorMessage = ( 49 | 'fieldsMapping Error: Wrong type of option value. ' 50 | + 'Expected: { [key: string]: string } | Array' 51 | ); 52 | 53 | expect( 54 | () => optionParse(), 55 | ).toThrow(errorMessage); 56 | 57 | expect( 58 | () => optionParse(null), 59 | ).toThrow(errorMessage); 60 | 61 | expect( 62 | () => optionParse(1), 63 | ).toThrow(errorMessage); 64 | 65 | expect( 66 | () => optionParse('string'), 67 | ).toThrow(errorMessage); 68 | 69 | expect( 70 | () => optionParse(true), 71 | ).toThrow(errorMessage); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /lib/options/fieldOptionsMapping.js: -------------------------------------------------------------------------------- 1 | const { compose, hasProperty } = require('../helpers'); 2 | 3 | const FIELDS_MAPPING = 'fieldOptionsMapping'; 4 | 5 | const ensureKeyValue = keyValue => ( 6 | Array.isArray(keyValue) 7 | ? keyValue 8 | : [keyValue, keyValue] 9 | ); 10 | 11 | const normalizeMap = map => ( 12 | Array.isArray(map) 13 | ? map.map(ensureKeyValue) 14 | : Object.entries(map) 15 | ); 16 | 17 | const assign = (dest, src) => (dest != null ? Object.assign(dest, src) : src); 18 | 19 | const processFields = mapping => typeDef => mapping.reduce( 20 | (fields, [mField, sField]) => ( 21 | hasProperty(typeDef, mField) 22 | ? assign(fields, { [sField]: typeDef[mField] }) 23 | : fields 24 | ), 25 | null, 26 | ); 27 | 28 | module.exports = [ 29 | FIELDS_MAPPING, // name of the option 30 | process.env.NODE_ENV === 'production' 31 | ? compose(processFields, normalizeMap) 32 | : compose(processFields, normalizeMap, require('./validateFieldOptMap')), 33 | () => null, // default value of the option 34 | ]; 35 | -------------------------------------------------------------------------------- /lib/options/forceRebuild.js: -------------------------------------------------------------------------------- 1 | module.exports = ['forceRebuild', false]; 2 | -------------------------------------------------------------------------------- /lib/options/validateFieldOptMap.js: -------------------------------------------------------------------------------- 1 | const isString = keyValue => typeof keyValue === 'string'; 2 | 3 | const isTupple = keyValue => ( 4 | Array.isArray(keyValue) 5 | && keyValue.length === 2 6 | && keyValue.every(isString) 7 | ); 8 | 9 | const stringOrTuple = keyValue => isString(keyValue) || isTupple(keyValue); 10 | 11 | const validateMap = map => { 12 | if (Array.isArray(map) && map.every(stringOrTuple)) return map; 13 | 14 | if (typeof map === 'object' && map != null && Object.entries(map).every(isTupple)) return map; 15 | 16 | throw TypeError( 17 | 'fieldsMapping Error: Wrong type of option value. ' 18 | + 'Expected: { [key: string]: string } | Array', 19 | ); 20 | }; 21 | 22 | module.exports = validateMap; 23 | -------------------------------------------------------------------------------- /lib/query.js: -------------------------------------------------------------------------------- 1 | const { plural } = require('pluralize'); 2 | 3 | module.exports = query_jsonSchema; 4 | 5 | /** 6 | * query_jsonSchema - returns json schema considering the query type and options 7 | * 8 | * @return {Object} json schema 9 | */ 10 | function query_jsonSchema() { 11 | let { populate } = this._mongooseOptions; 12 | if (populate) { 13 | populate = Object.keys(populate).map(k => populate[k]); 14 | } 15 | let jsonSchema = this.model.jsonSchema( 16 | this._fields, populate, 17 | ); 18 | 19 | delete jsonSchema.required; 20 | 21 | if (this.op.indexOf('findOne') === 0) return jsonSchema; 22 | 23 | delete jsonSchema.title; 24 | 25 | jsonSchema = { 26 | title: `List of ${plural(this.model.modelName)}`, 27 | type: 'array', 28 | items: jsonSchema, 29 | }; 30 | 31 | if (this.options.limit) { 32 | jsonSchema.maxItems = this.options.limit; 33 | } 34 | 35 | return jsonSchema; 36 | } 37 | -------------------------------------------------------------------------------- /lib/schema.js: -------------------------------------------------------------------------------- 1 | const { ensurePath } = require('./helpers'); 2 | const config = require('./config'); 3 | const [FORCE_REBUILD] = require('./options/forceRebuild'); 4 | 5 | module.exports = schema_jsonSchema; 6 | 7 | /** 8 | * schema_jsonSchema - builds the json schema based on the Mongooose schema. 9 | * if schema has been already built the method returns new deep copy 10 | * 11 | * Method considers the `schema.options.toJSON.virtuals` to included 12 | * the virtual paths (without detailed description) 13 | * 14 | * @memberof mongoose.Schema 15 | * 16 | * @param {String} name Name of the object 17 | * @return {Object} json schema 18 | */ 19 | function schema_jsonSchema(name) { 20 | name = name || this.options.name; 21 | 22 | if (this.__buildingSchema) { 23 | this.__jsonSchemaId = this.__jsonSchemaId 24 | || `#schema-${++schema_jsonSchema.__jsonSchemaIdCounter}`; 25 | return { $ref: this.__jsonSchemaId }; 26 | } 27 | 28 | if (!this.__jsonSchema || config.get(FORCE_REBUILD)) { 29 | this.__buildingSchema = true; 30 | this.__jsonSchema = __build(name, this); 31 | this.__buildingSchema = false; 32 | if (this.__jsonSchemaId) { 33 | this.__jsonSchema = { 34 | id: this.__jsonSchemaId, ...this.__jsonSchema, 35 | }; 36 | } 37 | } 38 | 39 | const result = JSON.parse(JSON.stringify(this.__jsonSchema)); 40 | 41 | if (name) { 42 | result.title = name; 43 | } else { 44 | delete result.title; 45 | } 46 | 47 | return result; 48 | } 49 | 50 | schema_jsonSchema.__jsonSchemaIdCounter = 0; 51 | 52 | function __build(name, schema) { 53 | const paths = Object 54 | .keys(schema.paths) 55 | .filter(path => !/\.\$\*$/.test(path)); // removing Map.item paths 56 | 57 | let path; let jss; let 58 | sch; 59 | const result = {}; 60 | if (name) { 61 | result.title = name; 62 | } 63 | result.type = 'object'; 64 | result.properties = {}; 65 | result.required = []; 66 | 67 | for (path of paths) { 68 | jss = ensurePath(result, path); 69 | sch = schema.paths[path].jsonSchema(jss.prop); 70 | jss.cont.properties[jss.prop] = sch; 71 | if (sch.__required) { 72 | jss.cont.required = jss.cont.required || []; 73 | jss.cont.required.push(jss.prop); 74 | } 75 | delete sch.__required; 76 | } 77 | 78 | if (result.required.length === 0) delete result.required; 79 | 80 | const needToAddVirtuals = schema.options && schema.options.toJSON 81 | && (schema.options.toJSON.virtuals || schema.options.toJSON.getters); 82 | 83 | if (needToAddVirtuals) { 84 | Object.keys(schema.virtuals).forEach(v => { 85 | result.properties[v] = {}; 86 | }); 87 | } 88 | 89 | return result; 90 | } 91 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | const config = require('./config'); 2 | const [FIELDS_MAPPING] = require('./options/fieldOptionsMapping'); 3 | const { readConstraint } = require('./helpers'); 4 | 5 | module.exports = { 6 | simpleType_jsonSchema, 7 | objectId_jsonSchema, 8 | date_jsonSchema, 9 | array_jsonSchema, 10 | mixed_jsonSchema, 11 | map_jsonSchema, 12 | }; 13 | 14 | /** 15 | * simpleType_jsonSchema - returns jsonSchema for simple-type parameter 16 | * 17 | * @memberof mongoose.Schema.Types.String 18 | * 19 | * @return {object} the jsonSchema for parameter 20 | */ 21 | function simpleType_jsonSchema() { 22 | const result = {}; 23 | 24 | result.type = this.instance.toLowerCase(); 25 | 26 | __processOptions(result, extendOptions(this)); 27 | 28 | return checkNullable(result); 29 | } 30 | 31 | function map_jsonSchema(name) { 32 | const result = { 33 | type: 'object', 34 | additionalProperties: 35 | this.options.of != null 36 | ? __describe(`itemOf_${name}`, this.options.of) 37 | : true, 38 | }; 39 | 40 | __processOptions(result, extendOptions(this)); 41 | 42 | delete result.additionalProperties.__required; 43 | 44 | return checkNullable(result); 45 | } 46 | 47 | /** 48 | * objectId_jsonSchema - returns the jsonSchema for ObjectId parameters 49 | * 50 | * @memberof mongoose.Schema.Types.ObjectId 51 | * 52 | * @param {String} name the name of parameter 53 | * @return {object} the jsonSchema for parameter 54 | */ 55 | function objectId_jsonSchema(name) { 56 | const result = simpleType_jsonSchema.call(this, name); 57 | 58 | result.type = 'string'; 59 | result.pattern = '^[0-9a-fA-F]{24}$'; 60 | 61 | return checkNullable(result); 62 | } 63 | 64 | /** 65 | * date_jsonSchema - description 66 | * 67 | * @param {type} name description 68 | * @return {type} description 69 | */ 70 | function date_jsonSchema(name) { 71 | const result = simpleType_jsonSchema.call(this, name); 72 | 73 | result.type = 'string'; 74 | result.format = 'date-time'; 75 | 76 | return checkNullable(result); 77 | } 78 | 79 | /** 80 | * array_jsonSchema - returns jsonSchema for array parameters 81 | * 82 | * @memberof mongoose.Schema.Types.SchemaArray 83 | * @memberof mongoose.Schema.Types.DocumentArray 84 | * 85 | * @param {String} name parameter name 86 | * @return {object} json schema 87 | */ 88 | function array_jsonSchema(name) { 89 | const result = {}; 90 | 91 | const itemName = `itemOf_${name}`; 92 | 93 | result.type = 'array'; 94 | if (this.options.required) result.__required = true; 95 | 96 | if (this.schema) { 97 | result.items = this.schema.jsonSchema(itemName); 98 | } else { 99 | result.items = this.caster.jsonSchema(itemName); 100 | } 101 | 102 | __processOptions(result, extendOptions(this)); 103 | delete result.items.__required; 104 | 105 | return checkNullable(result); 106 | } 107 | 108 | /** 109 | * mixed_jsonSchema - returns jsonSchema for Mixed parameter 110 | * 111 | * @memberof mongoose.Schema.Types.Mixed 112 | * 113 | * @param {String} name parameter name 114 | * @return {object} json schema 115 | */ 116 | function mixed_jsonSchema(name) { 117 | const result = __describe(name, this.options.type); 118 | __processOptions(result, extendOptions(this)); 119 | 120 | // support for monggose@8.x.x 121 | if (result.type === 'schemamixed') delete result.type; 122 | 123 | return checkNullable(result); 124 | } 125 | 126 | const transformMatch = match => ( 127 | match instanceof RegExp 128 | ? match.toString().split('/').slice(1, -1).join('/') 129 | : match.toString() 130 | ); 131 | 132 | function __processOptions(t, type) { 133 | t.__required = !!readConstraint(type.required); 134 | if (type.enum) { 135 | if (type.enum.slice) t.enum = type.enum.slice(); 136 | else if (type.enum.values) t.enum = type.enum.values; 137 | } 138 | if (type.ref) t['x-ref'] = type.ref; 139 | if (type.min != null) t.minimum = readConstraint(type.min); 140 | if (type.max != null) t.maximum = readConstraint(type.max); 141 | if (type.minLength != null) t.minLength = readConstraint(type.minLength); 142 | if (type.maxLength != null) t.maxLength = readConstraint(type.maxLength); 143 | if (type.minlength != null) t.minLength = readConstraint(type.minlength); 144 | if (type.maxlength != null) t.maxLength = readConstraint(type.maxlength); 145 | if (type.examples != null) t.examples = type.examples; 146 | if (type.match != null) t.pattern = transformMatch(readConstraint(type.match)); 147 | if (type.default !== undefined) t.default = type.default; 148 | 149 | t.description = type.description || type.descr || type.ref && `Refers to ${type.ref}`; 150 | if (!t.description) delete t.description; 151 | 152 | if (!t.title && type.title) t.title = type.title; 153 | 154 | const customFieldsMapping = config.get(FIELDS_MAPPING); 155 | 156 | Object.assign(t, customFieldsMapping(type)); 157 | 158 | return t; 159 | } 160 | 161 | function __describe(name, type) { 162 | if (!type) return {}; 163 | 164 | if (type.jsonSchema instanceof Function) return type.jsonSchema(name); 165 | 166 | if (type.__buildingSchema) { 167 | type.__jsonSchemaId = type.__jsonSchemaId 168 | || `#subschema_${++__describe.__jsonSchemaIdCounter}`; 169 | return { $ref: type.__jsonSchemaId }; 170 | } 171 | 172 | if (type instanceof Array) { 173 | type.__buildingSchema = true; 174 | const t = { 175 | type: 'array', 176 | items: __describe(name && (`itemOf_${name}`), type[0]), 177 | }; 178 | delete type.__buildingSchema; 179 | delete t.items.__required; 180 | return t; 181 | } 182 | 183 | if (type === Date) { 184 | return { 185 | type: 'string', 186 | format: 'date-time', 187 | }; 188 | } 189 | 190 | if (type instanceof Function) { 191 | type = type.name.toLowerCase(); 192 | if (type === 'objectid') { 193 | return { 194 | type: 'string', 195 | pattern: '^[0-9a-fA-F]{24}$', 196 | }; 197 | } if (type === 'mixed') { 198 | return { }; 199 | } 200 | return { 201 | type, 202 | }; 203 | } 204 | 205 | if (type.type) { 206 | type.__buildingSchema = true; 207 | const ts = __describe(name, type.type); 208 | delete type.__buildingSchema; 209 | __processOptions(ts, type); 210 | return checkNullable(ts); 211 | } 212 | 213 | if (type.constructor.name !== 'Object') { 214 | return { 215 | type: type.constructor.name.toLowerCase(), 216 | }; 217 | } 218 | 219 | const result = { 220 | type: 'object', 221 | properties: {}, 222 | required: [], 223 | }; 224 | 225 | result.title = name; 226 | 227 | const props = Object.keys(type); 228 | type.__buildingSchema = true; 229 | props.forEach(p => { 230 | result.properties[p] = __describe(p, type[p]); 231 | if (result.properties[p].__required) { 232 | result.required.push(p); 233 | } 234 | delete result.properties[p].__required; 235 | }); 236 | delete type.__buildingSchema; 237 | if (result.required.length === 0) delete result.required; 238 | return result; 239 | } 240 | 241 | __describe.__jsonSchemaIdCounter = 0; 242 | 243 | function checkNullable(typeDef) { 244 | if (typeDef.default === null) { 245 | typeDef.type = [typeDef.type, 'null']; 246 | } 247 | 248 | return typeDef; 249 | } 250 | 251 | function isRequired(options) { 252 | const { required } = options; 253 | return required === true || (Array.isArray(required) && required[0] === true); 254 | } 255 | 256 | function extendOptions(mongooseTypeDef) { 257 | return { 258 | ...mongooseTypeDef.options, 259 | required: isRequired(mongooseTypeDef.options), 260 | }; 261 | } 262 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongoose-schema-jsonschema", 3 | "version": "3.0.2", 4 | "description": "Mongoose extension that allows to build json schema for mongoose models, schemas and queries", 5 | "main": "index.js", 6 | "scripts": { 7 | "coveralls": "./coveralls.sh", 8 | "test": "jest", 9 | "lint": "eslint ." 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/DScheglov/mongoose-schema-jsonschema.git" 14 | }, 15 | "keywords": [ 16 | "jsonschema", 17 | "json", 18 | "schema", 19 | "mongoose", 20 | "model", 21 | "query", 22 | "json schema", 23 | "build", 24 | "create" 25 | ], 26 | "author": "Dmitry Scheglov ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/DScheglov/mongoose-schema-jsonschema/issues" 30 | }, 31 | "homepage": "https://github.com/DScheglov/mongoose-schema-jsonschema#readme", 32 | "devDependencies": { 33 | "ajv": "^6.12.0", 34 | "coveralls": "^3.0.2", 35 | "eslint": "^7.32.0", 36 | "eslint-config-airbnb-base": "^14.2.1", 37 | "eslint-plugin-import": "^2.24.2", 38 | "eslint-plugin-jest": "^24.4.0", 39 | "jest": "^27.1.0", 40 | "jsonschema": "^1.2.2" 41 | }, 42 | "peerDependencies": { 43 | "mongoose": ">=5.0.0 <9.0.0" 44 | }, 45 | "dependencies": { 46 | "pluralize": "^8.0.0" 47 | }, 48 | "publishConfig": { 49 | "registry": "https://registry.npmjs.org" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /samples/1.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const mongoose = require('mongoose'); 4 | require('../index')(mongoose); 5 | 6 | const { Schema } = mongoose; 7 | 8 | const BookSchema = new Schema({ 9 | title: { type: String, required: true }, 10 | year: Number, 11 | author: { type: String, required: true }, 12 | }); 13 | 14 | const jsonSchema = BookSchema.jsonSchema(); 15 | 16 | console.dir(jsonSchema, { depth: null }); 17 | -------------------------------------------------------------------------------- /samples/2.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const mongoose = require('mongoose'); 3 | require('../index')(mongoose); 4 | 5 | const { Schema } = mongoose; 6 | 7 | const BookSchema = new Schema({ 8 | title: { type: String, required: true }, 9 | year: Number, 10 | author: { type: Schema.Types.ObjectId, required: true, ref: 'Person' }, 11 | }); 12 | 13 | const PersonSchema = new Schema({ 14 | firstName: { type: String, required: true }, 15 | lastName: { type: String, required: true }, 16 | dateOfBirth: Date, 17 | }); 18 | 19 | const Book = mongoose.model('Book', BookSchema); 20 | mongoose.model('Person', PersonSchema); 21 | 22 | console.dir(Book.jsonSchema('title year'), { depth: null }); 23 | console.dir(Book.jsonSchema('', 'author'), { depth: null }); 24 | -------------------------------------------------------------------------------- /samples/3.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const mongoose = require('mongoose'); 4 | require('../index')(mongoose); 5 | 6 | const { Schema } = mongoose; 7 | 8 | const BookSchema = new Schema({ 9 | title: { type: String, required: true }, 10 | year: Number, 11 | author: { type: Schema.Types.ObjectId, required: true, ref: 'Person' }, 12 | }); 13 | 14 | const Book = mongoose.model('Book', BookSchema); 15 | const Q = Book.find().select('title').limit(5); 16 | 17 | console.dir(Q.jsonSchema(), { depth: null }); 18 | -------------------------------------------------------------------------------- /samples/4.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const { Schema } = require('../index')(); 3 | 4 | const BookSchema = new Schema({ 5 | title: { type: String, required: true }, 6 | year: Number, 7 | author: { type: Schema.Types.ObjectId, required: true, ref: 'Person' }, 8 | }); 9 | 10 | BookSchema.add({ 11 | isbn: { type: String, required: true }, 12 | }); 13 | 14 | BookSchema.path('year').required(true); 15 | 16 | console.dir(BookSchema.jsonSchema(), { depth: null }); 17 | 18 | console.dir(BookSchema); 19 | -------------------------------------------------------------------------------- /samples/5.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const mongoose = require('../index')(); 3 | const config = require('../config'); 4 | 5 | config({ 6 | fieldOptionsMapping: { 7 | notes: 'x-notes', 8 | }, 9 | }); 10 | 11 | const { Schema } = mongoose; 12 | 13 | const BookSchema = new Schema({ 14 | title: { type: String, required: true, notes: 'Book Title' }, 15 | year: Number, 16 | author: { type: String, required: true }, 17 | }); 18 | 19 | console.dir(BookSchema.jsonSchema(), { depth: null }); 20 | 21 | config({ 22 | fieldOptionsMapping: [], 23 | forceRebuild: true, 24 | }); 25 | 26 | console.dir(BookSchema.jsonSchema(), { depth: null }); 27 | 28 | config({ forceRebuild: false }); 29 | -------------------------------------------------------------------------------- /samples/6.js: -------------------------------------------------------------------------------- 1 | const { Schema } = require('../index')(); 2 | 3 | const BookSchema = new Schema({ 4 | title: { type: String, required: true, notes: 'Book Title' }, 5 | year: Number, 6 | author: { type: String, required: true }, 7 | comments: { 8 | type: Map, 9 | of: new Schema({ 10 | createdAt: { type: Number, required: true }, 11 | author: { type: String, required: true }, 12 | body: String, 13 | }, { _id: false }), 14 | }, 15 | }); 16 | 17 | // eslint-disable-next-line no-console 18 | console.dir(BookSchema.jsonSchema(), { depth: null }); 19 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -f ./coveralls.sh ]; then 4 | echo coveralls.sh 5 | ./coveralls.sh 6 | else 7 | jest && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage 8 | fi 9 | -------------------------------------------------------------------------------- /test/models/book.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const { Schema } = mongoose; 4 | 5 | const BookSchema = new Schema({ 6 | title: { type: String, required: true, index: true }, 7 | year: { type: Number, required: true, index: true }, 8 | author: { 9 | type: [{ type: Schema.Types.ObjectId, required: true, ref: 'Person' }], 10 | index: true, 11 | required: true, 12 | }, 13 | comment: [{ 14 | body: String, 15 | editor: { type: Schema.Types.ObjectId, required: true, ref: 'Person' }, 16 | }], 17 | official: { 18 | slogan: String, 19 | announcement: String, 20 | }, 21 | publisher: { type: Schema.Types.ObjectId, required: true, ref: 'Person' }, 22 | description: String, 23 | }); 24 | 25 | module.exports = mongoose.model('Book', BookSchema); 26 | -------------------------------------------------------------------------------- /test/models/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Person: require('./person'), 3 | Book: require('./book'), 4 | }; 5 | -------------------------------------------------------------------------------- /test/models/person.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const { Schema } = mongoose; 4 | 5 | const PersonSchema = new Schema({ 6 | firstName: { type: String, required: true }, 7 | lastName: { type: String, required: true }, 8 | email: { 9 | type: String, required: true, index: true, unique: true, 10 | }, 11 | isPoet: { type: Boolean, default: false }, 12 | }); 13 | 14 | PersonSchema.virtual('fullName').set(function setFullName(v) { 15 | const parts = v.split(/\s/); 16 | this.firtsName = parts.shift() || ''; 17 | this.lastName = parts.join(' ') || ''; 18 | return this.get('fullName'); 19 | }).get(function getFullName() { 20 | return `${this.firstName} ${this.lastName}`; 21 | }); 22 | 23 | PersonSchema.path('email').set(v => v.toLowerCase()); 24 | 25 | PersonSchema.methods.toUpperCase = function toLowerCase(req, callback) { 26 | this.firstName = this.firstName.toUpperCase(); 27 | this.lastName = this.lastName.toUpperCase(); 28 | this.save(err => { 29 | if (err) return callback(err, null); 30 | return callback(null, { status: 'Ok' }); 31 | }); 32 | }; 33 | 34 | PersonSchema.methods.Reverse = function Reverse(req, callback) { 35 | this.firstName = this.firstName.split('').reverse().join(''); 36 | this.lastName = this.lastName.split('').reverse().join(''); 37 | this.save(err => { 38 | if (err) return callback(err, null); 39 | return callback(null, { status: 'Ok' }); 40 | }); 41 | }; 42 | 43 | PersonSchema.statics.emailList = function emailList(req, callback) { 44 | this.find({}, { email: true }).sort('email').exec((err, results) => { 45 | if (err) return callback(err, null); 46 | return callback(null, results.map(p => p.email)); 47 | }); 48 | }; 49 | 50 | module.exports = mongoose.model('Person', PersonSchema); 51 | -------------------------------------------------------------------------------- /test/models/ugly.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const { Schema } = mongoose; 4 | 5 | const UglySchema = new Schema({ 6 | title: { type: String, required: true, index: true }, 7 | year: { type: Number, required: true, index: true }, 8 | publisher: { type: Schema.Types.ObjectId, required: true, ref: 'UnExistingModel' }, 9 | description: String, 10 | }); 11 | 12 | module.exports = mongoose.model('Ugly', UglySchema); 13 | -------------------------------------------------------------------------------- /test/suites/ajv-validation.test.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('../../index')(require('mongoose')); 2 | const Ajv = require('ajv'); 3 | const assert = require('assert'); 4 | 5 | const models = require('../models'); 6 | 7 | describe('Validation: schema.jsonSchema()', () => { 8 | it('should build schema and validate numbers', () => { 9 | const mSchema = new mongoose.Schema({ 10 | n: { type: Number, min: 0, max: 10 }, 11 | }); 12 | 13 | const jsonSchema = mSchema.jsonSchema(); 14 | 15 | assert.deepEqual(jsonSchema, { 16 | type: 'object', 17 | properties: { 18 | n: { 19 | type: 'number', 20 | minimum: 0, 21 | maximum: 10, 22 | }, 23 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 24 | }, 25 | }); 26 | 27 | const ajv = new Ajv(); 28 | const isValid = data => ajv.validate(jsonSchema, data); 29 | 30 | assert.ok(isValid({ n: 3 })); 31 | assert.ok(isValid({ n: 0 })); 32 | assert.ok(isValid({ n: 10 })); 33 | assert.ok(!isValid({ n: -1 })); 34 | assert.ok(!isValid({ n: 13 })); 35 | assert.ok(!isValid({ n: 'a' })); 36 | assert.ok(isValid({ })); 37 | }); 38 | 39 | it('should build schema and validate strings by length', () => { 40 | const mSchema = new mongoose.Schema({ 41 | s: { type: String, minLength: 3, maxLength: 5 }, 42 | }); 43 | 44 | const jsonSchema = mSchema.jsonSchema(); 45 | 46 | assert.deepEqual(jsonSchema, { 47 | type: 'object', 48 | properties: { 49 | s: { 50 | type: 'string', 51 | minLength: 3, 52 | maxLength: 5, 53 | }, 54 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 55 | }, 56 | }); 57 | 58 | const ajv = new Ajv(); 59 | const isValid = data => ajv.validate(jsonSchema, data); 60 | 61 | assert.ok(isValid({ s: 'abc' })); 62 | assert.ok(isValid({ s: 'abcd' })); 63 | assert.ok(isValid({ s: 'abcde' })); 64 | assert.ok(!isValid({ s: 'ab' })); 65 | assert.ok(!isValid({ s: '' })); 66 | assert.ok(!isValid({ s: 'abcdef' })); 67 | assert.ok(!isValid({ s: new Date() })); 68 | assert.ok(isValid({ })); 69 | }); 70 | 71 | it('should build schema and validate strings with enum', () => { 72 | const mSchema = new mongoose.Schema({ 73 | s: { type: String, enum: ['abc', 'bac', 'cab'] }, 74 | }); 75 | 76 | const jsonSchema = mSchema.jsonSchema(); 77 | 78 | assert.deepEqual(jsonSchema, { 79 | type: 'object', 80 | properties: { 81 | s: { 82 | type: 'string', 83 | enum: ['abc', 'bac', 'cab'], 84 | }, 85 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 86 | }, 87 | }); 88 | 89 | const ajv = new Ajv(); 90 | const isValid = data => ajv.validate(jsonSchema, data); 91 | 92 | assert.ok(isValid({ s: 'abc' })); 93 | assert.ok(isValid({ s: 'bac' })); 94 | assert.ok(isValid({ s: 'cab' })); 95 | assert.ok(!isValid({ s: 'bca' })); 96 | assert.ok(!isValid({ s: 'acb' })); 97 | assert.ok(!isValid({ s: 123 })); 98 | assert.ok(!isValid({ s: '' })); 99 | }); 100 | 101 | it('should build schema and validate strings with enum and error message', () => { 102 | const mSchema = new mongoose.Schema({ 103 | s: { 104 | type: String, 105 | enum: { 106 | values: ['1', '2', '3'], 107 | message: '{VALUE} is not supported', 108 | }, 109 | }, 110 | }); 111 | 112 | const jsonSchema = mSchema.jsonSchema(); 113 | 114 | assert.deepEqual(jsonSchema, { 115 | type: 'object', 116 | properties: { 117 | s: { 118 | type: 'string', 119 | enum: ['1', '2', '3'], 120 | }, 121 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 122 | }, 123 | }); 124 | 125 | const ajv = new Ajv(); 126 | const isValid = data => ajv.validate(jsonSchema, data); 127 | 128 | assert.ok(isValid({ s: '1' })); 129 | assert.ok(isValid({ s: '2' })); 130 | assert.ok(isValid({ s: '3' })); 131 | assert.ok(!isValid({ s: '4' })); 132 | assert.ok(!isValid({ s: '0' })); 133 | assert.ok(!isValid({ s: 1 })); 134 | assert.ok(!isValid({ s: '' })); 135 | }); 136 | 137 | it('should build schema and validate strings with regExp', () => { 138 | const mSchema = new mongoose.Schema({ 139 | s: { type: String, match: /^(abc|bac|cab)$/ }, 140 | }); 141 | 142 | const jsonSchema = mSchema.jsonSchema(); 143 | 144 | assert.deepEqual(jsonSchema, { 145 | type: 'object', 146 | properties: { 147 | s: { 148 | type: 'string', 149 | pattern: '^(abc|bac|cab)$', 150 | }, 151 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 152 | }, 153 | }); 154 | 155 | const ajv = new Ajv(); 156 | const isValid = data => ajv.validate(jsonSchema, data); 157 | 158 | assert.ok(!isValid({ s: '(abc|bac|cab)' })); 159 | assert.ok(isValid({ s: 'abc' })); 160 | assert.ok(!isValid({ s: 'ABC' })); 161 | assert.ok(!isValid({ s: 'cba' })); 162 | assert.ok(!isValid({ s: '' })); 163 | assert.ok(!isValid({ s: 12 })); 164 | assert.ok(!isValid({ _id: '^[0-9a-fA-F]{24}$' })); 165 | assert.ok(!isValid({ _id: 'Hello world' })); 166 | assert.ok(isValid({ _id: '564e0da0105badc887ef1d3e' })); 167 | }); 168 | 169 | it('should build schema and validate strings with regExp (as string constant)', () => { 170 | const mSchema = new mongoose.Schema({ 171 | s: { type: String, match: 'Hello world!' }, 172 | }); 173 | 174 | const jsonSchema = mSchema.jsonSchema(); 175 | 176 | assert.deepEqual(jsonSchema, { 177 | type: 'object', 178 | properties: { 179 | s: { 180 | type: 'string', 181 | pattern: 'Hello world!', 182 | }, 183 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 184 | }, 185 | }); 186 | 187 | const ajv = new Ajv(); 188 | const isValid = data => ajv.validate(jsonSchema, data); 189 | 190 | assert.ok(isValid({ s: 'Hello world!' })); 191 | assert.ok(!isValid({ s: '(abc|bac|cab)' })); 192 | assert.ok(!isValid({ s: 'abc' })); 193 | assert.ok(!isValid({ s: 'ABC' })); 194 | assert.ok(!isValid({ s: 'cba' })); 195 | assert.ok(!isValid({ s: '' })); 196 | assert.ok(!isValid({ s: 12 })); 197 | assert.ok(!isValid({ _id: '^[0-9a-fA-F]{24}$' })); 198 | assert.ok(!isValid({ _id: 'Hello world!' })); 199 | assert.ok(isValid({ _id: '564e0da0105badc887ef1d3e' })); 200 | }); 201 | 202 | it('should build schema and validate arrays with minItems constraint', () => { 203 | const mSchema = mongoose.Schema({ 204 | a: [{ 205 | type: Number, 206 | required: true, 207 | }], 208 | }); 209 | 210 | const jsonSchema = mSchema.jsonSchema(); 211 | 212 | assert.deepEqual(jsonSchema, { 213 | type: 'object', 214 | properties: { 215 | a: { 216 | type: 'array', 217 | items: { type: 'number' }, 218 | }, 219 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 220 | }, 221 | }); 222 | 223 | const ajv = new Ajv(); 224 | const isValid = data => ajv.validate(jsonSchema, data); 225 | 226 | assert.ok(isValid({ a: [0, 1] })); 227 | assert.ok(isValid({ a: [0] })); 228 | assert.ok(isValid({ })); 229 | assert.ok(isValid({ a: [] })); 230 | assert.ok(!isValid({ a: [0, 1, 'a'] })); 231 | }); 232 | 233 | it('should build schema and validate mixed', () => { 234 | const mSchema = new mongoose.Schema({ 235 | m: { type: mongoose.Schema.Types.Mixed, required: true, default: {} }, 236 | }); 237 | 238 | const jsonSchema = mSchema.jsonSchema(); 239 | 240 | assert.deepEqual(jsonSchema, { 241 | type: 'object', 242 | properties: { 243 | m: { }, 244 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 245 | }, 246 | required: ['m'], 247 | }); 248 | 249 | const ajv = new Ajv(); 250 | const isValid = data => ajv.validate(jsonSchema, data); 251 | 252 | assert.ok(isValid({ m: 3 })); 253 | assert.ok(isValid({ m: null })); 254 | assert.ok(isValid({ m: { } })); 255 | assert.ok(isValid({ m: 'Hello world' })); 256 | assert.ok(isValid({ m: '' })); 257 | assert.ok(isValid({ m: true })); 258 | assert.ok(isValid({ m: false })); 259 | 260 | assert.ok(!isValid({ })); 261 | assert.ok(!isValid({ s: '13234' })); 262 | }); 263 | 264 | it('should build schema and validate mixed with description', () => { 265 | const mSchema = new mongoose.Schema({ 266 | m: { 267 | type: mongoose.Schema.Types.Mixed, 268 | required: true, 269 | description: 'Some mixed content here', 270 | default: {}, 271 | }, 272 | }); 273 | 274 | const jsonSchema = mSchema.jsonSchema(); 275 | 276 | assert.deepEqual(jsonSchema, { 277 | type: 'object', 278 | properties: { 279 | m: { 280 | description: 'Some mixed content here', 281 | }, 282 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 283 | }, 284 | required: ['m'], 285 | }); 286 | 287 | const ajv = new Ajv(); 288 | const isValid = data => ajv.validate(jsonSchema, data); 289 | 290 | assert.ok(isValid({ m: 3 })); 291 | assert.ok(isValid({ m: null })); 292 | assert.ok(isValid({ m: { } })); 293 | assert.ok(isValid({ m: 'Hello world' })); 294 | assert.ok(isValid({ m: '' })); 295 | assert.ok(isValid({ m: true })); 296 | assert.ok(isValid({ m: false })); 297 | 298 | assert.ok(!isValid({ })); 299 | assert.ok(!isValid({ s: '13234' })); 300 | }); 301 | 302 | it('should build schema and validate mixed with description and title', () => { 303 | const mSchema = new mongoose.Schema({ 304 | m: { 305 | type: mongoose.Schema.Types.Mixed, 306 | title: 'MegaField', 307 | description: 'Some mixed content here', 308 | default: {}, 309 | }, 310 | }); 311 | 312 | const jsonSchema = mSchema.jsonSchema(); 313 | 314 | assert.deepEqual(jsonSchema, { 315 | type: 'object', 316 | properties: { 317 | m: { 318 | title: 'MegaField', 319 | description: 'Some mixed content here', 320 | }, 321 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 322 | }, 323 | }); 324 | 325 | const ajv = new Ajv(); 326 | const isValid = data => ajv.validate(jsonSchema, data); 327 | 328 | assert.ok(isValid({ m: 3 })); 329 | assert.ok(isValid({ m: null })); 330 | assert.ok(isValid({ m: { } })); 331 | assert.ok(isValid({ m: 'Hello world' })); 332 | assert.ok(isValid({ m: '' })); 333 | assert.ok(isValid({ m: true })); 334 | assert.ok(isValid({ m: false })); 335 | 336 | assert.ok(isValid({ })); 337 | assert.ok(isValid({ s: '13234' })); 338 | }); 339 | 340 | it('should work with nullable types', () => { 341 | const mSchema = new mongoose.Schema({ 342 | x: { type: Number, default: null }, 343 | y: { type: Number, default: 1 }, 344 | }); 345 | 346 | const jsonSchema = mSchema.jsonSchema(); 347 | 348 | assert.deepEqual(jsonSchema, { 349 | type: 'object', 350 | properties: { 351 | x: { type: ['number', 'null'], default: null }, 352 | y: { type: 'number', default: 1 }, 353 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 354 | }, 355 | }); 356 | 357 | const ajv = new Ajv(); 358 | const isValid = data => ajv.validate(jsonSchema, data); 359 | 360 | assert.ok(isValid({ y: 3 })); 361 | assert.ok(isValid({ y: 3, x: null })); 362 | assert.ok(isValid({ y: 3, x: 2 })); 363 | assert.ok(isValid({ })); 364 | assert.ok(isValid({ x: null })); 365 | 366 | assert.ok(!isValid({ y: null })); 367 | }); 368 | 369 | it('should build schema and validate Map types', () => { 370 | const mSchema = new mongoose.Schema({ 371 | m: { type: Map, required: true }, 372 | }, { _id: false }); 373 | 374 | const jsonSchema = mSchema.jsonSchema(); 375 | 376 | assert.deepEqual(jsonSchema, { 377 | type: 'object', 378 | properties: { 379 | m: { 380 | type: 'object', 381 | additionalProperties: true, 382 | }, 383 | }, 384 | required: ['m'], 385 | }); 386 | 387 | const ajv = new Ajv(); 388 | const isValid = data => ajv.validate(jsonSchema, data); 389 | 390 | assert.ok(isValid({ m: { x: 1, y: 'string' } })); 391 | assert.ok(isValid({ m: { } })); 392 | 393 | assert.ok(!isValid({ y: null })); 394 | }); 395 | 396 | it('should build schema and validate Map types (with subschema)', () => { 397 | const mSchema = new mongoose.Schema({ 398 | m: { type: Map, required: true, of: { type: String } }, 399 | }, { _id: false }); 400 | 401 | const jsonSchema = mSchema.jsonSchema(); 402 | 403 | assert.deepEqual(jsonSchema, { 404 | type: 'object', 405 | properties: { 406 | m: { 407 | type: 'object', 408 | additionalProperties: { 409 | type: 'string', 410 | }, 411 | }, 412 | }, 413 | required: ['m'], 414 | }); 415 | 416 | const ajv = new Ajv(); 417 | const isValid = data => ajv.validate(jsonSchema, data); 418 | 419 | assert.ok(isValid({ m: { x: 'x', y: 'y' } })); 420 | assert.ok(isValid({ m: { } })); 421 | 422 | assert.ok(!isValid({ m: { x: 1, y: 'string' } })); 423 | assert.ok(!isValid({ y: null })); 424 | }); 425 | }); 426 | 427 | describe('Validation: model.jsonSchema()', () => { 428 | it('should process flat schema and -- validate correct entity', () => { 429 | const jsonSchema = models.Person.jsonSchema(); 430 | 431 | const validPerson = { 432 | firstName: 'John', 433 | lastName: 'Smith', 434 | email: 'john.smith@mail.net', 435 | }; 436 | const ajv = new Ajv(); 437 | const aPerson = new models.Person(validPerson); 438 | assert.ok(!aPerson.validateSync()); 439 | assert.ok(ajv.validate(jsonSchema, validPerson)); 440 | }); 441 | 442 | it('should process flat schema and -- mark invalid entity (wrong field type)', () => { 443 | const jsonSchema = models.Person.jsonSchema(); 444 | 445 | const invalidPerson = { 446 | firstName: 'John', 447 | lastName: 'Smith', 448 | email: 12, 449 | }; 450 | const ajv = new Ajv(); 451 | const aPerson = new models.Person(invalidPerson); 452 | assert.ok(aPerson.validateSync()); 453 | assert.ok(!ajv.validate(jsonSchema, invalidPerson)); 454 | }); 455 | 456 | it('should process flat schema and -- mark invalid entity (required field missed)', () => { 457 | const jsonSchema = models.Person.jsonSchema(); 458 | 459 | const invalidPerson = { 460 | lastName: 'Smith', 461 | email: 'john.smith@mail.com', 462 | }; 463 | const ajv = new Ajv(); 464 | const aPerson = new models.Person(invalidPerson); 465 | assert.ok(aPerson.validateSync()); 466 | assert.ok(!ajv.validate(jsonSchema, invalidPerson)); 467 | }); 468 | }); 469 | -------------------------------------------------------------------------------- /test/suites/array-of-array.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bug Fix for: https://github.com/DScheglov/mongoose-schema-jsonschema/issues/37 3 | * 4 | * Based on the sample taken from the issue 5 | */ 6 | const mongoose = require('../../index')(require('mongoose')); 7 | 8 | describe('array of array', () => { 9 | const VariableSchema = mongoose.Schema({ 10 | name: { type: String, required: true }, 11 | value: {}, 12 | }); 13 | 14 | const ElementPathSchema = new mongoose.Schema({ 15 | paths: { 16 | paths: { 17 | type: [[VariableSchema]], 18 | validate: { 19 | validator: function validateTemplate(arrayOfVariables) { 20 | return arrayOfVariables.every(variables => VariableSchema.statics.validateTemplate(variables, 'elementQuery')); 21 | }, 22 | message: 'elementPath.paths not valid', 23 | }, 24 | }, 25 | }, 26 | }); 27 | 28 | it('builds the json schema', () => { 29 | expect(ElementPathSchema.jsonSchema()).toEqual({ 30 | type: 'object', 31 | properties: { 32 | paths: { 33 | title: 'paths', 34 | type: 'object', 35 | properties: { 36 | paths: { 37 | type: 'array', 38 | items: { 39 | type: 'array', 40 | items: { 41 | title: 'itemOf_itemOf_paths', 42 | type: 'object', 43 | properties: { 44 | name: { type: 'string' }, 45 | value: {}, 46 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 47 | }, 48 | required: ['name'], 49 | }, 50 | }, 51 | }, 52 | }, 53 | }, 54 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 55 | }, 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/suites/circular-refs.test.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('../../index')(require('mongoose')); 2 | const assert = require('assert'); 3 | 4 | describe('Circular refs: Schema.jsonSchema()', () => { 5 | it('should replace schema with $ref to it', () => { 6 | const mSchema = new mongoose.Schema({ 7 | title: String, 8 | }); 9 | mSchema.add({ child: mSchema }); 10 | 11 | const jsonSchema = mSchema.jsonSchema(); 12 | 13 | assert.deepEqual(jsonSchema, { 14 | id: '#schema-1', 15 | type: 'object', 16 | properties: { 17 | title: { type: 'string' }, 18 | child: { $ref: '#schema-1' }, 19 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 20 | }, 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/suites/custom-field-options-mapping.test.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('../../index')(require('mongoose')); 2 | const config = require('../../config'); 3 | 4 | const { Schema } = mongoose; 5 | 6 | describe('Custom Field Options Mapping', () => { 7 | it('should be possible to configure mapping', () => { 8 | const description = 'Some Descriptions'; 9 | const mSchema = new Schema({ 10 | s: { 11 | type: String, 12 | required: true, 13 | description, 14 | }, 15 | }); 16 | 17 | config({ 18 | fieldOptionsMapping: { 19 | description: 'x-notes', 20 | }, 21 | }); 22 | 23 | expect(mSchema.jsonSchema()).toEqual({ 24 | type: 'object', 25 | properties: { 26 | s: { 27 | type: 'string', 28 | description, 29 | 'x-notes': description, 30 | }, 31 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 32 | }, 33 | required: ['s'], 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/suites/description.test.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('../../index')(require('mongoose')); 2 | const isV6pl = require('mongoose/package.json').version > '6'; 3 | const assert = require('assert'); 4 | 5 | describe('Description: Schema.jsonSchema()', () => { 6 | it('should add description when it is specified (as description)', () => { 7 | const aDescrription = 'Just a string field'; 8 | 9 | const mSchema = new mongoose.Schema({ 10 | s: { 11 | type: String, 12 | required: true, 13 | description: aDescrription, 14 | }, 15 | }); 16 | 17 | const jsonSchema = mSchema.jsonSchema(); 18 | 19 | assert.equal(jsonSchema.properties.s.description, aDescrription); 20 | }); 21 | 22 | it('should add description when it is specified (as descr)', () => { 23 | const aDescrription = 'Just a string field'; 24 | 25 | const mSchema = new mongoose.Schema({ 26 | s: { 27 | type: String, 28 | required: true, 29 | descr: aDescrription, 30 | }, 31 | }); 32 | 33 | const jsonSchema = mSchema.jsonSchema(); 34 | 35 | assert.equal(jsonSchema.properties.s.description, aDescrription); 36 | }); 37 | 38 | it('should add title when it is specified', () => { 39 | const mSchema = new mongoose.Schema({ 40 | s: { 41 | title: 'S', 42 | type: String, 43 | required: true, 44 | }, 45 | }); 46 | 47 | const jsonSchema = mSchema.jsonSchema(); 48 | 49 | assert.equal(jsonSchema.properties.s.title, 'S'); 50 | }); 51 | 52 | it('should add title and description when they are specified', () => { 53 | const mSchema = new mongoose.Schema({ 54 | s: { 55 | title: 'S', 56 | type: mongoose.Schema.Types.Mixed, 57 | required: true, 58 | descr: 'mixed content', 59 | }, 60 | }, { _id: null }); 61 | 62 | const jsonSchema = mSchema.jsonSchema(); 63 | 64 | assert.deepEqual(jsonSchema, { 65 | type: 'object', 66 | properties: { 67 | s: { 68 | description: 'mixed content', 69 | title: 'S', 70 | }, 71 | }, 72 | required: ['s'], 73 | }); 74 | }); 75 | 76 | it('should add description for array', () => { 77 | const mSchema = new mongoose.Schema({ 78 | name: String, 79 | inputs: { 80 | type: [String], 81 | index: true, 82 | description: 'Information operated on by rule', 83 | }, 84 | outputs: { 85 | type: [String], 86 | description: 'Information produced by rule', 87 | }, 88 | }); 89 | 90 | const jsonSchema = mSchema.jsonSchema(); 91 | 92 | assert.deepEqual(jsonSchema, { 93 | type: 'object', 94 | properties: { 95 | name: { type: 'string' }, 96 | inputs: { 97 | type: 'array', 98 | items: { type: 'string' }, 99 | description: 'Information operated on by rule', 100 | }, 101 | outputs: { 102 | type: 'array', 103 | items: { type: 'string' }, 104 | description: 'Information produced by rule', 105 | }, 106 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 107 | }, 108 | }); 109 | }); 110 | 111 | it('should add examples when it is specified (as examples)', () => { 112 | const examples = ['Just a string field']; 113 | 114 | const mSchema = new mongoose.Schema({ 115 | s: { 116 | type: String, 117 | required: true, 118 | examples, 119 | }, 120 | }); 121 | 122 | const jsonSchema = mSchema.jsonSchema(); 123 | 124 | assert.deepEqual(jsonSchema.properties.s.examples, examples); 125 | }); 126 | 127 | it('should add examples for array', () => { 128 | const mSchema = new mongoose.Schema({ 129 | name: String, 130 | inputs: { 131 | type: [String], 132 | index: true, 133 | examples: [ 134 | ['a', 'b', 'c'], 135 | ['A', 'B', 'C'], 136 | ], 137 | }, 138 | outputs: { 139 | type: [String], 140 | examples: [ 141 | ['z', 'y', 'x'], 142 | ['Z', 'Y', 'X'], 143 | ], 144 | }, 145 | }); 146 | 147 | const jsonSchema = mSchema.jsonSchema(); 148 | 149 | assert.deepEqual(jsonSchema, { 150 | type: 'object', 151 | properties: { 152 | name: { type: 'string' }, 153 | inputs: { 154 | type: 'array', 155 | items: { type: 'string' }, 156 | examples: [ 157 | ['a', 'b', 'c'], 158 | ['A', 'B', 'C'], 159 | ], 160 | }, 161 | outputs: { 162 | type: 'array', 163 | items: { type: 'string' }, 164 | examples: [ 165 | ['z', 'y', 'x'], 166 | ['Z', 'Y', 'X'], 167 | ], 168 | }, 169 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 170 | }, 171 | }); 172 | }); 173 | 174 | it('should add examples for an object', () => { 175 | const mSchema = new mongoose.Schema({ 176 | name: String, 177 | vector: { 178 | type: { x: Number, y: Number }, 179 | default: null, 180 | examples: [ 181 | { x: 1, y: 2 }, 182 | { x: 3, y: -1 }, 183 | null, 184 | ], 185 | }, 186 | }); 187 | 188 | const jsonSchema = mSchema.jsonSchema(); 189 | 190 | assert.deepEqual(jsonSchema, { 191 | type: 'object', 192 | properties: { 193 | name: { type: 'string' }, 194 | vector: { 195 | title: 'vector', 196 | type: ['object', 'null'], 197 | default: null, 198 | properties: { 199 | ...(isV6pl ? { _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' } } : undefined), 200 | x: { type: 'number' }, 201 | y: { type: 'number' }, 202 | }, 203 | examples: [ 204 | { x: 1, y: 2 }, 205 | { x: 3, y: -1 }, 206 | null, 207 | ], 208 | }, 209 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 210 | }, 211 | }); 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /test/suites/discriminators.test.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('../../index')(require('mongoose')); 2 | const assert = require('assert'); 3 | 4 | const Generic = mongoose.model('Generic', new mongoose.Schema({ 5 | title: String, 6 | value: String, 7 | })); 8 | 9 | const Tagged = Generic.discriminator('Tagged', new mongoose.Schema({ 10 | tags: [String], 11 | })); 12 | 13 | describe('Discriminators: Model.jsonSchema()', () => { 14 | it('should build schema for discriminator', () => { 15 | const jsonSchema = Tagged.jsonSchema(); 16 | assert.deepEqual(jsonSchema, { 17 | title: 'Tagged', 18 | type: 'object', 19 | properties: { 20 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 21 | title: { type: 'string' }, 22 | value: { type: 'string' }, 23 | tags: { type: 'array', items: { type: 'string' } }, 24 | __v: { type: 'number' }, 25 | __t: { type: 'string', default: 'Tagged' }, 26 | }, 27 | }); 28 | }); 29 | 30 | it('should consider the field selection', () => { 31 | const jsonSchema = Tagged.find({}, 'title tags -_id').jsonSchema(); 32 | assert.deepEqual(jsonSchema, { 33 | title: 'List of Taggeds', 34 | type: 'array', 35 | items: { 36 | type: 'object', 37 | properties: { 38 | title: { type: 'string' }, 39 | tags: { type: 'array', items: { type: 'string' } }, 40 | }, 41 | }, 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/suites/force-rebuild.test.js: -------------------------------------------------------------------------------- 1 | const { Schema } = require('../../index')(require('mongoose')); 2 | const config = require('../../config'); 3 | 4 | describe('Force rebuild', () => { 5 | it('should cache by default', () => { 6 | const description = 'Some Descriptions'; 7 | const mSchema = new Schema({ 8 | s: { 9 | type: String, 10 | required: true, 11 | description, 12 | }, 13 | }); 14 | 15 | const jsonSchema = mSchema.jsonSchema(); 16 | mSchema.add({ t: Number }); 17 | 18 | expect(mSchema.jsonSchema()).toEqual(jsonSchema); 19 | }); 20 | 21 | it('should be able to rebuild schema', () => { 22 | const description = 'Some Descriptions'; 23 | const mSchema = new Schema({ 24 | s: { 25 | type: String, 26 | required: true, 27 | description, 28 | }, 29 | }); 30 | 31 | const jsonSchema = mSchema.jsonSchema(); 32 | mSchema.add({ t: Number }); 33 | 34 | config({ forceRebuild: true }); 35 | 36 | const newJsonSchema = mSchema.jsonSchema(); 37 | expect(newJsonSchema).not.toEqual(jsonSchema); 38 | expect(newJsonSchema).toEqual({ 39 | type: 'object', 40 | properties: { 41 | s: { 42 | type: 'string', 43 | description, 44 | }, 45 | t: { type: 'number' }, 46 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 47 | }, 48 | required: ['s'], 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/suites/nullable-types.test.js: -------------------------------------------------------------------------------- 1 | const { Schema } = require('../../index')(require('mongoose')); 2 | const isV6pl = require('mongoose/package.json').version > '6'; 3 | const assert = require('assert'); 4 | 5 | describe('nullable: schema.jsonSchema', () => { 6 | it('should correctly translate all simmple types with default equals to null', () => { 7 | const mSchema = new Schema({ 8 | n: { type: Number, default: null }, 9 | s: { type: String, default: null }, 10 | d: { type: Date, default: null }, 11 | b: { type: Boolean, default: null }, 12 | u: { type: Schema.Types.ObjectId, default: null }, 13 | }); 14 | 15 | const jsonSchema = mSchema.jsonSchema('Sample'); 16 | 17 | assert.deepEqual(jsonSchema, { 18 | title: 'Sample', 19 | type: 'object', 20 | properties: { 21 | n: { type: ['number', 'null'], default: null }, 22 | s: { type: ['string', 'null'], default: null }, 23 | d: { type: ['string', 'null'], default: null, format: 'date-time' }, 24 | b: { type: ['boolean', 'null'], default: null }, 25 | u: { type: ['string', 'null'], default: null, pattern: '^[0-9a-fA-F]{24}$' }, 26 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 27 | }, 28 | }); 29 | }); 30 | 31 | it('should correctly translate many levels of nested Schemas with default nulls', () => { 32 | const mSchema1 = new Schema({ x: Number }, { id: false, _id: false }); 33 | const mSchema2 = new Schema({ y: Number, x: mSchema1 }, { id: false, _id: false }); 34 | const mSchema3 = new Schema({ z: Number, y: mSchema2 }, { id: false, _id: false }); 35 | const mSchema4 = new Schema({ 36 | t: Number, 37 | xyz: { 38 | type: { 39 | x: [{ 40 | type: mSchema1, 41 | required: true, 42 | }], 43 | y: { type: mSchema2, required: true }, 44 | z: { type: [mSchema3] }, 45 | t: { type: [{ x: Number, y: Number }], default: null }, 46 | any: { type: Schema.Types.Mixed, required: true }, 47 | }, 48 | default: null, 49 | }, 50 | }); 51 | 52 | const jsonSchema = mSchema4.jsonSchema('Sample'); 53 | 54 | assert.deepEqual(jsonSchema, { 55 | title: 'Sample', 56 | type: 'object', 57 | properties: { 58 | t: { type: 'number' }, 59 | xyz: { 60 | title: 'xyz', 61 | type: ['object', 'null'], 62 | default: null, 63 | properties: { 64 | ...(isV6pl ? { _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' } } : undefined), 65 | x: { 66 | type: 'array', 67 | items: { 68 | type: 'object', 69 | title: 'itemOf_x', 70 | properties: { 71 | x: { type: 'number' }, 72 | }, 73 | }, 74 | }, 75 | y: { 76 | type: 'object', 77 | title: 'y', 78 | properties: { 79 | y: { type: 'number' }, 80 | x: { 81 | type: 'object', 82 | title: 'x', 83 | properties: { 84 | x: { type: 'number' }, 85 | }, 86 | }, 87 | }, 88 | }, 89 | z: { 90 | type: 'array', 91 | items: { 92 | type: 'object', 93 | title: 'itemOf_z', 94 | properties: { 95 | z: { type: 'number' }, 96 | y: { 97 | type: 'object', 98 | title: 'y', 99 | properties: { 100 | y: { type: 'number' }, 101 | x: { 102 | type: 'object', 103 | title: 'x', 104 | properties: { 105 | x: { type: 'number' }, 106 | }, 107 | }, 108 | }, 109 | }, 110 | }, 111 | }, 112 | }, 113 | t: { 114 | type: ['array', 'null'], 115 | default: null, 116 | items: { 117 | type: 'object', 118 | title: 'itemOf_t', 119 | properties: { 120 | ...(isV6pl ? { _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' } } : undefined), 121 | x: { type: 'number' }, 122 | y: { type: 'number' }, 123 | }, 124 | }, 125 | }, 126 | any: { }, 127 | }, 128 | required: ['y', 'any'], 129 | }, 130 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 131 | }, 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /test/suites/population.test.js: -------------------------------------------------------------------------------- 1 | require('../../index')(require('mongoose')); 2 | const assert = require('assert'); 3 | 4 | const models = require('../models'); 5 | 6 | describe('Population: Model.jsonSchema()', () => { 7 | it('should build schema and populate child-field', () => { 8 | const jsonSchema = models.Book.jsonSchema('title publisher', 'publisher'); 9 | assert.deepEqual(jsonSchema, { 10 | title: 'Book', 11 | type: 'object', 12 | properties: { 13 | title: { type: 'string' }, 14 | publisher: { 15 | title: 'Person', 16 | type: 'object', 17 | properties: { 18 | firstName: { type: 'string' }, 19 | lastName: { type: 'string' }, 20 | email: { type: 'string' }, 21 | isPoet: { type: 'boolean', default: false }, 22 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 23 | __v: { type: 'number' }, 24 | }, 25 | 'x-ref': 'Person', 26 | description: 'Refers to Person', 27 | }, 28 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 29 | }, 30 | }); 31 | }); 32 | 33 | it('should build schema and populate child-field with selected fields', () => { 34 | const jsonSchema = models.Book.jsonSchema('title publisher', { 35 | path: 'publisher', 36 | select: 'email -_id', 37 | }); 38 | 39 | assert.deepEqual(jsonSchema, { 40 | title: 'Book', 41 | type: 'object', 42 | properties: { 43 | title: { type: 'string' }, 44 | publisher: { 45 | title: 'Person', 46 | type: 'object', 47 | properties: { 48 | email: { type: 'string' }, 49 | }, 50 | 'x-ref': 'Person', 51 | description: 'Refers to Person', 52 | }, 53 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 54 | }, 55 | }); 56 | }); 57 | 58 | it('should build schema and populate array item', () => { 59 | const jsonSchema = models.Book.jsonSchema('title author', 'author'); 60 | assert.deepEqual(jsonSchema, { 61 | title: 'Book', 62 | type: 'object', 63 | properties: { 64 | title: { type: 'string' }, 65 | author: { 66 | type: 'array', 67 | items: { 68 | title: 'Person', 69 | type: 'object', 70 | properties: { 71 | firstName: { type: 'string' }, 72 | lastName: { type: 'string' }, 73 | email: { type: 'string' }, 74 | isPoet: { type: 'boolean', default: false }, 75 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 76 | __v: { type: 'number' }, 77 | }, 78 | 'x-ref': 'Person', 79 | description: 'Refers to Person', 80 | }, 81 | }, 82 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 83 | }, 84 | }); 85 | }); 86 | 87 | it('should build schema and populate array item with selected fields', () => { 88 | const jsonSchema = models.Book.jsonSchema('title author', { 89 | path: 'author', 90 | select: 'firstName isPoet', 91 | }); 92 | assert.deepEqual(jsonSchema, { 93 | title: 'Book', 94 | type: 'object', 95 | properties: { 96 | title: { type: 'string' }, 97 | author: { 98 | type: 'array', 99 | items: { 100 | title: 'Person', 101 | type: 'object', 102 | properties: { 103 | firstName: { type: 'string' }, 104 | isPoet: { type: 'boolean', default: false }, 105 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 106 | }, 107 | 'x-ref': 'Person', 108 | description: 'Refers to Person', 109 | }, 110 | }, 111 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 112 | }, 113 | }); 114 | }); 115 | 116 | it('should build schema and should not populate unselected child-field', () => { 117 | const jsonSchema = models.Book.jsonSchema('title', 'publisher'); 118 | assert.deepEqual(jsonSchema, { 119 | title: 'Book', 120 | type: 'object', 121 | properties: { 122 | title: { type: 'string' }, 123 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 124 | }, 125 | }); 126 | }); 127 | 128 | it('should build schema and populate field of array item', () => { 129 | const jsonSchema = models.Book.jsonSchema('title author comment', 'author comment.editor'); 130 | 131 | assert.deepEqual(jsonSchema, { 132 | title: 'Book', 133 | type: 'object', 134 | properties: { 135 | title: { type: 'string' }, 136 | comment: { 137 | type: 'array', 138 | items: { 139 | title: 'itemOf_comment', 140 | type: 'object', 141 | properties: { 142 | body: { type: 'string' }, 143 | editor: { 144 | title: 'Person', 145 | type: 'object', 146 | properties: { 147 | firstName: { type: 'string' }, 148 | lastName: { type: 'string' }, 149 | email: { type: 'string' }, 150 | isPoet: { type: 'boolean', default: false }, 151 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 152 | __v: { type: 'number' }, 153 | }, 154 | 'x-ref': 'Person', 155 | description: 'Refers to Person', 156 | }, 157 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 158 | }, 159 | }, 160 | }, 161 | author: { 162 | type: 'array', 163 | items: { 164 | title: 'Person', 165 | type: 'object', 166 | properties: { 167 | firstName: { type: 'string' }, 168 | lastName: { type: 'string' }, 169 | email: { type: 'string' }, 170 | isPoet: { type: 'boolean', default: false }, 171 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 172 | __v: { type: 'number' }, 173 | }, 174 | 'x-ref': 'Person', 175 | description: 'Refers to Person', 176 | }, 177 | }, 178 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 179 | }, 180 | }); 181 | }); 182 | 183 | it('should populate before paths excluded', () => { 184 | const jsonSchema = models.Book.jsonSchema( 185 | '-publisher._id -publisher.__v', 186 | 'publisher', 187 | ); 188 | 189 | assert.deepEqual(jsonSchema, { 190 | title: 'Book', 191 | type: 'object', 192 | properties: { 193 | title: { type: 'string' }, 194 | year: { type: 'number' }, 195 | author: { 196 | type: 'array', 197 | items: { 198 | type: 'string', 199 | 'x-ref': 'Person', 200 | description: 'Refers to Person', 201 | pattern: '^[0-9a-fA-F]{24}$', 202 | }, 203 | }, 204 | comment: { 205 | type: 'array', 206 | items: { 207 | title: 'itemOf_comment', 208 | type: 'object', 209 | properties: { 210 | body: { type: 'string' }, 211 | editor: { 212 | type: 'string', 213 | 'x-ref': 'Person', 214 | description: 'Refers to Person', 215 | pattern: '^[0-9a-fA-F]{24}$', 216 | }, 217 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 218 | }, 219 | }, 220 | }, 221 | official: { 222 | title: 'official', 223 | type: 'object', 224 | properties: { slogan: { type: 'string' }, announcement: { type: 'string' } }, 225 | }, 226 | publisher: { 227 | title: 'Person', 228 | type: 'object', 229 | properties: { 230 | firstName: { type: 'string' }, 231 | lastName: { type: 'string' }, 232 | email: { type: 'string' }, 233 | isPoet: { type: 'boolean', default: false }, 234 | }, 235 | 'x-ref': 'Person', 236 | description: 'Refers to Person', 237 | }, 238 | description: { type: 'string' }, 239 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 240 | __v: { type: 'number' }, 241 | }, 242 | }); 243 | }); 244 | 245 | it('should not fail when populating not a field', () => { 246 | models.Book.jsonSchema( 247 | '', 248 | 'a_publisher', 249 | ); 250 | }); 251 | 252 | it('should not fail when populating a non-object field', () => { 253 | models.Book.jsonSchema( 254 | '', 255 | 'year', 256 | ); 257 | }); 258 | 259 | it('should not fail when populating field with incorrect reference', () => { 260 | const Ugly = require('../models/ugly'); 261 | Ugly.jsonSchema('', 'publisher'); 262 | }); 263 | }); 264 | -------------------------------------------------------------------------------- /test/suites/queries.test.js: -------------------------------------------------------------------------------- 1 | require('../../index')(require('mongoose')); 2 | const assert = require('assert'); 3 | 4 | const models = require('../models'); 5 | 6 | describe('Queries: query.jsonSchema()', () => { 7 | it('should build schema for query result', () => { 8 | const jsonSchema = models.Book.find().jsonSchema(); 9 | 10 | assert.deepEqual(jsonSchema, { 11 | title: 'List of Books', 12 | type: 'array', 13 | items: { 14 | type: 'object', 15 | properties: { 16 | title: { type: 'string' }, 17 | year: { type: 'number' }, 18 | author: { 19 | type: 'array', 20 | items: { 21 | type: 'string', 22 | 'x-ref': 'Person', 23 | description: 'Refers to Person', 24 | 25 | pattern: '^[0-9a-fA-F]{24}$', 26 | }, 27 | }, 28 | comment: { 29 | type: 'array', 30 | items: { 31 | title: 'itemOf_comment', 32 | type: 'object', 33 | properties: { 34 | body: { type: 'string' }, 35 | editor: { 36 | type: 'string', 37 | 'x-ref': 'Person', 38 | description: 'Refers to Person', 39 | 40 | pattern: '^[0-9a-fA-F]{24}$', 41 | }, 42 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 43 | }, 44 | required: ['editor'], 45 | }, 46 | }, 47 | official: { 48 | title: 'official', 49 | type: 'object', 50 | properties: { 51 | slogan: { type: 'string' }, 52 | announcement: { type: 'string' }, 53 | }, 54 | }, 55 | publisher: { 56 | type: 'string', 57 | 'x-ref': 'Person', 58 | description: 'Refers to Person', 59 | 60 | pattern: '^[0-9a-fA-F]{24}$', 61 | }, 62 | description: { type: 'string' }, 63 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 64 | __v: { type: 'number' }, 65 | }, 66 | }, 67 | }); 68 | }); 69 | 70 | it('should build schema for query result (selected fields)', () => { 71 | const jsonSchema = models.Book.find({}, 'title author').jsonSchema(); 72 | 73 | assert.deepEqual(jsonSchema, { 74 | title: 'List of Books', 75 | type: 'array', 76 | items: { 77 | type: 'object', 78 | properties: { 79 | title: { type: 'string' }, 80 | author: { 81 | type: 'array', 82 | items: { 83 | type: 'string', 84 | 'x-ref': 'Person', 85 | description: 'Refers to Person', 86 | 87 | pattern: '^[0-9a-fA-F]{24}$', 88 | }, 89 | }, 90 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 91 | }, 92 | }, 93 | }); 94 | }); 95 | 96 | it('should build schema for query result (populate)', () => { 97 | const jsonSchema = models.Book 98 | .find({}, 'title author') 99 | .populate('author') 100 | .jsonSchema(); 101 | 102 | assert.deepEqual(jsonSchema, { 103 | title: 'List of Books', 104 | type: 'array', 105 | items: { 106 | type: 'object', 107 | properties: { 108 | title: { type: 'string' }, 109 | author: { 110 | type: 'array', 111 | items: { 112 | title: 'Person', 113 | type: 'object', 114 | properties: { 115 | firstName: { type: 'string' }, 116 | lastName: { type: 'string' }, 117 | email: { type: 'string' }, 118 | isPoet: { type: 'boolean', default: false }, 119 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 120 | __v: { type: 'number' }, 121 | }, 122 | 'x-ref': 'Person', 123 | description: 'Refers to Person', 124 | }, 125 | }, 126 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 127 | }, 128 | }, 129 | }); 130 | }); 131 | 132 | it('should build schema for query result and reflect limit', () => { 133 | const jsonSchema = models.Book 134 | .find({}, 'title year') 135 | .limit(5) 136 | .jsonSchema(); 137 | 138 | assert.deepEqual(jsonSchema, { 139 | title: 'List of Books', 140 | type: 'array', 141 | items: { 142 | type: 'object', 143 | properties: { 144 | title: { type: 'string' }, 145 | year: { type: 'number' }, 146 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 147 | }, 148 | }, 149 | maxItems: 5, 150 | }); 151 | }); 152 | 153 | it('should build schema for findOne-query result', () => { 154 | const jsonSchema = models.Book 155 | .findOne({}, 'title year') 156 | .limit(5) 157 | .jsonSchema(); 158 | 159 | assert.deepEqual(jsonSchema, { 160 | title: 'Book', 161 | type: 'object', 162 | properties: { 163 | title: { type: 'string' }, 164 | year: { type: 'number' }, 165 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 166 | }, 167 | }); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /test/suites/readonly.test.js: -------------------------------------------------------------------------------- 1 | require('../../index')(require('mongoose')); 2 | const assert = require('assert'); 3 | 4 | const models = require('../models'); 5 | 6 | describe('readonly: model.jsonSchema', () => { 7 | it('should consider the readonly parameter', () => { 8 | const rules = [ 9 | { 10 | name: '**_id', 11 | path: /^([^.]?\.?)*_id$/, 12 | strict: false, 13 | message: null, 14 | }, { 15 | name: '**__v', 16 | path: /^([^.]?\.?)*__v$/, 17 | strict: false, 18 | message: null, 19 | }, { 20 | name: 'author', 21 | path: /^author$/, 22 | strict: false, 23 | message: null, 24 | }, 25 | ]; 26 | 27 | const jsonSchema = models.Book.jsonSchema(null, null, rules); 28 | 29 | assert.deepEqual(jsonSchema, { 30 | title: 'Book', 31 | type: 'object', 32 | properties: { 33 | title: { type: 'string' }, 34 | year: { type: 'number' }, 35 | comment: { 36 | type: 'array', 37 | items: { 38 | title: 'itemOf_comment', 39 | type: 'object', 40 | properties: { 41 | body: { type: 'string' }, 42 | editor: { 43 | type: 'string', 44 | 'x-ref': 'Person', 45 | description: 'Refers to Person', 46 | pattern: '^[0-9a-fA-F]{24}$', 47 | }, 48 | }, 49 | required: ['editor'], 50 | }, 51 | }, 52 | official: { 53 | title: 'official', 54 | type: 'object', 55 | properties: { slogan: { type: 'string' }, announcement: { type: 'string' } }, 56 | }, 57 | publisher: { 58 | type: 'string', 59 | 'x-ref': 'Person', 60 | description: 'Refers to Person', 61 | pattern: '^[0-9a-fA-F]{24}$', 62 | }, 63 | description: { type: 'string' }, 64 | }, 65 | required: ['title', 'year', 'publisher'], 66 | }); 67 | }); 68 | 69 | it('should exclude comment.editor', () => { 70 | const rules = [ 71 | { 72 | name: '**_id', 73 | path: /^([^.]?\.?)*_id$/, 74 | strict: false, 75 | message: null, 76 | }, { 77 | name: '**__v', 78 | path: /^([^.]?\.?)*__v$/, 79 | strict: false, 80 | message: null, 81 | }, { 82 | name: 'comment.editor', 83 | path: /^comment\.editor$/, 84 | strict: false, 85 | message: null, 86 | }, 87 | ]; 88 | 89 | const jsonSchema = models.Book.jsonSchema(null, null, rules); 90 | assert.deepEqual(jsonSchema, { 91 | title: 'Book', 92 | type: 'object', 93 | properties: { 94 | title: { type: 'string' }, 95 | year: { type: 'number' }, 96 | author: { 97 | type: 'array', 98 | items: { 99 | type: 'string', 100 | 'x-ref': 'Person', 101 | description: 'Refers to Person', 102 | pattern: '^[0-9a-fA-F]{24}$', 103 | }, 104 | }, 105 | comment: { 106 | type: 'array', 107 | items: { 108 | title: 'itemOf_comment', 109 | type: 'object', 110 | properties: { 111 | body: { type: 'string' }, 112 | }, 113 | }, 114 | }, 115 | official: { 116 | title: 'official', 117 | type: 'object', 118 | properties: { slogan: { type: 'string' }, announcement: { type: 'string' } }, 119 | }, 120 | publisher: { 121 | type: 'string', 122 | 'x-ref': 'Person', 123 | description: 'Refers to Person', 124 | pattern: '^[0-9a-fA-F]{24}$', 125 | }, 126 | description: { type: 'string' }, 127 | }, 128 | required: ['title', 'year', 'author', 'publisher'], 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /test/suites/required-issue.test.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('../../index')(require('mongoose')); 2 | 3 | const { Schema } = mongoose; 4 | const assert = require('assert'); 5 | 6 | describe('schema.jsonSchema', () => { 7 | it('should correctly translate all simmple types', () => { 8 | const bookSchema = new Schema({ 9 | name: { 10 | type: String, 11 | required: true, 12 | unique: true, 13 | }, 14 | description: { 15 | type: String, 16 | }, 17 | internalName: { 18 | type: String, 19 | required: true, 20 | unique: true, 21 | }, 22 | manage: { 23 | offline: { 24 | type: Boolean, 25 | default: true, 26 | }, 27 | startAt: { 28 | type: Date, 29 | required: true, 30 | }, 31 | endAt: { 32 | type: Date, 33 | required: true, 34 | }, 35 | }, 36 | }, { _id: false }); 37 | 38 | const jsonSchema = bookSchema.jsonSchema('book'); 39 | 40 | assert.deepEqual(jsonSchema, { 41 | title: 'book', 42 | type: 'object', 43 | properties: { 44 | name: { 45 | type: 'string', 46 | }, 47 | description: { 48 | type: 'string', 49 | }, 50 | internalName: { 51 | type: 'string', 52 | }, 53 | manage: { 54 | title: 'manage', 55 | type: 'object', 56 | properties: { 57 | offline: { 58 | type: 'boolean', 59 | default: true, 60 | }, 61 | startAt: { 62 | type: 'string', 63 | format: 'date-time', 64 | }, 65 | endAt: { 66 | type: 'string', 67 | format: 'date-time', 68 | }, 69 | }, 70 | required: ['startAt', 'endAt'], 71 | }, 72 | }, 73 | required: [ 74 | 'name', 75 | 'internalName', 76 | ], 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/suites/required.test.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('../../index')(require('mongoose')); 2 | 3 | const { Schema } = mongoose; 4 | const assert = require('assert'); 5 | 6 | describe('Required fields: schema.jsonSchema', () => { 7 | it('should correctly translate field requirement', () => { 8 | const bookSchema = new Schema({ 9 | name: { 10 | type: String, 11 | required() { 12 | return this.year > 2000; 13 | }, 14 | }, 15 | year: { 16 | type: Number, 17 | required: true, 18 | }, 19 | description: { 20 | type: String, 21 | }, 22 | internalName: { 23 | type: String, 24 | required: [true, 'Internal name is required'], 25 | }, 26 | author: { 27 | type: String, 28 | required: [ 29 | function () { 30 | return this.year > 2000; 31 | }, 32 | 'Author is required if year > 2000', 33 | ], 34 | }, 35 | }, { _id: false }); 36 | 37 | const jsonSchema = bookSchema.jsonSchema('book'); 38 | 39 | assert.deepEqual(jsonSchema, { 40 | title: 'book', 41 | type: 'object', 42 | properties: { 43 | name: { 44 | type: 'string', 45 | }, 46 | year: { 47 | type: 'number', 48 | }, 49 | description: { 50 | type: 'string', 51 | }, 52 | internalName: { 53 | type: 'string', 54 | }, 55 | author: { 56 | type: 'string', 57 | }, 58 | }, 59 | required: [ 60 | 'year', 61 | 'internalName', 62 | ], 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/suites/selection.test.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('../../index')(require('mongoose')); 2 | const assert = require('assert'); 3 | 4 | const models = require('../models'); 5 | 6 | describe('Field selection: model.jsonSchema()', () => { 7 | it('should build schema for fields pointed explicitly (string)', () => { 8 | const jsonSchema = models.Book.jsonSchema('title year'); 9 | assert.deepEqual(jsonSchema, { 10 | title: 'Book', 11 | type: 'object', 12 | properties: { 13 | title: { type: 'string' }, 14 | year: { type: 'number' }, 15 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 16 | }, 17 | }); 18 | }); 19 | 20 | it('should build schema for fields pointed explicitly (array)', () => { 21 | const jsonSchema = models.Book.jsonSchema(['title', 'year']); 22 | assert.deepEqual(jsonSchema, { 23 | title: 'Book', 24 | type: 'object', 25 | properties: { 26 | title: { type: 'string' }, 27 | year: { type: 'number' }, 28 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 29 | }, 30 | }); 31 | }); 32 | 33 | it('should build schema for fields pointed explicitly (object)', () => { 34 | const jsonSchema = models.Book.jsonSchema({ title: 1, year: true }); 35 | assert.deepEqual(jsonSchema, { 36 | title: 'Book', 37 | type: 'object', 38 | properties: { 39 | title: { type: 'string' }, 40 | year: { type: 'number' }, 41 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 42 | }, 43 | }); 44 | }); 45 | 46 | it('should build schema for fields pointed explicitly, excluding _id (string)', () => { 47 | const jsonSchema = models.Book.jsonSchema('title year -_id'); 48 | assert.deepEqual(jsonSchema, { 49 | title: 'Book', 50 | type: 'object', 51 | properties: { 52 | title: { type: 'string' }, 53 | year: { type: 'number' }, 54 | }, 55 | }); 56 | }); 57 | 58 | it('should build schema for fields pointed explicitly, excluding _id (array)', () => { 59 | const jsonSchema = models.Book.jsonSchema(['title', 'year', '-_id']); 60 | assert.deepEqual(jsonSchema, { 61 | title: 'Book', 62 | type: 'object', 63 | properties: { 64 | title: { type: 'string' }, 65 | year: { type: 'number' }, 66 | }, 67 | }); 68 | }); 69 | 70 | it('should build schema for fields pointed explicitly, excluding _id (object)', () => { 71 | const jsonSchema = models.Book.jsonSchema({ title: 1, year: true, _id: 0 }); 72 | assert.deepEqual(jsonSchema, { 73 | title: 'Book', 74 | type: 'object', 75 | properties: { 76 | title: { type: 'string' }, 77 | year: { type: 'number' }, 78 | }, 79 | }); 80 | }); 81 | 82 | it('should build schema excluding pointed fields (string)', () => { 83 | const jsonSchema = models.Book.jsonSchema( 84 | '-author -comment -official -publisher -description -__v', 85 | ); 86 | assert.deepEqual(jsonSchema, { 87 | title: 'Book', 88 | type: 'object', 89 | properties: { 90 | title: { type: 'string' }, 91 | year: { type: 'number' }, 92 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 93 | }, 94 | }); 95 | }); 96 | 97 | it('should build schema excluding pointed fields (array)', () => { 98 | const jsonSchema = models.Book.jsonSchema([ 99 | '-author', '-comment', '-official', '-publisher', '-description', '-__v', 100 | ]); 101 | assert.deepEqual(jsonSchema, { 102 | title: 'Book', 103 | type: 'object', 104 | properties: { 105 | title: { type: 'string' }, 106 | year: { type: 'number' }, 107 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 108 | }, 109 | }); 110 | }); 111 | 112 | it('should build schema excluding pointed fields (object)', () => { 113 | const jsonSchema = models.Book.jsonSchema({ 114 | author: false, 115 | comment: false, 116 | official: false, 117 | publisher: false, 118 | description: false, 119 | __v: false, 120 | }); 121 | assert.deepEqual(jsonSchema, { 122 | title: 'Book', 123 | type: 'object', 124 | properties: { 125 | title: { type: 'string' }, 126 | year: { type: 'number' }, 127 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 128 | }, 129 | }); 130 | }); 131 | 132 | it('should build schema for fields of nested objects (string)', () => { 133 | const jsonSchema = models.Book.jsonSchema('title official.slogan'); 134 | 135 | assert.deepEqual(jsonSchema, { 136 | title: 'Book', 137 | type: 'object', 138 | properties: { 139 | title: { type: 'string' }, 140 | official: { 141 | title: 'official', 142 | type: 'object', 143 | properties: { 144 | slogan: { type: 'string' }, 145 | }, 146 | }, 147 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 148 | }, 149 | }); 150 | }); 151 | 152 | it('should build schema for fields of nested array (string)', () => { 153 | const jsonSchema = models.Book.jsonSchema('title comment.body'); 154 | assert.deepEqual(jsonSchema, { 155 | title: 'Book', 156 | type: 'object', 157 | properties: { 158 | title: { type: 'string' }, 159 | comment: { 160 | type: 'array', 161 | items: { 162 | title: 'itemOf_comment', 163 | type: 'object', 164 | properties: { 165 | body: { type: 'string' }, 166 | }, 167 | }, 168 | }, 169 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 170 | }, 171 | }); 172 | }); 173 | 174 | it('should build schema for fields of nested array explicitly included by array name (string)', () => { 175 | const jsonSchema = models.Book.jsonSchema('title comment'); 176 | 177 | assert.deepEqual(jsonSchema, { 178 | title: 'Book', 179 | type: 'object', 180 | properties: { 181 | title: { type: 'string' }, 182 | comment: { 183 | type: 'array', 184 | items: { 185 | title: 'itemOf_comment', 186 | type: 'object', 187 | properties: { 188 | body: { type: 'string' }, 189 | editor: { 190 | type: 'string', 191 | pattern: '^[0-9a-fA-F]{24}$', 192 | 'x-ref': 'Person', 193 | description: 'Refers to Person', 194 | }, 195 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 196 | }, 197 | }, 198 | }, 199 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 200 | }, 201 | }); 202 | }); 203 | 204 | it('should build schema for fields of nested array explicitly included by array name (string) and excluding some nested field', () => { 205 | const jsonSchema = models.Book.jsonSchema('title comment -comment._id'); 206 | 207 | assert.deepEqual(jsonSchema, { 208 | title: 'Book', 209 | type: 'object', 210 | properties: { 211 | title: { type: 'string' }, 212 | comment: { 213 | type: 'array', 214 | items: { 215 | title: 'itemOf_comment', 216 | type: 'object', 217 | properties: { 218 | body: { type: 'string' }, 219 | editor: { 220 | type: 'string', 221 | pattern: '^[0-9a-fA-F]{24}$', 222 | 'x-ref': 'Person', 223 | description: 'Refers to Person', 224 | }, 225 | }, 226 | }, 227 | }, 228 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 229 | }, 230 | }); 231 | }); 232 | 233 | it('should correctly process fields deselected on schema-level', () => { 234 | const mSchema = new mongoose.Schema({ 235 | x: Number, 236 | y: { type: Number, required: true, select: false }, 237 | }); 238 | 239 | const aModel = mongoose.model('aModel', mSchema); 240 | 241 | const jsonSchema = aModel.jsonSchema('x y'); 242 | 243 | assert.deepEqual(jsonSchema, { 244 | title: 'aModel', 245 | type: 'object', 246 | properties: { 247 | x: { type: 'number' }, 248 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 249 | }, 250 | }); 251 | }); 252 | 253 | it('should correctly process overriding of deselection on schema-level', () => { 254 | const mSchema = new mongoose.Schema({ 255 | x: Number, 256 | y: { type: Number, required: true, select: false }, 257 | }); 258 | 259 | const zModel = mongoose.model('zModel', mSchema); 260 | 261 | const jsonSchema = zModel.jsonSchema('x +y'); 262 | 263 | assert.deepEqual(jsonSchema, { 264 | title: 'zModel', 265 | type: 'object', 266 | properties: { 267 | x: { type: 'number' }, 268 | y: { type: 'number' }, 269 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 270 | }, 271 | }); 272 | }); 273 | 274 | it('should correctly process id field selection (virtuals)', () => { 275 | const mSchema = new mongoose.Schema({ 276 | x: Number, 277 | }, { 278 | toJSON: { virtuals: true }, 279 | }); 280 | 281 | const zModel = mongoose.model('z1Model', mSchema); 282 | 283 | const jsonSchema = zModel.jsonSchema('x +y -_id id'); 284 | 285 | assert.deepEqual(jsonSchema, { 286 | title: 'z1Model', 287 | type: 'object', 288 | properties: { 289 | x: { type: 'number' }, 290 | id: { }, 291 | }, 292 | }); 293 | }); 294 | 295 | it('should correctly process id field selection (getters)', () => { 296 | const mSchema = new mongoose.Schema({ 297 | x: Number, 298 | }, { 299 | toJSON: { getters: true }, 300 | }); 301 | 302 | const zModel = mongoose.model('z2Model', mSchema); 303 | 304 | const jsonSchema = zModel.jsonSchema('x +y id'); 305 | 306 | assert.deepEqual(jsonSchema, { 307 | title: 'z2Model', 308 | type: 'object', 309 | properties: { 310 | x: { type: 'number' }, 311 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 312 | id: { }, 313 | }, 314 | }); 315 | }); 316 | }); 317 | -------------------------------------------------------------------------------- /test/suites/translation.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | const mongoose = require('../../index')(require('mongoose')); 3 | const isV6pl = require('mongoose/package.json').version > '6'; 4 | 5 | const { Schema } = mongoose; 6 | const assert = require('assert'); 7 | 8 | describe('schema.jsonSchema', () => { 9 | it('should correctly translate all simmple types', () => { 10 | const mSchema = new Schema({ 11 | n: Number, 12 | s: String, 13 | d: Date, 14 | b: Boolean, 15 | u: Schema.Types.ObjectId, 16 | }); 17 | 18 | const jsonSchema = mSchema.jsonSchema('Sample'); 19 | 20 | assert.deepEqual(jsonSchema, { 21 | title: 'Sample', 22 | type: 'object', 23 | properties: { 24 | n: { type: 'number' }, 25 | s: { type: 'string' }, 26 | d: { type: 'string', format: 'date-time' }, 27 | b: { type: 'boolean' }, 28 | u: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 29 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 30 | }, 31 | }); 32 | }); 33 | 34 | it('should correctly translate all simmple types and virtuals', () => { 35 | const mSchema = new Schema({ 36 | n: Number, 37 | s: String, 38 | d: Date, 39 | b: Boolean, 40 | u: Schema.Types.ObjectId, 41 | }, { 42 | toJSON: { virtuals: true }, 43 | }); 44 | 45 | mSchema.virtual('id').get(function () { 46 | return this._id; 47 | }); 48 | 49 | mSchema.virtual('summary').get(function () { 50 | return this.s + this.n; 51 | }); 52 | 53 | const jsonSchema = mSchema.jsonSchema('Sample'); 54 | 55 | assert.deepEqual(jsonSchema, { 56 | title: 'Sample', 57 | type: 'object', 58 | properties: { 59 | n: { type: 'number' }, 60 | s: { type: 'string' }, 61 | d: { type: 'string', format: 'date-time' }, 62 | b: { type: 'boolean' }, 63 | u: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 64 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 65 | id: {}, 66 | summary: {}, 67 | }, 68 | }); 69 | }); 70 | 71 | it('should correctly translate all simmple types and virtuals (through getters options)', () => { 72 | const mSchema = new Schema({ 73 | n: Number, 74 | s: String, 75 | d: Date, 76 | b: Boolean, 77 | u: Schema.Types.ObjectId, 78 | }, { 79 | toJSON: { getters: true }, 80 | }); 81 | 82 | mSchema.virtual('id').get(function () { 83 | return this._id; 84 | }); 85 | 86 | mSchema.virtual('summary').get(function () { 87 | return this.s + this.n; 88 | }); 89 | 90 | const jsonSchema = mSchema.jsonSchema('Sample'); 91 | 92 | assert.deepEqual(jsonSchema, { 93 | title: 'Sample', 94 | type: 'object', 95 | properties: { 96 | n: { type: 'number' }, 97 | s: { type: 'string' }, 98 | d: { type: 'string', format: 'date-time' }, 99 | b: { type: 'boolean' }, 100 | u: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 101 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 102 | id: {}, 103 | summary: {}, 104 | }, 105 | }); 106 | }); 107 | 108 | it('should correctly translate all simmple types in arrays', () => { 109 | const mSchema = new Schema({ 110 | n: [Number], 111 | s: [String], 112 | d: [Date], 113 | b: [Boolean], 114 | u: [Schema.Types.ObjectId], 115 | }); 116 | 117 | const jsonSchema = mSchema.jsonSchema('Sample'); 118 | 119 | assert.deepEqual(jsonSchema, { 120 | title: 'Sample', 121 | type: 'object', 122 | properties: { 123 | n: { 124 | type: 'array', 125 | items: { type: 'number' }, 126 | }, 127 | s: { 128 | type: 'array', 129 | items: { type: 'string' }, 130 | }, 131 | d: { 132 | type: 'array', 133 | items: { type: 'string', format: 'date-time' }, 134 | }, 135 | b: { 136 | type: 'array', 137 | items: { type: 'boolean' }, 138 | }, 139 | u: { 140 | type: 'array', 141 | items: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 142 | }, 143 | _id: { 144 | type: 'string', pattern: '^[0-9a-fA-F]{24}$', 145 | }, 146 | }, 147 | }); 148 | }); 149 | 150 | it('should correctly translate all simmple types when required', () => { 151 | const mSchema = new Schema({ 152 | n: { type: Number, required: true }, 153 | s: { type: String, required: true }, 154 | d: { type: Date, required: true }, 155 | b: { type: Boolean, required: true }, 156 | u: { type: Schema.Types.ObjectId, required: true }, 157 | }); 158 | 159 | const jsonSchema = mSchema.jsonSchema('Sample'); 160 | 161 | assert.deepEqual(jsonSchema, { 162 | title: 'Sample', 163 | type: 'object', 164 | properties: { 165 | n: { type: 'number' }, 166 | s: { type: 'string' }, 167 | d: { type: 'string', format: 'date-time' }, 168 | b: { type: 'boolean' }, 169 | u: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 170 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 171 | }, 172 | required: ['n', 's', 'd', 'b', 'u'], 173 | }); 174 | }); 175 | 176 | it('should correctly translate all simmple types in embedded doc', () => { 177 | const mSchema = new Schema({ 178 | embededDoc: { 179 | n: Number, 180 | s: String, 181 | d: Date, 182 | b: Boolean, 183 | u: Schema.Types.ObjectId, 184 | }, 185 | }); 186 | 187 | const jsonSchema = mSchema.jsonSchema('Sample'); 188 | 189 | assert.deepEqual(jsonSchema, { 190 | title: 'Sample', 191 | type: 'object', 192 | properties: { 193 | embededDoc: { 194 | title: 'embededDoc', 195 | type: 'object', 196 | properties: { 197 | n: { type: 'number' }, 198 | s: { type: 'string' }, 199 | d: { type: 'string', format: 'date-time' }, 200 | b: { type: 'boolean' }, 201 | u: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 202 | }, 203 | }, 204 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 205 | }, 206 | }); 207 | }); 208 | 209 | it('should correctly translate all simmple types in required embedded doc', () => { 210 | const mSchema = new Schema({ 211 | embededDoc: { 212 | type: { 213 | n: Number, 214 | s: String, 215 | d: Date, 216 | b: Boolean, 217 | u: Schema.Types.ObjectId, 218 | }, 219 | required: true, 220 | }, 221 | }); 222 | 223 | const jsonSchema = mSchema.jsonSchema('Sample'); 224 | 225 | assert.deepEqual(jsonSchema, { 226 | title: 'Sample', 227 | type: 'object', 228 | properties: { 229 | embededDoc: { 230 | title: 'embededDoc', 231 | type: 'object', 232 | properties: { 233 | ...(isV6pl ? { _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' } } : undefined), 234 | n: { type: 'number' }, 235 | s: { type: 'string' }, 236 | d: { type: 'string', format: 'date-time' }, 237 | b: { type: 'boolean' }, 238 | u: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 239 | }, 240 | }, 241 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 242 | }, 243 | required: ['embededDoc'], 244 | }); 245 | }); 246 | 247 | it('should corretly translate all supported types', () => { 248 | const mSchema = new Schema({ 249 | n: Number, 250 | s: String, 251 | d: Date, 252 | b: Boolean, 253 | u: Schema.Types.ObjectId, 254 | r: { type: Schema.Types.ObjectId, ref: 'Book' }, 255 | nestedDoc: { 256 | n: Number, 257 | s: String, 258 | }, 259 | an: [Number], 260 | as: [String], 261 | ad: [Date], 262 | ab: [Boolean], 263 | au: [Schema.Types.ObjectId], 264 | ar: [{ type: Schema.Types.ObjectId, ref: 'Book' }], 265 | aNestedDoc: [{ 266 | n: Number, 267 | s: String, 268 | }], 269 | rn: { type: Number, required: true }, 270 | rs: { type: String, required: true }, 271 | rd: { type: Date, required: true }, 272 | rb: { type: Boolean, required: true }, 273 | ru: { 274 | type: mongoose.Schema.Types.ObjectId, 275 | required: true, 276 | }, 277 | rr: { 278 | type: Schema.Types.ObjectId, 279 | required: true, 280 | ref: 'Book', 281 | }, 282 | rNestedDoc: { 283 | type: { 284 | n: Number, 285 | s: String, 286 | }, 287 | required: true, 288 | }, 289 | rar: { 290 | type: [{ type: Schema.Types.ObjectId, ref: 'Book' }], 291 | required: true, 292 | }, 293 | described: { 294 | type: String, 295 | description: 'Described field', 296 | required: true, 297 | }, 298 | }); 299 | 300 | const jsonSchema = mSchema.jsonSchema('Sample'); 301 | 302 | assert.deepEqual(jsonSchema, { 303 | title: 'Sample', 304 | type: 'object', 305 | properties: { 306 | n: { type: 'number' }, 307 | s: { type: 'string' }, 308 | d: { type: 'string', format: 'date-time' }, 309 | b: { type: 'boolean' }, 310 | u: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 311 | r: { 312 | type: 'string', 313 | pattern: '^[0-9a-fA-F]{24}$', 314 | 'x-ref': 'Book', 315 | description: 'Refers to Book', 316 | }, 317 | nestedDoc: { 318 | title: 'nestedDoc', 319 | type: 'object', 320 | properties: { 321 | n: { type: 'number' }, 322 | s: { type: 'string' }, 323 | }, 324 | }, 325 | an: { type: 'array', items: { type: 'number' } }, 326 | as: { type: 'array', items: { type: 'string' } }, 327 | ad: { 328 | type: 'array', 329 | items: { type: 'string', format: 'date-time' }, 330 | }, 331 | ab: { type: 'array', items: { type: 'boolean' } }, 332 | au: { 333 | type: 'array', 334 | items: { 335 | type: 'string', 336 | pattern: '^[0-9a-fA-F]{24}$', 337 | }, 338 | }, 339 | ar: { 340 | type: 'array', 341 | items: { 342 | type: 'string', 343 | pattern: '^[0-9a-fA-F]{24}$', 344 | 'x-ref': 'Book', 345 | description: 'Refers to Book', 346 | }, 347 | }, 348 | aNestedDoc: { 349 | type: 'array', 350 | items: { 351 | title: 'itemOf_aNestedDoc', 352 | type: 'object', 353 | properties: { 354 | s: { type: 'string' }, 355 | n: { type: 'number' }, 356 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 357 | }, 358 | }, 359 | }, 360 | rn: { type: 'number' }, 361 | rs: { type: 'string' }, 362 | rd: { type: 'string', format: 'date-time' }, 363 | rb: { type: 'boolean' }, 364 | ru: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 365 | rr: { 366 | type: 'string', 367 | pattern: '^[0-9a-fA-F]{24}$', 368 | 'x-ref': 'Book', 369 | description: 'Refers to Book', 370 | }, 371 | rNestedDoc: { 372 | title: 'rNestedDoc', 373 | type: 'object', 374 | properties: { 375 | ...(isV6pl ? { _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' } } : undefined), 376 | n: { type: 'number' }, 377 | s: { type: 'string' }, 378 | }, 379 | }, 380 | rar: { 381 | type: 'array', 382 | items: { 383 | type: 'string', 384 | pattern: '^[0-9a-fA-F]{24}$', 385 | 'x-ref': 'Book', 386 | description: 'Refers to Book', 387 | }, 388 | }, 389 | described: { 390 | type: 'string', 391 | description: 'Described field', 392 | }, 393 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 394 | }, 395 | required: [ 396 | 'rn', 'rs', 'rd', 'rb', 'ru', 'rr', 'rNestedDoc', 'rar', 'described', 397 | ], 398 | }); 399 | }); 400 | 401 | it('should correctly translate nested schemas', () => { 402 | const mSchema1 = new Schema({ 403 | k: String, 404 | v: Number, 405 | }, { id: false, _id: false }); 406 | const mSchema2 = new Schema({ 407 | t: { type: String, required: true }, 408 | nD: mSchema1, 409 | rND: { 410 | type: mSchema1, 411 | required: true, 412 | }, 413 | aND: [mSchema1], 414 | rAND: { 415 | type: [mSchema1], 416 | required: true, 417 | }, 418 | }); 419 | 420 | const jsonSchema = mSchema2.jsonSchema(); 421 | 422 | assert.deepEqual(jsonSchema, { 423 | type: 'object', 424 | properties: { 425 | t: { type: 'string' }, 426 | nD: { 427 | type: 'object', 428 | title: 'nD', 429 | properties: { 430 | k: { type: 'string' }, 431 | v: { type: 'number' }, 432 | }, 433 | }, 434 | rND: { 435 | title: 'rND', 436 | type: 'object', 437 | properties: { 438 | k: { type: 'string' }, 439 | v: { type: 'number' }, 440 | }, 441 | }, 442 | aND: { 443 | type: 'array', 444 | items: { 445 | title: 'itemOf_aND', 446 | type: 'object', 447 | properties: { 448 | k: { type: 'string' }, 449 | v: { type: 'number' }, 450 | }, 451 | }, 452 | }, 453 | rAND: { 454 | type: 'array', 455 | items: { 456 | title: 'itemOf_rAND', 457 | type: 'object', 458 | properties: { 459 | k: { type: 'string' }, 460 | v: { type: 'number' }, 461 | }, 462 | }, 463 | }, 464 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 465 | }, 466 | required: ['t', 'rND', 'rAND'], 467 | }); 468 | }); 469 | 470 | it('should correctly translate many levels of nested Schemas', () => { 471 | const mSchema1 = new Schema({ x: Number }, { id: false, _id: false }); 472 | const mSchema2 = new Schema({ y: Number, x: mSchema1 }, { id: false, _id: false }); 473 | const mSchema3 = new Schema({ z: Number, y: mSchema2 }, { id: false, _id: false }); 474 | const mSchema4 = new Schema({ 475 | t: Number, 476 | xyz: { 477 | type: { 478 | x: [{ 479 | type: mSchema1, 480 | required: true, 481 | }], 482 | y: { type: mSchema2, required: true }, 483 | z: { type: [mSchema3] }, 484 | any: { type: Schema.Types.Mixed, required: true }, 485 | }, 486 | required: true, 487 | }, 488 | }); 489 | 490 | const jsonSchema = mSchema4.jsonSchema('Sample'); 491 | 492 | assert.deepEqual(jsonSchema, { 493 | title: 'Sample', 494 | type: 'object', 495 | properties: { 496 | t: { type: 'number' }, 497 | xyz: { 498 | title: 'xyz', 499 | type: 'object', 500 | properties: { 501 | ...(isV6pl ? { _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' } } : undefined), 502 | x: { 503 | type: 'array', 504 | items: { 505 | type: 'object', 506 | title: 'itemOf_x', 507 | properties: { 508 | x: { type: 'number' }, 509 | }, 510 | }, 511 | }, 512 | y: { 513 | type: 'object', 514 | title: 'y', 515 | properties: { 516 | y: { type: 'number' }, 517 | x: { 518 | type: 'object', 519 | title: 'x', 520 | properties: { 521 | x: { type: 'number' }, 522 | }, 523 | }, 524 | }, 525 | }, 526 | z: { 527 | type: 'array', 528 | items: { 529 | type: 'object', 530 | title: 'itemOf_z', 531 | properties: { 532 | z: { type: 'number' }, 533 | y: { 534 | type: 'object', 535 | title: 'y', 536 | properties: { 537 | y: { type: 'number' }, 538 | x: { 539 | type: 'object', 540 | title: 'x', 541 | properties: { 542 | x: { type: 'number' }, 543 | }, 544 | }, 545 | }, 546 | }, 547 | }, 548 | }, 549 | }, 550 | any: { }, 551 | }, 552 | required: ['y', 'any'], 553 | }, 554 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 555 | }, 556 | required: ['xyz'], 557 | }); 558 | }); 559 | 560 | it('should correctly translate number value constaints', () => { 561 | const mSchema = new Schema({ 562 | value: { 563 | type: Number, min: -5, max: 50, default: 0, required: true, 564 | }, 565 | }, { id: false, _id: false }); 566 | 567 | const jsonSchema = mSchema.jsonSchema('Sample'); 568 | 569 | assert.deepEqual(jsonSchema, { 570 | title: 'Sample', 571 | type: 'object', 572 | properties: { 573 | value: { 574 | type: 'number', 575 | minimum: -5, 576 | maximum: 50, 577 | default: 0, 578 | }, 579 | }, 580 | required: ['value'], 581 | }); 582 | }); 583 | 584 | it('should correctly translate number value constaints with error messages', () => { 585 | const mSchema = new Schema({ 586 | value: { 587 | type: Number, 588 | min: [-5, 'Value shoule be greater or equal to -5'], 589 | max: [50, 'Value should be less or equal to 50'], 590 | required: [true, 'Value should be specified'], 591 | default: 0, 592 | }, 593 | }, { id: false, _id: false }); 594 | 595 | const jsonSchema = mSchema.jsonSchema('Sample'); 596 | 597 | assert.deepEqual(jsonSchema, { 598 | title: 'Sample', 599 | type: 'object', 600 | properties: { 601 | value: { 602 | type: 'number', 603 | minimum: -5, 604 | maximum: 50, 605 | default: 0, 606 | }, 607 | }, 608 | required: ['value'], 609 | }); 610 | }); 611 | 612 | it('should correctly translate number value constaints with error messages (not requires)', () => { 613 | const mSchema = new Schema({ 614 | value: { 615 | type: Number, 616 | min: [-5, 'Value shoule be greater or equal to -5'], 617 | max: [50, 'Value should be less or equal to 50'], 618 | required: [false, 'Value is not required'], 619 | default: 0, 620 | }, 621 | }, { id: false, _id: false }); 622 | 623 | const jsonSchema = mSchema.jsonSchema('Sample'); 624 | 625 | assert.deepEqual(jsonSchema, { 626 | title: 'Sample', 627 | type: 'object', 628 | properties: { 629 | value: { 630 | type: 'number', 631 | minimum: -5, 632 | maximum: 50, 633 | default: 0, 634 | }, 635 | }, 636 | }); 637 | }); 638 | 639 | it('should correctly translate string value constaints', () => { 640 | const mSchema = new Schema({ 641 | valueFromList: { 642 | type: String, 643 | enum: ['red', 'green', 'yellow'], 644 | required: true, 645 | }, 646 | value20_30: { 647 | type: String, 648 | minLength: 20, 649 | maxLength: 30, 650 | }, 651 | value: { type: String, match: /^(?:H|h)ello, .+$/ }, 652 | }, { id: false, _id: false }); 653 | 654 | const jsonSchema = mSchema.jsonSchema('Sample'); 655 | 656 | assert.deepEqual(jsonSchema, { 657 | title: 'Sample', 658 | type: 'object', 659 | properties: { 660 | valueFromList: { 661 | type: 'string', 662 | enum: ['red', 'green', 'yellow'], 663 | }, 664 | value20_30: { 665 | type: 'string', 666 | minLength: 20, 667 | maxLength: 30, 668 | }, 669 | value: { 670 | type: 'string', 671 | pattern: '^(?:H|h)ello, .+$', 672 | }, 673 | }, 674 | required: ['valueFromList'], 675 | }); 676 | }); 677 | 678 | it('should correctly translate string value constaints with error message', () => { 679 | const mSchema = new Schema({ 680 | valueFromList: { 681 | type: String, 682 | enum: ['red', 'green', 'yellow'], 683 | required: true, 684 | }, 685 | value20_30: { 686 | type: String, 687 | minLength: [20, 'Value should have at least 20 characters'], 688 | maxLength: [30, 'Value should not be longer then 30 characters'], 689 | }, 690 | value: { type: String, match: [/^(?:H|h)ello, .+$/, 'Value should start from greating'] }, 691 | }, { id: false, _id: false }); 692 | 693 | const jsonSchema = mSchema.jsonSchema('Sample'); 694 | 695 | assert.deepEqual(jsonSchema, { 696 | title: 'Sample', 697 | type: 'object', 698 | properties: { 699 | valueFromList: { 700 | type: 'string', 701 | enum: ['red', 'green', 'yellow'], 702 | }, 703 | value20_30: { 704 | type: 'string', 705 | minLength: 20, 706 | maxLength: 30, 707 | }, 708 | value: { 709 | type: 'string', 710 | pattern: '^(?:H|h)ello, .+$', 711 | }, 712 | }, 713 | required: ['valueFromList'], 714 | }); 715 | }); 716 | 717 | it('should correctly translate string value constaints (minlength and maxlength)', () => { 718 | const mSchema = new Schema({ 719 | valueFromList: { 720 | type: String, 721 | enum: ['red', 'green', 'yellow'], 722 | required: true, 723 | }, 724 | value20_30: { 725 | type: String, 726 | minlength: 20, 727 | maxlength: 30, 728 | }, 729 | value: { type: String, match: /^(?:H|h)ello, .+$/ }, 730 | }, { id: false, _id: false }); 731 | 732 | const jsonSchema = mSchema.jsonSchema('Sample'); 733 | 734 | assert.deepEqual(jsonSchema, { 735 | title: 'Sample', 736 | type: 'object', 737 | properties: { 738 | valueFromList: { 739 | type: 'string', 740 | enum: ['red', 'green', 'yellow'], 741 | }, 742 | value20_30: { 743 | type: 'string', 744 | minLength: 20, 745 | maxLength: 30, 746 | }, 747 | value: { 748 | type: 'string', 749 | pattern: '^(?:H|h)ello, .+$', 750 | }, 751 | }, 752 | required: ['valueFromList'], 753 | }); 754 | }); 755 | 756 | it('should correctly translate Mixed type', () => { 757 | const mSchema = new Schema({ 758 | m: Schema.Types.Mixed, 759 | }); 760 | 761 | const jsonSchema = mSchema.jsonSchema('Sample'); 762 | 763 | assert.deepEqual(jsonSchema, { 764 | title: 'Sample', 765 | type: 'object', 766 | properties: { 767 | m: { }, 768 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 769 | }, 770 | }); 771 | }); 772 | 773 | it('should correctly transform Map type (String)', () => { 774 | const mS = new Schema({ 775 | m: { type: Map, of: String }, 776 | }); 777 | 778 | const jsonSchema = mS.jsonSchema('Sample'); 779 | 780 | assert.deepEqual(jsonSchema, { 781 | title: 'Sample', 782 | type: 'object', 783 | properties: { 784 | m: { 785 | type: 'object', 786 | additionalProperties: { type: 'string' }, 787 | }, 788 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 789 | }, 790 | }); 791 | }); 792 | 793 | it('should correctly transform Map type (String, required: false)', () => { 794 | const mS = mongoose.Schema({ 795 | name: { type: String }, 796 | language: { 797 | type: Map, 798 | of: { type: String }, 799 | }, 800 | }); 801 | 802 | const jsonSchema = mS.jsonSchema('Sample'); 803 | 804 | assert.deepEqual(jsonSchema, { 805 | title: 'Sample', 806 | type: 'object', 807 | properties: { 808 | name: { type: 'string' }, 809 | language: { 810 | type: 'object', 811 | additionalProperties: { type: 'string' }, 812 | }, 813 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 814 | }, 815 | }); 816 | }); 817 | 818 | it('should correctly transform Map type (Mixed)', () => { 819 | const mS = new Schema({ 820 | m: Map, 821 | }); 822 | 823 | const jsonSchema = mS.jsonSchema('Sample'); 824 | 825 | assert.deepEqual(jsonSchema, { 826 | title: 'Sample', 827 | type: 'object', 828 | properties: { 829 | m: { 830 | type: 'object', 831 | additionalProperties: true, 832 | }, 833 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 834 | }, 835 | }); 836 | }); 837 | 838 | it('should correctly transform Map type (SubSchema)', () => { 839 | const mS = new Schema({ 840 | m: { 841 | type: Map, 842 | of: new Schema({ 843 | x: Number, 844 | y: String, 845 | }, { _id: false }), 846 | }, 847 | }); 848 | 849 | const jsonSchema = mS.jsonSchema('Sample'); 850 | 851 | assert.deepEqual(jsonSchema, { 852 | title: 'Sample', 853 | type: 'object', 854 | properties: { 855 | m: { 856 | type: 'object', 857 | additionalProperties: { 858 | title: 'itemOf_m', 859 | type: 'object', 860 | properties: { x: { type: 'number' }, y: { type: 'string' } }, 861 | }, 862 | }, 863 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 864 | }, 865 | }); 866 | }); 867 | }); 868 | -------------------------------------------------------------------------------- /test/suites/uploading-mongoose-implicitly.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | describe('Mongoose', () => { 4 | it('should be uploaded without explicit request', () => { 5 | const mongoose = require('../../index')(); 6 | assert.equal(mongoose.constructor.name, 'Mongoose'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /test/suites/validation.test.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('../../index')(require('mongoose')); 2 | const { validate } = require('jsonschema'); 3 | const assert = require('assert'); 4 | 5 | const models = require('../models'); 6 | 7 | describe('Validation: schema.jsonSchema()', () => { 8 | it('should build schema and validate numbers', () => { 9 | const mSchema = new mongoose.Schema({ 10 | n: { type: Number, min: 0, max: 10 }, 11 | }); 12 | 13 | const jsonSchema = mSchema.jsonSchema(); 14 | 15 | assert.deepEqual(jsonSchema, { 16 | type: 'object', 17 | properties: { 18 | n: { 19 | type: 'number', 20 | minimum: 0, 21 | maximum: 10, 22 | }, 23 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 24 | }, 25 | }); 26 | 27 | let errors; 28 | errors = validate({ n: 3 }, jsonSchema).errors; 29 | assert.equal(errors.length, 0); 30 | 31 | errors = validate({ n: 0 }, jsonSchema).errors; 32 | assert.equal(errors.length, 0); 33 | 34 | errors = validate({ n: 10 }, jsonSchema).errors; 35 | assert.equal(errors.length, 0); 36 | 37 | errors = validate({ n: -1 }, jsonSchema).errors; 38 | assert.equal(errors.length, 1); 39 | 40 | errors = validate({ n: 13 }, jsonSchema).errors; 41 | assert.equal(errors.length, 1); 42 | 43 | errors = validate({ n: 'a' }, jsonSchema).errors; 44 | assert.equal(errors.length, 1); 45 | 46 | errors = validate({}, jsonSchema).errors; 47 | assert.equal(errors.length, 0); 48 | }); 49 | 50 | it('should build schema and validate strings by length', () => { 51 | let errors; 52 | 53 | const mSchema = new mongoose.Schema({ 54 | s: { type: String, minLength: 3, maxLength: 5 }, 55 | }); 56 | 57 | const jsonSchema = mSchema.jsonSchema(); 58 | 59 | assert.deepEqual(jsonSchema, { 60 | type: 'object', 61 | properties: { 62 | s: { 63 | type: 'string', 64 | minLength: 3, 65 | maxLength: 5, 66 | }, 67 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 68 | }, 69 | }); 70 | 71 | errors = validate({ s: 'abc' }, jsonSchema).errors; 72 | assert.equal(errors.length, 0); 73 | 74 | errors = validate({ s: 'abcd' }, jsonSchema).errors; 75 | assert.equal(errors.length, 0); 76 | 77 | errors = validate({ s: 'abcde' }, jsonSchema).errors; 78 | assert.equal(errors.length, 0); 79 | 80 | errors = validate({ s: 'ab' }, jsonSchema).errors; 81 | assert.equal(errors.length, 1); 82 | 83 | errors = validate({ s: '' }, jsonSchema).errors; 84 | assert.equal(errors.length, 1); 85 | 86 | errors = validate({ s: 'abcdef' }, jsonSchema).errors; 87 | assert.equal(errors.length, 1); 88 | 89 | errors = validate({ s: new Date() }, jsonSchema).errors; 90 | assert.equal(errors.length, 1); 91 | }); 92 | 93 | it('should build schema and validate strings with enum', () => { 94 | const mSchema = new mongoose.Schema({ 95 | s: { type: String, enum: ['abc', 'bac', 'cab'] }, 96 | }); 97 | 98 | const jsonSchema = mSchema.jsonSchema(); 99 | 100 | assert.deepEqual(jsonSchema, { 101 | type: 'object', 102 | properties: { 103 | s: { 104 | type: 'string', 105 | enum: ['abc', 'bac', 'cab'], 106 | }, 107 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 108 | }, 109 | }); 110 | 111 | let errors; 112 | errors = validate({ s: 'abc' }, jsonSchema).errors; 113 | assert.equal(errors.length, 0); 114 | 115 | errors = validate({ s: 'bac' }, jsonSchema).errors; 116 | assert.equal(errors.length, 0); 117 | 118 | errors = validate({ s: 'cab' }, jsonSchema).errors; 119 | assert.equal(errors.length, 0); 120 | 121 | errors = validate({ s: 'bca' }, jsonSchema).errors; 122 | assert.equal(errors.length, 1); 123 | 124 | errors = validate({ s: 'acb' }, jsonSchema).errors; 125 | assert.equal(errors.length, 1); 126 | 127 | errors = validate({ s: 123 }, jsonSchema).errors; 128 | assert.equal(errors.length, 2); 129 | 130 | errors = validate({ s: '' }, jsonSchema).errors; 131 | assert.equal(errors.length, 1); 132 | }); 133 | 134 | it('should build schema and validate strings with regExp', () => { 135 | const mSchema = new mongoose.Schema({ 136 | s: { type: String, match: /^(abc|bac|cab)$/ }, 137 | }); 138 | 139 | const jsonSchema = mSchema.jsonSchema(); 140 | 141 | assert.deepEqual(jsonSchema, { 142 | type: 'object', 143 | properties: { 144 | s: { 145 | type: 'string', 146 | pattern: '^(abc|bac|cab)$', 147 | }, 148 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 149 | }, 150 | }); 151 | 152 | let errors; 153 | errors = validate({ s: '(abc|bac|cab)' }, jsonSchema).errors; 154 | assert.equal(errors.length, 1); 155 | 156 | errors = validate({ s: 'abc' }, jsonSchema).errors; 157 | assert.equal(errors.length, 0); 158 | 159 | errors = validate({ s: 'ABC' }, jsonSchema).errors; 160 | assert.equal(errors.length, 1); 161 | 162 | errors = validate({ s: 'cba' }, jsonSchema).errors; 163 | assert.equal(errors.length, 1); 164 | 165 | errors = validate({ s: '' }, jsonSchema).errors; 166 | assert.equal(errors.length, 1); 167 | 168 | errors = validate({ s: 12 }, jsonSchema).errors; 169 | assert.equal(errors.length, 1); 170 | 171 | errors = validate({ _id: '^[0-9a-fA-F]{24}$' }, jsonSchema).errors; 172 | assert.equal(errors.length, 1); 173 | 174 | errors = validate({ _id: 'Hello World' }, jsonSchema).errors; 175 | assert.equal(errors.length, 1); 176 | 177 | errors = validate({ _id: '564e0da0105badc887ef1d3e' }, jsonSchema).errors; 178 | assert.equal(errors.length, 0); 179 | }); 180 | 181 | it('should build schema and validate strings with regExp (Hello world)', () => { 182 | const mSchema = new mongoose.Schema({ 183 | s: { type: String, match: 'Hello world!' }, 184 | }); 185 | 186 | const jsonSchema = mSchema.jsonSchema(); 187 | 188 | assert.deepEqual(jsonSchema, { 189 | type: 'object', 190 | properties: { 191 | s: { 192 | type: 'string', 193 | pattern: 'Hello world!', 194 | }, 195 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 196 | }, 197 | }); 198 | 199 | let errors; 200 | errors = validate({ s: 'Hello world!' }, jsonSchema).errors; 201 | assert.equal(errors.length, 0); 202 | 203 | errors = validate({ s: '(abc|bac|cab)' }, jsonSchema).errors; 204 | assert.equal(errors.length, 1); 205 | 206 | errors = validate({ s: 'abc' }, jsonSchema).errors; 207 | assert.equal(errors.length, 1); 208 | 209 | errors = validate({ s: 'ABC' }, jsonSchema).errors; 210 | assert.equal(errors.length, 1); 211 | 212 | errors = validate({ s: 'cba' }, jsonSchema).errors; 213 | assert.equal(errors.length, 1); 214 | 215 | errors = validate({ s: '' }, jsonSchema).errors; 216 | assert.equal(errors.length, 1); 217 | 218 | errors = validate({ s: 12 }, jsonSchema).errors; 219 | assert.equal(errors.length, 1); 220 | 221 | errors = validate({ _id: '564e0da0105badc887ef1d3e' }, jsonSchema).errors; 222 | assert.equal(errors.length, 0); 223 | }); 224 | 225 | it('should build schema and validate arrays with**out** minItems constraint', () => { 226 | const mSchema = mongoose.Schema({ 227 | a: [{ 228 | type: Number, 229 | required: true, 230 | }], 231 | }); 232 | 233 | const jsonSchema = mSchema.jsonSchema(); 234 | 235 | assert.deepEqual(jsonSchema, { 236 | type: 'object', 237 | properties: { 238 | a: { 239 | type: 'array', 240 | items: { type: 'number' }, 241 | }, 242 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 243 | }, 244 | }); 245 | 246 | let errors; 247 | errors = validate({ a: [0, 1] }, jsonSchema).errors; 248 | assert.equal(errors.length, 0); 249 | 250 | errors = validate({ a: [0] }, jsonSchema).errors; 251 | assert.equal(errors.length, 0); 252 | 253 | errors = validate({}, jsonSchema).errors; 254 | assert.equal(errors.length, 0); 255 | 256 | errors = validate({ a: [] }, jsonSchema).errors; 257 | assert.equal(errors.length, 0); 258 | 259 | errors = validate({ a: [0, 1, 'a'] }, jsonSchema).errors; 260 | assert.equal(errors.length, 1); 261 | }); 262 | 263 | it('should build schema and validate mixed', () => { 264 | const mSchema = new mongoose.Schema({ 265 | m: { type: mongoose.Schema.Types.Mixed, required: true, default: {} }, 266 | }); 267 | 268 | const jsonSchema = mSchema.jsonSchema(); 269 | 270 | assert.deepEqual(jsonSchema, { 271 | type: 'object', 272 | properties: { 273 | m: { }, 274 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 275 | }, 276 | required: ['m'], 277 | }); 278 | 279 | let errors; 280 | errors = validate({ m: 3 }, jsonSchema).errors; 281 | assert.equal(errors.length, 0); 282 | 283 | errors = validate({ m: null }, jsonSchema).errors; 284 | assert.equal(errors.length, 0); 285 | 286 | errors = validate({ m: {} }, jsonSchema).errors; 287 | assert.equal(errors.length, 0); 288 | 289 | errors = validate({ m: 'Hello world' }, jsonSchema).errors; 290 | assert.equal(errors.length, 0); 291 | 292 | errors = validate({ m: '' }, jsonSchema).errors; 293 | assert.equal(errors.length, 0); 294 | 295 | errors = validate({ m: true }, jsonSchema).errors; 296 | assert.equal(errors.length, 0); 297 | 298 | errors = validate({ m: false }, jsonSchema).errors; 299 | assert.equal(errors.length, 0); 300 | 301 | errors = validate({ }, jsonSchema).errors; 302 | assert.equal(errors.length, 1); 303 | 304 | errors = validate({ s: '13234' }, jsonSchema).errors; 305 | assert.equal(errors.length, 1); 306 | }); 307 | 308 | it('should build schema and validate mixed with description', () => { 309 | const mSchema = new mongoose.Schema({ 310 | m: { 311 | type: mongoose.Schema.Types.Mixed, 312 | descr: 'some mixed content here', 313 | required: true, 314 | default: {}, 315 | }, 316 | }); 317 | 318 | const jsonSchema = mSchema.jsonSchema(); 319 | 320 | assert.deepEqual(jsonSchema, { 321 | type: 'object', 322 | properties: { 323 | m: { 324 | description: 'some mixed content here', 325 | }, 326 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 327 | }, 328 | required: ['m'], 329 | }); 330 | 331 | let errors; 332 | errors = validate({ m: 3 }, jsonSchema).errors; 333 | assert.equal(errors.length, 0); 334 | 335 | errors = validate({ m: null }, jsonSchema).errors; 336 | assert.equal(errors.length, 0); 337 | 338 | errors = validate({ m: {} }, jsonSchema).errors; 339 | assert.equal(errors.length, 0); 340 | 341 | errors = validate({ m: 'Hello world' }, jsonSchema).errors; 342 | assert.equal(errors.length, 0); 343 | 344 | errors = validate({ m: '' }, jsonSchema).errors; 345 | assert.equal(errors.length, 0); 346 | 347 | errors = validate({ m: true }, jsonSchema).errors; 348 | assert.equal(errors.length, 0); 349 | 350 | errors = validate({ m: false }, jsonSchema).errors; 351 | assert.equal(errors.length, 0); 352 | 353 | errors = validate({ }, jsonSchema).errors; 354 | assert.equal(errors.length, 1); 355 | 356 | errors = validate({ s: '13234' }, jsonSchema).errors; 357 | assert.equal(errors.length, 1); 358 | }); 359 | 360 | it('should build schema and validate mixed with description and title', () => { 361 | const mSchema = new mongoose.Schema({ 362 | m: { 363 | title: 'MegaField', 364 | type: mongoose.Schema.Types.Mixed, 365 | descr: 'some mixed content here', 366 | default: {}, 367 | }, 368 | }); 369 | 370 | const jsonSchema = mSchema.jsonSchema(); 371 | 372 | assert.deepEqual(jsonSchema, { 373 | type: 'object', 374 | properties: { 375 | m: { 376 | title: 'MegaField', 377 | description: 'some mixed content here', 378 | }, 379 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 380 | }, 381 | }); 382 | 383 | let errors; 384 | errors = validate({ m: 3 }, jsonSchema).errors; 385 | assert.equal(errors.length, 0); 386 | 387 | errors = validate({ m: null }, jsonSchema).errors; 388 | assert.equal(errors.length, 0); 389 | 390 | errors = validate({ m: {} }, jsonSchema).errors; 391 | assert.equal(errors.length, 0); 392 | 393 | errors = validate({ m: 'Hello world' }, jsonSchema).errors; 394 | assert.equal(errors.length, 0); 395 | 396 | errors = validate({ m: '' }, jsonSchema).errors; 397 | assert.equal(errors.length, 0); 398 | 399 | errors = validate({ m: true }, jsonSchema).errors; 400 | assert.equal(errors.length, 0); 401 | 402 | errors = validate({ m: false }, jsonSchema).errors; 403 | assert.equal(errors.length, 0); 404 | 405 | errors = validate({ }, jsonSchema).errors; 406 | assert.equal(errors.length, 0); 407 | 408 | errors = validate({ s: '13234' }, jsonSchema).errors; 409 | assert.equal(errors.length, 0); 410 | }); 411 | 412 | it('should work with nullable types', () => { 413 | const mSchema = new mongoose.Schema({ 414 | x: { type: Number, default: null }, 415 | y: { type: Number, default: 1 }, 416 | }); 417 | 418 | const jsonSchema = mSchema.jsonSchema(); 419 | 420 | assert.deepEqual(jsonSchema, { 421 | type: 'object', 422 | properties: { 423 | x: { type: ['number', 'null'], default: null }, 424 | y: { type: 'number', default: 1 }, 425 | _id: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, 426 | }, 427 | }); 428 | 429 | let errors; 430 | errors = validate({ y: 3 }, jsonSchema).errors; 431 | assert.equal(errors.length, 0); 432 | 433 | errors = validate({ y: 3, x: null }, jsonSchema).errors; 434 | assert.equal(errors.length, 0); 435 | 436 | errors = validate({ y: 3, x: 2 }, jsonSchema).errors; 437 | assert.equal(errors.length, 0); 438 | 439 | errors = validate({ y: null }, jsonSchema).errors; 440 | assert.equal(errors.length, 1); 441 | 442 | errors = validate({ }, jsonSchema).errors; 443 | assert.equal(errors.length, 0); 444 | 445 | errors = validate({ x: null }, jsonSchema).errors; 446 | assert.equal(errors.length, 0); 447 | }); 448 | 449 | it('should build schema and validate Map types', () => { 450 | const mSchema = new mongoose.Schema({ 451 | m: { type: Map, required: true }, 452 | }, { _id: false }); 453 | 454 | const jsonSchema = mSchema.jsonSchema(); 455 | 456 | assert.deepEqual(jsonSchema, { 457 | type: 'object', 458 | properties: { 459 | m: { 460 | type: 'object', 461 | additionalProperties: true, 462 | }, 463 | }, 464 | required: ['m'], 465 | }); 466 | 467 | const isValid = data => validate(data, jsonSchema).errors.length === 0; 468 | 469 | assert.ok(isValid({ m: { x: 1, y: 'string' } })); 470 | assert.ok(isValid({ m: { } })); 471 | 472 | assert.ok(!isValid({ y: null })); 473 | }); 474 | 475 | it('should build schema and validate Map types (with subschema)', () => { 476 | const mSchema = new mongoose.Schema({ 477 | m: { type: Map, required: true, of: { type: String } }, 478 | }, { _id: false }); 479 | 480 | const jsonSchema = mSchema.jsonSchema(); 481 | 482 | assert.deepEqual(jsonSchema, { 483 | type: 'object', 484 | properties: { 485 | m: { 486 | type: 'object', 487 | additionalProperties: { 488 | type: 'string', 489 | }, 490 | }, 491 | }, 492 | required: ['m'], 493 | }); 494 | 495 | const isValid = data => validate(data, jsonSchema).errors.length === 0; 496 | 497 | assert.ok(isValid({ m: { x: 'x', y: 'y' } })); 498 | assert.ok(isValid({ m: { } })); 499 | 500 | assert.ok(!isValid({ m: { x: 1, y: 'string' } })); 501 | assert.ok(!isValid({ y: null })); 502 | }); 503 | }); 504 | 505 | describe('Validation: model.jsonSchema()', () => { 506 | it('should process flat schema and -- validate correct entity', () => { 507 | const jsonSchema = models.Person.jsonSchema(); 508 | 509 | const validPerson = { 510 | firstName: 'John', 511 | lastName: 'Smith', 512 | email: 'john.smith@mail.net', 513 | }; 514 | const aPerson = new models.Person(validPerson); 515 | assert.ok(!aPerson.validateSync()); 516 | assert.equal(validate(validPerson, jsonSchema).errors.length, 0); 517 | }); 518 | 519 | it('should process flat schema and -- mark invalid entity', () => { 520 | const jsonSchema = models.Person.jsonSchema(); 521 | 522 | const invalidPerson = { 523 | firstName: 'John', 524 | lastName: 'Smith', 525 | email: 12, 526 | }; 527 | const aPerson = new models.Person(invalidPerson); 528 | assert.ok(aPerson.validateSync()); 529 | assert.equal(validate(invalidPerson, jsonSchema).errors.length, 1); 530 | }); 531 | }); 532 | -------------------------------------------------------------------------------- /version.sh: -------------------------------------------------------------------------------- 1 | PACKAGE_NAME=$(npm pkg get name | tr -d '"') 2 | PACKAGE_VERSION=$(npm pkg get version | tr -d '"') 3 | PACKAGE=$PACKAGE_NAME@$PACKAGE_VERSION 4 | 5 | PUBLISHED_VERSION=$(npm view $PACKAGE version || echo "") 6 | if [[ -n $PUBLISHED_VERSION ]]; then echo "The version is already published"; exit 1; fi 7 | --------------------------------------------------------------------------------