loopback-datasource-juggler/test/model-definition.test.js

726 lines
21 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright IBM Corp. 2013,2018. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
// This test written in mocha+should.js
'use strict';
const should = require('./init.js');
const assert = require('assert');
const jdb = require('../');
const ModelBuilder = jdb.ModelBuilder;
const DataSource = jdb.DataSource;
const Memory = require('../lib/connectors/memory');
const ModelDefinition = require('../lib/model-definition');
describe('ModelDefinition class', function() {
let memory;
beforeEach(function() {
memory = new DataSource({connector: Memory});
});
it('should be able to define plain models', function(done) {
const modelBuilder = new ModelBuilder();
const User = new ModelDefinition(modelBuilder, 'User', {
name: 'string',
bio: ModelBuilder.Text,
approved: Boolean,
joinedAt: Date,
age: 'number',
});
User.build();
assert.equal(User.properties.name.type, String);
assert.equal(User.properties.bio.type, ModelBuilder.Text);
assert.equal(User.properties.approved.type, Boolean);
assert.equal(User.properties.joinedAt.type, Date);
assert.equal(User.properties.age.type, Number);
const json = User.toJSON();
assert.equal(json.name, 'User');
assert.equal(json.properties.name.type, 'String');
assert.equal(json.properties.bio.type, 'Text');
assert.equal(json.properties.approved.type, 'Boolean');
assert.equal(json.properties.joinedAt.type, 'Date');
assert.equal(json.properties.age.type, 'Number');
assert.deepEqual(User.toJSON(), json);
done();
});
it('should be able to define additional properties', function(done) {
const modelBuilder = new ModelBuilder();
const User = new ModelDefinition(modelBuilder, 'User', {
name: 'string',
bio: ModelBuilder.Text,
approved: Boolean,
joinedAt: Date,
age: 'number',
});
User.build();
let json = User.toJSON();
User.defineProperty('id', {type: 'number', id: true});
assert.equal(User.properties.name.type, String);
assert.equal(User.properties.bio.type, ModelBuilder.Text);
assert.equal(User.properties.approved.type, Boolean);
assert.equal(User.properties.joinedAt.type, Date);
assert.equal(User.properties.age.type, Number);
assert.equal(User.properties.id.type, Number);
json = User.toJSON();
assert.deepEqual(json.properties.id, {type: 'Number', id: true});
done();
});
it('should be able to define nesting models', function(done) {
const modelBuilder = new ModelBuilder();
const User = new ModelDefinition(modelBuilder, 'User', {
name: String,
bio: ModelBuilder.Text,
approved: Boolean,
joinedAt: Date,
age: Number,
address: {
street: String,
city: String,
zipCode: String,
state: String,
},
});
User.build();
assert.equal(User.properties.name.type, String);
assert.equal(User.properties.bio.type, ModelBuilder.Text);
assert.equal(User.properties.approved.type, Boolean);
assert.equal(User.properties.joinedAt.type, Date);
assert.equal(User.properties.age.type, Number);
assert.equal(typeof User.properties.address.type, 'function');
const json = User.toJSON();
assert.equal(json.name, 'User');
assert.equal(json.properties.name.type, 'String');
assert.equal(json.properties.bio.type, 'Text');
assert.equal(json.properties.approved.type, 'Boolean');
assert.equal(json.properties.joinedAt.type, 'Date');
assert.equal(json.properties.age.type, 'Number');
assert.deepEqual(json.properties.address.type, {street: {type: 'String'},
city: {type: 'String'},
zipCode: {type: 'String'},
state: {type: 'String'}});
done();
});
it('should be able to define referencing models', function(done) {
const modelBuilder = new ModelBuilder();
const Address = modelBuilder.define('Address', {
street: String,
city: String,
zipCode: String,
state: String,
});
const User = new ModelDefinition(modelBuilder, 'User', {
name: String,
bio: ModelBuilder.Text,
approved: Boolean,
joinedAt: Date,
age: Number,
address: Address,
});
User.build();
assert.equal(User.properties.name.type, String);
assert.equal(User.properties.bio.type, ModelBuilder.Text);
assert.equal(User.properties.approved.type, Boolean);
assert.equal(User.properties.joinedAt.type, Date);
assert.equal(User.properties.age.type, Number);
assert.equal(User.properties.address.type, Address);
const json = User.toJSON();
assert.equal(json.name, 'User');
assert.equal(json.properties.name.type, 'String');
assert.equal(json.properties.bio.type, 'Text');
assert.equal(json.properties.approved.type, 'Boolean');
assert.equal(json.properties.joinedAt.type, 'Date');
assert.equal(json.properties.age.type, 'Number');
assert.equal(json.properties.address.type, 'Address');
done();
});
it('should be able to define referencing models by name', function(done) {
const modelBuilder = new ModelBuilder();
const Address = modelBuilder.define('Address', {
street: String,
city: String,
zipCode: String,
state: String,
});
const User = new ModelDefinition(modelBuilder, 'User', {
name: String,
bio: ModelBuilder.Text,
approved: Boolean,
joinedAt: Date,
age: Number,
address: 'Address',
});
User.build();
assert.equal(User.properties.name.type, String);
assert.equal(User.properties.bio.type, ModelBuilder.Text);
assert.equal(User.properties.approved.type, Boolean);
assert.equal(User.properties.joinedAt.type, Date);
assert.equal(User.properties.age.type, Number);
assert.equal(User.properties.address.type, Address);
const json = User.toJSON();
assert.equal(json.name, 'User');
assert.equal(json.properties.name.type, 'String');
assert.equal(json.properties.bio.type, 'Text');
assert.equal(json.properties.approved.type, 'Boolean');
assert.equal(json.properties.joinedAt.type, 'Date');
assert.equal(json.properties.age.type, 'Number');
assert.equal(json.properties.address.type, 'Address');
done();
});
it('should report correct id names', function(done) {
const modelBuilder = new ModelBuilder();
const User = new ModelDefinition(modelBuilder, 'User', {
userId: {type: String, id: true},
name: 'string',
bio: ModelBuilder.Text,
approved: Boolean,
joinedAt: Date,
age: 'number',
});
assert.equal(User.idName(), 'userId');
assert.deepEqual(User.idNames(), ['userId']);
done();
});
it('should sort id properties by its index', function() {
const modelBuilder = new ModelBuilder();
const User = new ModelDefinition(modelBuilder, 'User', {
userId: {type: String, id: 2},
userType: {type: String, id: 1},
name: 'string',
bio: ModelBuilder.Text,
approved: Boolean,
joinedAt: Date,
age: 'number',
});
const ids = User.ids();
assert.ok(Array.isArray(ids));
assert.equal(ids.length, 2);
assert.equal(ids[0].id, 1);
assert.equal(ids[0].name, 'userType');
assert.equal(ids[1].id, 2);
assert.equal(ids[1].name, 'userId');
});
it('should report correct table/column names', function(done) {
const modelBuilder = new ModelBuilder();
const User = new ModelDefinition(modelBuilder, 'User', {
userId: {type: String, id: true, oracle: {column: 'ID'}},
name: 'string',
}, {oracle: {table: 'USER'}});
assert.equal(User.tableName('oracle'), 'USER');
assert.equal(User.tableName('mysql'), 'User');
assert.equal(User.columnName('oracle', 'userId'), 'ID');
assert.equal(User.columnName('mysql', 'userId'), 'userId');
done();
});
describe('maxDepthOfQuery', function() {
it('should report errors for deep query than maxDepthOfQuery', function(done) {
const MyModel = memory.createModel('my-model', {}, {
maxDepthOfQuery: 5,
});
const filter = givenComplexFilter();
MyModel.find(filter, function(err) {
should.exist(err);
err.message.should.match('The query object exceeds maximum depth 5');
done();
});
});
it('should honor maxDepthOfQuery setting', function(done) {
const MyModel = memory.createModel('my-model', {}, {
maxDepthOfQuery: 20,
});
const filter = givenComplexFilter();
MyModel.find(filter, function(err) {
should.not.exist(err);
done();
});
});
it('should honor maxDepthOfQuery in options', function(done) {
const MyModel = memory.createModel('my-model', {}, {
maxDepthOfQuery: 5,
});
const filter = givenComplexFilter();
MyModel.find(filter, {maxDepthOfQuery: 20}, function(err) {
should.not.exist(err);
done();
});
});
function givenComplexFilter() {
const filter = {where: {and: [{and: [{and: [{and: [{and: [{and:
[{and: [{and: [{and: [{x: 1}]}]}]}]}]}]}]}]}]}};
return filter;
}
});
it('should serialize protected properties into JSON', function() {
const ProtectedModel = memory.createModel('protected', {}, {
protected: ['protectedProperty'],
});
const pm = new ProtectedModel({
id: 1, foo: 'bar', protectedProperty: 'protected',
});
const serialized = pm.toJSON();
assert.deepEqual(serialized, {
id: 1, foo: 'bar', protectedProperty: 'protected',
});
});
it('should not serialize protected properties of nested models into JSON', function(done) {
const Parent = memory.createModel('parent');
const Child = memory.createModel('child', {}, {protected: ['protectedProperty']});
Parent.hasMany(Child);
Parent.create({
name: 'parent',
}, function(err, parent) {
if (err) return done(err);
parent.children.create({
name: 'child',
protectedProperty: 'protectedValue',
}, function(err, child) {
if (err) return done(err);
Parent.find({include: 'children'}, function(err, parents) {
if (err) return done(err);
const serialized = parents[0].toJSON();
const child = serialized.children[0];
assert.equal(child.name, 'child');
assert.notEqual(child.protectedProperty, 'protectedValue');
done();
});
});
});
});
it('should not serialize hidden properties into JSON', function() {
const HiddenModel = memory.createModel('hidden', {}, {
hidden: ['secret'],
});
const hm = new HiddenModel({
id: 1,
foo: 'bar',
secret: 'secret',
});
const serialized = hm.toJSON();
assert.deepEqual(serialized, {
id: 1,
foo: 'bar',
});
});
it('should not serialize hidden properties of nested models into JSON', function(done) {
const Parent = memory.createModel('parent');
const Child = memory.createModel('child', {}, {hidden: ['secret']});
Parent.hasMany(Child);
Parent.create({
name: 'parent',
}, function(err, parent) {
if (err) return done(err);
parent.children.create({
name: 'child',
secret: 'secret',
}, function(err, child) {
if (err) return done(err);
Parent.find({include: 'children'}, function(err, parents) {
if (err) return done(err);
const serialized = parents[0].toJSON();
const child = serialized.children[0];
assert.equal(child.name, 'child');
assert.notEqual(child.secret, 'secret');
done();
});
});
});
});
describe('hidden properties', function() {
let Child;
describe('with hidden array', function() {
beforeEach(function() { givenChildren(); });
it('should be removed if used in where', function() {
return Child.find({
where: {secret: 'guess'},
}, optionsFromRemoteReq).then(assertHiddenPropertyIsIgnored);
});
it('should be removed if used in where.and', function() {
return Child.find({
where: {and: [{secret: 'guess'}]},
}, optionsFromRemoteReq).then(assertHiddenPropertyIsIgnored);
});
it('should be allowed for update', function() {
return Child.update({name: 'childA'}, {secret: 'new-secret'}, optionsFromRemoteReq).then(
function(result) {
result.count.should.equal(1);
}
);
});
it('should be allowed if prohibitHiddenPropertiesInQuery is `false`', function() {
Child.definition.settings.prohibitHiddenPropertiesInQuery = false;
return Child.find({
where: {secret: 'guess'},
}).then(function(children) {
children.length.should.equal(1);
children[0].secret.should.equal('guess');
});
});
it('should be allowed by default if not remote call', function() {
return Child.find({
where: {secret: 'guess'},
}).then(function(children) {
children.length.should.equal(1);
children[0].secret.should.equal('guess');
});
});
it('should be allowed if prohibitHiddenPropertiesInQuery is `false` in options', function() {
return Child.find({
where: {secret: 'guess'},
}, {
prohibitHiddenPropertiesInQuery: false,
}).then(function(children) {
children.length.should.equal(1);
children[0].secret.should.equal('guess');
});
});
});
describe('with hidden object', function() {
beforeEach(function() { givenChildren({hiddenProperties: {secret: true}}); });
it('should be removed if used in where', function() {
return Child.find({
where: {secret: 'guess'},
}, optionsFromRemoteReq).then(assertHiddenPropertyIsIgnored);
});
it('should be removed if used in where.and', function() {
return Child.find({
where: {and: [{secret: 'guess'}]},
}, optionsFromRemoteReq).then(assertHiddenPropertyIsIgnored);
});
});
/**
* Create two children with a hidden property, one with a matching
* value, the other with a non-matching value
*/
function givenChildren(hiddenProps) {
hiddenProps = hiddenProps || {hidden: ['secret']};
Child = memory.createModel('child', {
name: String,
secret: String,
}, hiddenProps);
return Child.create([{
name: 'childA',
secret: 'secret',
}, {
name: 'childB',
secret: 'guess',
}]);
}
function assertHiddenPropertyIsIgnored(children) {
// All children are found whether the `secret` condition matches or not
// as the condition is removed because it's hidden
children.length.should.equal(2);
}
});
/**
* Mock up for default values set by the remote model
*/
const optionsFromRemoteReq = {
prohibitHiddenPropertiesInQuery: true,
maxDepthOfQuery: 12,
maxDepthOfQuery: 32,
};
describe('hidden nested properties', function() {
let Child;
beforeEach(givenChildren);
it('should be removed if used in where as a composite key - x.secret', function() {
return Child.find({
where: {'x.secret': 'guess'},
}, optionsFromRemoteReq).then(assertHiddenPropertyIsIgnored);
});
it('should be removed if used in where as a composite key - secret.y', function() {
return Child.find({
where: {'secret.y': 'guess'},
}, optionsFromRemoteReq).then(assertHiddenPropertyIsIgnored);
});
it('should be removed if used in where as a composite key - a.secret.b', function() {
return Child.find({
where: {'a.secret.b': 'guess'},
}, optionsFromRemoteReq).then(assertHiddenPropertyIsIgnored);
});
function givenChildren() {
const hiddenProps = {hidden: ['secret']};
Child = memory.createModel('child', {
name: String,
x: {
secret: String,
},
secret: {
y: String,
},
a: {
secret: {
b: String,
},
},
}, hiddenProps);
return Child.create([{
name: 'childA',
x: {secret: 'secret'},
secret: {y: 'secret'},
a: {secret: {b: 'secret'}},
}, {
name: 'childB',
x: {secret: 'guess'},
secret: {y: 'guess'},
a: {secret: {b: 'guess'}},
}]);
}
function assertHiddenPropertyIsIgnored(children) {
// All children are found whether the `secret` condition matches or not
// as the condition is removed because it's hidden
children.length.should.equal(2);
}
});
function assertParentIncludeChildren(parents) {
parents[0].toJSON().children.length.should.equal(1);
}
describe('protected properties', function() {
let Parent;
let Child;
beforeEach(givenParentAndChild);
it('should be removed if used in include scope', function() {
Parent.find({
include: {
relation: 'children',
scope: {
where: {
secret: 'x',
},
},
},
}, optionsFromRemoteReq).then(assertParentIncludeChildren);
});
it('should be rejected if used in include scope.where.and', function() {
return Parent.find({
include: {
relation: 'children',
scope: {
where: {
and: [{secret: 'x'}],
},
},
},
}, optionsFromRemoteReq).then(assertParentIncludeChildren);
});
it('should be removed if a hidden property is used in include scope', function() {
return Parent.find({
include: {
relation: 'children',
scope: {
where: {
secret: 'x',
},
},
},
}, optionsFromRemoteReq).then(assertParentIncludeChildren);
});
function givenParentAndChild() {
Parent = memory.createModel('parent');
Child = memory.createModel('child', {}, {protected: ['secret']});
Parent.hasMany(Child);
return Parent.create({
name: 'parent',
}).then(parent => {
return parent.children.create({
name: 'child',
secret: 'secret',
});
});
}
});
describe('hidden properties in include', function() {
let Parent;
let Child;
beforeEach(givenParentAndChildWithHiddenProperty);
it('should be rejected if used in scope', function() {
return Parent.find({
include: {
relation: 'children',
scope: {
where: {
secret: 'x',
},
},
},
}, optionsFromRemoteReq).then(assertParentIncludeChildren);
});
function givenParentAndChildWithHiddenProperty() {
Parent = memory.createModel('parent');
Child = memory.createModel('child', {}, {hidden: ['secret']});
Parent.hasMany(Child);
return Parent.create({
name: 'parent',
}).then(parent => {
return parent.children.create({
name: 'child',
secret: 'secret',
});
});
}
});
it('should throw error for property names containing dot', function() {
(function() { memory.createModel('Dotted', {'dot.name': String}); })
.should
.throw(/dot\(s\).*Dotted.*dot\.name/);
});
it('should report deprecation warning for property named constructor', function() {
let message = 'deprecation not reported';
process.once('deprecation', function(err) { message = err.message; });
memory.createModel('Ctor', {'constructor': String});
message.should.match(/Property name should not be "constructor" in Model: Ctor/);
});
it('should throw error for dynamic property names containing dot',
function(done) {
const Model = memory.createModel('DynamicDotted');
Model.create({'dot.name': 'dot.value'}, function(err) {
err.should.be.instanceOf(Error);
err.message.should.match(/dot\(s\).*DynamicDotted.*dot\.name/);
done();
});
});
it('should throw error for dynamic property named constructor', function(done) {
const Model = memory.createModel('DynamicCtor');
Model.create({'constructor': 'myCtor'}, function(err) {
assert.equal(err.message, 'Property name "constructor" is not allowed in DynamicCtor data');
done();
});
});
it('should support "array" type shortcut', function() {
const Model = memory.createModel('TwoArrays', {
regular: Array,
sugar: 'array',
});
const props = Model.definition.properties;
props.regular.type.should.equal(props.sugar.type);
});
context('hasPK', function() {
context('with primary key defined', function() {
let Todo;
before(function prepModel() {
Todo = new ModelDefinition(new ModelBuilder(), 'Todo', {
content: 'string',
});
Todo.defineProperty('id', {
type: 'number',
id: true,
});
Todo.build();
});
it('should return true', function() {
Todo.hasPK().should.be.ok;
});
});
context('without primary key defined', function() {
let Todo;
before(function prepModel() {
Todo = new ModelDefinition(new ModelBuilder(), 'Todo', {
content: 'string',
});
Todo.build();
});
it('should return false', function() {
Todo.hasPK().should.not.be.ok;
});
});
});
});