├── .gitignore ├── .jshintrc ├── .node-version ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── index.js ├── lib ├── domain │ ├── collection-schema-factory.js │ ├── collection-schema.js │ ├── extended-validation.js │ ├── indexes.js │ ├── item-schema.js │ ├── json-schema-validator.js │ ├── links.js │ ├── model-properties-converter.js │ ├── models.js │ ├── readonly-default-values-handler.js │ ├── registry-models.js │ ├── relation-schema.js │ └── relations.js ├── http │ ├── create-location-hook.js │ ├── item-schema-hooks.js │ ├── json-schema-routes.js │ ├── ljs-request.js │ ├── ljs-url.js │ ├── location-header-correlator.js │ ├── register-loopback-model.js │ ├── register-loopback-model.middleware.js │ ├── schema-correlator-hooks.js │ ├── schema-correlator.js │ ├── schema-link-rewriter.js │ └── validate-request.middleware.js └── support │ ├── config.js │ └── logger.js ├── package-lock.json ├── package.json └── test ├── acceptance ├── get-collection-schema.test.js ├── get-collection.test.js ├── get-item-schema.test.js ├── get-item-schemas.test.js ├── get-item.test.js ├── middleware │ ├── json-schema.middleware.test.js │ └── register-loopback-model.middleware.test.js ├── options-collection-schema.test.js ├── post-item-schema.test.js ├── post-item.test.js ├── put-item.test.js └── relation-schema.test.js ├── integration ├── domain │ ├── collection-schema-factory.test.js │ ├── collection-schema.test.js │ └── item-schema.test.js └── http │ └── register-loopback-model.test.js ├── support.js └── unit ├── domain ├── extended-validation.test.js ├── item-schema.test.js ├── json-schema-validator.test.js ├── links.test.js ├── model-properties-converter.test.js ├── models.test.js ├── readonly-default-values-handler.test.js ├── registry-models.test.js └── relations.test.js ├── http ├── ljs-request.test.js ├── ljs-url.test.js ├── location-header-correlator.test.js ├── schema-correlator.test.js └── schema-link-rewriter.test.js └── loopback-jsonschema.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | *.orig 11 | 12 | pids 13 | logs 14 | results 15 | 16 | npm-debug.log 17 | node_modules 18 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "boss": false, 3 | "browser": true, 4 | "camelcase": true, 5 | "curly": true, 6 | "eqeqeq": true, 7 | "eqnull": false, 8 | "expr": true, 9 | "jquery": true, 10 | "mocha": true, 11 | "multistr": true, 12 | "newcap": true, 13 | "node": true, 14 | "predef": ["xit", "xdescribe"], 15 | "quotmark": true, 16 | "strict": false, 17 | "sub": true, 18 | "trailing": true, 19 | "undef": true, 20 | "unused": true 21 | } 22 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 4.8.4 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | cache: 4 | directories: 5 | - node_modules 6 | node_js: 7 | - "4.8.4" 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Globo.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo ' setup ....................................... sets up project' 3 | @echo ' clean ....................................... cleans project' 4 | @echo ' test ........................................ runs tests' 5 | @echo ' test-verbose ................................ runs tests with spec reporter' 6 | @echo ' debug ....................................... runs tests with debug enable' 7 | @echo ' testing ..................................... runs tests continuously on file changes' 8 | @echo ' bump_patch_version .......................... bumps patch version' 9 | @echo ' bump_minor_version .......................... bumps minor version' 10 | @echo ' bump_major_version .......................... bumps major version' 11 | 12 | link: 13 | npm link ../JSV 14 | 15 | unlink: 16 | npm unlink ../JSV 17 | 18 | setup: 19 | npm install 20 | 21 | clean: 22 | rm -rf node_modules 23 | 24 | TESTER = ./node_modules/.bin/mocha 25 | OPTS = -G --recursive 26 | TESTS = test/**/*.test.js 27 | 28 | test: 29 | $(TESTER) $(OPTS) "$(TESTS)" 30 | test-verbose: 31 | $(TESTER) $(OPTS) --reporter spec "$(TESTS)" 32 | testing: 33 | $(TESTER) $(OPTS) --watch "$(TESTS)" 34 | debug: 35 | $(TESTER) $(OPTS) --debug-brk --watch "$(TESTS)" 36 | 37 | .PHONY: test 38 | 39 | # info on setting up artifactory https://artifactory.globoi.com/static/repo_pkg.html 40 | bump_%_artifactory_version: 41 | -npm version $* --allow-same-version 42 | git push origin master 43 | git push origin --tags 44 | npm publish --registry https://artifactory.globoi.com/artifactory/api/npm/npm-local 45 | 46 | 47 | bump_%_version: bump_%_artifactory_version 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Loopback JSON Schema [![Build Status](https://travis-ci.org/backstage/loopback-jsonschema.png?branch=master)](https://travis-ci.org/backstage/loopback-jsonschema) 2 | 3 | DEPRECATED. This project does not receive active maintenance. 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | 4 | var loopback = require('loopback'); 5 | 6 | var config = require('./lib/support/config'); 7 | var ItemSchema = require('./lib/domain/item-schema'); 8 | var Relations = require('./lib/domain/relations'); 9 | var registerLoopbackModelMiddleware = require('./lib/http/register-loopback-model.middleware'); 10 | var validateRequestMiddleware = require('./lib/http/validate-request.middleware'); 11 | var schemaCorrelatorHooks = require('./lib/http/schema-correlator-hooks'); 12 | var createLocationHook = require('./lib/http/create-location-hook'); 13 | var locationHeaderCorrelator = require('./lib/http/location-header-correlator'); 14 | var jsonSchemaRoutes = require('./lib/http/json-schema-routes'); 15 | var ItemSchemaHooks = require('./lib/http/item-schema-hooks'); 16 | var schemaCorrelator = require('./lib/http/schema-correlator'); 17 | var RegistryModels = require('./lib/domain/registry-models'); 18 | 19 | var loopbackJsonSchema = module.exports = {}; 20 | loopbackJsonSchema.init = function(app, customConfig) { 21 | _.extend(config, customConfig); 22 | var remoting = app.get('remoting') || {}; 23 | remoting.json = remoting.json || {}; 24 | remoting.json.type = ['json', '+json']; 25 | app.set('remoting', remoting); 26 | 27 | // save app pointer 28 | ItemSchema.app = app; 29 | 30 | var relations = Relations.init(app); 31 | relations.bindAfterRemoteHook('hasMany', 'create', function correlateLocationHeader(relationCtx, ctx, result, next) { 32 | locationHeaderCorrelator(ctx, result, next); 33 | }); 34 | 35 | var schemaHook = function correlateInstance(relationCtx, ctx, result, next) { 36 | schemaCorrelator.instance(relationCtx.toPluralModelName, ctx, result, next); 37 | }; 38 | 39 | relations.bindAfterRemoteHook('belongsTo', 'get', schemaHook); 40 | relations.bindAfterRemoteHook('hasMany', 'get', function correlateCollection (relationCtx, ctx, result, next) { 41 | schemaCorrelator.collection(relationCtx.toPluralModelName, ctx, result, next); 42 | }); 43 | relations.bindAfterRemoteHook('hasMany', 'create', schemaHook); 44 | relations.bindAfterRemoteHook('hasMany', 'findById', schemaHook); 45 | relations.bindAfterRemoteHook('hasMany', 'updateById', schemaHook); 46 | 47 | 48 | ItemSchema.app._registeredLoopbackHooks = {}; 49 | 50 | // start with default hooks 51 | ItemSchema.remoteHookInitializers = ItemSchema.defaultRemoteHookInitializers.slice(0); 52 | ItemSchema.registerRemoteHookInitializers([ 53 | createLocationHook, 54 | schemaCorrelatorHooks 55 | ]); 56 | 57 | var db = dataSource(app); 58 | ItemSchema.attachTo(db); 59 | new RegistryModels().appendModelV1(ItemSchema.modelName, ItemSchema); 60 | app.model(ItemSchema); 61 | ItemSchemaHooks.initialize(); 62 | 63 | var restApiRoot = app.get('restApiRoot') || '/api'; 64 | var middlewares = [ 65 | validateRequestMiddleware(app) 66 | ]; 67 | 68 | if (config.registerItemSchemaAtRequest) { 69 | middlewares.push(registerLoopbackModelMiddleware(app)); 70 | } else { 71 | // load all item schemas at boot 72 | ItemSchema.preLoadModels(); 73 | } 74 | 75 | app.use(restApiRoot, middlewares); 76 | }; 77 | 78 | loopbackJsonSchema.enableJsonSchemaMiddleware = function(app) { 79 | var corsOptions = (app.get('remoting') && app.get('remoting').cors) || {}; 80 | var restApiRoot = app.get('restApiRoot') || '/api'; 81 | 82 | app.use(restApiRoot, [ 83 | jsonSchemaRoutes.drawRouter(corsOptions) 84 | ]); 85 | }; 86 | 87 | loopbackJsonSchema.CollectionSchema = require('./lib/domain/collection-schema'); 88 | loopbackJsonSchema.LJSRequest = require('./lib/http/ljs-request'); 89 | loopbackJsonSchema.LJSUrl = require('./lib/http/ljs-url'); 90 | loopbackJsonSchema.indexes = require('./lib/domain/indexes'); 91 | loopbackJsonSchema.schemaLinkRewriter = require('./lib/http/schema-link-rewriter'); 92 | loopbackJsonSchema.schemaCorrelator = require('./lib/http/schema-correlator'); 93 | loopbackJsonSchema.locationHeaderCorrelator = require('./lib/http/location-header-correlator'); 94 | loopbackJsonSchema.RegistryModels = require('./lib/domain/registry-models'); 95 | 96 | loopbackJsonSchema.Relations = Relations; 97 | loopbackJsonSchema.ItemSchema = ItemSchema; 98 | loopbackJsonSchema.Links = require('./lib/domain/links'); 99 | 100 | function dataSource(app) { 101 | return app.dataSources.loopbackJsonSchemaDb || loopback.memory(); 102 | } 103 | -------------------------------------------------------------------------------- /lib/domain/collection-schema-factory.js: -------------------------------------------------------------------------------- 1 | var config = require('../support/config'); 2 | var ItemSchema = require('./item-schema'); 3 | var logger = require('../support/logger'); 4 | 5 | function CollectionSchemaFactory() {} 6 | 7 | CollectionSchemaFactory.buildFromSchemaId = function(schemaId, navigationRoot, callback) { 8 | ItemSchema.findById(schemaId, function(err, itemSchema) { 9 | if (err) { return callback(err); } 10 | 11 | if (itemSchema === null) { 12 | logger.info("Item Schema for schemaId", schemaId, "not found."); 13 | return callback(null, null); 14 | } 15 | 16 | var collectionSchema = new config.CollectionSchemaClass(itemSchema, navigationRoot); 17 | 18 | callback(null, collectionSchema); 19 | }); 20 | }; 21 | 22 | module.exports = CollectionSchemaFactory; 23 | -------------------------------------------------------------------------------- /lib/domain/collection-schema.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('json-schema'); 2 | 3 | var Links = require('./links'); 4 | var logger = require('../support/logger'); 5 | 6 | function CollectionSchema(itemSchema, navigationRoot) { 7 | this.itemSchema = itemSchema; 8 | this.navigationRoot = navigationRoot || ''; 9 | } 10 | 11 | CollectionSchema.pluralModelName = 'collection-schemas'; 12 | 13 | CollectionSchema.prototype.type = function() { 14 | return 'array'; 15 | }; 16 | 17 | CollectionSchema.prototype.items = function(itemSchemaUrl) { 18 | return { $ref: itemSchemaUrl }; 19 | }; 20 | 21 | CollectionSchema.prototype.properties = function(itemSchemaUrl) { 22 | return null; 23 | }; 24 | 25 | CollectionSchema.prototype.defaultLinks = function(itemSchemaUrl) { 26 | var collectionUrl = this.itemSchema.collectionUrl(); 27 | var collectionNavigationUrl = collectionUrl + this.navigationRoot; 28 | 29 | return [ 30 | { 31 | rel: 'self', 32 | href: collectionNavigationUrl 33 | }, 34 | { 35 | rel: 'list', 36 | href: collectionNavigationUrl 37 | }, 38 | { 39 | rel: 'add', 40 | method: 'POST', 41 | href: collectionUrl, 42 | schema: { 43 | $ref: itemSchemaUrl 44 | } 45 | }, 46 | { 47 | rel: 'previous', 48 | href: collectionNavigationUrl + '?filter[limit]={limit}&filter[offset]={previousOffset}{&paginateQs*}' 49 | }, 50 | { 51 | rel: 'next', 52 | href: collectionNavigationUrl + '?filter[limit]={limit}&filter[offset]={nextOffset}{&paginateQs*}' 53 | }, 54 | { 55 | rel: 'page', 56 | href: collectionNavigationUrl + '?filter[limit]={limit}&filter[offset]={offset}{&paginateQs*}' 57 | }, 58 | { 59 | rel: 'order', 60 | href: collectionNavigationUrl + '?filter[order]={orderAttribute}%20{orderDirection}{&orderQs*}' 61 | } 62 | ]; 63 | }; 64 | 65 | CollectionSchema.prototype.links = function(customLinks, itemSchemaUrl) { 66 | var links = new Links(this.defaultLinks(itemSchemaUrl), [], customLinks); 67 | return links.all(); 68 | }; 69 | 70 | CollectionSchema.prototype.data = function() { 71 | var schema; 72 | 73 | if (this.itemSchema) { 74 | schema = { 75 | $schema: this.itemSchema.$schema, 76 | collectionName: this.itemSchema.collectionName, 77 | title: this.itemSchema.collectionTitle 78 | }; 79 | 80 | var itemSchemaUrl = this.itemSchema.url(); 81 | var customLinks = this.itemSchema.collectionLinks; 82 | 83 | var type = this.type(); 84 | if (type) { 85 | schema.type = type; 86 | } 87 | var items = this.items(itemSchemaUrl); 88 | if (items) { 89 | schema.items = items; 90 | } 91 | var properties = this.properties(itemSchemaUrl); 92 | if (properties) { 93 | schema.properties = properties; 94 | } 95 | var links = this.links(customLinks, itemSchemaUrl); 96 | if (links) { 97 | schema.links = links; 98 | } 99 | } 100 | 101 | return schema; 102 | }; 103 | 104 | CollectionSchema.prototype.url = function() { 105 | return CollectionSchema.urlForCollectionName(this.itemSchema.collectionName); 106 | }; 107 | 108 | CollectionSchema.urlForCollectionName = function urlForCollectionName (collectionName) { 109 | return '/' + CollectionSchema.pluralModelName + '/' + collectionName; 110 | }; 111 | 112 | CollectionSchema.urlV2ForCollectionName = function urlV2ForCollectionName (collectionName) { 113 | return '/v2/' + CollectionSchema.pluralModelName + '/' + collectionName; 114 | }; 115 | 116 | module.exports = CollectionSchema; 117 | -------------------------------------------------------------------------------- /lib/domain/extended-validation.js: -------------------------------------------------------------------------------- 1 | function validateProperties(schema) { 2 | var reservedNames = [ 3 | '__cachedRelations', 4 | '__data', 5 | '__dataSource', 6 | '__strict', 7 | '__persisted', 8 | 'id', 9 | 'created', 10 | 'modified', 11 | 'createdBy', 12 | 'tenantId', 13 | 'tenant', 14 | 'versionId' 15 | ]; 16 | 17 | var violations = []; 18 | var message = ' is a reserved property name'; 19 | 20 | if(schema.properties) { 21 | for(var p in schema.properties) { 22 | if(reservedNames.includes(p)) { 23 | violations.push("'" + p + "'" + message) 24 | } 25 | } 26 | } 27 | 28 | return violations; 29 | } 30 | 31 | function validateRelations(schema) { 32 | var violations = []; 33 | 34 | if(!schema.properties || !schema.relations) { 35 | return violations; 36 | } 37 | 38 | var message = ' relation conflicts with property with the same name'; 39 | var propertyNames = Object.keys(schema.properties); 40 | 41 | for(var r in schema.relations) { 42 | if(propertyNames.includes(r)) { 43 | violations.push("'" + r + "'" + message); 44 | } 45 | } 46 | 47 | return violations; 48 | } 49 | 50 | function extendedValidation(schema) { 51 | var errors = []; 52 | 53 | if(schema && !schema.weakValidation) { 54 | errors = validateProperties(schema); 55 | errors = errors.concat(validateRelations(schema)); 56 | } 57 | 58 | return errors; 59 | } 60 | 61 | 62 | module.exports = extendedValidation 63 | -------------------------------------------------------------------------------- /lib/domain/indexes.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var logger = require('../support/logger'); 3 | 4 | 5 | module.exports = { 6 | create: function(dataSource, collectionName, callback) { 7 | callback = callback || _.noop; 8 | 9 | if (dataSource.connector && dataSource.connector.autoupdate) { 10 | logger.info('Ensuring indexes for: ' + collectionName); 11 | 12 | dataSource.connector.autoupdate([collectionName], function ensureIndexCallback(err) { 13 | if (err) { 14 | return callback(err); 15 | } 16 | 17 | logger.info('Ensured index for: ' + collectionName); 18 | callback(null, true); 19 | }); 20 | } else { 21 | callback(null, false); 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /lib/domain/item-schema.js: -------------------------------------------------------------------------------- 1 | var loopback = require('loopback'); 2 | var Q = require('q'); 3 | var traverse = require('traverse'); 4 | 5 | var config = require('../support/config'); 6 | var logger = require('../support/logger'); 7 | 8 | var indexes = require('./indexes'); 9 | var models = require('./models'); 10 | var Relations = require('./relations'); 11 | 12 | var JsonSchemaValidator = require('./json-schema-validator'); 13 | var Links = require('./links'); 14 | var modelPropertiesConverter = require('./model-properties-converter'); 15 | var readOnlyDefaultValuesHandler = require('./readonly-default-values-handler'); 16 | var RegistryModels = require('./registry-models'); 17 | var extendedValidation = require('./extended-validation'); 18 | 19 | var itemSchemaProperties = { 20 | collectionName: {type: 'string', required: true, id: true, generated: false} 21 | }; 22 | 23 | var relationSchema = require('./relation-schema'); 24 | var ItemSchema = module.exports = config.Model.extend('item-schemas', itemSchemaProperties); 25 | 26 | // fill in loopbackJsonSchema.init 27 | ItemSchema.app = null; 28 | 29 | ItemSchema.prototype.allLinks = function() { 30 | var links = new Links(this.defaultLinks(), this.relationLinks(), this.links); 31 | return links.all(); 32 | }; 33 | 34 | ItemSchema.validate('relations', function customValidator(err) { 35 | if (!this.relations) { 36 | return; 37 | } 38 | 39 | var validator = new JsonSchemaValidator(relationSchema.$schema); 40 | var validationResult = validator.validate(relationSchema, this); 41 | 42 | var errors = validationResult.items; 43 | var hasErrors = false; 44 | for (var error in errors) { 45 | this.errors.add(errors[error].property, errors[error].message, errors[error].code); 46 | hasErrors = true; 47 | } 48 | 49 | if (hasErrors) { 50 | err(); 51 | } 52 | }, { message: 'relations is invalid' }); 53 | 54 | ItemSchema.prototype.customLinks = function() { 55 | var links = new Links(this.defaultLinks(), this.relationLinks(), this.links); 56 | return links.custom(); 57 | }; 58 | 59 | ItemSchema.prototype.relationLinks = function() { 60 | var relationKey, relationLinks = [], relation; 61 | if (this.relations) { 62 | for (relationKey in this.relations) { 63 | relation = this.relations[relationKey]; 64 | 65 | if (relation.type == 'belongsTo') { 66 | relationLinks.push({ rel: relationKey, href: '/'+relation.collectionName+'/{'+relation.foreignKey+'}'}); 67 | } else { 68 | relationLinks.push({ rel: relationKey, href: '/'+this.collectionName+'/{id}/'+relationKey }); 69 | } 70 | } 71 | } 72 | 73 | return relationLinks; 74 | }; 75 | 76 | ItemSchema.prototype.defaultLinks = function() { 77 | var itemUrlTemplate = this.itemUrlTemplate(); 78 | var collectionUrl = this.collectionUrl(); 79 | var schemaUrl = this.url(); 80 | return [ 81 | { rel: 'self', href: itemUrlTemplate }, 82 | { rel: 'item', href: itemUrlTemplate }, 83 | { rel: 'create', method: 'POST', href: collectionUrl, schema: { $ref: schemaUrl } }, 84 | { rel: 'update', method: 'PUT', href: itemUrlTemplate }, 85 | { rel: 'delete', method: 'DELETE', href: itemUrlTemplate }, 86 | { rel: 'parent', href: collectionUrl } 87 | ]; 88 | }; 89 | 90 | ItemSchema.prototype.url = function() { 91 | return ItemSchema.urlForCollectionName(this.collectionName); 92 | }; 93 | 94 | ItemSchema.urlForCollectionName = function urlForCollectionName (collectionName) { 95 | return '/' + ItemSchema.pluralModelName + '/' + collectionName; 96 | }; 97 | 98 | ItemSchema.urlV2ForCollectionName = function urlForCollectionName (collectionName) { 99 | return '/v2/' + ItemSchema.pluralModelName + '/' + collectionName; 100 | }; 101 | 102 | ItemSchema.prototype.itemUrlTemplate = function() { 103 | return this.collectionUrl() + '/{id}'; 104 | }; 105 | 106 | ItemSchema.prototype.collectionUrl = function() { 107 | var path = '/' + this.collectionName; 108 | return path; 109 | }; 110 | 111 | ItemSchema.prototype.update$schema = function() { 112 | if (!this.$schema) { 113 | this.$schema = 'http://json-schema.org/draft-04/hyper-schema#'; 114 | } 115 | }; 116 | 117 | ItemSchema.prototype.beforeRegisterLoopbackModel = function(app, JsonSchemaModel, callback) { 118 | var name = JsonSchemaModel.modelName; 119 | logger.debug('Entered beforeRegisterLoopbackModel', name); 120 | 121 | if (ItemSchema.app._registeredLoopbackHooks[name]) { 122 | logger.debug('Loopback model name: '+name+' is already registered'); 123 | } else { 124 | ItemSchema.defineRemoteHooks(JsonSchemaModel); 125 | ItemSchema.app._registeredLoopbackHooks[name] = true; 126 | } 127 | 128 | callback(); 129 | }; 130 | 131 | ItemSchema.prototype.constructModel = function() { 132 | var indexes = this.indexes || {}; 133 | var model; 134 | try { 135 | model = config.Model.extend( 136 | this.collectionName, 137 | modelProperties.call(this), 138 | { plural: this.collectionName, indexes: indexes } 139 | ); 140 | 141 | model.cachedItemSchema = this; 142 | model.isV2 = false; 143 | addJsonSchemaValidation.call(this, model); 144 | ItemSchema.attachModel(model); 145 | } catch (err) { 146 | logger.error('Error constructing model:', this.collectionName, 'Error:', err); 147 | } 148 | 149 | return model; 150 | }; 151 | 152 | ItemSchema.prototype.modelV2UrlPath = function(tenantId) { 153 | // tenant not accept tenantID in url 154 | if (this.collectionName === 'tenants') { 155 | return 'v2/tenants'; 156 | } 157 | if (tenantId) { 158 | return `v2/${this.collectionName}/${tenantId}`; 159 | } else { 160 | return `v2/${this.collectionName}/:tenantId`; 161 | } 162 | }; 163 | 164 | ItemSchema.prototype.modelV2className = function(tenantId) { 165 | if (tenantId) { 166 | return `v2:${this.collectionName}:${tenantId}`; 167 | } else { 168 | return `v2:${this.collectionName}`; 169 | } 170 | }; 171 | 172 | ItemSchema.prototype.constructModelV2 = function(tenantId) { 173 | var indexes = this.indexes || {}; 174 | var model; 175 | try { 176 | model = config.Model.extend( 177 | this.collectionName, 178 | modelProperties.call(this), 179 | { plural: this.collectionName, indexes, http: { path: this.modelV2UrlPath(tenantId) } } 180 | ); 181 | model.sharedClass.name = this.modelV2className(tenantId); 182 | model.cachedItemSchema = this; 183 | model.isV2 = true; 184 | 185 | if (tenantId) { 186 | model.__tenantId = tenantId; 187 | } else { 188 | model.__tenantId = 'default'; 189 | } 190 | 191 | addJsonSchemaValidation.call(this, model); 192 | ItemSchema.attachModel(model); 193 | 194 | return model; 195 | } catch (err) { 196 | logger.error('Error constructing model V2:', this.collectionName, 'tenantId:', tenantId, 'Error:', err); 197 | } 198 | }; 199 | 200 | ItemSchema.prototype.createIndexes = function(callback) { 201 | indexes.create(this.getDataSource(), this.collectionName, callback); 202 | }; 203 | 204 | ItemSchema.prototype.model = function() { 205 | return loopback.findModel(this.collectionName) || null; 206 | }; 207 | 208 | ItemSchema.prototype.collectionSchema = function() { 209 | return new config.CollectionSchemaClass(this); 210 | }; 211 | 212 | ItemSchema.findByCollectionName = function(collectionName, callback) { 213 | ItemSchema.findOne({ where: { collectionName: collectionName }}, function(err, jsonSchema) { 214 | if (err) { 215 | logger.error('Error fetching JSON Schema for collectionName:', collectionName, 'Error:', err); 216 | } else if (jsonSchema === null) { 217 | logger.warn('JSON Schema for collectionName', collectionName, 'not found.'); 218 | } 219 | callback(err, jsonSchema); 220 | }); 221 | }; 222 | 223 | ItemSchema.preLoadedModels = false; 224 | 225 | ItemSchema.preLoadModels = function(currentAttemptNumber) { 226 | if (currentAttemptNumber === undefined) { 227 | currentAttemptNumber = 0; 228 | } 229 | 230 | ItemSchema.find({}, function(err, itemSchemas) { 231 | if (err) { 232 | if (currentAttemptNumber >= config.registerItemSchemaMaxAttempts) { 233 | ItemSchema.app.emit('loadModels', err); 234 | } else { 235 | logger.error('Attempt: '+currentAttemptNumber+', find all itemschemas error: ' + err.message); 236 | 237 | setTimeout(function() { 238 | ItemSchema.preLoadModels(currentAttemptNumber + 1); 239 | }, config.registerItemSchemaAttemptDelay); 240 | } 241 | 242 | return; 243 | } 244 | 245 | var promises = []; 246 | 247 | registryModels = new RegistryModels; 248 | 249 | for(var i = 0; i < itemSchemas.length; i++) { 250 | var schema = itemSchemas[i]; 251 | 252 | promises = promises.concat(schema.prepareRegisterModel(false)); 253 | } 254 | 255 | Q.allSettled(promises).done(function() { 256 | logger.info('Pre loaded all models'); 257 | ItemSchema.preLoadedModels = true; 258 | ItemSchema.app.emit('loadModels', null); 259 | }); 260 | }); 261 | }; 262 | 263 | ItemSchema.prototype.modelTenantsPool = function(model) { 264 | return config.modelTenantsPool[model] || []; 265 | }; 266 | 267 | ItemSchema.prototype.associateModel = function(model, callback) { 268 | Relations.getInstance().bindRelation(this, model); 269 | callback(); 270 | }; 271 | 272 | ItemSchema.prototype.registerModel = function(model, callback) { 273 | var schema = this; 274 | this.beforeRegisterLoopbackModel(ItemSchema.app, model, function(err) { 275 | if (err) { return callback(err); } 276 | 277 | schema.associateModel(model, function(err) { 278 | if (err) { return callback(err); } 279 | ItemSchema.app.model(model); 280 | callback(null); 281 | }); 282 | 283 | }); 284 | }; 285 | 286 | ItemSchema.prototype.prepareRegisterModel = function(afterSave) { 287 | var promises = []; 288 | 289 | registryModels = new RegistryModels(); 290 | 291 | if (!this.disableV1) { 292 | var model = this.constructModel(); 293 | registryModels.appendModelV1(this.collectionName, model); 294 | 295 | ItemSchema.attachModel(model); 296 | promises.push(ItemSchema.executeRegisterModel(this, model, afterSave)); 297 | } 298 | 299 | var modelV2 = this.constructModelV2(); 300 | registryModels.appendModelV2(this.collectionName, 'default', modelV2); 301 | 302 | var tenantsPoolMap = this.modelTenantsPool(modelV2); 303 | 304 | tenantsPoolMap.forEach((tenantId) => { 305 | var scopedModel = this.constructModelV2(tenantId); 306 | registryModels.appendModelV2(this.collectionName, tenantId, scopedModel); 307 | scopedModel.tenantId = tenantId; 308 | ItemSchema.attachModel(scopedModel); 309 | promises.push(ItemSchema.executeRegisterModel(this, scopedModel, afterSave)); 310 | }); 311 | 312 | ItemSchema.attachModel(modelV2); 313 | promises.push(ItemSchema.executeRegisterModel(this, modelV2, afterSave)); 314 | 315 | return promises; 316 | }; 317 | 318 | ItemSchema.executeRegisterModel = function(schema, model, afterSave) { 319 | var deferred = Q.defer(); 320 | var collectionName = schema.collectionName; 321 | 322 | schema.registerModel(model, function(err) { 323 | if (err) { 324 | logger.error('Failed to register model '+collectionName+': ' + err); 325 | } else { 326 | logger.debug('registered model: '+collectionName); 327 | } 328 | 329 | model.sharedClass.name = model.modelName; // to before and after work on v2 model 330 | 331 | if (afterSave) { 332 | schema.links = schema.allLinks(); 333 | } 334 | 335 | deferred.resolve(); 336 | }); 337 | 338 | return deferred; 339 | }; 340 | 341 | ItemSchema.attachModel = function(model) { 342 | model.attachTo(ItemSchema.dataSource); 343 | }; 344 | 345 | ItemSchema.afterInitialize = function() { 346 | modelPropertiesConverter.restore(this); 347 | this.__customLinks = this.links; 348 | this.links = this.allLinks(); 349 | }; 350 | 351 | ItemSchema.beforeSave = function(next, data) { 352 | this.update$schema(); 353 | this.links = this.customLinks(); 354 | modelPropertiesConverter.convert(data); 355 | next(); 356 | }; 357 | 358 | ItemSchema.prototype.sanitizeForDatabase = function () { 359 | this.update$schema(); 360 | this.links = this.customLinks(); 361 | modelPropertiesConverter.convert(this); 362 | }; 363 | 364 | ItemSchema.observe('after save', function afterSave(ctx, next) { 365 | var schema = ctx.instance; 366 | modelPropertiesConverter.restore(schema); 367 | 368 | var promises = schema.prepareRegisterModel(true); 369 | 370 | Q.allSettled(promises).done(function() { 371 | schema.createIndexes(function(err) { 372 | next.call(err); 373 | }); 374 | }); 375 | 376 | // THE COMMENTED CODE BELOW WORKS FOR V1 377 | // ON BRANCH MASTER. IF THE CODE ABOVE 378 | // STARTS TO WORK ON V1 AND V2, WE CAN 379 | // DELETE THESE COMMENTED LINES! 380 | 381 | // var model = schema.constructModel(); 382 | // ItemSchema.attachModel(model); 383 | 384 | // schema.registerModel(model, function(err) { 385 | // if (err) { 386 | // logger.error('Failed to register model '+ schema.collectionName+': ' + err); 387 | // } else { 388 | // logger.debug('registered model: '+schema.collectionName); 389 | // } 390 | // schema.links = schema.allLinks(); 391 | // }); 392 | 393 | // schema.createIndexes(function(err) { 394 | // next(err); 395 | // }); 396 | }); 397 | 398 | ItemSchema.observe('before save', function beforeSave(ctx, next) { 399 | var schema = ctx.instance || ctx.data; 400 | 401 | this.errors = extendedValidation(schema); 402 | 403 | if(this.errors.length) { 404 | next('Your API schema is invalid. Fix these violations: ' + this.errors.join('; ')); 405 | } 406 | 407 | next(); 408 | }); 409 | 410 | ItemSchema.defaultRemoteHookInitializers = [ 411 | includeBodyFieldInjector 412 | ]; 413 | 414 | ItemSchema.remoteHookInitializers = []; 415 | 416 | ItemSchema.defineRemoteHooks = function(model) { 417 | logger.debug('Defining remote hooks for ' + model.modelName); 418 | 419 | var hooksInitializers = ItemSchema.remoteHookInitializers; 420 | 421 | for (var i=0; i 0) { 447 | err(); 448 | } 449 | 450 | }, { message: 'Instance is invalid' }); 451 | } 452 | 453 | function modelProperties() { 454 | var type; 455 | var properties = traverse(this.properties).map(function(property) { 456 | if (this.node) { 457 | delete this.node.required; 458 | } 459 | 460 | type = traverse(property).get(['type']); 461 | if (type === 'array') { 462 | this.remove(); 463 | } 464 | else if (type instanceof Array) { 465 | this.remove(); 466 | } 467 | else if (type === 'integer') { 468 | this.node.type = 'number'; 469 | } 470 | }); 471 | 472 | if (!config.generatedId && properties) { 473 | properties.id = {type: 'string', generated: false, id: true}; 474 | } 475 | 476 | return properties; 477 | } 478 | 479 | function includeBodyFieldInjector (model) { 480 | logger.debug('include field body injector for model: ', model.modelName); 481 | model.beforeRemote('**', function(ctx, user, next) { 482 | readOnlyDefaultValuesHandler(ctx); 483 | next(); 484 | }); 485 | } 486 | -------------------------------------------------------------------------------- /lib/domain/json-schema-validator.js: -------------------------------------------------------------------------------- 1 | var JSV = require('jsvgcom').JSV; 2 | var _ = require('lodash'); 3 | var jsonPath = require('json-path'); 4 | var traverse = require('traverse'); 5 | var tv4 = require('tv4'); 6 | tv4.addFormat(require('tv4-formats')); 7 | 8 | var config = require('../support/config'); 9 | 10 | 11 | function JsonSchemaValidator(schemaVersion) { 12 | schemaVersion = typeof schemaVersion !== 'undefined' ? schemaVersion : 'http://json-schema.org/draft-04/hyper-schema#'; 13 | this.version = schemaVersion; 14 | } 15 | 16 | JsonSchemaValidator.prototype.validate = function(schema, model) { 17 | var data = removeNullUndefinedProperties(model.toObject()); 18 | 19 | if (this.version === 'http://json-schema.org/draft-04/hyper-schema#') { 20 | return Draft4.validate(schema, data); 21 | } else if (this.version === 'http://json-schema.org/draft-03/hyper-schema#') { 22 | return Draft3.validate(schema, data); 23 | } else { 24 | return null; 25 | } 26 | }; 27 | 28 | var Draft3 = (function(){ 29 | function formatError(result) { 30 | var errors = []; 31 | 32 | var error; 33 | result.errors.forEach(function(err) { 34 | error = translateMessage(err.message); 35 | var obj = {}; 36 | obj.code = error.codeNumber; 37 | obj.property = extractPathFromUri(err.uri); 38 | obj.message = error.message; 39 | 40 | if (obj.code === 500 && err.details) { 41 | obj.message = obj.message + ' ('+ err.details+')'; 42 | } 43 | 44 | obj.dataPath = extractPathFromUri(err.uri); 45 | obj.schemaPath = extractPathFromUri(err.schemaUri); 46 | 47 | deleteEmptyKeys(obj); 48 | errors.push(obj); 49 | }); 50 | 51 | return { 52 | items: errors, 53 | itemCount: result.errors.length 54 | }; 55 | } 56 | 57 | function deleteEmptyKeys(obj) { 58 | Object.keys(obj).forEach(function(k) { 59 | if (!obj[k]) { 60 | delete obj[k]; 61 | } 62 | }); 63 | } 64 | 65 | function extractPathFromUri(uri) { 66 | var re = /.*#\/(.*)/; 67 | var ary = re.exec(uri); 68 | 69 | if (!_.isEmpty(ary)) { 70 | return '/' + ary[1]; 71 | } 72 | } 73 | 74 | function translateMessage(originalMessage) { 75 | var codesMapping = { 76 | INVALID_TYPE: 0, 77 | ENUM_MISMATCH: 1, 78 | ANY_OF_MISSING: 10, 79 | ONE_OF_MISSING: 11, 80 | ONE_OF_MULTIPLE: 12, 81 | NOT_PASSED: 13, 82 | // Numeric errors 83 | NUMBER_MULTIPLE_OF: 100, 84 | NUMBER_MINIMUM: 101, 85 | NUMBER_MINIMUM_EXCLUSIVE: 102, 86 | NUMBER_MAXIMUM: 103, 87 | NUMBER_MAXIMUM_EXCLUSIVE: 104, 88 | // String errors 89 | STRING_LENGTH_SHORT: 200, 90 | STRING_LENGTH_LONG: 201, 91 | STRING_PATTERN: 202, 92 | // Object errors 93 | OBJECT_PROPERTIES_MINIMUM: 300, 94 | OBJECT_PROPERTIES_MAXIMUM: 301, 95 | OBJECT_REQUIRED: 302, 96 | OBJECT_ADDITIONAL_PROPERTIES: 303, 97 | OBJECT_DEPENDENCY_KEY: 304, 98 | // Array errors 99 | ARRAY_LENGTH_SHORT: 400, 100 | ARRAY_LENGTH_LONG: 401, 101 | ARRAY_UNIQUE: 402, 102 | ARRAY_ADDITIONAL_ITEMS: 403, 103 | // Custom/user-defined errors 104 | FORMAT_CUSTOM: 500, 105 | KEYWORD_CUSTOM: 501, 106 | // Schema structure 107 | CIRCULAR_REFERENCE: 600, 108 | // DRAFT 3 109 | MAX_DECIMAL: 700, 110 | DISALLOWED_TYPE: 701, 111 | URI_DOESNOT_START_WITH: 702, 112 | 113 | // Non-standard validation options 114 | UNKNOWN_PROPERTY: 1000 115 | }; 116 | 117 | var errorMessagesMapping = { 118 | 'Format validation failed' : 'FORMAT_CUSTOM', 119 | 'Property is required' : 'OBJECT_REQUIRED', 120 | 'Instance is not a required type' : 'INVALID_TYPE', 121 | 'Additional items are not allowed' : 'ARRAY_ADDITIONAL_ITEMS', 122 | 'Number is less than the required minimum value' : 'NUMBER_MINIMUM', 123 | 'Number is greater than the required maximum value' : 'NUMBER_MAXIMUM', 124 | 'The number of items is less than the required minimum' : 'ARRAY_LENGTH_SHORT', 125 | 'The number of items is greater than the required maximum' : 'ARRAY_LENGTH_LONG', 126 | 'Invalid pattern' : 'STRING_PATTERN', 127 | 'String is less than the required minimum length' : 'STRING_LENGTH_SHORT', 128 | 'String is greater than the required maximum length' : 'STRING_LENGTH_LONG', 129 | 'Instance is not one of the possible values' : 'ENUM_MISMATCH', 130 | 'String is not in the required format' : 'FORMAT_CUSTOM', 131 | 'Array can only contain unique items' : 'ARRAY_UNIQUE', 132 | 'The number of decimal places is greater than the allowed maximum' : 'MAX_DECIMAL', 133 | 'Instance is a disallowed type' : 'DISALLOWED_TYPE', 134 | 'Instance\'s URI does not start with' : 'URI_DOESNOT_START_WITH', 135 | 'String does not match pattern': 'STRING_PATTERN' 136 | }; 137 | 138 | var msg = { 139 | message: originalMessage 140 | }; 141 | 142 | var translation = traverse(config.jsonSchemaValidatorTranslation).get(['draft3','mapping']); 143 | if (translation !== undefined) { 144 | msg.message = translation[errorMessagesMapping[originalMessage]] || originalMessage; 145 | } 146 | 147 | msg.codeNumber = codesMapping[errorMessagesMapping[originalMessage]] || codesMapping.UNKNOWN_PROPERTY; 148 | return msg; 149 | } 150 | 151 | return { 152 | validate : function(schema, data) { 153 | var env = JSV.createEnvironment('json-schema-draft-03'); 154 | // FIXME: This is needed because JSV validates the schema, and the `id` keyword cannot be an ObjectId. See issue #12. 155 | schema.$id = String(schema.$id); 156 | var result = env.validate(data, schema); 157 | return formatError(result); 158 | } 159 | }; 160 | })(); 161 | 162 | var Draft4 = (function(){ 163 | function formatError(schema, result) { 164 | var errors = []; 165 | result.errors.forEach(function(error) { 166 | var obj = {}; 167 | obj.code = error.code; 168 | 169 | if ((error.schemaPath.indexOf('required') !== -1) && (error.schemaPath.indexOf('/patternProperties/') === -1)) { 170 | obj.property = error.dataPath +'/' + jsonPath.resolve(schema, error.schemaPath); 171 | } else { 172 | obj.property = error.dataPath; 173 | } 174 | obj.message = error.message; 175 | obj.dataPath = error.dataPath; 176 | obj.schemaPath = error.schemaPath; 177 | errors.push(obj); 178 | }); 179 | 180 | return { 181 | items: errors, 182 | itemCount: result.errors.length 183 | }; 184 | } 185 | 186 | return { 187 | validate: function(schema, data) { 188 | 189 | var translation = config.jsonSchemaValidatorTranslation.draft4 || {}; 190 | if (!_.isEmpty(translation)) { 191 | tv4.addLanguage(translation.language, translation.mapping); 192 | tv4.language(translation.language); 193 | } 194 | 195 | var result = tv4.validateMultiple(data, schema); 196 | return formatError(schema, result); 197 | } 198 | }; 199 | })(); 200 | 201 | var removeNullUndefinedProperties = function(obj) { 202 | return traverse(obj).map(function() { 203 | if (this.node == null) { 204 | this.delete(); 205 | } 206 | }); 207 | } 208 | 209 | module.exports = JsonSchemaValidator; 210 | -------------------------------------------------------------------------------- /lib/domain/links.js: -------------------------------------------------------------------------------- 1 | function Links(defaultLinks, relationLinks, customLinks) { 2 | this.defaultLinks = defaultLinks; 3 | this.relationLinks = relationLinks || []; 4 | this.customLinks = customLinks || []; 5 | } 6 | 7 | Links.prototype.all = function() { 8 | var customLinks = this.custom(); 9 | var relationLinks = this.relations(); 10 | 11 | return this.defaultLinks.concat(relationLinks).concat(customLinks); 12 | }; 13 | 14 | Links.prototype.relations = function() { 15 | return relationLinksNotOverridingDefaultLinks.call(this, this.relationLinks); 16 | }; 17 | 18 | Links.prototype.custom = function() { 19 | return customLinksNotOverridingDefaultLinks.call(this, this.customLinks); 20 | }; 21 | 22 | function customLinksNotOverridingDefaultLinks(links) { 23 | var defaultRels = []; 24 | var i; 25 | 26 | for (i = 0; i < this.defaultLinks.length; i++) { 27 | defaultRels.push(this.defaultLinks[i].rel); 28 | } 29 | 30 | for (i = 0; i < this.relationLinks.length; i++) { 31 | defaultRels.push(this.relationLinks[i].rel); 32 | } 33 | 34 | var customLinks = links || []; 35 | customLinks = customLinks.filter(function(link) { 36 | return defaultRels.indexOf(link.rel) === -1; 37 | }); 38 | 39 | return customLinks; 40 | } 41 | 42 | function relationLinksNotOverridingDefaultLinks(links) { 43 | var defaultRels = this.defaultLinks.map(function(link) { 44 | return link.rel; 45 | }); 46 | 47 | var customLinks = links || []; 48 | customLinks = customLinks.filter(function(link) { 49 | return defaultRels.indexOf(link.rel) === -1; 50 | }); 51 | 52 | return customLinks; 53 | } 54 | 55 | module.exports = Links; 56 | -------------------------------------------------------------------------------- /lib/domain/model-properties-converter.js: -------------------------------------------------------------------------------- 1 | var modelPropertiesConverter = { 2 | convert: function(data) { 3 | if (data.indexes) { 4 | normalizeKeys(data.indexes); 5 | sanitizeIndexes(data.indexes, '.', '%2E'); 6 | } 7 | 8 | if (data.$schema) { 9 | sanitizeSchema.call(this, data, data.$schema); 10 | if (data.__data) { 11 | sanitizeSchema.call(this, data.__data, data['%24schema']); 12 | } 13 | } 14 | 15 | if (data.collectionLinks) { 16 | sanitizeLinks(data.collectionLinks, '.', '%2E'); 17 | } 18 | 19 | if (data.links) { 20 | sanitizeLinks(data.links, '.', '%2E'); 21 | } 22 | 23 | if (data.versionIndexes) { 24 | sanitizeIndexes(data.versionIndexes, '.', '%2E'); 25 | } 26 | }, 27 | 28 | restore: function(model) { 29 | if (model.indexes) { 30 | sanitizeIndexes(model.indexes, '%2E', '.'); 31 | } 32 | 33 | if (model.collectionLinks) { 34 | sanitizeLinks(model.collectionLinks, '%2E', '.'); 35 | } 36 | 37 | if (model.links) { 38 | sanitizeLinks(model.links, '%2E', '.'); 39 | } 40 | 41 | if (model.versionIndexes) { 42 | sanitizeIndexes(model.versionIndexes, '%2E', '.'); 43 | } 44 | 45 | if (model['%24schema']) { 46 | model.$schema = model['%24schema']; 47 | model.__data.$schema = model['%24schema']; 48 | delete model['%24schema']; 49 | delete model.__data['%24schema']; 50 | } 51 | } 52 | }; 53 | 54 | function normalizeKeys(indexes) { 55 | for(var k in indexes) { 56 | var idx = indexes[k]; 57 | if('orderedKeys' in idx) { 58 | idx.keys = arrayToOrderedObject(idx.orderedKeys) || idx.keys; 59 | delete idx['orderedKeys']; 60 | } 61 | } 62 | } 63 | 64 | function arrayToOrderedObject(orderedKeys) { 65 | if(!orderedKeys || !Array.isArray(orderedKeys)) { 66 | return null; 67 | } 68 | 69 | var ordered = {}; 70 | for(var k = 0; k < orderedKeys.length; k++) { 71 | ordered = Object.assign(ordered, orderedKeys[k]); 72 | } 73 | 74 | return ordered; 75 | } 76 | 77 | function getIndexKeys(index) { 78 | return index.keys? index.keys: index; 79 | } 80 | 81 | function sanitizeSchema(data, value) { 82 | data['%24schema'] = value; 83 | delete data.$schema; 84 | } 85 | 86 | function sanitizeIndexes(indexes, orig, replace) { 87 | sanitizeKeys(indexes, function(keys) { 88 | var sanitized = {}; 89 | 90 | for (var keyIndex in keys) { 91 | var value = keys[keyIndex]; 92 | if (keyIndex.indexOf(orig) >= 0){ 93 | sanitized[sanitizeKey(keyIndex, orig, replace)] = value; 94 | } else { 95 | sanitized[keyIndex] = value; 96 | } 97 | } 98 | 99 | return sanitized; 100 | }); 101 | } 102 | 103 | function sanitizeKey(key, orig, replace) { 104 | while (key.indexOf(orig) >= 0){ 105 | key = key.replace(orig, replace) 106 | } 107 | return key 108 | } 109 | 110 | function sanitizeLinks(links, orig, replace) { 111 | for (var i=0; i= 0) { 121 | properties[propertyName.replace(orig, replace)] = properties[propertyName]; 122 | delete properties[propertyName]; 123 | } 124 | } 125 | } 126 | } 127 | 128 | function sanitizeKeys(indexes, operation) { 129 | for (var indexObject in indexes) { 130 | var index = indexes[indexObject]; 131 | var sanitizedKeys = operation(getIndexKeys(indexes[indexObject])) 132 | 133 | // kudos on consistency on your data structures ... 134 | if('keys' in index) { 135 | index.keys = sanitizedKeys; 136 | } else { 137 | indexes[indexObject] = sanitizedKeys; 138 | } 139 | } 140 | } 141 | 142 | module.exports = modelPropertiesConverter; 143 | -------------------------------------------------------------------------------- /lib/domain/models.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var util = require('util'); 3 | 4 | 5 | module.exports = { 6 | fromPluralModelName: util.deprecate(function(app, modelName) { 7 | return _.find(app.models(), function(Model) { 8 | return Model.modelName === modelName; 9 | }); 10 | }, 'fromPluralModelName is deprecated, use loopback.findModel instead') 11 | }; 12 | -------------------------------------------------------------------------------- /lib/domain/readonly-default-values-handler.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var traverse = require('traverse'); 3 | 4 | var logger = require('../support/logger'); 5 | 6 | 7 | module.exports = function readOnlyDefaultValuesHandler(ctx) { 8 | logger.debug('Entered readOnlyDefaultValuesHandler.'); 9 | 10 | var method = ctx.req.method; 11 | 12 | if (method !== 'POST' && method !== 'PUT') { 13 | return; 14 | } 15 | 16 | var payload = ctx.req.body; 17 | var properties = ctx.method.ctor.definition.rawProperties; 18 | var traversePayload = traverse(payload); 19 | var traverseInstance = traverse(ctx.instance ? ctx.instance.__data : null); 20 | 21 | return schemaWalk(properties, [], traversePayload, method, traverseInstance); 22 | }; 23 | 24 | function isNullValue(value) { 25 | return (value === null || value === undefined); 26 | } 27 | 28 | function walkOnObjectProperties(schema, bodyItems, method) { 29 | for(var i = 0; i < bodyItems.length; i++) { 30 | schemaWalk(schema.properties, [], traverse(bodyItems[i]), method); 31 | } 32 | } 33 | 34 | function walkOnArrayProperties(schemas, additionalSchema, bodyItems, method) { 35 | var arraySchema; 36 | var nestedProperties; 37 | var i; 38 | 39 | for(i = 0; i < schemas.length; i++) { 40 | arraySchema = schemas[i]; 41 | 42 | if (arraySchema.type === 'object') { 43 | nestedProperties = arraySchema.properties; 44 | 45 | if (nestedProperties) { 46 | schemaWalk(nestedProperties, [], traverse(bodyItems[i]), method); 47 | } 48 | } 49 | } 50 | 51 | if (additionalSchema) { 52 | for(i = schemas.length; i < bodyItems.length; i++) { 53 | schemaWalk(additionalSchema.properties, [], traverse(bodyItems[i]), method); 54 | } 55 | } 56 | } 57 | 58 | function schemaWalk(properties, propertyPath, traversePayload, method, traverseInstance) { 59 | var bodyItems; 60 | var nestedProperties; 61 | var path; 62 | var property; 63 | var propertyType; 64 | var schema; 65 | 66 | for (var key in properties) { 67 | path = propertyPath.slice(0); 68 | path.push(key); 69 | property = properties[key]; 70 | 71 | propertyType = property.type; 72 | if (propertyType === 'object') { 73 | nestedProperties = property.properties; 74 | 75 | if (nestedProperties) { 76 | schemaWalk(nestedProperties, path, traversePayload, method, traverseInstance); 77 | } 78 | 79 | continue; 80 | } else if (propertyType === 'array') { 81 | bodyItems = traversePayload.get(path); 82 | 83 | if (!_.isArray(bodyItems)) { 84 | continue; 85 | } 86 | 87 | schema = property.items; 88 | if (_.isArray(schema)) { 89 | walkOnArrayProperties(schema, property.additionalItems, bodyItems, method); 90 | } else if (_.isObject(schema)) { 91 | walkOnObjectProperties(schema, bodyItems, method); 92 | } 93 | } 94 | 95 | if (property.readOnly) { 96 | removeReadOnlyProperty(traversePayload, path); 97 | } 98 | 99 | if (typeof property.default !== 'undefined') { 100 | applyDefaultPropertyValue(traversePayload, path, property.default, method, traverseInstance); 101 | } 102 | } 103 | 104 | return traversePayload.value; 105 | } 106 | 107 | function removeReadOnlyProperty(traversePayload, path) { 108 | var node = traversePayload.value; 109 | var pathSize = path.length - 1; 110 | 111 | for (var i = 0; i < pathSize; i++) { 112 | node = node[path[i]]; 113 | 114 | if (isNullValue(node)) { 115 | return; 116 | } 117 | } 118 | 119 | delete node[path[pathSize]]; 120 | } 121 | 122 | function applyDefaultPropertyValue(traversePayload, path, defaultValue, method, traverseInstance) { 123 | var rootPath = path.slice(0, 1); 124 | var value = traversePayload.has(path); 125 | 126 | if (value) { 127 | return; 128 | } 129 | 130 | if (method === "PUT" && traverseInstance && traverseInstance.value) { 131 | var hasRootOnInstanceProperty = traverseInstance.has(rootPath); 132 | var hasRootOnPayloadProperty = traversePayload.has(rootPath); 133 | 134 | if (!hasRootOnInstanceProperty && !hasRootOnPayloadProperty) { 135 | traversePayload.set(path, defaultValue); 136 | } else if (hasRootOnPayloadProperty) { 137 | traversePayload.set(path, defaultValue); 138 | } 139 | } 140 | 141 | if (method === 'POST') { 142 | traversePayload.set(path, defaultValue); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/domain/registry-models.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let registryModelsInstance; 4 | 5 | class RegistryModels { 6 | constructor() { 7 | if(!registryModelsInstance) { 8 | registryModelsInstance = this; 9 | } 10 | 11 | this.reset(); 12 | return registryModelsInstance; 13 | } 14 | 15 | reset() { 16 | this.v1Models = {}; 17 | this.v2Models = {}; 18 | } 19 | 20 | appendModelV1(collectionName, model) { 21 | this.v1Models[collectionName] = model; 22 | } 23 | 24 | appendModelV2(collectionName, tenantId, model) { 25 | if(!this.v2Models.hasOwnProperty(collectionName)) { 26 | this.v2Models[collectionName] = {}; 27 | } 28 | this.v2Models[collectionName][tenantId] = model; 29 | } 30 | 31 | findV1(collectionName) { 32 | if(this.v1Models.hasOwnProperty(collectionName)) { 33 | return this.v1Models[collectionName]; 34 | } 35 | 36 | return; 37 | } 38 | 39 | findV2(tenantId, collectionName) { 40 | if(this.v2Models.hasOwnProperty(collectionName)) { 41 | if(!this.v2Models[collectionName].hasOwnProperty(tenantId)) { 42 | tenantId = 'default'; 43 | } 44 | 45 | return this.v2Models[collectionName][tenantId]; 46 | } 47 | 48 | return; 49 | } 50 | 51 | stats() { 52 | return { 53 | v1: Object.keys(this.v1Models), 54 | v2: Object.keys(this.v2Models).map((collectionName) => { 55 | return { 56 | collectionName: collectionName, 57 | tenants: Object.keys(this.v2Models[collectionName]) 58 | }; 59 | }) 60 | }; 61 | } 62 | } 63 | 64 | module.exports = RegistryModels; 65 | -------------------------------------------------------------------------------- /lib/domain/relation-schema.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | $schema: "http://json-schema.org/draft-04/hyper-schema#", 3 | type: "object", 4 | properties: { 5 | relations: { 6 | type: "object", 7 | patternProperties: { 8 | "[a-z]+": { 9 | type: "object", 10 | properties: { 11 | collectionName: { 12 | type: "string" 13 | }, 14 | type: { 15 | type: "string", 16 | "enum": [ 17 | "belongsTo", 18 | "hasMany" 19 | ] 20 | }, 21 | foreignKey: { 22 | type: "string" 23 | } 24 | }, 25 | required: ["collectionName", "type", "foreignKey"], 26 | additionalProperties: false 27 | } 28 | } 29 | } 30 | } 31 | 32 | }; 33 | -------------------------------------------------------------------------------- /lib/domain/relations.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var inherits = require('util').inherits; 3 | var traverse = require('traverse'); 4 | 5 | var logger = require('../support/logger'); 6 | 7 | var RegistryModels = require('./registry-models'); 8 | 9 | var Relations = function(app) { 10 | this.app = app; 11 | this.pendingRelationsV1 = {}; 12 | this.pendingRelationsV2 = {}; 13 | this.afterRemoteHooks = {}; 14 | this.beforeRemoteHooks = {}; 15 | this.boundRemoteHooks = {}; 16 | }; 17 | 18 | Relations.init = function(app) { 19 | Relations._instance = new Relations(app); 20 | return Relations._instance; 21 | }; 22 | 23 | Relations.getInstance = function() { 24 | return Relations._instance; 25 | }; 26 | 27 | inherits(Relations, EventEmitter); 28 | 29 | Relations.prototype.bindAfterRemoteHook = function(relationType, methodName, hook) { 30 | var path = [relationType, methodName]; 31 | 32 | if (!traverse(this.afterRemoteHooks).has(path)) { 33 | traverse(this.afterRemoteHooks).set(path, []); 34 | } 35 | 36 | this.afterRemoteHooks[relationType][methodName].push(hook); 37 | }; 38 | 39 | Relations.prototype.bindBeforeRemoteHook = function(relationType, methodName, hook) { 40 | var path = [relationType, methodName]; 41 | 42 | if (!traverse(this.beforeRemoteHooks).has(path)) { 43 | traverse(this.beforeRemoteHooks).set(path, []); 44 | } 45 | 46 | this.beforeRemoteHooks[relationType][methodName].push(hook); 47 | }; 48 | 49 | Relations.prototype.bindRelation = function bindRelation(schema, modelClass) { 50 | if (schema.relations) { 51 | bindRelations.call(this, schema.collectionName, modelClass, schema.relations); 52 | } 53 | bindPendingRelations.call(this, schema.collectionName, modelClass); 54 | }; 55 | 56 | 57 | function bindRelations(fromCollectionName, fromModel, relations) { 58 | var relation; 59 | var collectionName; 60 | var toModel; 61 | 62 | var registryModels = new RegistryModels(); 63 | 64 | for (var relationKey in relations) { 65 | relation = relations[relationKey]; 66 | collectionName = relation.collectionName; 67 | 68 | if(fromModel.isV2) { 69 | toModel = registryModels.findV2(fromModel.__tenantId, collectionName); 70 | } else { 71 | toModel = registryModels.findV1(collectionName); 72 | } 73 | 74 | if (!toModel) { 75 | logger.debug('[bindRelation] collectionName: "'+collectionName+'" not found, storing as pending relation'); 76 | 77 | if (fromModel.isV2) { 78 | pushPendingRelationV2.call(this, fromCollectionName, collectionName, relationKey, relation); 79 | } else { 80 | pushPendingRelationV1.call(this, fromCollectionName, collectionName, relationKey, relation); 81 | } 82 | continue; 83 | } 84 | 85 | makeAssociation.call(this, fromModel, toModel, relationKey, relation); 86 | } 87 | } 88 | 89 | function bindPendingRelations(fromCollectionName, fromModel) { 90 | var pendingRelations; 91 | if (fromModel.isV2) { 92 | pendingRelations = this.pendingRelationsV2[fromCollectionName]; 93 | } else { 94 | pendingRelations = this.pendingRelationsV1[fromCollectionName]; 95 | } 96 | var pendingRelation; 97 | var targetModel; 98 | var relation; 99 | 100 | if (!pendingRelations){ 101 | return; 102 | } 103 | 104 | var registryModels = new RegistryModels(); 105 | 106 | for (var i=0; i -1; 189 | } 190 | 191 | function setBoundHook(modelName, hookMethodName, hookName) { 192 | if (!this.boundRemoteHooks[modelName]) { 193 | this.boundRemoteHooks[modelName] = {}; 194 | } 195 | 196 | if (!this.boundRemoteHooks[modelName][hookMethodName]) { 197 | this.boundRemoteHooks[modelName][hookMethodName] = []; 198 | } 199 | 200 | this.boundRemoteHooks[modelName][hookMethodName].push(hookName); 201 | } 202 | 203 | function pushPendingRelationV1(fromCollectionName, toCollectionName, relationKey, relation) { 204 | if (!this.pendingRelationsV1[toCollectionName]) { 205 | this.pendingRelationsV1[toCollectionName] = []; 206 | } 207 | 208 | this.pendingRelationsV1[toCollectionName].push({ 209 | relationKey: relationKey, 210 | targetCollectionName: fromCollectionName, 211 | relation: relation 212 | }); 213 | } 214 | 215 | function pushPendingRelationV2(fromCollectionName, toCollectionName, relationKey, relation) { 216 | if (!this.pendingRelationsV2[toCollectionName]) { 217 | this.pendingRelationsV2[toCollectionName] = []; 218 | } 219 | 220 | this.pendingRelationsV2[toCollectionName].push({ 221 | relationKey: relationKey, 222 | targetCollectionName: fromCollectionName, 223 | relation: relation 224 | }); 225 | } 226 | 227 | module.exports = Relations; 228 | -------------------------------------------------------------------------------- /lib/http/create-location-hook.js: -------------------------------------------------------------------------------- 1 | var logger = require('../support/logger'); 2 | var locationHeaderCorrelator = require('./location-header-correlator'); 3 | 4 | module.exports = function createLocationHook(model) { 5 | /* 6 | * Create a hook to correct response in after create method 7 | */ 8 | 9 | logger.debug('include field location injector for model: ', model.modelName); 10 | model.afterRemote('create', locationHeaderCorrelator); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/http/item-schema-hooks.js: -------------------------------------------------------------------------------- 1 | 2 | var ItemSchema = require('../domain/item-schema'); 3 | 4 | var LJSRequest = require('./ljs-request'); 5 | var schemaLinkRewriter = require('./schema-link-rewriter'); 6 | var createLocationHook = require('./create-location-hook'); 7 | 8 | module.exports = { 9 | initialize: function() { 10 | ItemSchema.afterRemote('**', function setContentTypeHeader (ctx, instance, next) { 11 | ctx.res.set('Content-Type', 'application/schema+json; charset=utf-8'); 12 | next(); 13 | }); 14 | 15 | var remotes = ['findById', 'findOne', 'upsert', 'create', 'prototype.updateAttributes']; 16 | 17 | remotes.forEach(function(remote) { 18 | ItemSchema.afterRemote(remote, function(ctx, instance, next) { 19 | if (instance) { 20 | var baseUrl = new LJSRequest(ctx.req, ctx.req.app).baseUrl(); 21 | schemaLinkRewriter(baseUrl, instance); 22 | } 23 | 24 | next(); 25 | }); 26 | }); 27 | 28 | ItemSchema.afterRemote('find', function(ctx, items, next) { 29 | var baseUrl = new LJSRequest(ctx.req, ctx.req.app).baseUrl(); 30 | 31 | for (var i=0; i; rel="describedby"'); 63 | } 64 | -------------------------------------------------------------------------------- /lib/http/schema-link-rewriter.js: -------------------------------------------------------------------------------- 1 | var traverse = require('traverse'); 2 | 3 | var LJSUrl = require('../http/ljs-url'); 4 | 5 | module.exports = function schemaLinkRewriter(baseUrl, instance) { 6 | traverseProperties(baseUrl, instance.links, function(context) { 7 | return context.key === 'href' || context.key === '$ref'; 8 | }); 9 | 10 | traverseProperties(baseUrl, instance.properties, function(context) { 11 | return context.key === '$ref'; 12 | }); 13 | 14 | traverseProperties(baseUrl, instance.items, function(context) { 15 | return context.key === '$ref'; 16 | }); 17 | 18 | return instance; 19 | }; 20 | 21 | function traverseProperties(baseUrl, properties, shouldUpdate) { 22 | traverse(properties).forEach(function(property) { 23 | if (shouldUpdate(this) && new LJSUrl(property).isRelative()) { 24 | this.update(baseUrl + property); 25 | } 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /lib/http/validate-request.middleware.js: -------------------------------------------------------------------------------- 1 | var LJSRequest = require('../http/ljs-request'); 2 | var logger = require('../support/logger'); 3 | 4 | 5 | module.exports = function validateRequestMiddleware() { 6 | return function validateRequestHandler(req, res, next) { 7 | var ljsReq = new LJSRequest(req, req.app); 8 | 9 | if (ljsReq.method === "OPTIONS") { 10 | return next(); 11 | } 12 | 13 | if (!ljsReq.isContentTypeSupported()) { 14 | var errMessage = 'Unsupported Content-Type: <' + req.headers['content-type'] + '>.'; 15 | var err = new Error(errMessage); 16 | err.status = 415; 17 | return next(err); 18 | } 19 | next(); 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /lib/support/config.js: -------------------------------------------------------------------------------- 1 | var Model = require('loopback').PersistedModel; 2 | 3 | var CollectionSchema = require('../domain/collection-schema'); 4 | var logger = require('./logger'); 5 | 6 | var config = module.exports = { 7 | CollectionSchemaClass: CollectionSchema, 8 | jsonSchemaValidatorTranslation: { }, 9 | Model: Model, 10 | registerItemSchemaAtRequest: true, 11 | registerItemSchemaMaxAttempts: 5, 12 | registerItemSchemaAttemptDelay: 200, 13 | collectionRemoteName: 'find', 14 | modelTenantsPool: process.env['LOOPBACK_JSONSCHEMA_MODEL_TENANTS'] || {}, 15 | instanceRemoteNames: ['findById', 'findOne', 'upsert', 'create', 'prototype.updateAttributes', 'prototype.delete', 'deleteById'] 16 | }; 17 | 18 | config.generatedId = true; 19 | if (process.env['LOOPBACK_JSONSCHEMA_GENERATED_ID']) { 20 | config.generatedId = process.env['LOOPBACK_JSONSCHEMA_GENERATED_ID'] === 'true'; 21 | } 22 | 23 | if (logger.debug.enabled) { 24 | logger.debug('config: ', JSON.stringify(config, null, 2)); 25 | } 26 | -------------------------------------------------------------------------------- /lib/support/logger.js: -------------------------------------------------------------------------------- 1 | var stackTrace = require('stack-trace'); 2 | var winston = require('winston'); 3 | 4 | var logLevel = process.env['LOG_LEVEL'] || 'warn'; 5 | 6 | var loggerInstance = new (winston.Logger)({ 7 | transports: [ 8 | new (winston.transports.Console)({ 9 | colorize: true, 10 | label: 'loopback-jsonschema', 11 | level: logLevel, 12 | timestamp: false 13 | }) 14 | ] 15 | }); 16 | 17 | var logger = module.exports = loggerInstance; 18 | 19 | if (logLevel === 'debug') { 20 | logger.debug = function() { 21 | var callSite = stackTrace.get()[1]; 22 | var fileNameParts = callSite.getFileName().split('/'); 23 | var fileName = fileNameParts[fileNameParts.length - 1]; 24 | var callSiteInfo = '[' + fileName + ':' + callSite.getLineNumber() + ']'; 25 | var args = ['debug', callSiteInfo].concat(Array.prototype.slice.call(arguments)); 26 | logger.log.apply(this, args); 27 | }; 28 | logger.debug.enabled = true; 29 | } else { 30 | logger.debug = function() {}; 31 | logger.debug.enabled = false; 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@globocom/loopback-jsonschema", 3 | "version": "5.0.17", 4 | "description": "JSON Schema support for Loopback", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/globocom/loopback-jsonschema.git" 12 | }, 13 | "author": "Backstage ", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/globocom/loopback-jsonschema/issues" 17 | }, 18 | "homepage": "https://github.com/globocom/loopback-jsonschema", 19 | "dependencies": { 20 | "async": "^0.9.0", 21 | "cors": "2.x", 22 | "debug": "", 23 | "errorhandler": "1.x", 24 | "fast-url-parser": "^1.1.3", 25 | "json-path": "", 26 | "jsvgcom": "4.2.6", 27 | "lodash": "3.x", 28 | "q": "1.x", 29 | "stack-trace": "", 30 | "traverse": "", 31 | "tv4": "", 32 | "tv4-formats": "", 33 | "type-is": "1.x.x", 34 | "winston": "0.x" 35 | }, 36 | "peerDependencies": { 37 | "loopback": "2.x" 38 | }, 39 | "devDependencies": { 40 | "chai": "1.9.x", 41 | "loopback": "2.x", 42 | "mocha": "1.x", 43 | "mocha-sinon": "", 44 | "sinon": "^2.1.0", 45 | "sinon-chai": "2.x", 46 | "supertest": "" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/acceptance/get-collection-schema.test.js: -------------------------------------------------------------------------------- 1 | var support = require('../support'); 2 | 3 | var expect = require('chai').expect; 4 | var request = require('supertest'); 5 | 6 | var ItemSchema = require('../../lib/domain/item-schema'); 7 | 8 | 9 | describe('GET /collection-schemas/:id', function () { 10 | var app; 11 | 12 | before(function() { 13 | app = support.newLoopbackJsonSchemaApp(); 14 | }); 15 | 16 | describe('when corresponding item schema exists', function () { 17 | var collectionSchema, 18 | collectionSchemaResponse, 19 | collectionSchemaCollectionName, 20 | schemeAndAuthority; 21 | 22 | before(function (done) { 23 | ItemSchema.create({ 24 | collectionName: 'people', 25 | title: 'Person', 26 | collectionTitle: 'People', 27 | type: 'object', 28 | properties: {}, 29 | collectionLinks: [ 30 | { rel: 'custom', href: '/custom' } 31 | ] 32 | }, function(err, itemSchema) { 33 | if (err) { return done(err); } 34 | collectionSchemaCollectionName = itemSchema.collectionName; 35 | done(); 36 | }); 37 | }); 38 | 39 | before(function(done) { 40 | request(app) 41 | .get('/api/collection-schemas/' + collectionSchemaCollectionName) 42 | .expect(200) 43 | .end(function (err, res) { 44 | if (err) { return done(err); } 45 | schemeAndAuthority = 'http://' + res.req._headers.host; 46 | collectionSchemaResponse = res; 47 | collectionSchema = JSON.parse(res.text); 48 | done(); 49 | }); 50 | }); 51 | 52 | it('should have application/schema+json content type', function() { 53 | expect(collectionSchemaResponse.headers['content-type']).to.eq('application/schema+json; charset=utf-8'); 54 | }); 55 | 56 | it('should include CORS headers', function() { 57 | expect(collectionSchemaResponse.headers['access-control-allow-origin']).to.eq('*'); 58 | }); 59 | 60 | it('should include $schema', function() { 61 | expect(collectionSchema['$schema']).to.eq('http://json-schema.org/draft-04/hyper-schema#'); 62 | }); 63 | 64 | it('should include type', function() { 65 | expect(collectionSchema['type']).to.eq('array'); 66 | }); 67 | 68 | it('should include title', function() { 69 | expect(collectionSchema['title']).to.eq('People'); 70 | }); 71 | 72 | it('should include items', function() { 73 | expect(collectionSchema['items']).to.eql({ 74 | $ref: schemeAndAuthority + '/api/item-schemas/' + collectionSchemaCollectionName 75 | }); 76 | }); 77 | 78 | it('should not include properties', function() { 79 | expect(collectionSchema['properties']).to.be.undefined; 80 | }); 81 | 82 | it('should include links', function() { 83 | expect(collectionSchema['links']).to.eql([ 84 | { 85 | rel: 'self', 86 | href: schemeAndAuthority + '/api/people' 87 | }, 88 | { 89 | rel: 'list', 90 | href: schemeAndAuthority + '/api/people' 91 | }, 92 | { 93 | rel: 'add', 94 | method: 'POST', 95 | href: schemeAndAuthority + '/api/people', 96 | schema: { 97 | $ref: schemeAndAuthority + '/api/item-schemas/' + collectionSchemaCollectionName 98 | } 99 | }, 100 | { 101 | rel: 'previous', 102 | href: schemeAndAuthority + '/api/people?filter[limit]={limit}&filter[offset]={previousOffset}{&paginateQs*}' 103 | }, 104 | { 105 | rel: 'next', 106 | href: schemeAndAuthority + '/api/people?filter[limit]={limit}&filter[offset]={nextOffset}{&paginateQs*}' 107 | }, 108 | { 109 | rel: 'page', 110 | href: schemeAndAuthority + '/api/people?filter[limit]={limit}&filter[offset]={offset}{&paginateQs*}' 111 | }, 112 | { 113 | rel: 'order', 114 | href: schemeAndAuthority + '/api/people?filter[order]={orderAttribute}%20{orderDirection}{&orderQs*}' 115 | }, 116 | { 117 | rel: 'custom', 118 | href: schemeAndAuthority + '/api/custom' 119 | } 120 | ]); 121 | }); 122 | }); 123 | 124 | describe('when corresponding item schema does not exist', function () { 125 | it('should return 404', function (done) { 126 | request(app) 127 | .get('/api/collection-schemas/invalid-schema-id') 128 | .expect(404) 129 | .end(function (err) { 130 | if (err) { return done(err); } 131 | done(); 132 | }); 133 | }); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /test/acceptance/get-collection.test.js: -------------------------------------------------------------------------------- 1 | var support = require('../support'); 2 | 3 | var expect = require('chai').expect; 4 | var request = require('supertest'); 5 | 6 | var ItemSchema = require('../../lib/domain/item-schema'); 7 | 8 | 9 | describe('GET /:collection', function () { 10 | var app; 11 | 12 | before(function() { 13 | app = support.newLoopbackJsonSchemaApp(); 14 | }); 15 | 16 | describe('when the collection exists', function() { 17 | var jsonSchemaCollectionName, response, schemeAndAuthority; 18 | 19 | before(function (done) { 20 | ItemSchema.create({ 21 | collectionName: 'people', 22 | title: 'Person', 23 | collectionTitle: 'People', 24 | type: 'object', 25 | properties: {} 26 | }, function(err, jsonSchema) { 27 | if (err) { return done(err); } 28 | jsonSchemaCollectionName = jsonSchema.collectionName; 29 | done(); 30 | }); 31 | }); 32 | 33 | before(function(done) { 34 | request(app) 35 | .get('/api/people') 36 | .expect(200) 37 | .end(function (err, res) { 38 | if (err) { return done(err); } 39 | schemeAndAuthority = 'http://' + res.req._headers.host; 40 | response = res; 41 | done(); 42 | }); 43 | }); 44 | 45 | it('should correlate the collection with its schema', function() { 46 | var collectionSchemaUrl = schemeAndAuthority + '/api/collection-schemas/' + jsonSchemaCollectionName; 47 | expect(response.headers['link']).to.eq('<' + collectionSchemaUrl + '>; rel="describedby"'); 48 | expect(response.headers['content-type']).to.eq('application/json; charset=utf-8; profile="' + collectionSchemaUrl +'"'); 49 | }); 50 | }); 51 | 52 | describe('when the collection does not exist', function() { 53 | it('should return 404', function(done) { 54 | request(app) 55 | .get('/api/non-existent') 56 | .expect(404) 57 | .end(function (err) { 58 | if (err) { return done(err); } 59 | done(); 60 | }); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/acceptance/get-item-schema.test.js: -------------------------------------------------------------------------------- 1 | var support = require('../support'); 2 | 3 | var expect = require('chai').expect; 4 | var request = require('supertest'); 5 | 6 | var ItemSchema = require('../../lib/domain/item-schema'); 7 | 8 | var app = support.newLoopbackJsonSchemaApp(); 9 | 10 | describe('GET /item-schemas/:id', function() { 11 | describe('when schema exists', function() { 12 | var itemSchema, itemSchemaCollectionName, response, schemeAndAuthority; 13 | 14 | before(function(done) { 15 | ItemSchema.create({ 16 | collectionName: 'people', 17 | title: 'Person', 18 | collectionTitle: 'People', 19 | type: 'object', 20 | properties: {}, 21 | links: [ 22 | { rel: 'custom', href: '/custom' } 23 | ] 24 | }, function(err, itemSchema) { 25 | if (err) { return done(err); } 26 | itemSchemaCollectionName = itemSchema.collectionName; 27 | done(); 28 | }); 29 | }); 30 | 31 | before(function(done) { 32 | request(app) 33 | .get('/api/item-schemas/' + itemSchemaCollectionName) 34 | .expect(200) 35 | .end(function(err, res) { 36 | if (err) { return done(err); } 37 | schemeAndAuthority = 'http://' + res.req._headers.host; 38 | response = res; 39 | itemSchema = JSON.parse(res.text); 40 | done(); 41 | }); 42 | }); 43 | 44 | it('should have application/schema+json content type', function() { 45 | expect(response.headers['content-type']).to.eq('application/schema+json; charset=utf-8'); 46 | }); 47 | 48 | it('should include $schema', function() { 49 | expect(itemSchema['$schema']).to.eq('http://json-schema.org/draft-04/hyper-schema#'); 50 | }); 51 | 52 | it('should include type', function() { 53 | expect(itemSchema['type']).to.eq('object'); 54 | }); 55 | 56 | it('should include title', function() { 57 | expect(itemSchema['title']).to.eq('Person'); 58 | }); 59 | 60 | it('should include properties', function() { 61 | expect(itemSchema['properties']).to.eql({}); 62 | }); 63 | 64 | it('should include links', function() { 65 | expect(itemSchema['links']).to.eql([ 66 | { 67 | rel: 'self', 68 | href: schemeAndAuthority + '/api/people/{id}' 69 | }, 70 | { 71 | rel: 'item', 72 | href: schemeAndAuthority + '/api/people/{id}' 73 | }, 74 | { 75 | rel: 'create', 76 | method: 'POST', 77 | href: schemeAndAuthority + '/api/people', 78 | schema: { 79 | $ref: schemeAndAuthority + '/api/item-schemas/' + itemSchemaCollectionName 80 | } 81 | }, 82 | { 83 | rel: 'update', 84 | method: 'PUT', 85 | href: schemeAndAuthority + '/api/people/{id}' 86 | }, 87 | { 88 | rel: 'delete', 89 | method: 'DELETE', 90 | href: schemeAndAuthority + '/api/people/{id}' 91 | }, 92 | { 93 | rel: 'parent', 94 | href: schemeAndAuthority + '/api/people' 95 | }, 96 | { 97 | rel: 'custom', 98 | href: schemeAndAuthority + '/api/custom' 99 | } 100 | ]); 101 | }); 102 | }); 103 | 104 | describe('when schema does not exist', function() { 105 | it('should return 404', function (done) { 106 | request(app) 107 | .get('/api/item-schemas/invalid-schema-id') 108 | .expect(404) 109 | .end(function (err, res) { 110 | if (err) { return done(err); } 111 | done(); 112 | }); 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /test/acceptance/get-item-schemas.test.js: -------------------------------------------------------------------------------- 1 | var support = require('../support'); 2 | 3 | var expect = require('chai').expect; 4 | var request = require('supertest'); 5 | 6 | var ItemSchema = require('../../lib/domain/item-schema'); 7 | 8 | var app = support.newLoopbackJsonSchemaApp(); 9 | 10 | describe('GET /item-schemas', function() { 11 | describe('when there is an item-schema', function() { 12 | var itemSchemas, itemSchemaCollectionName, response, schemeAndAuthority; 13 | 14 | before(function(done) { 15 | ItemSchema.create({ 16 | collectionName: 'people', 17 | title: 'Person', 18 | collectionTitle: 'People', 19 | type: 'object', 20 | properties: {}, 21 | links: [ 22 | { rel: 'custom', href: '/custom' } 23 | ] 24 | }, function(err, itemSchemas) { 25 | if (err) { return done(err); } 26 | itemSchemaCollectionName = itemSchemas.collectionName; 27 | done(); 28 | }); 29 | }); 30 | 31 | before(function(done) { 32 | request(app) 33 | .get('/api/item-schemas') 34 | .expect(200) 35 | .end(function(err, res) { 36 | if (err) { return done(err); } 37 | schemeAndAuthority = 'http://' + res.req._headers.host; 38 | response = res; 39 | itemSchemas = JSON.parse(res.text); 40 | done(); 41 | }); 42 | }); 43 | 44 | it('should include links', function() { 45 | expect(itemSchemas[0]['links']).to.eql([ 46 | { 47 | rel: 'self', 48 | href: schemeAndAuthority + '/api/people/{id}' 49 | }, 50 | { 51 | rel: 'item', 52 | href: schemeAndAuthority + '/api/people/{id}' 53 | }, 54 | { 55 | rel: 'create', 56 | method: 'POST', 57 | href: schemeAndAuthority + '/api/people', 58 | schema: { 59 | $ref: schemeAndAuthority + '/api/item-schemas/' + itemSchemaCollectionName 60 | } 61 | }, 62 | { 63 | rel: 'update', 64 | method: 'PUT', 65 | href: schemeAndAuthority + '/api/people/{id}' 66 | }, 67 | { 68 | rel: 'delete', 69 | method: 'DELETE', 70 | href: schemeAndAuthority + '/api/people/{id}' 71 | }, 72 | { 73 | rel: 'parent', 74 | href: schemeAndAuthority + '/api/people' 75 | }, 76 | { 77 | rel: 'custom', 78 | href: schemeAndAuthority + '/api/custom' 79 | } 80 | ]); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/acceptance/get-item.test.js: -------------------------------------------------------------------------------- 1 | var support = require('../support'); 2 | 3 | var expect = require('chai').expect; 4 | var request = require('supertest'); 5 | 6 | var ItemSchema = require('../../lib/domain/item-schema'); 7 | 8 | 9 | describe('GET /:collection/:id', function () { 10 | var itemId, jsonSchemaCollectionName, response, schemeAndAuthority; 11 | var app; 12 | 13 | before(function() { 14 | app = support.newLoopbackJsonSchemaApp(); 15 | }); 16 | before(function(done) { 17 | ItemSchema.create({ 18 | type: 'object', 19 | title: 'Person', 20 | collectionTitle: 'People', 21 | collectionName: 'people', 22 | properties: {} 23 | }, function(err, jsonSchema) { 24 | if (err) { return done(err); } 25 | jsonSchemaCollectionName = jsonSchema.collectionName; 26 | done(); 27 | }); 28 | }); 29 | 30 | before(function(done) { 31 | request(app) 32 | .post('/api/people') 33 | .set('Content-Type', 'application/json') 34 | .send('{"name": "Alice"}') 35 | .expect(201) 36 | .end(function (err, res) { 37 | if (err) { return done(err); } 38 | itemId = res.body.id; 39 | done(); 40 | }); 41 | }); 42 | 43 | before(function(done) { 44 | request(app) 45 | .get('/api/people/' + itemId) 46 | .expect(200) 47 | .end(function (err, res) { 48 | if (err) { return done(err); } 49 | schemeAndAuthority = 'http://' + res.req._headers.host; 50 | response = res; 51 | done(); 52 | }); 53 | }); 54 | 55 | it('should correlate the item with its schema', function() { 56 | var itemSchemaUrl = schemeAndAuthority + '/api/item-schemas/' + jsonSchemaCollectionName; 57 | expect(response.headers['link']).to.eq('<' + itemSchemaUrl + '>; rel="describedby"'); 58 | expect(response.headers['content-type']).to.eq('application/json; charset=utf-8; profile="' + itemSchemaUrl + '"'); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/acceptance/middleware/json-schema.middleware.test.js: -------------------------------------------------------------------------------- 1 | var support = require('../../support'); 2 | 3 | var expect = require('chai').expect; 4 | var loopback = require('loopback'); 5 | var request = require('supertest'); 6 | 7 | var app = support.newLoopbackJsonSchemaApp(); 8 | 9 | describe('json-schema.middleware', function() { 10 | it('should register a json-schema model', function (done) { 11 | request(app) 12 | .post('/api/item-schemas') 13 | .set('Content-Type', 'application/json') 14 | .send('{"collectionName": "people"}') 15 | .expect(200) 16 | .end(function (err, res) { 17 | var body = JSON.parse(res.text); 18 | expect(res.headers['link']).to.not.exist; 19 | expect(body.collectionName).to.eq('people'); 20 | expect(body).to.include.keys('links'); 21 | done(); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/acceptance/middleware/register-loopback-model.middleware.test.js: -------------------------------------------------------------------------------- 1 | var support = require('../../support'); 2 | 3 | var expect = require('chai').expect; 4 | var loopback = require('loopback'); 5 | var request = require('supertest'); 6 | 7 | describe('register-loopback-model.middleware', function() { 8 | it('should register the loopback model', function (done) { 9 | var app = support.newLoopbackJsonSchemaApp(); 10 | request(app) 11 | .post('/api/item-schemas') 12 | .set('Content-Type', 'application/json') 13 | .send('{"collectionName": "people"}') 14 | .expect(200) 15 | .end(function (err, res) { 16 | var body = JSON.parse(res.text); 17 | expect(body.collectionName).to.eq('people'); 18 | 19 | request(app) 20 | .post('/api/people') 21 | .set('Content-Type', 'application/json') 22 | .send('{"name": "test"}') 23 | .expect(200) 24 | .end(function (err, res) { 25 | var body = JSON.parse(res.text); 26 | expect(body.name).to.eq('test'); 27 | done(); 28 | }); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/acceptance/options-collection-schema.test.js: -------------------------------------------------------------------------------- 1 | var support = require('../support'); 2 | 3 | var expect = require('chai').expect; 4 | var loopback = require('loopback'); 5 | var request = require('supertest'); 6 | 7 | var app = support.newLoopbackJsonSchemaApp(); 8 | 9 | describe('OPTION /collection-schemas/:id', function() { 10 | var response; 11 | 12 | before(function(done) { 13 | request(app) 14 | .options('/api/collection-schemas/1') 15 | .set('Access-Control-Request-Headers', 'Content-Type') 16 | .expect(204) 17 | .end(function (err, res) { 18 | if (err) { return done(err); }; 19 | response = res; 20 | done(); 21 | }); 22 | }); 23 | 24 | it('should include CORS headers', function() { 25 | expect(response.headers['access-control-allow-origin']).to.eq('*'); 26 | expect(response.headers['access-control-allow-methods']).to.eq('GET,HEAD,PUT,PATCH,POST,DELETE'); 27 | expect(response.headers['access-control-allow-headers']).to.eq('Content-Type'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/acceptance/post-item-schema.test.js: -------------------------------------------------------------------------------- 1 | var support = require('../support'); 2 | 3 | var expect = require('chai').expect; 4 | var request = require('supertest'); 5 | 6 | describe('POST /item-schemas', function() { 7 | var app, itemSchema, itemSchemaCollectionName, response, schemeAndAuthority; 8 | 9 | describe('successfully', function() { 10 | before(function(done) { 11 | app = support.newLoopbackJsonSchemaApp(); 12 | var schemaJson = { 13 | type: 'object', 14 | collectionName: 'people', 15 | links: [ 16 | { 17 | rel: 'blog', 18 | method: 'GET', 19 | href: '{+blog}' 20 | } 21 | ] 22 | }; 23 | 24 | request(app) 25 | .post('/api/item-schemas') 26 | .set('Content-Type', 'application/schema+json') 27 | .send(JSON.stringify(schemaJson)) 28 | .end(function (err, res) { 29 | if (err) { return done(err); } 30 | itemSchema = JSON.parse(res.text); 31 | itemSchemaCollectionName = itemSchema.collectionName; 32 | 33 | schemeAndAuthority = 'http://' + res.req._headers.host; 34 | response = res; 35 | itemSchema = JSON.parse(res.text); 36 | done(); 37 | }); 38 | }); 39 | 40 | it('should return 201', function() { 41 | expect(response.status).to.eq(201); 42 | }); 43 | 44 | it('should correlate the Location header', function(){ 45 | var locationUrl = schemeAndAuthority + '/api/item-schemas/' + itemSchema.collectionName; 46 | expect(response.headers['location']).to.eq(locationUrl); 47 | }); 48 | 49 | it('should return correct id', function() { 50 | expect(itemSchema.collectionName).to.be.eq(itemSchemaCollectionName); 51 | }); 52 | 53 | it('should include default links', function() { 54 | expect(itemSchema.links).to.eql([ 55 | { rel: 'self', href: schemeAndAuthority + '/api/people/{id}' }, 56 | { rel: 'item', href: schemeAndAuthority + '/api/people/{id}' }, 57 | { 58 | rel: 'create', 59 | method: 'POST', 60 | href: schemeAndAuthority + '/api/people', 61 | schema: { 62 | $ref: schemeAndAuthority + '/api/item-schemas/' + itemSchema.collectionName 63 | } 64 | }, 65 | { rel: 'update', method: 'PUT', href: schemeAndAuthority + '/api/people/{id}' }, 66 | { rel: 'delete', method: 'DELETE', href: schemeAndAuthority + '/api/people/{id}' }, 67 | { rel: 'parent', href: schemeAndAuthority + '/api/people' }, 68 | { rel: 'blog', method: 'GET', href: '{+blog}' } 69 | ]); 70 | }); 71 | }); 72 | 73 | describe('with unsupported Content-Type', function() { 74 | before(function(done) { 75 | app = support.newLoopbackJsonSchemaApp(); 76 | var schemaJson = { 77 | type: 'object', 78 | collectionName: 'people' 79 | }; 80 | request(app) 81 | .post('/api/item-schemas') 82 | .set('Accept', 'application/json') 83 | .set('Content-Type', 'text/plain') 84 | .send(JSON.stringify(schemaJson)) 85 | .expect(415) 86 | .end(function (err, res) { 87 | if (err) { return done(err); }; 88 | response = res; 89 | done(); 90 | }); 91 | }); 92 | 93 | it('should return 415', function() { 94 | expect(response.status).to.eq(415); 95 | }); 96 | 97 | it('should return error message', function() { 98 | expect(response.body.error.message).to.eq('Unsupported Content-Type: .') 99 | }); 100 | }); 101 | 102 | describe('without required fields', function(){ 103 | var bodyError; 104 | 105 | before(function(done) { 106 | var schemaJson = { 107 | type: 'object' 108 | }; 109 | 110 | app = support.newLoopbackJsonSchemaApp(); 111 | request(app) 112 | .post('/api/item-schemas') 113 | .set('Accept', 'application/json') 114 | .set('Content-Type', 'application/json') 115 | .send(JSON.stringify(schemaJson)) 116 | .expect(422) 117 | .end(function (err, res) { 118 | if (err) { return done(err); }; 119 | response = res; 120 | bodyError = JSON.parse(response.error.text); 121 | done(); 122 | }); 123 | }); 124 | 125 | it('should error be a ValidationError', function(){ 126 | console.info(bodyError.error); 127 | expect(bodyError.error.name).to.eql('ValidationError'); 128 | }); 129 | 130 | it('should error have the message: "`collectionName` can\'t be blank"', function(){ 131 | expect(bodyError.error.message).to.contain('`collectionName` can\'t be blank'); 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /test/acceptance/post-item.test.js: -------------------------------------------------------------------------------- 1 | var support = require('../support'); 2 | 3 | var expect = require('chai').expect; 4 | var request = require('supertest'); 5 | 6 | var ItemSchema = require('../../lib/domain/item-schema'); 7 | 8 | 9 | describe('POST /:collection', function() { 10 | var app, itemResponse, jsonSchemaCollectionName, schemeAndAuthority; 11 | 12 | describe('successfully', function() { 13 | before(function (done) { 14 | app = support.newLoopbackJsonSchemaApp(); 15 | ItemSchema.create({ 16 | collectionName: 'people', 17 | title: 'Person', 18 | collectionTitle: 'People', 19 | type: 'object', 20 | properties: {} 21 | }, function(err, jsonSchema) { 22 | if (err) { return done(err); }; 23 | jsonSchemaCollectionName = jsonSchema.collectionName; 24 | done(); 25 | }); 26 | }); 27 | 28 | before(function(done) { 29 | request(app) 30 | .post('/api/people') 31 | .set('Content-Type', 'application/json') 32 | .send('{"name": "Alice"}') 33 | .end(function (err, res) { 34 | if (err) { return done(err); }; 35 | schemeAndAuthority = 'http://' + res.req._headers.host; 36 | itemResponse = res; 37 | done(); 38 | }); 39 | }); 40 | 41 | it('should return 201', function() { 42 | expect(itemResponse.status).to.eq(201); 43 | }); 44 | 45 | it('should correlate the Location header', function(){ 46 | var locationUrl = schemeAndAuthority + '/api/people/' + itemResponse.body.id; 47 | expect(itemResponse.headers['location']).to.eq(locationUrl); 48 | }); 49 | 50 | it('should correlate the item with its schema', function() { 51 | var itemSchemaUrl = schemeAndAuthority + '/api/item-schemas/' + jsonSchemaCollectionName; 52 | expect(itemResponse.headers['link']).to.eq('<' + itemSchemaUrl + '>; rel="describedby"'); 53 | expect(itemResponse.headers['content-type']).to.eq('application/json; charset=utf-8; profile="' + itemSchemaUrl + '"'); 54 | }); 55 | }); 56 | 57 | describe('successfully with readOnly and default fields', function(){ 58 | before(function (done) { 59 | app = support.newLoopbackJsonSchemaApp(); 60 | ItemSchema.create({ 61 | collectionName: 'people-readonly', 62 | title: 'Person', 63 | collectionTitle: 'People-readonly', 64 | type: 'object', 65 | properties: { 66 | personal: { 67 | type: 'object', 68 | properties: { 69 | firstName: {type: 'string'}, 70 | lastName: {type: 'string', default: 'Junior'}, 71 | active: {type: 'boolean', default: true, readOnly: true}, 72 | status: {type: 'string', default: 'single', readOnly: false} 73 | } 74 | }, 75 | professional: { 76 | type: 'object', 77 | properties: { 78 | awards: { 79 | type: 'array', 80 | items: [ 81 | {type: 'string'} 82 | ], 83 | additionalItems: {type: 'boolean', default: true} 84 | }, 85 | jobs: { 86 | type: 'array', 87 | items: { 88 | type: 'object', 89 | properties: { 90 | company: {type: 'string'} 91 | } 92 | } 93 | } 94 | } 95 | 96 | } 97 | } 98 | }, function(err, jsonSchema) { 99 | if (err) { return done(err); }; 100 | jsonSchemaCollectionName = jsonSchema.collectionName; 101 | 102 | var person = { 103 | personal: { 104 | firstName: 'Bob', 105 | active: false, 106 | status: 'maried' 107 | }, 108 | professional: { 109 | awards: ['inovation', false, true], 110 | jobs: [{company: 'Globo.com'}, {company: 'TV Globo'}] 111 | } 112 | }; 113 | 114 | request(app) 115 | .post('/api/people-readonly') 116 | .set('Content-Type', 'application/json') 117 | .send(JSON.stringify(person)) 118 | .end(function (err, res) { 119 | if (err) { return done(err); }; 120 | itemResponse = res; 121 | done(); 122 | }); 123 | }); 124 | }); 125 | 126 | it('should apply default value for lastName', function() { 127 | expect(itemResponse.body.personal.lastName).to.be.eql('Junior'); 128 | }); 129 | 130 | it('should remove readOnly properties', function() { 131 | expect(itemResponse.body.personal.active).to.be.true; 132 | }); 133 | 134 | it('should ignore when readOnly is falsy', function() { 135 | expect(itemResponse.body.personal.status).to.eql('maried'); 136 | }); 137 | }); 138 | 139 | describe('successfully when the schema includes a default value', function(){ 140 | before(function (done) { 141 | app = support.newLoopbackJsonSchemaApp(); 142 | ItemSchema.create({ 143 | collectionName: 'people-readonly', 144 | title: 'Person', 145 | collectionTitle: 'People-readonly', 146 | type: 'object', 147 | properties: { 148 | status: { 149 | type: 'string', 150 | default: 'inactive' 151 | } 152 | } 153 | }, function(err, jsonSchema) { 154 | if (err) { return done(err); }; 155 | jsonSchemaCollectionName = jsonSchema.collectionName; 156 | done(); 157 | }); 158 | }); 159 | 160 | before(function(done) { 161 | request(app) 162 | .post('/api/people-readonly') 163 | .set('Content-Type', 'application/json') 164 | .send('{"name": "Alice"}') 165 | .end(function (err, res) { 166 | if (err) { return done(err); }; 167 | itemResponse = res; 168 | done(); 169 | }); 170 | }); 171 | 172 | it('should set default value', function() { 173 | expect(itemResponse.body.status).to.eql('inactive'); 174 | }); 175 | }); 176 | 177 | describe('with unsupported Content-Type', function() { 178 | before(function(done) { 179 | app = support.newLoopbackJsonSchemaApp(); 180 | request(app) 181 | .post('/api/people') 182 | .set('Accept', 'application/json') 183 | .set('Content-Type', 'text/plain') 184 | .send('{"name": "Alice"}') 185 | .expect(415) 186 | .end(function (err, res) { 187 | if (err) { return done(err); }; 188 | itemResponse = res; 189 | done(); 190 | }); 191 | }); 192 | 193 | it('should return error message', function() { 194 | expect(itemResponse.body.error.message).to.eq('Unsupported Content-Type: .') 195 | }); 196 | }); 197 | 198 | describe('with validation errors', function() { 199 | before(function(done) { 200 | app = support.newLoopbackJsonSchemaApp(); 201 | ItemSchema.create({ 202 | collectionName: 'people', 203 | title: 'Person', 204 | collectionTitle: 'People', 205 | type: 'object', 206 | properties: { 207 | name: { 208 | type: 'string' 209 | } 210 | }, 211 | required: ['name'] 212 | }, function(err, jsonSchema) { 213 | if (err) { return done(err); }; 214 | jsonSchemaCollectionName = jsonSchema.collectionName; 215 | done(); 216 | }); 217 | }); 218 | 219 | before(function(done) { 220 | request(app) 221 | .post('/api/people') 222 | .set('Content-Type', 'application/json') 223 | .expect(422) 224 | .send('{}') 225 | .end(function (err, res) { 226 | if (err) { return done(err); }; 227 | itemResponse = res; 228 | done(); 229 | }); 230 | }); 231 | 232 | it('should return 422 status code', function() { 233 | expect(itemResponse.status).to.eq(422); 234 | }); 235 | 236 | it('should return errors in body', function() { 237 | var error = itemResponse.body.error; 238 | expect(error.details).to.eql({ 239 | codes: { 240 | '/name': [ 241 | 302 242 | ], 243 | '_all': [ 244 | 'custom' 245 | ] 246 | }, 247 | context: 'people', 248 | messages: { 249 | '/name': [ 250 | 'Missing required property: name' 251 | ], 252 | '_all': [ 253 | 'Instance is invalid' 254 | ] 255 | } 256 | }); 257 | expect(error.message).to.contain('The `people` instance is not valid.'); 258 | expect(error.message).to.contain('`_all` Instance is invalid'); 259 | expect(error.message).to.contain('`/name` Missing required property: name'); 260 | expect(error.name).to.eq('ValidationError'); 261 | expect(error.status).to.eq(422); 262 | expect(error.statusCode).to.eq(422); 263 | }); 264 | }); 265 | }); 266 | -------------------------------------------------------------------------------- /test/acceptance/put-item.test.js: -------------------------------------------------------------------------------- 1 | var support = require('../support'); 2 | 3 | var expect = require('chai').expect; 4 | var request = require('supertest'); 5 | 6 | var ItemSchema = require('../../lib/domain/item-schema'); 7 | 8 | 9 | describe('PUT /:collection/:id', function() { 10 | var app, itemResponse, itemId, jsonSchemaCollectionName, schemeAndAuthority; 11 | before(function() { 12 | app = support.newLoopbackJsonSchemaApp(); 13 | }); 14 | 15 | describe('successfully', function() { 16 | before(function(done) { 17 | ItemSchema.create({ 18 | collectionName: 'people', 19 | title: 'Person', 20 | collectionTitle: 'People', 21 | type: 'object', 22 | properties: {} 23 | }, function(err, jsonSchema) { 24 | if (err) { return done(err); } 25 | jsonSchemaCollectionName = jsonSchema.collectionName; 26 | done(); 27 | }); 28 | }); 29 | 30 | before(function(done) { 31 | request(app) 32 | .post('/api/people') 33 | .set('Content-Type', 'application/json') 34 | .send('{"name": "Alice"}') 35 | .end(function (err, item) { 36 | if (err) { return done(err); } 37 | itemId = item.body.id; 38 | done(); 39 | }); 40 | }); 41 | 42 | before(function(done) { 43 | request(app) 44 | .put('/api/people/' + itemId) 45 | .set('Content-Type', 'application/json') 46 | .send('{"name": "Alice", "age": 30}') 47 | .end(function (err, res) { 48 | if (err) { return done(err); } 49 | schemeAndAuthority = 'http://' + res.req._headers.host; 50 | itemResponse = res; 51 | done(); 52 | }); 53 | }); 54 | 55 | it('should return 200 status code', function() { 56 | expect(itemResponse.status).to.eq(200); 57 | }); 58 | 59 | it('should correlate the item with its schema', function() { 60 | var itemSchemaUrl = schemeAndAuthority + '/api/item-schemas/' + jsonSchemaCollectionName; 61 | expect(itemResponse.headers['link']).to.eq('<' + itemSchemaUrl + '>; rel="describedby"'); 62 | expect(itemResponse.headers['content-type']).to.eq('application/json; charset=utf-8; profile="' + itemSchemaUrl + '"'); 63 | }); 64 | }); 65 | 66 | describe('successfully with readOnly and default fields', function() { 67 | before(function (done) { 68 | app = support.newLoopbackJsonSchemaApp(); 69 | ItemSchema.create({ 70 | collectionName: 'people-readonly', 71 | title: 'Person', 72 | collectionTitle: 'People-readonly', 73 | type: 'object', 74 | properties: { 75 | personal: { 76 | type: 'object', 77 | properties: { 78 | firstName: {type: 'string'}, 79 | lastName: {type: 'string', default: 'Junior'}, 80 | active: {type: 'boolean', default: true, readOnly: true}, 81 | status: {type: 'string', default: 'single', readOnly: false} 82 | } 83 | }, 84 | professional: { 85 | type: 'object', 86 | properties: { 87 | awards: { 88 | type: 'array', 89 | items: [ 90 | {type: 'string'} 91 | ], 92 | additionalItems: {type: 'boolean', default: true} 93 | }, 94 | jobs: { 95 | type: 'array', 96 | items: { 97 | type: 'object', 98 | properties: { 99 | company: {type: 'string'} 100 | } 101 | } 102 | } 103 | } 104 | 105 | } 106 | } 107 | }, function(err, jsonSchema) { 108 | if (err) { return done(err); } 109 | jsonSchemaCollectionName = jsonSchema.collectionName; 110 | 111 | var person = { 112 | personal: { 113 | firstName: 'Bob', 114 | active: false, 115 | status: 'maried' 116 | }, 117 | professional: { 118 | awards: ['inovation', false, true], 119 | jobs: [{company: 'Globo.com'}, {company: 'TV Globo'}] 120 | } 121 | }; 122 | 123 | request(app) 124 | .post('/api/people-readonly') 125 | .set('Content-Type', 'application/json') 126 | .send(JSON.stringify(person)) 127 | .end(function (err, item) { 128 | if (err) { return done(err); } 129 | itemId = item.body.id; 130 | 131 | done(); 132 | }); 133 | }); 134 | }); 135 | 136 | before(function(done) { 137 | request(app) 138 | .put('/api/people-readonly/' + itemId) 139 | .set('Content-Type', 'application/json') 140 | .send('{"personal": {"active": false}}') 141 | .end(function (err, res) { 142 | if (err) { return done(err); } 143 | itemResponse = res; 144 | done(); 145 | }); 146 | }); 147 | 148 | it('should ignore posted property', function() { 149 | expect(itemResponse.body.personal.active).to.be.true; 150 | }); 151 | }); 152 | 153 | describe('successfully when the schema includes a default value', function(){ 154 | before(function (done) { 155 | app = support.newLoopbackJsonSchemaApp(); 156 | ItemSchema.create({ 157 | collectionName: 'people-readonly', 158 | title: 'Person', 159 | collectionTitle: 'People-readonly', 160 | type: 'object', 161 | properties: { 162 | status: { 163 | type: 'string', 164 | default: 'inactive' 165 | } 166 | } 167 | }, function(err, jsonSchema) { 168 | if (err) { return done(err); } 169 | jsonSchemaCollectionName = jsonSchema.collectionName; 170 | done(); 171 | }); 172 | }); 173 | 174 | before(function(done) { 175 | request(app) 176 | .post('/api/people-readonly') 177 | .set('Content-Type', 'application/json') 178 | .send('{"name": "Alice"}') 179 | .end(function (err, res) { 180 | if (err) { return done(err); } 181 | itemId = res.body.id; 182 | done(); 183 | }); 184 | }); 185 | 186 | before(function(done) { 187 | request(app) 188 | .put('/api/people-readonly/' + itemId) 189 | .set('Content-Type', 'application/json') 190 | .send('{"name": "Alice"}') 191 | .end(function (err, res) { 192 | if (err) { return done(err); } 193 | itemResponse = res; 194 | done(); 195 | }); 196 | }); 197 | 198 | it('should set default value', function() { 199 | expect(itemResponse.body.status).to.eql('inactive'); 200 | }); 201 | }); 202 | 203 | describe('with unsupported Content-Type', function() { 204 | before(function(done) { 205 | request(app) 206 | .put('/api/people/123') 207 | .set('Accept', 'application/json') 208 | .set('Content-Type', 'text/plain') 209 | .send('{"name": "Alice"}') 210 | .expect(415) 211 | .end(function (err, res) { 212 | if (err) { return done(err); } 213 | itemResponse = res; 214 | done(); 215 | }); 216 | }); 217 | 218 | it('should return error message', function() { 219 | expect(itemResponse.body.error.message).to.eq('Unsupported Content-Type: .'); 220 | }); 221 | }); 222 | 223 | describe('with validation errors', function() { 224 | before(function(done) { 225 | ItemSchema.create({ 226 | collectionName: 'people', 227 | title: 'Person', 228 | collectionTitle: 'People', 229 | type: 'object', 230 | properties: { 231 | name: { 232 | type: 'string', 233 | minLength: 1 234 | } 235 | }, 236 | required: ['name'] 237 | }, function(err, jsonSchema) { 238 | if (err) { return done(err); } 239 | jsonSchemaCollectionName = jsonSchema.collectionName; 240 | done(); 241 | }); 242 | }); 243 | 244 | before(function(done) { 245 | request(app) 246 | .post('/api/people') 247 | .set('Content-Type', 'application/json') 248 | .send('{"name": "Alice"}') 249 | .end(function (err, item) { 250 | if (err) { return done(err); } 251 | itemId = item.body.id; 252 | done(); 253 | }); 254 | }); 255 | 256 | before(function(done) { 257 | request(app) 258 | .put('/api/people/' + itemId) 259 | .set('Content-Type', 'application/json') 260 | .send('{"name": ""}') 261 | .expect(422) 262 | .end(function (err, res) { 263 | if (err) { return done(err); } 264 | itemResponse = res; 265 | done(); 266 | }); 267 | }); 268 | 269 | it('should return errors in body', function() { 270 | var error = itemResponse.body.error; 271 | expect(error.details).to.eql({ 272 | codes: { 273 | '/name': [ 274 | 200 275 | ], 276 | '_all': [ 277 | 'custom' 278 | ] 279 | }, 280 | context: 'people', 281 | messages: { 282 | '/name': [ 283 | 'String is too short (0 chars), minimum 1' 284 | ], 285 | '_all': [ 286 | 'Instance is invalid' 287 | ] 288 | } 289 | }); 290 | expect(error.message).to.contain('The `people` instance is not valid.'); 291 | expect(error.message).to.contain('`_all` Instance is invalid'); 292 | expect(error.message).to.contain('`/name` String is too short (0 chars), minimum 1'); 293 | expect(error.name).to.eq('ValidationError'); 294 | expect(error.status).to.eq(422); 295 | expect(error.statusCode).to.eq(422); 296 | }); 297 | }); 298 | }); 299 | -------------------------------------------------------------------------------- /test/integration/domain/collection-schema-factory.test.js: -------------------------------------------------------------------------------- 1 | var support = require('../../support'); 2 | 3 | var expect = require('chai').expect; 4 | 5 | var ItemSchema = require('../../../lib/domain/item-schema'); 6 | var CollectionSchemaFactory = require('../../../lib/domain/collection-schema-factory'); 7 | var config = require('../../../lib/support/config'); 8 | 9 | var app = support.newLoopbackJsonSchemaApp(); 10 | 11 | describe('CollectionSchemaFactory', function() { 12 | 13 | describe('#buildFromSchemaId', function() { 14 | describe('invalid item-schema', function(){ 15 | it('should return undefined', function(done){ 16 | var callback = function(err, collectionSchema){ 17 | expect(collectionSchema).to.be.null; 18 | done(); 19 | }; 20 | 21 | CollectionSchemaFactory.buildFromSchemaId('invalid-id', null, callback); 22 | }); 23 | }); 24 | 25 | describe('existing item schema', function() { 26 | var itemSchema; 27 | 28 | beforeEach(function(done) { 29 | ItemSchema.create({collectionName: 'test'}, function(err, data) { 30 | if (err) { return done(err); } 31 | itemSchema = data; 32 | done(); 33 | }); 34 | }); 35 | 36 | it('should return an instance of CollectionSchema', function (done) { 37 | var callback = function(err, collectionSchema){ 38 | expect(collectionSchema).to.be.instanceof(config.CollectionSchemaClass); 39 | done(); 40 | }; 41 | 42 | CollectionSchemaFactory.buildFromSchemaId(itemSchema.collectionName, null, callback); 43 | }); 44 | }); 45 | }); 46 | 47 | }); 48 | -------------------------------------------------------------------------------- /test/integration/domain/collection-schema.test.js: -------------------------------------------------------------------------------- 1 | var support = require('../../support'); 2 | 3 | var expect = require('chai').expect; 4 | 5 | var ItemSchema = require('../../../lib/domain/item-schema'); 6 | var CollectionSchema = require('../../../lib/domain/collection-schema'); 7 | var LJSRequest = require('../../../lib/http/ljs-request'); 8 | 9 | var app = support.newLoopbackJsonSchemaApp(); 10 | 11 | describe('CollectionSchema', function() { 12 | describe('#data', function() { 13 | describe('when corresponding item schema exists', function() { 14 | var collectionSchema, itemSchemaCollectionName; 15 | 16 | beforeEach(function (done) { 17 | ItemSchema.create({ 18 | collectionName: 'people', 19 | title: 'Person', 20 | collectionTitle: 'People', 21 | type: 'object', 22 | properties: {}, 23 | collectionLinks: [ 24 | { rel: 'custom', href: '/custom' } 25 | ] 26 | }, function(err, itemSchema) { 27 | if (err) { return done(err); } 28 | itemSchemaCollectionName = itemSchema.collectionName; 29 | collectionSchema = new CollectionSchema(itemSchema); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('should include type array', function () { 35 | var data = collectionSchema.data(); 36 | expect(data.type).to.eq('array'); 37 | }); 38 | 39 | it('should include "items" key pointing to itemSchema url', function () { 40 | var data = collectionSchema.data(); 41 | expect(data.items.$ref).to.eq('/item-schemas/' + itemSchemaCollectionName); 42 | }); 43 | 44 | it('should include $schema from ItemSchema', function () { 45 | var data = collectionSchema.data(); 46 | expect(data.$schema).to.eq('http://json-schema.org/draft-04/hyper-schema#'); 47 | }); 48 | 49 | it('should include collectionName from ItemSchema', function () { 50 | var data = collectionSchema.data(); 51 | expect(data.collectionName).to.eq('people'); 52 | }); 53 | 54 | it('should use the property "collectionTitle" from ItemSchema as title', function () { 55 | var data = collectionSchema.data(); 56 | expect(data.title).to.eq('People'); 57 | }); 58 | 59 | it('should include default and custom links', function() { 60 | var data = collectionSchema.data(); 61 | expect(data.links).to.eql([ 62 | { 63 | rel: 'self', 64 | href: '/people' 65 | }, 66 | { 67 | rel: 'list', 68 | href: '/people' 69 | }, 70 | { 71 | rel: 'add', 72 | method: 'POST', 73 | href: '/people', 74 | schema: { 75 | $ref: '/item-schemas/' + itemSchemaCollectionName 76 | } 77 | }, 78 | { 79 | rel: 'previous', 80 | href: '/people?filter[limit]={limit}&filter[offset]={previousOffset}{&paginateQs*}' 81 | }, 82 | { 83 | rel: 'next', 84 | href: '/people?filter[limit]={limit}&filter[offset]={nextOffset}{&paginateQs*}' 85 | }, 86 | { 87 | rel: 'page', 88 | href: '/people?filter[limit]={limit}&filter[offset]={offset}{&paginateQs*}' 89 | }, 90 | { 91 | rel: 'order', 92 | href: '/people?filter[order]={orderAttribute}%20{orderDirection}{&orderQs*}' 93 | }, 94 | { 95 | rel: 'custom', 96 | href: '/custom' 97 | } 98 | ]); 99 | }); 100 | }); 101 | 102 | describe('when navigationRoot exists', function() { 103 | var collectionSchema, itemSchemaCollectionName; 104 | 105 | beforeEach(function (done) { 106 | ItemSchema.create({ 107 | collectionName: 'people', 108 | title: 'Person', 109 | collectionTitle: 'People', 110 | type: 'object', 111 | properties: {} 112 | }, function(err, itemSchema) { 113 | if (err) { return done(err); } 114 | itemSchemaCollectionName = itemSchema.collectionName; 115 | collectionSchema = new CollectionSchema(itemSchema, '/search'); 116 | done(); 117 | }); 118 | }); 119 | 120 | it('should include links', function() { 121 | var data = collectionSchema.data(); 122 | expect(data.links).to.eql([ 123 | { 124 | rel: 'self', 125 | href: '/people/search' 126 | }, 127 | { 128 | rel: 'list', 129 | href: '/people/search' 130 | }, 131 | { 132 | rel: 'add', 133 | method: 'POST', 134 | href: '/people', 135 | schema: { 136 | $ref: '/item-schemas/' + itemSchemaCollectionName 137 | } 138 | }, 139 | { 140 | rel: 'previous', 141 | href: '/people/search?filter[limit]={limit}&filter[offset]={previousOffset}{&paginateQs*}' 142 | }, 143 | { 144 | rel: 'next', 145 | href: '/people/search?filter[limit]={limit}&filter[offset]={nextOffset}{&paginateQs*}' 146 | }, 147 | { 148 | rel: 'page', 149 | href: '/people/search?filter[limit]={limit}&filter[offset]={offset}{&paginateQs*}' 150 | }, 151 | { 152 | rel: 'order', 153 | href: '/people/search?filter[order]={orderAttribute}%20{orderDirection}{&orderQs*}' 154 | } 155 | ]); 156 | }); 157 | }); 158 | }); 159 | 160 | describe('#url', function() { 161 | var collectionSchema, schema; 162 | 163 | beforeEach(function() { 164 | schema = { id: 1 }; 165 | collectionSchema = new CollectionSchema(schema); 166 | }); 167 | 168 | it('should return URL this collection schema', function() { 169 | expect(collectionSchema.url()).to.eq('/collection-schemas/' + schema.collectionName); 170 | }); 171 | }); 172 | 173 | it('has a pluraModelName property', function () { 174 | expect(CollectionSchema.pluralModelName).to.eql('collection-schemas'); 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /test/integration/domain/item-schema.test.js: -------------------------------------------------------------------------------- 1 | var support = require('../../support'); 2 | 3 | var expect = require('chai').expect; 4 | var loopback = require('loopback'); 5 | 6 | var logger = require('../../../lib/support/logger') 7 | var ItemSchema = require('../../../lib/domain/item-schema'); 8 | var LJSRequest = require('../../../lib/http/ljs-request'); 9 | 10 | var app = support.newLoopbackJsonSchemaApp(); 11 | 12 | describe('ItemSchema', function() { 13 | describe('.findOne', function() { 14 | beforeEach(function(done) { 15 | ItemSchema.create({collectionName: 'test'}, function(err, instance) { 16 | if (err) { return done(err); }; 17 | done(); 18 | }); 19 | }); 20 | 21 | it('should have $schema', function(done) { 22 | ItemSchema.findOne({where: {collectionName: 'test'}}, function(err, itemSchema) { 23 | if (err) { return done(err); }; 24 | expect(itemSchema.$schema).to.exist; 25 | done(); 26 | }); 27 | }); 28 | }); 29 | 30 | describe('.create', function() { 31 | it('should set $schema', function(done) { 32 | ItemSchema.create({collectionName: 'test'}, function(err, itemSchema) { 33 | if (err) { return done(err); }; 34 | expect(itemSchema.$schema).to.exist; 35 | done(); 36 | }); 37 | }); 38 | 39 | it('should create model defined by the json schema provided', function(done) { 40 | ItemSchema.create({collectionName: 'test'}, function(err) { 41 | if (err) { return done(err); } 42 | expect(loopback.getModel('test')).to.exist; 43 | done(); 44 | }); 45 | }); 46 | 47 | it('should save custom links', function(done) { 48 | var customLinks = [{ rel: 'custom', href: '/custom' }]; 49 | ItemSchema.create({collectionName: 'people', links: customLinks}, function(err, itemSchema) { 50 | if (err) { return done(err); } 51 | expect(itemSchema.links).to.eql([ 52 | { rel: 'self', href: '/people/{id}' }, 53 | { rel: 'item', href: '/people/{id}' }, 54 | { 55 | rel: 'create', 56 | method: 'POST', 57 | href: '/people', 58 | schema: { 59 | $ref: '/item-schemas/' + itemSchema.collectionName 60 | } 61 | }, 62 | { rel: 'update', method: 'PUT', href: '/people/{id}' }, 63 | { rel: 'delete', method: 'DELETE', href: '/people/{id}' }, 64 | { rel: 'parent', href: '/people' }, 65 | { rel: 'custom', href: '/custom' } 66 | ]); 67 | done(); 68 | }); 69 | }); 70 | 71 | it('should not allow overriding default links', function(done) { 72 | var customLinks = [{ rel: 'self', href: '/custom' }]; 73 | ItemSchema.create({collectionName: 'people', links: customLinks}, function(err, itemSchema) { 74 | if (err) { return done(err); } 75 | expect(itemSchema.links).to.eql([ 76 | { rel: 'self', href: '/people/{id}' }, 77 | { rel: 'item', href: '/people/{id}' }, 78 | { 79 | rel: 'create', 80 | method: 'POST', 81 | href: '/people', 82 | schema: { 83 | $ref: '/item-schemas/' + itemSchema.collectionName 84 | } 85 | }, 86 | { rel: 'update', method: 'PUT', href: '/people/{id}' }, 87 | { rel: 'delete', method: 'DELETE', href: '/people/{id}' }, 88 | { rel: 'parent', href: '/people' } 89 | ]); 90 | done(); 91 | }); 92 | }); 93 | 94 | it('should persist only custom links', function(done) { 95 | var customLinks = [{ rel: 'self', href: '/people/{id}' }, { rel: 'custom', href: '/custom' }]; 96 | 97 | ItemSchema.create({collectionName: 'people', links: customLinks}, function(err, itemSchema) { 98 | if (err) { return done(err); } 99 | 100 | ItemSchema.findOne({'collectionName': 'people'}, function(err, itemSchema) { 101 | if (err) { return done(err); } 102 | 103 | expect(itemSchema.__data.links).to.eql([ 104 | { rel: 'custom', href: '/custom' } 105 | ]); 106 | 107 | done(err); 108 | }); 109 | }); 110 | }); 111 | }); 112 | 113 | describe('.findByCollectionName', function() { 114 | beforeEach(function() { 115 | this.sinon.stub(logger, 'info'); 116 | this.sinon.stub(logger, 'warn'); 117 | }); 118 | 119 | it('should find ItemSchema by collection name', function(done) { 120 | var callback = function(err, itemSchema) { 121 | expect(itemSchema.collectionName).to.eq('people'); 122 | done(); 123 | }; 124 | 125 | ItemSchema.create({collectionName: 'people' }, function(err) { 126 | if (err) { return done(err); } 127 | ItemSchema.findByCollectionName('people', callback); 128 | }); 129 | }); 130 | 131 | it('should log when collection JSON schema was not found', function(done) { 132 | var callback = function(err) { 133 | if (err) { return done(err); } 134 | expect(logger.warn).to.have.been.calledWith('JSON Schema for collectionName', 'people', 'not found.'); 135 | done(); 136 | }; 137 | 138 | ItemSchema.findByCollectionName('people', callback); 139 | }); 140 | }); 141 | 142 | describe('#registerModel', function() { 143 | describe('hooks', function(){ 144 | describe('when called twice', function() { 145 | var citySchema; 146 | 147 | beforeEach(function(done) { 148 | ItemSchema.defineRemoteHooks = this.sinon.spy(ItemSchema.defineRemoteHooks); 149 | citySchema = new ItemSchema({collectionName: 'cities'}); 150 | 151 | var cityModel = citySchema.constructModel(); 152 | 153 | citySchema.registerModel(cityModel, function(err) { 154 | if (err) { return done(err); } 155 | 156 | citySchema.registerModel(cityModel, function(err) { 157 | if (err) { return done(err); } 158 | done(); 159 | }); 160 | }); 161 | }); 162 | 163 | it('should register remote hooks only once', function() { 164 | expect(ItemSchema.defineRemoteHooks).to.have.callCount(1); 165 | }); 166 | }); 167 | }); 168 | 169 | describe('validation', function() { 170 | var schemaDefinition = { 171 | collectionName: 'people', 172 | properties: { 173 | firstName: { 174 | type: "string" 175 | }, 176 | age: { 177 | "type": "integer", 178 | "minimum": 18 179 | } 180 | }, 181 | required : ["firstName", "age"], 182 | }; 183 | 184 | it('should not return error when instance is valid', function(done) { 185 | ItemSchema.create(schemaDefinition, function(err, itemSchema) { 186 | if (err) { return done(err); } 187 | 188 | var PersonInvalid = itemSchema.constructModel(); 189 | 190 | itemSchema.registerModel(PersonInvalid, function(err) { 191 | var alice = new PersonInvalid({ firstName: 'Alice', age : 18 }); 192 | alice.isValid(function(valid) { 193 | expect(valid).to.be.true; 194 | expect(alice.errors).to.be.false; 195 | done(err); 196 | }); 197 | }); 198 | }); 199 | }); 200 | 201 | it('should return error when instance is invalid', function(done) { 202 | ItemSchema.create(schemaDefinition, function(err, itemSchema) { 203 | if (err) { return done(err); } 204 | 205 | var PersonInvalid = itemSchema.constructModel(); 206 | 207 | itemSchema.registerModel(PersonInvalid, function(err) { 208 | var alice = new PersonInvalid({ age : 1 }); 209 | 210 | alice.isValid(function(valid) { 211 | expect(valid).to.be.false; 212 | expect(alice.errors['/firstName'][0]).to.be.eql('Missing required property: firstName'); 213 | expect(alice.errors['/age'][0]).to.be.eql('Value 1 is less than minimum 18'); 214 | expect(alice.errors['_all'][0]).to.be.eql('Instance is invalid'); 215 | expect(alice.errors.codes['/firstName'][0]).to.be.eql(302); 216 | expect(alice.errors.codes['/age'][0]).to.be.eql(101); 217 | expect(alice.errors.codes['_all'][0]).to.be.eql('custom'); 218 | done(err); 219 | }); 220 | }); 221 | }); 222 | }); 223 | }); 224 | }); 225 | }); 226 | -------------------------------------------------------------------------------- /test/integration/http/register-loopback-model.test.js: -------------------------------------------------------------------------------- 1 | var support = require('../../support'); 2 | 3 | var expect = require('chai').expect; 4 | var loopback = require('loopback'); 5 | 6 | var logger = require('../../../lib/support/logger') 7 | var LJSRequest = require('../../../lib/http/ljs-request'); 8 | var ItemSchema = require('../../../lib/domain/item-schema'); 9 | var registerLoopbackModel = require('../../../lib/http/register-loopback-model'); 10 | 11 | var app = support.newLoopbackJsonSchemaApp(); 12 | 13 | describe('registerLoopbackModel', function() { 14 | describe('#handle', function() { 15 | var ljsReq; 16 | 17 | beforeEach(function() { 18 | var req = { body: 'body', protocol: 'http', url: '/cars', originalUrl: '/api/cars', app: app, get: this.sinon.stub() }; 19 | ljsReq = new LJSRequest(req, app); 20 | 21 | this.sinon.stub(logger, 'info'); 22 | this.sinon.stub(logger, 'warn'); 23 | }); 24 | 25 | it('should register loopback model for an existing collection JSON schema', function(done) { 26 | var callback = function(err) { 27 | if (err) { return done(err); } 28 | app.models().splice(0, app.models().length); 29 | var Car = loopback.getModel('cars'); 30 | expect(Car).to.not.be.null; 31 | expect(Car.definition.settings.plural).to.equal('cars'); 32 | done(); 33 | }; 34 | 35 | ItemSchema.create({collectionName: 'cars' }, function(err) { 36 | if (err) { return done(err); } 37 | registerLoopbackModel.handle(ljsReq, callback); 38 | }); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/support.js: -------------------------------------------------------------------------------- 1 | require('mocha-sinon')(); 2 | 3 | var chai = require('chai'); 4 | var sinonChai = require('sinon-chai'); 5 | 6 | var ItemSchema = require('../lib/domain/item-schema'); 7 | var loopback = require('loopback'); 8 | var loopbackJsonSchema = require('../index'); 9 | 10 | chai.use(sinonChai); 11 | 12 | afterEach(function(done) { 13 | ItemSchema.deleteAll(function(err) { 14 | if (err) { return done(err); }; 15 | done(); 16 | }); 17 | }); 18 | 19 | var support = { 20 | newLoopbackJsonSchemaApp: function(config) { 21 | var app = loopback(); 22 | app.set('restApiRoot', '/api'); 23 | loopbackJsonSchema.init(app, config || {}); 24 | loopbackJsonSchema.enableJsonSchemaMiddleware(app); 25 | app.use(app.get('restApiRoot'), loopback.rest()); 26 | app.use(loopback.errorHandler()); 27 | return app; 28 | } 29 | }; 30 | 31 | module.exports = support; 32 | -------------------------------------------------------------------------------- /test/unit/domain/extended-validation.test.js: -------------------------------------------------------------------------------- 1 | require('../../support'); 2 | 3 | var expect = require('chai').expect; 4 | var loopback = require('loopback'); 5 | 6 | var ItemSchema = require('../../../lib/domain/item-schema'); 7 | var extendedValidation = require('../../../lib/domain/extended-validation'); 8 | 9 | var app = loopback(); 10 | app.set('restApiRoot', '/api'); 11 | 12 | describe('ItemSchema extended validation', function() { 13 | var errors; 14 | 15 | describe('when property and relation names clash', function() { 16 | var itemSchema; 17 | 18 | beforeEach(function(done) { 19 | var clashingSchema = { 20 | collectionName: "the-clash", 21 | properties: { 22 | name: { 23 | type: "string" 24 | }, 25 | clash: { 26 | type: "string" 27 | } 28 | }, 29 | relations: { 30 | clash: { 31 | collectionName: "the-clash", 32 | type: "belongsToMe", 33 | foreignKey: "clash" 34 | } 35 | } 36 | } 37 | 38 | itemSchema = new ItemSchema(clashingSchema); 39 | 40 | done(); 41 | }); 42 | 43 | it('should be detected', function() { 44 | errors = extendedValidation(itemSchema); 45 | 46 | expect(errors.length).to.be.above(0); 47 | }); 48 | 49 | describe('with weak validation enabled', function() { 50 | it('should be ignored', function() { 51 | itemSchema.weakValidation = true; 52 | errors = extendedValidation(itemSchema); 53 | 54 | expect(errors.length).to.eql(0); 55 | }); 56 | }); 57 | }); 58 | 59 | describe('when property and relation names don\'t clash', function() { 60 | var itemSchema; 61 | 62 | beforeEach(function(done) { 63 | var clashingSchema = { 64 | collectionName: "no-clash", 65 | properties: { 66 | name: { 67 | type: "string" 68 | }, 69 | clash: { 70 | type: "string" 71 | } 72 | }, 73 | relations: { 74 | clashRelation: { 75 | collectionName: "no-clash", 76 | type: "belongsToMe", 77 | foreignKey: "noClash" 78 | } 79 | } 80 | } 81 | 82 | itemSchema = new ItemSchema(clashingSchema); 83 | 84 | done(); 85 | }); 86 | 87 | it('should not be detected', function() { 88 | errors = extendedValidation(itemSchema); 89 | 90 | expect(errors.length).to.eql(0); 91 | }); 92 | 93 | describe('with weak validation enabled', function() { 94 | it('should be ignored', function() { 95 | itemSchema.weakValidation = true; 96 | errors = extendedValidation(itemSchema); 97 | 98 | expect(errors.length).to.eql(0); 99 | }); 100 | }); 101 | }); 102 | 103 | describe('when reserved property names are used', function() { 104 | var itemSchema; 105 | var reservedNames; 106 | 107 | beforeEach(function(done) { 108 | var reservedProperties = {}; 109 | reservedNames = [ 110 | '__cachedRelations', 111 | '__data', 112 | '__dataSource', 113 | '__strict', 114 | '__persisted', 115 | 'id', 116 | 'created', 117 | 'modified', 118 | 'createdBy', 119 | 'tenantId', 120 | 'tenant', 121 | 'versionId' 122 | ]; 123 | reservedNames.forEach(n => reservedProperties[n] = { type: "string" }); 124 | 125 | var schema = { 126 | collectionName: "reserved-properties", 127 | properties: reservedProperties 128 | } 129 | 130 | itemSchema = new ItemSchema(schema); 131 | 132 | done(); 133 | }); 134 | 135 | it('should be detected', function() { 136 | errors = extendedValidation(itemSchema); 137 | 138 | expect(errors.length).to.eql(reservedNames.length); 139 | }); 140 | 141 | describe('with weak validation enabled', function() { 142 | it('should be ignored', function() { 143 | itemSchema.weakValidation = true; 144 | errors = extendedValidation(itemSchema); 145 | 146 | expect(errors.length).to.eql(0); 147 | }); 148 | }); 149 | }); 150 | 151 | describe('when no properties or relations are declared', function() { 152 | var itemSchema; 153 | 154 | beforeEach(function(done) { 155 | var schema = { 156 | collectionName: "reserved-properties" 157 | } 158 | 159 | itemSchema = new ItemSchema(schema); 160 | 161 | done(); 162 | }); 163 | 164 | it('should be executed without errors', function() { 165 | errors = extendedValidation(itemSchema); 166 | expect(errors.length).to.eql(0); 167 | }); 168 | }); 169 | 170 | describe('when no properties are declared', function() { 171 | var itemSchema; 172 | 173 | beforeEach(function(done) { 174 | var schema = { 175 | collectionName: "reserved-properties", 176 | relations: { 177 | clashRelation: { 178 | collectionName: "no-clash", 179 | type: "belongsToMe", 180 | foreignKey: "noClash" 181 | } 182 | } 183 | } 184 | 185 | itemSchema = new ItemSchema(schema); 186 | 187 | done(); 188 | }); 189 | 190 | it('should be executed without errors', function() { 191 | errors = extendedValidation(itemSchema); 192 | expect(errors.length).to.eql(0); 193 | }); 194 | }); 195 | 196 | describe('when no properties are declared', function() { 197 | var itemSchema; 198 | 199 | beforeEach(function(done) { 200 | var schema = { 201 | collectionName: "reserved-properties", 202 | properties: { 203 | name: { 204 | type: "string" 205 | }, 206 | clash: { 207 | type: "string" 208 | } 209 | } 210 | } 211 | 212 | itemSchema = new ItemSchema(schema); 213 | 214 | done(); 215 | }); 216 | 217 | it('should be executed without errors', function() { 218 | errors = extendedValidation(itemSchema); 219 | expect(errors.length).to.eql(0); 220 | }); 221 | }); 222 | }); 223 | -------------------------------------------------------------------------------- /test/unit/domain/links.test.js: -------------------------------------------------------------------------------- 1 | require('../../support'); 2 | 3 | var expect = require('chai').expect; 4 | var loopback = require('loopback'); 5 | 6 | var Links = require('../../../lib/domain/links'); 7 | var LJSRequest = require('../../../lib/http/ljs-request'); 8 | 9 | var app = loopback(); 10 | app.set('restApiRoot', '/api'); 11 | 12 | describe('Links', function() { 13 | describe('#all', function() { 14 | var allLinks; 15 | 16 | before(function() { 17 | var defaultLinks = [ 18 | { rel: 'self', href: '/api' } 19 | ]; 20 | var relationLinks = [ 21 | { rel: 'work', href: '/api/MyApi/{id}/work' } 22 | ]; 23 | var customLinks = [ 24 | { rel: 'custom-absolute', href: 'http://other.example.org/custom-absolute' }, 25 | { rel: 'custom-relative', href: '/custom-relative' }, 26 | { rel: 'self', href: 'http://example.org/api/override/self' } 27 | ]; 28 | var links = new Links(defaultLinks, relationLinks, customLinks); 29 | allLinks = links.all(); 30 | }); 31 | 32 | it('should include default links', function() { 33 | expect(allLinks[0]).to.eql({ rel: 'self', href: '/api' }); 34 | }); 35 | 36 | it('should include relation links', function(){ 37 | expect(allLinks[1]).to.eql({ rel: 'work', href: '/api/MyApi/{id}/work' }); 38 | }); 39 | 40 | it('should include custom absolute links', function() { 41 | expect(allLinks[2]).to.eql({ rel: 'custom-absolute', href: 'http://other.example.org/custom-absolute' }); 42 | }); 43 | 44 | it('should include custom relative links', function() { 45 | expect(allLinks[3]).to.eql({ rel: 'custom-relative', href: '/custom-relative' }); 46 | }); 47 | 48 | it('should not allow overriding default links', function() { 49 | expect(allLinks).to.have.length(4); 50 | expect(allLinks[0]).to.eql({ rel: 'self', href: '/api' }); 51 | }); 52 | }); 53 | 54 | describe('#customLinks', function() { 55 | var returnedCustomLinks; 56 | before(function() { 57 | var defaultLinks = [ 58 | { rel: 'self', href: '/api' } 59 | ]; 60 | var relationLinks = []; 61 | var customLinks = [ 62 | { rel: 'custom-absolute', href: 'http://other.example.org/custom-absolute' }, 63 | { rel: 'custom-relative', href: '/custom-relative' }, 64 | { rel: 'self', href: 'http://example.org/api/override/self' } 65 | ]; 66 | var links = new Links(defaultLinks, relationLinks, customLinks); 67 | returnedCustomLinks = links.custom(); 68 | }); 69 | 70 | it('should include custom absolute links', function() { 71 | expect(returnedCustomLinks[0]).to.eql({ rel: 'custom-absolute', href: 'http://other.example.org/custom-absolute' }); 72 | }); 73 | 74 | it('should include custom relative links', function() { 75 | expect(returnedCustomLinks[1]).to.eql({ rel: 'custom-relative', href: '/custom-relative' }); 76 | }); 77 | 78 | it('should not allow overriding default links', function() { 79 | expect(returnedCustomLinks).to.have.length(2); 80 | }); 81 | }); 82 | 83 | describe('#relationLinks', function() { 84 | var returnedRelationLinks; 85 | before(function() { 86 | var defaultLinks = [ 87 | { rel: 'self', href: '/api' } 88 | ]; 89 | var relationLinks = [ 90 | { rel: 'friend', href: '/api/people/{id}/friend' }, 91 | { rel: 'self', href: 'http://example.org/api/override/self' } 92 | ]; 93 | var customLinks = []; 94 | 95 | var links = new Links(defaultLinks, relationLinks, customLinks); 96 | returnedRelationLinks = links.relations(); 97 | }); 98 | 99 | it('should include custom absolute links', function() { 100 | expect(returnedRelationLinks[0]).to.eql({ rel: 'friend', href: '/api/people/{id}/friend' }); 101 | }); 102 | 103 | it('should not allow overriding default links', function() { 104 | expect(returnedRelationLinks).to.have.length(1); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /test/unit/domain/model-properties-converter.test.js: -------------------------------------------------------------------------------- 1 | require('../../support'); 2 | 3 | var expect = require('chai').expect; 4 | 5 | var ItemSchema = require('../../../lib/domain/item-schema'); 6 | var modelPropertiesConverter = require('../../../lib/domain/model-properties-converter'); 7 | 8 | 9 | describe('modelPropertiesConverter', function() { 10 | function findLink(links, rel) { 11 | return links.find(function (link) { 12 | return link.rel === rel; 13 | }); 14 | } 15 | describe('converting $schema', function() { 16 | beforeEach(function() { 17 | this.jsonSchema = new ItemSchema({ 18 | collectionName: 'test', 19 | indexes: { 20 | 'file_width_index': { 21 | 'file.width': 1, 22 | 'file.resource.size': 1 23 | } 24 | }, 25 | collectionLinks: [ 26 | { 27 | rel: 'search', 28 | href: '/search', 29 | schema: { 30 | properties: { 31 | 'dot.value': { 32 | type: 'object' 33 | } 34 | } 35 | } 36 | }, 37 | { 38 | rel: 'item', 39 | href: '/bar' 40 | } 41 | ], 42 | links: [ 43 | { 44 | rel: 'publish', 45 | href: '/publish', 46 | schema: { 47 | properties: { 48 | 'dot.value': { 49 | type: 'object' 50 | } 51 | } 52 | } 53 | }, 54 | { 55 | rel: 'item', 56 | href: '/bar' 57 | } 58 | ], 59 | versionIndexes: { 60 | 'file_width_index': { 61 | 'file.width': 1, 62 | 'file.resource.size': 1 63 | } 64 | } 65 | }); 66 | 67 | this.jsonSchemaWithKeys = new ItemSchema({ 68 | 'collectionName': 'testKeys', 69 | "indexes": { 70 | "file_width_index": { 71 | "keys": { 72 | "file.width": 1, 73 | "file.height": 1, 74 | "file.resource.size": 1 75 | }, 76 | "options": { 77 | "unique": true 78 | } 79 | } 80 | }, 81 | "versionIndexes": { 82 | "file_width_index": { 83 | "keys": { 84 | "file.width": 1, 85 | "file.height": 1, 86 | "file.resource.size": 1 87 | }, 88 | "options": { 89 | "unique": true 90 | } 91 | } 92 | } 93 | }); 94 | 95 | this.jsonSchema.update$schema(); 96 | this.jsonSchema.__data.$schema = this.jsonSchema.$schema; // __data.$schema will be defined when a post is received 97 | }); 98 | 99 | describe('.convert', function() { 100 | it('should convert only if there is a $schema', function() { 101 | delete this.jsonSchema.$schema; 102 | 103 | modelPropertiesConverter.convert(this.jsonSchema); 104 | 105 | expect(this.jsonSchema).to.not.have.ownProperty('%24schema'); 106 | }); 107 | 108 | it('should convert $schema to %24schema', function() { 109 | modelPropertiesConverter.convert(this.jsonSchema); 110 | 111 | expect(this.jsonSchema['%24schema']).to.exist; 112 | expect(this.jsonSchema.__data['%24schema']).to.exist; 113 | expect(this.jsonSchema.$schema).to.not.exist; 114 | expect(this.jsonSchema.__data.$schema).to.not.exist; 115 | }); 116 | 117 | it('should convert indexes with dot to %2E', function() { 118 | modelPropertiesConverter.convert(this.jsonSchema); 119 | var parentNode = this.jsonSchema.indexes['file_width_index']; 120 | expect(parentNode['file%2Ewidth']).to.exist; 121 | expect(parentNode['file.width']).to.not.exist; 122 | }); 123 | 124 | it('should convert indexes with dotted keys to %2E', function() { 125 | modelPropertiesConverter.convert(this.jsonSchemaWithKeys); 126 | var keys = this.jsonSchemaWithKeys.indexes['file_width_index'].keys; 127 | expect(keys['file%2Ewidth']).to.exist; 128 | expect(keys['file.width']).to.not.exist; 129 | }); 130 | 131 | it('should convert indexes with dotted keys to %2E', function() { 132 | modelPropertiesConverter.convert(this.jsonSchemaWithKeys); 133 | var keys = this.jsonSchemaWithKeys.indexes['file_width_index'].keys; 134 | expect(keys['file%2Ewidth']).to.exist; 135 | expect(keys['file.width']).to.not.exist; 136 | }); 137 | 138 | it('should convert indexes with more than one dotted keys to %2E', function() { 139 | modelPropertiesConverter.convert(this.jsonSchemaWithKeys); 140 | var keys = this.jsonSchemaWithKeys.indexes['file_width_index'].keys; 141 | expect(keys['file%2Eresource%2Esize']).to.exist; 142 | expect(keys['file.resource.size']).to.not.exist; 143 | }); 144 | 145 | it('should convert indexes with more than one dot to %2E', function() { 146 | modelPropertiesConverter.convert(this.jsonSchema); 147 | var parentNode = this.jsonSchema.indexes['file_width_index']; 148 | expect(parentNode['file%2Eresource%2Esize']).to.exist; 149 | expect(parentNode['file.resource.size']).to.not.exist; 150 | }); 151 | 152 | it('should convert versionIndexes with dot to %2E', function() { 153 | modelPropertiesConverter.convert(this.jsonSchema); 154 | var parentNode = this.jsonSchema.versionIndexes['file_width_index']; 155 | expect(parentNode['file%2Ewidth']).to.exist; 156 | expect(parentNode['file.width']).to.not.exist; 157 | }); 158 | 159 | it('should convert versionIndexes with more than one dot to %2E', function() { 160 | modelPropertiesConverter.convert(this.jsonSchema); 161 | var parentNode = this.jsonSchema.versionIndexes['file_width_index']; 162 | expect(parentNode['file%2Eresource%2Esize']).to.exist; 163 | expect(parentNode['file.resource.size']).to.not.exist; 164 | }); 165 | 166 | it('should convert versionIndexes with dotted keys to %2E', function() { 167 | modelPropertiesConverter.convert(this.jsonSchemaWithKeys); 168 | var keys = this.jsonSchemaWithKeys.versionIndexes['file_width_index'].keys; 169 | expect(keys['file%2Ewidth']).to.exist; 170 | expect(keys['file.width']).to.not.exist; 171 | }); 172 | 173 | it('should convert versionIndexes with more than onedotted keys to %2E', function() { 174 | modelPropertiesConverter.convert(this.jsonSchemaWithKeys); 175 | var keys = this.jsonSchemaWithKeys.versionIndexes['file_width_index'].keys; 176 | expect(keys['file%2Eresource%2Esize']).to.exist; 177 | expect(keys['file.resource.size']).to.not.exist; 178 | }); 179 | 180 | it('should convert collectionLinks schema with dotted keys to %2E', function() { 181 | modelPropertiesConverter.convert(this.jsonSchema); 182 | var link = findLink(this.jsonSchema.collectionLinks, 'search'); 183 | var props = link.schema.properties; 184 | expect(props['dot%2Evalue']).to.exist; 185 | expect(props['dot.value']).to.not.exist; 186 | }); 187 | 188 | it('should convert links schema with dotted keys to %2E', function() { 189 | modelPropertiesConverter.convert(this.jsonSchema); 190 | var link = findLink(this.jsonSchema.links, 'publish'); 191 | var props = link.schema.properties; 192 | expect(props['dot%2Evalue']).to.exist; 193 | expect(props['dot.value']).to.not.exist; 194 | }); 195 | 196 | it('should not change options in the indexes definitions while converting dotted keys to %2E', function() { 197 | var options = this.jsonSchemaWithKeys['indexes']['file_width_index']['options']; 198 | expect(options.unique).to.be.true; 199 | modelPropertiesConverter.convert(this.jsonSchemaWithKeys); 200 | expect(options.unique).to.be.true; 201 | }); 202 | }); 203 | 204 | describe('.restore', function() { 205 | beforeEach(function() { 206 | modelPropertiesConverter.convert(this.jsonSchema); 207 | modelPropertiesConverter.restore(this.jsonSchema); 208 | }); 209 | 210 | it('should restore %24schema to $schema', function() { 211 | expect(this.jsonSchema.$schema).to.exist; 212 | expect(this.jsonSchema.__data.$schema).to.exist; 213 | expect(this.jsonSchema['%24schema']).to.not.exist; 214 | expect(this.jsonSchema.__data['%24schema']).to.not.exist; 215 | }); 216 | 217 | it('should restore indexes with %2E to dot', function() { 218 | var opts = this.jsonSchema['indexes']['file_width_index']; 219 | expect(opts['file%2Ewidth']).to.not.exist; 220 | expect(opts['file.width']).to.exist; 221 | }); 222 | 223 | it('should restore indexes with %2E to dot', function() { 224 | var opts = this.jsonSchema['indexes']['file_width_index']; 225 | expect(opts['file%2Ewidth']).to.not.exist; 226 | expect(opts['file.width']).to.exist; 227 | }); 228 | 229 | it('should restore indexes with %2E\'d keys to dot', function() { 230 | var opts = this.jsonSchema['indexes']['file_width_index']; 231 | expect(opts['file%2Ewidth']).to.not.exist; 232 | expect(opts['file.width']).to.exist; 233 | }); 234 | 235 | it('should restore indexes with more than one %2E to dot', function() { 236 | var opts = this.jsonSchema['indexes']['file_width_index']; 237 | expect(opts['file%2Eresource%2Esize']).to.not.exist; 238 | expect(opts['file.resource.size']).to.exist; 239 | }); 240 | 241 | it('should restore versionIndexes with more than one %2E to dot', function() { 242 | var opts = this.jsonSchema['indexes']['file_width_index']; 243 | expect(opts['file%2Eresource%2Esize']).to.not.exist; 244 | expect(opts['file.resource.size']).to.exist; 245 | }); 246 | 247 | it('should restore versionIndexes with %2E to dot', function() { 248 | var opts = this.jsonSchema['versionIndexes']['file_width_index']; 249 | expect(opts['file%2Ewidth']).to.not.exist; 250 | expect(opts['file.width']).to.exist; 251 | }); 252 | 253 | it('should restore versionIndexes with %2E to dot', function() { 254 | var opts = this.jsonSchema['versionIndexes']['file_width_index']; 255 | expect(opts['file%2Ewidth']).to.not.exist; 256 | expect(opts['file.width']).to.exist; 257 | }); 258 | 259 | it('should restore versionIndexes with more than one %2E to dot', function() { 260 | var opts = this.jsonSchema['versionIndexes']['file_width_index']; 261 | expect(opts['file%2Eresource%2Esize']).to.not.exist; 262 | expect(opts['file.resource.size']).to.exist; 263 | }); 264 | 265 | it('should restore versionIndexes with %2E\'d keys to dot', function() { 266 | var opts = this.jsonSchema['versionIndexes']['file_width_index']; 267 | expect(opts['file%2Ewidth']).to.not.exist; 268 | expect(opts['file.width']).to.exist; 269 | }); 270 | 271 | it('should restore collectionLinks schema', function() { 272 | var link = findLink(this.jsonSchema.collectionLinks, 'search'); 273 | var props = link.schema.properties; 274 | expect(props['dot%2Evalue']).to.not.exist; 275 | expect(props['dot.value']).to.exist; 276 | }); 277 | 278 | it('should restore links schema', function() { 279 | var link = findLink(this.jsonSchema.links, 'publish'); 280 | var props = link.schema.properties; 281 | expect(props['dot%2Evalue']).to.not.exist; 282 | expect(props['dot.value']).to.exist; 283 | }); 284 | }); 285 | }); 286 | }); 287 | -------------------------------------------------------------------------------- /test/unit/domain/models.test.js: -------------------------------------------------------------------------------- 1 | require('../../support'); 2 | 3 | var expect = require('chai').expect; 4 | var loopback = require('loopback'); 5 | 6 | var models = require('../../../lib/domain/models'); 7 | 8 | describe('models', function() { 9 | describe('.fromPluralModelName', function() { 10 | var app = loopback(); 11 | var Model; 12 | 13 | beforeEach(function() { 14 | var TestModel = { modelName: 'person-test-models'}; 15 | app.models().push(TestModel); 16 | Model = models.fromPluralModelName(app, 'person-test-models'); 17 | }); 18 | 19 | afterEach(function() { 20 | app.models().splice(0, app.models().length); 21 | }); 22 | 23 | it('should return Model class from plural model name', function() { 24 | expect(Model.modelName).to.eq('person-test-models'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/unit/domain/registry-models.test.js: -------------------------------------------------------------------------------- 1 | require('../../support'); 2 | 3 | var expect = require('chai').expect; 4 | var loopback = require('loopback'); 5 | 6 | var RegistryModels = require('../../../lib/domain/registry-models'); 7 | 8 | describe('RegistryModels', function() { 9 | var registryModels; 10 | 11 | beforeEach(function() { 12 | registryModels = new RegistryModels; 13 | }); 14 | 15 | describe('#appendModelV1', function() { 16 | var model = {collectionName: 'my-collection'}; 17 | 18 | beforeEach(function() { 19 | registryModels.appendModelV1('my-collection', model); 20 | }); 21 | 22 | it('should add model to V1 map', function() { 23 | expect(registryModels.v1Models.hasOwnProperty('my-collection')).to.be.true; 24 | expect(registryModels.v1Models['my-collection']).to.eql(model); 25 | }); 26 | 27 | it('should overwrite an existing V1 map item', function() { 28 | var newModel = { collectionName: 'new-collection' }; 29 | registryModels.appendModelV1('my-collection', newModel); 30 | expect(registryModels.v1Models['my-collection']).to.eql(newModel); 31 | }); 32 | }); 33 | 34 | describe('#appendModelV2', function() { 35 | var model = {collectionName: 'my-collection', tenantId: 'my-tenant'}; 36 | var otherModel = {collectionName: 'my-collection', tenantId: 'other-tenant'}; 37 | 38 | beforeEach(function() { 39 | registryModels.appendModelV2('my-collection', 'my-tenant', model); 40 | registryModels.appendModelV2('my-collection', 'other-tenant', otherModel); 41 | }); 42 | 43 | it('should add model to V2 map', function() { 44 | expect(registryModels.v2Models.hasOwnProperty('my-collection')).to.be.true; 45 | 46 | expect(registryModels.v2Models['my-collection'].hasOwnProperty('my-tenant')).to.be.true; 47 | expect(registryModels.v2Models['my-collection']['my-tenant']).to.eql(model); 48 | 49 | expect(registryModels.v2Models['my-collection'].hasOwnProperty('other-tenant')).to.be.true; 50 | expect(registryModels.v2Models['my-collection']['other-tenant']).to.eql(otherModel); 51 | }); 52 | 53 | it('should not overwrite an existing V2 map item', function() { 54 | registryModels.appendModelV1('my-collection', {collectionName: 'new-collection', tenantId: 'new-tenant'}); 55 | expect(registryModels.v2Models['my-collection']['my-tenant']).to.eql(model); 56 | }); 57 | }); 58 | 59 | describe('#stats', function () { 60 | it('should export summary about all items in registry', function() { 61 | registryModels.reset(); 62 | registryModels.appendModelV1('my-collection', { collectionName: 'my-collection' }); 63 | registryModels.appendModelV2('my-tenant', 'my-collection', { collectionName: 'my-collection' }); 64 | expect(registryModels.stats()).to.be.eql({ 65 | v1: [ 66 | 'my-collection' 67 | ], 68 | v2: [ 69 | { 70 | collectionName: 'my-tenant', 71 | tenants: [ 72 | 'my-collection' 73 | ], 74 | }, 75 | ] 76 | }); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/unit/http/ljs-request.test.js: -------------------------------------------------------------------------------- 1 | require('../../support'); 2 | 3 | var expect = require('chai').expect; 4 | 5 | var LJSRequest = require('../../../lib/http/ljs-request'); 6 | var LJSUrl = require('../../../lib/http/ljs-url'); 7 | 8 | describe('LJSRequest', function() { 9 | var ljsReq, req; 10 | 11 | describe('#properties', function() { 12 | beforeEach(function() { 13 | req = { body: 'body', protocol: 'http', url: '/people/1' }; 14 | ljsReq = new LJSRequest(req); 15 | }); 16 | 17 | it("should return inner request's body", function() { 18 | expect(ljsReq.body).to.equal(req.body); 19 | }); 20 | }); 21 | 22 | describe('#schemeAndAuthority', function() { 23 | beforeEach(function() { 24 | req = { protocol: 'http', app:{}, url: '/people/1' }; 25 | req.get = this.sinon.stub(); 26 | req.get.withArgs('Host').returns('example.org'); 27 | ljsReq = new LJSRequest(req, req.app); 28 | }); 29 | 30 | it("should return inner request's scheme and authority concatenated together", function() { 31 | expect(ljsReq.schemeAndAuthority()).to.equal('http://example.org'); 32 | }); 33 | }); 34 | 35 | describe('#baseUrl', function() { 36 | beforeEach(function() { 37 | req = { protocol: 'http', app: {}, url: '/people/1' }; 38 | req.app.get = this.sinon.stub().returns('/api');; 39 | req.get = this.sinon.stub(); 40 | req.get.withArgs('Host').returns('example.org'); 41 | ljsReq = new LJSRequest(req, req.app); 42 | }); 43 | 44 | it("should return inner request's scheme, authority and api rest concatenated together", function() { 45 | expect(ljsReq.baseUrl()).to.equal('http://example.org/api'); 46 | }); 47 | }); 48 | 49 | describe('#fullUrl', function() { 50 | beforeEach(function() { 51 | req = { protocol: 'http', app: {}, url: '/people/1', originalUrl: '/api/people/1' }; 52 | req.get = this.sinon.stub(); 53 | req.get.withArgs('Host').returns('example.org'); 54 | ljsReq = new LJSRequest(req, req.app); 55 | }); 56 | 57 | it("should return full url", function() { 58 | expect(ljsReq.fullUrl()).to.equal('http://example.org/api/people/1'); 59 | }); 60 | }); 61 | 62 | describe('#ljsUrl', function() { 63 | beforeEach(function() { 64 | req = { protocol: 'http', app: {}, url: '/people/1', originalUrl: '/api/people/1' }; 65 | req.get = this.sinon.stub(); 66 | req.get.withArgs('Host').returns('example.org'); 67 | ljsReq = new LJSRequest(req, req.app); 68 | }); 69 | 70 | it('should build ljsUrl object from current request', function () { 71 | expect(ljsReq.ljsUrl()).to.be.an.instanceof(LJSUrl) 72 | }); 73 | }); 74 | 75 | describe('#safeHeaders', function() { 76 | describe('when authorization header is present', function () { 77 | beforeEach(function() { 78 | req.headers = {'authorization': 'Bearer'}; 79 | ljsReq = new LJSRequest(req, req.app); 80 | }); 81 | 82 | it('should replace Authorization header value', function() { 83 | expect(ljsReq.safeHeaders()['authorization']).to.eq('CONFIDENTIAL'); 84 | }); 85 | }); 86 | 87 | describe('when authorization header is not present', function () { 88 | beforeEach(function() { 89 | req.headers = {}; 90 | ljsReq = new LJSRequest(req, req.app); 91 | }); 92 | 93 | it('should not include Authorization header', function() { 94 | expect(ljsReq.safeHeaders()).to.not.contain.key('authorization'); 95 | }); 96 | }); 97 | }); 98 | 99 | describe('#isContentTypeSupported', function() { 100 | beforeEach(function() { 101 | req = {headers: {}}; 102 | }); 103 | 104 | describe('when request does not have content length', function() { 105 | beforeEach(function() { 106 | req.headers['content-length'] = undefined; 107 | ljsReq = new LJSRequest(req, req.app); 108 | }); 109 | 110 | it('should be true', function() { 111 | expect(ljsReq.isContentTypeSupported()).to.be.true; 112 | }); 113 | }); 114 | 115 | describe('when request has content length 0', function() { 116 | beforeEach(function() { 117 | req.headers['content-length'] = '0'; 118 | ljsReq = new LJSRequest(req, req.app); 119 | }); 120 | 121 | it('should be true', function() { 122 | expect(ljsReq.isContentTypeSupported()).to.be.true; 123 | }); 124 | }); 125 | 126 | describe('when request has content length > 0', function() { 127 | beforeEach(function() { 128 | req.headers['content-length'] = '1'; 129 | }); 130 | 131 | describe('and Content-Type is application/json', function() { 132 | beforeEach(function() { 133 | req.headers['content-type'] = 'application/json'; 134 | ljsReq = new LJSRequest(req, req.app); 135 | }); 136 | 137 | it('should be truthy', function() { 138 | expect(ljsReq.isContentTypeSupported()).to.be.truthy; 139 | }); 140 | }); 141 | 142 | describe('and Content-Type is application/schema+json', function() { 143 | beforeEach(function() { 144 | req.headers['content-type'] = 'application/schema+json'; 145 | ljsReq = new LJSRequest(req, req.app); 146 | }); 147 | 148 | it('should be truthy', function() { 149 | expect(ljsReq.isContentTypeSupported()).to.be.truthy; 150 | }); 151 | }); 152 | 153 | describe('and Content-Type is vendor specific json', function() { 154 | beforeEach(function() { 155 | req.headers['content-type'] = 'application/vnd.acme+json'; 156 | ljsReq = new LJSRequest(req, req.app); 157 | }); 158 | 159 | it('should be truthy', function() { 160 | expect(ljsReq.isContentTypeSupported()).to.be.truthy; 161 | }); 162 | }); 163 | 164 | describe('and Content-Type is not json', function() { 165 | beforeEach(function() { 166 | req.headers['content-type'] = 'text/plain'; 167 | ljsReq = new LJSRequest(req, req.app); 168 | }); 169 | 170 | it('should return false', function() { 171 | expect(ljsReq.isContentTypeSupported()).to.be.false; 172 | }); 173 | }); 174 | 175 | describe('and Content-Type is vendor specific but not json', function() { 176 | beforeEach(function() { 177 | req.headers['content-type'] = 'application/vnd.acme'; 178 | ljsReq = new LJSRequest(req, req.app); 179 | }); 180 | 181 | it('should return false', function() { 182 | expect(ljsReq.isContentTypeSupported()).to.be.false; 183 | }); 184 | }); 185 | 186 | describe('and Content-Type is undefined', function() { 187 | beforeEach(function() { 188 | delete req.headers['content-type']; 189 | ljsReq = new LJSRequest(req, req.app); 190 | }); 191 | 192 | it('should return false', function() { 193 | expect(ljsReq.isContentTypeSupported()).to.be.false; 194 | }); 195 | }); 196 | }); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /test/unit/http/ljs-url.test.js: -------------------------------------------------------------------------------- 1 | require('../../support'); 2 | 3 | var expect = require('chai').expect; 4 | 5 | var LJSUrl = require('../../../lib/http/ljs-url'); 6 | 7 | describe('LJSUrl', function() { 8 | var ljsUrl; 9 | 10 | describe('properties', function() { 11 | describe('when url represents an item', function() { 12 | beforeEach(function() { 13 | ljsUrl = new LJSUrl('http://example.org/api/people/1?query=string'); 14 | }); 15 | 16 | it('should return example.org as host', function() { 17 | expect(ljsUrl.host).to.equal('example.org'); 18 | }); 19 | 20 | it("should return 'people' as collectionName", function() { 21 | expect(ljsUrl.collectionName).to.equal('people'); 22 | }); 23 | 24 | it("should return '1' as resourceId", function() { 25 | expect(ljsUrl.resourceId).to.equal('1'); 26 | }); 27 | 28 | it("should return 'api' as restApiRoot", function() { 29 | expect(ljsUrl.restApiRoot).to.equal('api'); 30 | }); 31 | }); 32 | 33 | describe('when url represents a collection', function() { 34 | beforeEach(function() { 35 | ljsUrl = new LJSUrl('http://example.org/api/people?query=string'); 36 | }); 37 | 38 | it('should return example.org as host', function() { 39 | expect(ljsUrl.host).to.equal('example.org'); 40 | }); 41 | 42 | it("should return 'people' as collectionName", function() { 43 | expect(ljsUrl.collectionName).to.equal('people'); 44 | }); 45 | 46 | it("should return undefined as resourceId", function() { 47 | expect(ljsUrl.resourceId).to.be.undefined; 48 | }); 49 | 50 | it("should return 'api' as restApiRoot", function() { 51 | expect(ljsUrl.restApiRoot).to.equal('api'); 52 | }); 53 | }); 54 | 55 | describe('when url is undefined', function() { 56 | beforeEach(function() { 57 | ljsUrl = new LJSUrl(undefined); 58 | }); 59 | 60 | it('should return undefined as host', function() { 61 | expect(ljsUrl.host).to.be.undefined; 62 | }); 63 | 64 | it("should return undefined collectionName", function() { 65 | expect(ljsUrl.collectionName).to.be.undefined; 66 | }); 67 | 68 | it("should return undefined as resourceId", function() { 69 | expect(ljsUrl.resourceId).to.be.undefined; 70 | }); 71 | 72 | it("should return undefined as restApiRoot", function() { 73 | expect(ljsUrl.restApiRoot).to.be.undefined; 74 | }); 75 | }); 76 | 77 | describe('when url is null', function() { 78 | beforeEach(function() { 79 | ljsUrl = new LJSUrl(null); 80 | }); 81 | 82 | it('should return undefined as host', function() { 83 | expect(ljsUrl.host).to.be.undefined; 84 | }); 85 | 86 | it("should return undefined collectionName", function() { 87 | expect(ljsUrl.collectionName).to.be.undefined; 88 | }); 89 | 90 | it("should return undefined as resourceId", function() { 91 | expect(ljsUrl.resourceId).to.be.undefined; 92 | }); 93 | 94 | it("should return undefined as restApiRoot", function() { 95 | expect(ljsUrl.restApiRoot).to.be.undefined; 96 | }); 97 | }); 98 | 99 | describe('when url is empty strng', function() { 100 | beforeEach(function() { 101 | ljsUrl = new LJSUrl(''); 102 | }); 103 | 104 | it('should return undefined as host', function() { 105 | expect(ljsUrl.host).to.be.undefined; 106 | }); 107 | 108 | it("should return undefined collectionName", function() { 109 | expect(ljsUrl.collectionName).to.be.undefined; 110 | }); 111 | 112 | it("should return undefined as resourceId", function() { 113 | expect(ljsUrl.resourceId).to.be.undefined; 114 | }); 115 | 116 | it("should return undefined as restApiRoot", function() { 117 | expect(ljsUrl.restApiRoot).to.be.undefined; 118 | }); 119 | }); 120 | }); 121 | 122 | describe('#isInstance', function() { 123 | describe('when url represents an item', function() { 124 | beforeEach(function() { 125 | ljsUrl = new LJSUrl('http://example.org/api/people/1'); 126 | }); 127 | 128 | it('should return true', function () { 129 | expect(ljsUrl.isInstance()).to.be.true; 130 | }); 131 | }); 132 | 133 | describe('when url represents a collection', function() { 134 | beforeEach(function() { 135 | ljsUrl = new LJSUrl('http://example.org/api/people'); 136 | }); 137 | 138 | it('should return true', function () { 139 | expect(ljsUrl.isInstance()).to.be.true; 140 | }); 141 | }); 142 | 143 | describe('when url represents an item schema', function() { 144 | beforeEach(function() { 145 | ljsUrl = new LJSUrl('http://example.org/api/item-schemas/1'); 146 | }); 147 | 148 | it('should return false', function() { 149 | expect(ljsUrl.isInstance()).to.be.false; 150 | }); 151 | }); 152 | 153 | describe('when url represents a collection schema', function() { 154 | beforeEach(function() { 155 | ljsUrl = new LJSUrl('http://example.org/api/collection-schemas/1'); 156 | }); 157 | 158 | it('should return false', function() { 159 | expect(ljsUrl.isInstance()).to.be.false; 160 | }); 161 | }); 162 | 163 | describe('when url represents swagger resources', function() { 164 | beforeEach(function() { 165 | ljsUrl = new LJSUrl('http://example.org/api/swagger/resources'); 166 | }); 167 | 168 | it('should return false', function() { 169 | expect(ljsUrl.isInstance()).to.be.false; 170 | }); 171 | }); 172 | }); 173 | 174 | describe('#isCollection', function () { 175 | describe('when url represents a collection', function () { 176 | beforeEach(function() { 177 | ljsUrl = new LJSUrl('http://example.org/api/people'); 178 | }); 179 | 180 | it('should return true', function () { 181 | expect(ljsUrl.isCollection()).to.be.true; 182 | }); 183 | }); 184 | 185 | describe('when url represents an item', function () { 186 | beforeEach(function() { 187 | ljsUrl = new LJSUrl('http://example.org/api/people/1'); 188 | }); 189 | 190 | it('should return false', function () { 191 | expect(ljsUrl.isCollection()).to.be.false; 192 | }); 193 | }); 194 | }); 195 | 196 | describe('#isSchema', function() { 197 | describe('when url represents an item schema', function() { 198 | beforeEach(function() { 199 | ljsUrl = new LJSUrl('http://example.org/api/item-schemas/1'); 200 | }); 201 | 202 | it('should return true', function() { 203 | expect(ljsUrl.isSchema()).to.be.true; 204 | }); 205 | }); 206 | 207 | describe('when url represents a collection schema', function() { 208 | beforeEach(function() { 209 | ljsUrl = new LJSUrl('http://example.org/api/collection-schemas/1'); 210 | }); 211 | 212 | it('should return true', function() { 213 | expect(ljsUrl.isSchema()).to.be.true; 214 | }); 215 | }); 216 | 217 | describe('when url does not represent a schema', function() { 218 | beforeEach(function() { 219 | ljsUrl = new LJSUrl('http://example.org/api/people/1'); 220 | }); 221 | 222 | it('should return true', function() { 223 | expect(ljsUrl.isSchema()).to.be.false; 224 | }); 225 | }); 226 | }); 227 | 228 | describe('#isRelative', function() { 229 | describe('when host is null', function() { 230 | beforeEach(function() { 231 | ljsUrl = new LJSUrl('/api/people'); 232 | }); 233 | 234 | it('should be true', function() { 235 | expect(ljsUrl.isRelative()).to.be.true; 236 | }); 237 | }); 238 | 239 | describe('when url has protocol http://', function() { 240 | beforeEach(function() { 241 | ljsUrl = new LJSUrl('http://example.com/api/people'); 242 | }); 243 | 244 | it('should be false', function() { 245 | expect(ljsUrl.isRelative()).to.be.false; 246 | }); 247 | }); 248 | 249 | describe('when url has protocol https://', function() { 250 | beforeEach(function() { 251 | ljsUrl = new LJSUrl('https://example.com/api/people'); 252 | }); 253 | 254 | it('should be false', function() { 255 | expect(ljsUrl.isRelative()).to.be.false; 256 | }); 257 | }); 258 | 259 | describe('when url has protocol http and template', function() { 260 | beforeEach(function() { 261 | ljsUrl = new LJSUrl('http://{template}/api/people'); 262 | }); 263 | 264 | it('should be false', function() { 265 | expect(ljsUrl.isRelative()).to.be.false; 266 | }); 267 | }); 268 | 269 | describe('when url has protocol https and template', function() { 270 | beforeEach(function() { 271 | ljsUrl = new LJSUrl('https://{template}/api/people'); 272 | }); 273 | 274 | it('should be false', function() { 275 | expect(ljsUrl.isRelative()).to.be.false; 276 | }); 277 | }); 278 | 279 | describe('when url start with template', function() { 280 | beforeEach(function() { 281 | ljsUrl = new LJSUrl('{template}/api/people'); 282 | }); 283 | 284 | it('should be false', function() { 285 | expect(ljsUrl.isRelative()).to.be.false; 286 | }); 287 | }); 288 | }); 289 | }); -------------------------------------------------------------------------------- /test/unit/http/location-header-correlator.test.js: -------------------------------------------------------------------------------- 1 | require('../../support'); 2 | 3 | var expect = require('chai').expect; 4 | 5 | var locationHeaderCorrelator = require('../../../lib/http/location-header-correlator'); 6 | var LJSRequest = require('../../../lib/http/ljs-request'); 7 | 8 | 9 | describe('locationHeaderCorrelator', function() { 10 | var fullUrl = 'http://api.example.org/api/test'; 11 | var ctx, fullUrlStub, result, nextSpy; 12 | 13 | describe('when result was a default id', function(){ 14 | describe('when url not end switch /', function(){ 15 | beforeEach(function() { 16 | fullUrlStub = this.sinon.stub(LJSRequest.prototype, 'fullUrl').returns(fullUrl); 17 | 18 | result = { 19 | id: '123', 20 | constructor: { 21 | getIdName: function() { 22 | return 'id'; 23 | } 24 | } 25 | }; 26 | ctx = { 27 | req: { 28 | app: null 29 | }, 30 | res: { 31 | set: this.sinon.stub(), 32 | status: this.sinon.stub() 33 | } 34 | }; 35 | 36 | nextSpy = this.sinon.spy(); 37 | 38 | locationHeaderCorrelator(ctx, result, nextSpy); 39 | }); 40 | 41 | afterEach(function() { 42 | fullUrlStub.restore(); 43 | }); 44 | 45 | it('should call `next` parameter', function(){ 46 | expect(nextSpy).to.be.called; 47 | }); 48 | 49 | it('should correlate `Location` header', function(){ 50 | expect(ctx.res.set).to.have.been.calledWith('Location', 'http://api.example.org/api/test/123'); 51 | }); 52 | 53 | it('should change response status to 201', function(){ 54 | expect(ctx.res.status).to.have.been.calledWith(201); 55 | }); 56 | }); 57 | 58 | describe('when url end switch /', function(){ 59 | before(function() { 60 | fullUrlStub = this.sinon.stub(LJSRequest.prototype, 'fullUrl').returns(fullUrl); 61 | ctx = { 62 | req: { 63 | app: null 64 | }, 65 | res: { 66 | set: this.sinon.stub(), 67 | status: this.sinon.stub() 68 | } 69 | }; 70 | 71 | result = { 72 | id: '123', 73 | constructor: { 74 | getIdName: function() { 75 | return 'id'; 76 | } 77 | } 78 | }; 79 | 80 | nextSpy = this.sinon.spy(); 81 | 82 | locationHeaderCorrelator(ctx, result, nextSpy); 83 | }); 84 | 85 | after(function() { 86 | fullUrlStub.restore(); 87 | }); 88 | 89 | it('should correlate `Location` header', function(){ 90 | expect(ctx.res.set).to.have.been.calledWith('Location', 'http://api.example.org/api/test/123'); 91 | }); 92 | 93 | it('should change response status to 201', function(){ 94 | expect(ctx.res.status).to.have.been.calledWith(201); 95 | }); 96 | }); 97 | }); 98 | 99 | describe('when result was a custom id', function(){ 100 | before(function() { 101 | fullUrlStub = this.sinon.stub(LJSRequest.prototype, 'fullUrl').returns(fullUrl); 102 | 103 | ctx = { 104 | req: { 105 | app: null 106 | }, 107 | res: { 108 | set: this.sinon.stub(), 109 | status: this.sinon.stub() 110 | } 111 | }; 112 | 113 | result = { 114 | name: '321', 115 | constructor: { 116 | getIdName: function() { 117 | return 'name'; 118 | } 119 | } 120 | }; 121 | 122 | nextSpy = this.sinon.spy(); 123 | 124 | locationHeaderCorrelator(ctx, result, nextSpy); 125 | }); 126 | 127 | after(function() { 128 | fullUrlStub.restore(); 129 | }); 130 | 131 | it('should correlate `Location` header', function(){ 132 | expect(ctx.res.set).to.have.been.calledWith('Location', 'http://api.example.org/api/test/321'); 133 | }); 134 | 135 | it('should change response status to 201', function(){ 136 | expect(ctx.res.status).to.have.been.calledWith(201); 137 | }); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /test/unit/http/schema-correlator.test.js: -------------------------------------------------------------------------------- 1 | require('../../support'); 2 | 3 | var expect = require('chai').expect; 4 | 5 | var schemaCorrelator = require('../../../lib/http/schema-correlator'); 6 | var ItemSchema = require('../../../lib/domain/item-schema'); 7 | var LJSRequest = require('../../../lib/http/ljs-request'); 8 | 9 | describe('schemaCorrelator', function() { 10 | var ctx, 11 | baseUrl = 'http://api.example.org', 12 | baseUrlStub, 13 | result, 14 | itemSchema = new ItemSchema({ 15 | collectionName: 'people', 16 | title: 'Person', 17 | collectionTitle: 'People', 18 | type: 'object', 19 | properties: {} 20 | }); 21 | 22 | describe('.collection', function() { 23 | before(function() { 24 | ctx = { 25 | req: { 26 | app: null, 27 | protocol: 'http', 28 | get: (name) => { 29 | if (name === 'Host') { 30 | return 'example.org'; 31 | } 32 | }, 33 | }, 34 | res: { 35 | set: this.sinon.stub() 36 | } 37 | }; 38 | 39 | result = { 40 | constructor: { 41 | pluralModelName: 'people' 42 | } 43 | }; 44 | }); 45 | 46 | describe('without queryparams', function(done){ 47 | beforeEach(function(done) { 48 | baseUrlStub = this.sinon.stub(LJSRequest.prototype, 'baseUrl').returns(baseUrl); 49 | schemaCorrelator.collection('people', ctx, result, function() { 50 | done(); 51 | }); 52 | }); 53 | 54 | after(function() { 55 | baseUrlStub.restore(); 56 | }); 57 | 58 | it('should correlate `Content-Type` header', function(){ 59 | var schemaUrl = baseUrl + '/collection-schemas/people'; 60 | expect(ctx.res.set).to.have.been.calledWith('Content-Type', 'application/json; charset=utf-8; profile="' + schemaUrl + '"'); 61 | }); 62 | 63 | it('should correlate `Link` header', function(){ 64 | var schemaUrl = baseUrl + '/collection-schemas/people'; 65 | expect(ctx.res.set).to.have.been.calledWith('Link', '<' + schemaUrl + '>; rel="describedby"'); 66 | }); 67 | }); 68 | 69 | describe('with queryparams', function(done){ 70 | beforeEach(function(done) { 71 | baseUrlStub = this.sinon.stub(LJSRequest.prototype, 'baseUrl').returns(baseUrl); 72 | schemaCorrelator.collection('people', {_debug: 'true'}, ctx, result, function() { 73 | done(); 74 | }); 75 | }); 76 | 77 | afterEach(function() { 78 | baseUrlStub.restore(); 79 | }); 80 | 81 | it('should correlate `Content-Type` header', function(){ 82 | var schemaUrl = baseUrl + '/collection-schemas/people?_debug=true'; 83 | expect(ctx.res.set).to.have.been.calledWith('Content-Type', 'application/json; charset=utf-8; profile="' + schemaUrl + '"'); 84 | }); 85 | 86 | it('should correlate `Link` header', function(){ 87 | var schemaUrl = baseUrl + '/collection-schemas/people?_debug=true'; 88 | expect(ctx.res.set).to.have.been.calledWith('Link', '<' + schemaUrl + '>; rel="describedby"'); 89 | }); 90 | }); 91 | }); 92 | 93 | describe('.instance', function() { 94 | before(function() { 95 | ctx = { 96 | req: { 97 | app: null, 98 | protocol: 'http', 99 | get: (name) => { 100 | if (name === 'Host') { 101 | return 'example.org'; 102 | } 103 | }, 104 | }, 105 | res: { 106 | set: this.sinon.stub() 107 | } 108 | }; 109 | 110 | result = { 111 | constructor: { 112 | pluralModelName: 'people' 113 | } 114 | }; 115 | }); 116 | 117 | describe('with querystring', function(){ 118 | before(function(done) { 119 | baseUrlStub = this.sinon.stub(LJSRequest.prototype, 'baseUrl').returns(baseUrl); 120 | 121 | schemaCorrelator.instance('people', {compact: 'false'}, ctx, result, function() { 122 | done(); 123 | }); 124 | }); 125 | 126 | after(function() { 127 | baseUrlStub.restore(); 128 | }); 129 | 130 | it('should correlate `Content-Type` header', function(){ 131 | var schemaUrl = baseUrl + '/item-schemas/people?compact=false'; 132 | expect(ctx.res.set).to.have.been.calledWith('Content-Type', 'application/json; charset=utf-8; profile="' + schemaUrl + '"'); 133 | }); 134 | 135 | it('should correlate `Link` header', function(){ 136 | var schemaUrl = baseUrl + '/item-schemas/people?compact=false'; 137 | expect(ctx.res.set).to.have.been.calledWith('Link', '<' + schemaUrl + '>; rel="describedby"'); 138 | }); 139 | }); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /test/unit/http/schema-link-rewriter.test.js: -------------------------------------------------------------------------------- 1 | require('../../support'); 2 | 3 | var expect = require('chai').expect; 4 | 5 | var schemaLinkRewriter = require('../../../lib/http/schema-link-rewriter'); 6 | 7 | describe('schemaLinkRewriter', function() { 8 | var body; 9 | var baseUrl = 'http://example.org'; 10 | var originalBody; 11 | 12 | beforeEach(function() { 13 | originalBody = {}; 14 | }); 15 | 16 | describe('when links href is relative', function() { 17 | beforeEach(function() { 18 | originalBody.links = [ 19 | { href: '/relative' } 20 | ]; 21 | 22 | body = schemaLinkRewriter(baseUrl, originalBody); 23 | }); 24 | 25 | it('should make href absolute', function() { 26 | expect(body.links[0].href).to.eq('http://example.org/relative'); 27 | }); 28 | }); 29 | 30 | 31 | describe('when links href is absolute', function() { 32 | beforeEach(function() { 33 | originalBody.links = [ 34 | { href: 'http://example.org/absolute' } 35 | ]; 36 | 37 | body = schemaLinkRewriter(baseUrl, originalBody); 38 | }); 39 | 40 | it('should do nothing', function() { 41 | expect(body.links[0].href).to.eq('http://example.org/absolute'); 42 | }); 43 | }); 44 | 45 | describe('when links $ref is relative', function() { 46 | beforeEach(function() { 47 | originalBody.links = [ 48 | { schema: { $ref: '/relative' } } 49 | ]; 50 | body = schemaLinkRewriter(baseUrl, originalBody); 51 | }); 52 | 53 | it('should make schema.$ref absolute', function() { 54 | expect(body.links[0].schema.$ref).to.eq('http://example.org/relative'); 55 | }); 56 | }); 57 | 58 | describe('when links $ref is absolute', function() { 59 | beforeEach(function() { 60 | originalBody.links = [ 61 | { schema: { $ref: 'http://example.org/absolute' } } 62 | ]; 63 | body = schemaLinkRewriter(baseUrl, originalBody); 64 | }); 65 | 66 | it('should do nothing', function() { 67 | expect(body.links[0].schema.$ref).to.eq('http://example.org/absolute'); 68 | }); 69 | }); 70 | 71 | describe('when properties $ref is relative', function() { 72 | beforeEach(function() { 73 | originalBody.properties = { 74 | items: { $ref: '/relative' } 75 | }; 76 | body = schemaLinkRewriter(baseUrl, originalBody); 77 | }); 78 | 79 | it('should make schema.$ref absolute', function() { 80 | expect(body.properties.items.$ref).to.eq('http://example.org/relative'); 81 | }); 82 | }); 83 | 84 | describe('when properties $ref is absolute', function() { 85 | beforeEach(function() { 86 | originalBody.properties = { 87 | items: { $ref: 'http://example.org/absolute' } 88 | }; 89 | body = schemaLinkRewriter(baseUrl, originalBody); 90 | }); 91 | 92 | it('should do nothing', function() { 93 | expect(body.properties.items.$ref).to.eq('http://example.org/absolute'); 94 | }); 95 | }); 96 | 97 | describe('when items $ref is relative', function() { 98 | beforeEach(function() { 99 | originalBody.items = { $ref: '/relative' }; 100 | body = schemaLinkRewriter(baseUrl, originalBody); 101 | }); 102 | 103 | it('should make schema.$ref absolute', function() { 104 | expect(body.items.$ref).to.eq('http://example.org/relative'); 105 | }); 106 | }); 107 | 108 | describe('when items $ref is absolute', function() { 109 | beforeEach(function() { 110 | originalBody.items = { $ref: 'http://example.org/absolute' }; 111 | body = schemaLinkRewriter(baseUrl, originalBody); 112 | }); 113 | 114 | it('should do nothing', function() { 115 | expect(body.items.$ref).to.eq('http://example.org/absolute'); 116 | }); 117 | }); 118 | 119 | describe('when links, properties and items do not exist', function() { 120 | beforeEach(function() { 121 | originalBody.links = undefined; 122 | originalBody.properties = undefined; 123 | originalBody.items = undefined; 124 | body = schemaLinkRewriter(baseUrl, originalBody); 125 | }); 126 | 127 | it('should do nothing', function() { 128 | expect(body.links).to.be.undefined; 129 | expect(body.properties).to.be.undefined; 130 | expect(body.items).to.be.undefined; 131 | }); 132 | }); 133 | 134 | describe('when no keys with url exist', function() { 135 | beforeEach(function() { 136 | originalBody.links = [{}]; 137 | originalBody.properties = {}; 138 | originalBody.items = {}; 139 | body = schemaLinkRewriter(baseUrl, originalBody); 140 | }); 141 | 142 | it('should do nothing', function() { 143 | expect(body.links[0]).to.eql({}); 144 | expect(body.properties).to.eql({}); 145 | expect(body.items).to.eql({}); 146 | }); 147 | }); 148 | 149 | describe('when href is a child of something other than links', function() { 150 | beforeEach(function() { 151 | originalBody.properties = { 152 | href: '/relative' 153 | }; 154 | originalBody.items = { 155 | href: '/relative' 156 | }; 157 | body = schemaLinkRewriter(baseUrl, originalBody); 158 | }); 159 | 160 | it('should do nothing', function() { 161 | expect(body.properties.href).to.eq('/relative'); 162 | expect(body.items.href).to.eq('/relative'); 163 | }); 164 | }); 165 | 166 | describe('when $ref is a child of something other than links, properties or items', function() { 167 | beforeEach(function() { 168 | originalBody.something = { 169 | $ref: '/relative' 170 | }; 171 | body = schemaLinkRewriter(baseUrl, originalBody); 172 | }); 173 | 174 | it('should do nothing', function() { 175 | expect(body.something.$ref).to.eq('/relative'); 176 | }); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /test/unit/loopback-jsonschema.test.js: -------------------------------------------------------------------------------- 1 | require('../support'); 2 | 3 | var expect = require('chai').expect; 4 | var loopback = require('loopback'); 5 | var _ = require('lodash'); 6 | 7 | var CollectionSchema = require('../../lib/domain/collection-schema'); 8 | var ItemSchema = require('../../lib/domain/item-schema'); 9 | var schemaCorrelatorHooks = require('../../lib/http/schema-correlator-hooks'); 10 | 11 | var loopbackJsonSchema = require('../../index'); 12 | var config = require('../../lib/support/config'); 13 | var configPath = require.resolve('../../lib/support/config'); 14 | 15 | 16 | describe('loopbackJsonSchema', function() { 17 | describe('.init', function() { 18 | var app; 19 | 20 | before(function() { 21 | app = loopback(); 22 | }); 23 | 24 | it('should allow overriding default config', function() { 25 | var myConfig = { 26 | Model: 'MyModel', 27 | myConfigOption: 'myValue' 28 | }; 29 | loopbackJsonSchema.init(app, myConfig); 30 | expect(config.Model).to.eql('MyModel'); 31 | expect(config.myConfigOption).to.eql('myValue'); 32 | }); 33 | 34 | it('should set strong-remoting params', function(){ 35 | loopbackJsonSchema.init(app, { CollectionSchemaClass: CollectionSchema }); 36 | expect(app.get('remoting')).to.eql({ 37 | json: { 38 | type: [ 39 | 'json', 40 | '+json' 41 | ] 42 | }}); 43 | }); 44 | 45 | it('should register schema correlator hook', function(){ 46 | loopbackJsonSchema.init(app); 47 | var hooksFound = _.filter(ItemSchema.remoteHookInitializers, function(hook){ 48 | return hook === schemaCorrelatorHooks; 49 | }); 50 | 51 | expect(hooksFound.length).to.be.eql(1); 52 | }); 53 | 54 | 55 | it('should redefine `ItemSchema.remoteHookInitializers` before attach the ItemSchema', function(done){ 56 | var wrongHook = function() {}; 57 | ItemSchema.remoteHookInitializers = [wrongHook]; 58 | 59 | ItemSchema.once('attached', function() { 60 | expect(ItemSchema.remoteHookInitializers).to.not.include(wrongHook); 61 | done(); 62 | }); 63 | 64 | loopbackJsonSchema.init(app); 65 | }); 66 | 67 | it('should populate `ItemSchema.remoteHookInitializers` with two default hooks', function(done){ 68 | 69 | ItemSchema.once('attached', function() { 70 | expect(ItemSchema.remoteHookInitializers.length).to.be.eql(3); 71 | done(); 72 | }); 73 | 74 | loopbackJsonSchema.init(app); 75 | }); 76 | 77 | 78 | describe('when registerItemSchemaAtRequest is false', function() { 79 | var app, findStub; 80 | 81 | before(function(done) { 82 | app = loopback(); 83 | 84 | findStub = this.sinon.stub(ItemSchema, 'find').yields(null, []); 85 | loopbackJsonSchema.init(app, { registerItemSchemaAtRequest: false }); 86 | 87 | app.once('loadModels', function() { 88 | done(); 89 | }); 90 | }); 91 | 92 | it('ItemSchema.find to have been called with {}', function(){ 93 | expect(findStub).to.have.been.calledWith({}); 94 | }); 95 | }); 96 | }); 97 | }); 98 | --------------------------------------------------------------------------------