├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── lib
└── index.js
├── package.json
└── test
├── index.js
├── mocha.opts
├── schemas
├── constraints.js
├── nested.js
└── types.js
└── support
└── node.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | typings
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | before_install:
2 | - npm install -g mocha istanbul coveralls
3 |
4 | language: node_js
5 |
6 | node_js:
7 | - "4"
8 | - "6"
9 | - "7"
10 |
11 | sudo: false
12 |
13 | script:
14 | - "test -z $(npm -g -ps ls istanbul) || npm run-script test-travis"
15 |
16 | after_script:
17 | - "test -e ./coverage/lcov.info && cat ./coverage/lcov.info | coveralls"
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changes
2 |
3 | ## 0.3.0 / 2017-01-31
4 |
5 | * Added Mixed type support ([@tsturzl](https://github.com/tsturzl))
6 |
7 | ## 0.2.0 / 2016-11-14
8 |
9 | * Add ObjectIds to JSON exports ([@creynders](https://github.com/creynders))
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015-17 Vlad Stirbu
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 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mongoose schema to JSON schema and back
2 |
3 | [](https://badge.fury.io/js/mongoose-jsonschema)
4 | [](https://travis-ci.org/vstirbu/mongoose-jsonschema)
5 | [](https://coveralls.io/github/vstirbu/mongoose-jsonschema?branch=master)
6 |
7 | ## Motivation
8 |
9 | This library represents a practical approach to convert the schemas used in a Mongoose model so that they can conveyed to hypermedia clients that interact with the web service.
10 |
11 | ## Installation
12 |
13 | ```
14 | npm install mongoose-jsonschema
15 | ```
16 |
17 | ## Usage
18 |
19 | ### Adding hypermedia controls in the Mongoose model
20 |
21 | A Mongoose model should be augmented so that the schema options contains a JSON tranformation function:
22 |
23 | ```javascript
24 | var mongoose = require('mongoose');
25 |
26 | var schema = new mongoose.Schema({
27 | ...
28 | }, {
29 | toJSON: {
30 | transform: function (doc, ret, options) {
31 | ret._links = {
32 | describedBy: {
33 | href: '/meta/schemas/example'
34 | }
35 | };
36 | }
37 | }
38 | });
39 |
40 | var model = mongoose.model('Example', schema);
41 | ```
42 |
43 | Now, every time the model is converted to JSON, the representation will convey to the client the link that describes the schema of the document. The representation uses the [HAL](http://stateless.co/hal_specification.html) convention but other san be used as well.
44 |
45 | ### Exposing the schemas
46 |
47 | ```javascript
48 | var express = require('express'),
49 | mongoose = require('mongoose'),
50 | jsonSchema = require('mongoose-jsonschema').modelToJSONSchema;
51 |
52 | var app = express();
53 |
54 | app.get('/meta/schemas/:schema', function (req, res) {
55 | res.set({
56 | 'Content-Type': 'application/schema+json'
57 | }).send(jsonSchema(mongoose.model(req.params.schema))).end();
58 | });
59 | ```
60 |
61 | ## API
62 |
63 | ### modelToJSONSchema(model, options) ⇒ Object
⏏
64 | **Kind**: Exported function
65 | **Returns**: Object
- JSONSchema
66 |
67 | | Param | Type | Description |
68 | | --- | --- | --- |
69 | | model | object
| Mongoose model to be converted |
70 | | options | object
| Options for customising model conversion |
71 | | options.reserved | Array.<string>
| object
| Model properties found in the array are not included in the schema or map of properties to be converted |
72 | | options.reserved.property | boolean
| Include/do not include model `property` into schema |
73 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A module for converting Mongoose schema to JSON schema.
3 | * @module mongoose-jsonschema
4 | */
5 |
6 | module.exports = {
7 | modelToJSONSchema: modelToJSONSchema
8 | };
9 |
10 | /**
11 | * @alias module:mongoose-jsonschema
12 | * @param {object} model Mongoose model to be converted
13 | * @param {object} options Options for customising model conversion
14 | * @param {string[]|object} options.reserved - Model properties found in the array are not included in the schema or map of properties to be converted
15 | * @param {boolean} options.reserved.property - Include/do not include model `property` into schema
16 | * @returns {Object} JSONSchema
17 | */
18 | function modelToJSONSchema(model, options) {
19 | 'use strict';
20 |
21 | var schema = model.schema,
22 | reserved = {
23 | '_id': true,
24 | '__v': true
25 | },
26 | result = {
27 | $schema: 'http://json-schema.org/schema#',
28 | title: model.modelName,
29 | type: 'object',
30 | properties: {},
31 | required: schema.requiredPaths().filter(function (requiredProp) {
32 | return requiredProp.indexOf('.') === -1;
33 | })
34 | };
35 |
36 | options = options || {};
37 |
38 | if (options.reserved) {
39 | if (Array.isArray(options.reserved)) {
40 | options.reserved.forEach(function(r) {
41 | reserved[r] = true;
42 | });
43 | } else {
44 | reserved = Object.assign(reserved, options.reserved);
45 | }
46 | }
47 |
48 | if (result.required.length === 0) {
49 | delete result.required;
50 | }
51 |
52 | schema.eachPath(function (path) {
53 | if (!reserved[path]) {
54 | var property = {};
55 |
56 | var frags = path.split('.'),
57 | name = frags.pop();
58 |
59 | var pathOptions = schema.path(path).options;
60 |
61 | switch (pathOptions.type.name) {
62 | case 'Array': // untyped array
63 | case 'Boolean':
64 | case 'Number':
65 | case 'Object':
66 | case 'String':
67 | property.type = pathOptions.type.name.toLowerCase();
68 | break;
69 | case 'Date':
70 | property.type = 'string';
71 | property.format = 'date-time';
72 | break;
73 | case 'ObjectId':
74 | property.type = 'string';
75 | break;
76 | default:
77 | // mixed type
78 | if(pathOptions.type.schemaName) {
79 | property.type = 'object';
80 | }
81 |
82 | // typed array
83 | if (Array.isArray(pathOptions.type)) {
84 | property.type = 'array';
85 | property.items = {
86 | type: schema.path(path).casterConstructor.name.toLowerCase()
87 | };
88 | }
89 | }
90 |
91 | if (pathOptions.enum) {
92 | property.enum = pathOptions.enum;
93 | }
94 |
95 | if (pathOptions.default) {
96 | property.default = pathOptions.default;
97 | }
98 |
99 | //console.log(schema.path(path));
100 | var properties = result.properties;
101 |
102 | frags.forEach(function (frag, index) {
103 | properties[frag] = properties[frag] || {
104 | title: frag,
105 | type: 'object',
106 | properties: {}
107 | };
108 |
109 | if (index === (frags.length - 1) && schema.requiredPaths().indexOf(frags.join('.') + '.' + name) !== -1) {
110 | properties[frag].required = properties[frag].required || [];
111 | properties[frag].required.push(name);
112 | }
113 |
114 | properties = properties[frag].properties;
115 | });
116 |
117 | properties[name] = property;
118 | }
119 | });
120 |
121 | return result;
122 | }
123 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mongoose-jsonschema",
3 | "version": "0.3.0",
4 | "description": "Mongoose schema to JSON schema and back",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "test": "mocha",
8 | "test-cov": "istanbul cover _mocha -- -R spec --check-leaks",
9 | "test-travis": "istanbul cover _mocha --report lcovonly -- --reporter dot"
10 | },
11 | "keywords": [
12 | "mongoose",
13 | "schema",
14 | "json"
15 | ],
16 | "author": "Vlad Stirbu ",
17 | "license": "MIT",
18 | "dependencies": {
19 | },
20 | "devDependencies": {
21 | "chai": "^3.5.0",
22 | "express": "^4.14.0",
23 | "mocha": "^3.1.0",
24 | "mongoose": "^4.6.1"
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "https://github.com/vstirbu/mongoose-jsonschema.git"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | var chai = require('chai'),
6 | mongoose = require('mongoose'),
7 |
8 | lib = require('../'),
9 |
10 | expect = chai.expect;
11 |
12 | describe('modelToJSONSchema', function () {
13 | it('should convert supported types', function () {
14 | var jsonSchema = lib.modelToJSONSchema(mongoose.model('Types'));
15 |
16 | expect(jsonSchema.properties.arrayProp.type).to.be.equal('array');
17 |
18 | expect(jsonSchema.properties.arrayTypedProp.type).to.be.equal('array');
19 | expect(jsonSchema.properties.arrayTypedProp.items).to.be.deep.equal({
20 | type: 'object'
21 | });
22 |
23 | expect(jsonSchema.properties.mixedProp.type).to.be.equal('object');
24 |
25 | expect(jsonSchema.properties.objectId.type).to.be.equal('string');
26 |
27 | expect(jsonSchema.properties.booleanProp.type).to.be.equal('boolean');
28 |
29 | expect(jsonSchema.properties.numberProp.type).to.be.equal('number');
30 |
31 | expect(jsonSchema.properties.objectProp.type).to.be.equal('object');
32 |
33 | expect(jsonSchema.properties.stringProp.type).to.be.equal('string');
34 |
35 | expect(jsonSchema.properties.dateProp.type).to.be.equal('string');
36 | expect(jsonSchema.properties.dateProp.format).to.be.equal('date-time');
37 | });
38 |
39 | it('should convert constraints', function (done) {
40 | var jsonSchema = lib.modelToJSONSchema(mongoose.model('Constraints'));
41 |
42 | expect(jsonSchema.properties.simpleProp).to.exist;
43 |
44 | expect(jsonSchema.properties.requiredProp).to.exist;
45 | expect(jsonSchema.required).to.be.deep.equal(['requiredProp']);
46 |
47 | expect(jsonSchema.properties.enumedProp).to.exist;
48 | expect(jsonSchema.properties.enumedProp.enum).to.be.deep.equal(['one', 'two']);
49 |
50 | expect(jsonSchema.properties.defaultProp).to.exist;
51 | expect(jsonSchema.properties.defaultProp.default).to.be.equal('default-value');
52 |
53 | done();
54 | });
55 |
56 | it('should convert nested schema', function () {
57 | var jsonSchema = lib.modelToJSONSchema(mongoose.model('Nested'));
58 |
59 | expect(jsonSchema.properties.root.properties).to.exist;
60 | expect(jsonSchema.properties.root.properties.nestedProp).to.exist;
61 | expect(jsonSchema.properties.root.required).to.be.deep.equal(['nestedProp']);
62 | });
63 |
64 | describe('options', function(){
65 | describe('reserved', function(){
66 | it('should filter out fields provided as an array', function(){
67 | var jsonSchema = lib.modelToJSONSchema(mongoose.model('Types'), {
68 | reserved: ['stringProp']
69 | });
70 | expect(jsonSchema.properties.stringProp).to.be.undefined;
71 | });
72 | it('should filter out fields as defined in a flag map', function(){
73 | var jsonSchema = lib.modelToJSONSchema(mongoose.model('Types'), {
74 | reserved: {
75 | stringProp: true,
76 | _id: false
77 | }
78 | });
79 | expect(jsonSchema.properties.stringProp).to.be.undefined;
80 | expect(jsonSchema.properties._id).to.exist;
81 | });
82 | })
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --reporter list
2 | --require test/support/node.js
3 |
--------------------------------------------------------------------------------
/test/schemas/constraints.js:
--------------------------------------------------------------------------------
1 | var mongoose = require('mongoose');
2 |
3 | var schema = new mongoose.Schema({
4 | simpleProp: {
5 | type: String
6 | },
7 | requiredProp: {
8 | type: String,
9 | required: true
10 | },
11 | enumedProp: {
12 | type: String,
13 | enum: ['one', 'two']
14 | },
15 | defaultProp: {
16 | type: String,
17 | default: 'default-value'
18 | }
19 | });
20 |
21 | var model = mongoose.model('Constraints', schema);
22 |
23 | module.exports = model;
24 |
--------------------------------------------------------------------------------
/test/schemas/nested.js:
--------------------------------------------------------------------------------
1 | var mongoose = require('mongoose');
2 |
3 | var schema = new mongoose.Schema({
4 | root: {
5 | nestedProp: {
6 | type: String,
7 | required: true
8 | }
9 | }
10 | });
11 |
12 | var model = mongoose.model('Nested', schema);
13 |
14 | module.exports = model;
15 |
--------------------------------------------------------------------------------
/test/schemas/types.js:
--------------------------------------------------------------------------------
1 | var mongoose = require('mongoose');
2 |
3 | var schema = new mongoose.Schema({
4 | arrayProp: Array,
5 | arrayTypedProp: [{
6 | type: Object
7 | }],
8 | mixedProp: {type: mongoose.Schema.Types.Mixed},
9 | objectId: {type: mongoose.Schema.Types.ObjectId},
10 | booleanProp: Boolean,
11 | dateProp: Date,
12 | numberProp: Number,
13 | objectProp: Object,
14 | stringProp: String
15 | });
16 |
17 | var model = mongoose.model('Types', schema);
18 |
19 | module.exports = model;
20 |
--------------------------------------------------------------------------------
/test/support/node.js:
--------------------------------------------------------------------------------
1 | /* global global */
2 | require('../schemas/constraints');
3 | require('../schemas/nested');
4 | require('../schemas/types');
--------------------------------------------------------------------------------