622 lines
18 KiB
JavaScript
622 lines
18 KiB
JavaScript
// Copyright IBM Corp. 2017,2019. 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 is 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('Model class inheritance', function() {
|
|
let memory;
|
|
beforeEach(function() {
|
|
memory = new DataSource({connector: Memory});
|
|
});
|
|
|
|
describe('ModelBaseClass.getMergePolicy()', function() {
|
|
const legacyMergePolicy = {
|
|
description: {replace: true},
|
|
properties: {patch: true},
|
|
hidden: {replace: false},
|
|
protected: {replace: false},
|
|
relations: {patch: true},
|
|
acls: {rank: true},
|
|
};
|
|
|
|
const recommendedMergePolicy = {
|
|
description: {replace: true},
|
|
options: {patch: true},
|
|
hidden: {replace: false},
|
|
protected: {replace: false},
|
|
indexes: {patch: true},
|
|
methods: {patch: true},
|
|
mixins: {patch: true},
|
|
relations: {patch: true},
|
|
scope: {replace: true},
|
|
scopes: {patch: true},
|
|
acls: {rank: true},
|
|
__delete: null,
|
|
__default: {replace: true},
|
|
};
|
|
|
|
let modelBuilder, base;
|
|
|
|
beforeEach(function() {
|
|
modelBuilder = memory.modelBuilder;
|
|
base = modelBuilder.define('base');
|
|
});
|
|
|
|
it('returns legacy merge policy by default', function() {
|
|
const mergePolicy = base.getMergePolicy();
|
|
should.deepEqual(mergePolicy, legacyMergePolicy);
|
|
});
|
|
|
|
it('returns recommended merge policy when called with option ' +
|
|
'`{configureModelMerge: true}`', function() {
|
|
const mergePolicy = base.getMergePolicy({configureModelMerge: true});
|
|
should.deepEqual(mergePolicy, recommendedMergePolicy);
|
|
});
|
|
|
|
it('handles custom merge policy defined via model.settings', function() {
|
|
let mergePolicy;
|
|
const newMergePolicy = {
|
|
relations: {patch: true},
|
|
};
|
|
|
|
// saving original getMergePolicy method
|
|
const originalGetMergePolicy = base.getMergePolicy;
|
|
|
|
// the injected getMergePolicy method captures the provided configureModelMerge option
|
|
base.getMergePolicy = function(options) {
|
|
mergePolicy = options && options.configureModelMerge;
|
|
return originalGetMergePolicy(options);
|
|
};
|
|
|
|
// calling extend() on base model calls base.getMergePolicy() internally
|
|
// child model settings are passed as 3rd parameter
|
|
const child = base.extend('child', {}, {configureModelMerge: newMergePolicy});
|
|
|
|
should.deepEqual(mergePolicy, newMergePolicy);
|
|
|
|
// restoring original getMergePolicy method
|
|
base.getMergePolicy = originalGetMergePolicy;
|
|
});
|
|
|
|
it('can be extended by user', function() {
|
|
const alteredMergePolicy = Object.assign({}, recommendedMergePolicy, {
|
|
__delete: false,
|
|
});
|
|
// extending the builtin getMergePolicy function
|
|
base.getMergePolicy = function(options) {
|
|
const origin = base.base.getMergePolicy(options);
|
|
return Object.assign({}, origin, {
|
|
__delete: false,
|
|
});
|
|
};
|
|
const mergePolicy = base.getMergePolicy({configureModelMerge: true});
|
|
should.deepEqual(mergePolicy, alteredMergePolicy);
|
|
});
|
|
|
|
it('is inherited by child model', function() {
|
|
const child = base.extend('child', {}, {configureModelMerge: true});
|
|
// get mergePolicy from child
|
|
const mergePolicy = child.getMergePolicy({configureModelMerge: true});
|
|
should.deepEqual(mergePolicy, recommendedMergePolicy);
|
|
});
|
|
});
|
|
|
|
describe('Merge policy WITHOUT flag `configureModelMerge`', function() {
|
|
it('inherits prototype using option.base', function() {
|
|
const modelBuilder = memory.modelBuilder;
|
|
const parent = memory.createModel('parent', {}, {
|
|
relations: {
|
|
children: {
|
|
type: 'hasMany',
|
|
model: 'anotherChild',
|
|
},
|
|
},
|
|
});
|
|
const baseChild = modelBuilder.define('baseChild');
|
|
baseChild.attachTo(memory);
|
|
// the name of this must begin with a letter < b
|
|
// for this test to fail
|
|
const anotherChild = baseChild.extend('anotherChild');
|
|
|
|
assert(anotherChild.prototype instanceof baseChild);
|
|
});
|
|
|
|
it('ignores inherited options.base', function() {
|
|
const modelBuilder = memory.modelBuilder;
|
|
const base = modelBuilder.define('base');
|
|
const child = base.extend('child', {}, {base: 'base'});
|
|
const grandChild = child.extend('grand-child');
|
|
assert.equal('child', grandChild.base.modelName);
|
|
assert(grandChild.prototype instanceof child);
|
|
});
|
|
|
|
it('ignores inherited options.super', function() {
|
|
const modelBuilder = memory.modelBuilder;
|
|
const base = modelBuilder.define('base');
|
|
const child = base.extend('child', {}, {super: 'base'});
|
|
const grandChild = child.extend('grand-child');
|
|
assert.equal('child', grandChild.base.modelName);
|
|
assert(grandChild.prototype instanceof child);
|
|
});
|
|
|
|
it('allows model extension', function(done) {
|
|
const modelBuilder = new ModelBuilder();
|
|
|
|
const User = modelBuilder.define('User', {
|
|
name: String,
|
|
bio: ModelBuilder.Text,
|
|
approved: Boolean,
|
|
joinedAt: Date,
|
|
age: Number,
|
|
});
|
|
|
|
const Customer = User.extend('Customer', {customerId: {type: String, id: true}});
|
|
|
|
const customer = new Customer({name: 'Joe', age: 20, customerId: 'c01'});
|
|
|
|
customer.should.be.type('object').and.have.property('name', 'Joe');
|
|
customer.should.have.property('name', 'Joe');
|
|
customer.should.have.property('age', 20);
|
|
customer.should.have.property('customerId', 'c01');
|
|
customer.should.have.property('bio', undefined);
|
|
|
|
// The properties are defined at prototype level
|
|
assert.equal(Object.keys(customer).filter(function(k) {
|
|
// Remove internal properties
|
|
return k.indexOf('__') === -1;
|
|
}).length, 0);
|
|
let count = 0;
|
|
for (const p in customer.toObject()) {
|
|
if (p.indexOf('__') === 0) {
|
|
continue;
|
|
}
|
|
if (typeof customer[p] !== 'function') {
|
|
count++;
|
|
}
|
|
}
|
|
assert.equal(count, 6);
|
|
assert.equal(Object.keys(customer.toObject()).filter(function(k) {
|
|
// Remove internal properties
|
|
return k.indexOf('__') === -1;
|
|
}).length, 6);
|
|
|
|
done(null, customer);
|
|
});
|
|
|
|
it('allows model extension with merged settings', function(done) {
|
|
const modelBuilder = new ModelBuilder();
|
|
|
|
const User = modelBuilder.define('User', {
|
|
name: String,
|
|
}, {
|
|
defaultPermission: 'ALLOW',
|
|
acls: [
|
|
{
|
|
principalType: 'ROLE',
|
|
principalId: '$everyone',
|
|
permission: 'ALLOW',
|
|
},
|
|
],
|
|
relations: {
|
|
posts: {
|
|
type: 'hasMany',
|
|
model: 'Post',
|
|
},
|
|
},
|
|
});
|
|
|
|
const Customer = User.extend('Customer',
|
|
{customerId: {type: String, id: true}}, {
|
|
defaultPermission: 'DENY',
|
|
acls: [
|
|
{
|
|
principalType: 'ROLE',
|
|
principalId: '$unauthenticated',
|
|
permission: 'DENY',
|
|
},
|
|
],
|
|
relations: {
|
|
orders: {
|
|
type: 'hasMany',
|
|
model: 'Order',
|
|
},
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(User.settings, {
|
|
// forceId is set to 'auto' in memory if idProp.generated && forceId !== false
|
|
forceId: 'auto',
|
|
defaultPermission: 'ALLOW',
|
|
acls: [
|
|
{
|
|
principalType: 'ROLE',
|
|
principalId: '$everyone',
|
|
permission: 'ALLOW',
|
|
},
|
|
],
|
|
relations: {
|
|
posts: {
|
|
type: 'hasMany',
|
|
model: 'Post',
|
|
},
|
|
},
|
|
strict: false,
|
|
});
|
|
|
|
assert.deepEqual(Customer.settings, {
|
|
forceId: false,
|
|
defaultPermission: 'DENY',
|
|
acls: [
|
|
{
|
|
principalType: 'ROLE',
|
|
principalId: '$everyone',
|
|
permission: 'ALLOW',
|
|
},
|
|
{
|
|
principalType: 'ROLE',
|
|
principalId: '$unauthenticated',
|
|
permission: 'DENY',
|
|
},
|
|
],
|
|
relations: {
|
|
posts: {
|
|
type: 'hasMany',
|
|
model: 'Post',
|
|
},
|
|
orders: {
|
|
type: 'hasMany',
|
|
model: 'Order',
|
|
},
|
|
},
|
|
strict: false,
|
|
base: User,
|
|
});
|
|
|
|
done();
|
|
});
|
|
|
|
it('defines rank of ACLs according to model\'s inheritance rank', function() {
|
|
// a simple test is enough as we already fully tested option `{rank: true}`
|
|
// in tests with flag `configureModelMerge`
|
|
const modelBuilder = memory.modelBuilder;
|
|
const base = modelBuilder.define('base', {}, {acls: [
|
|
{
|
|
principalType: 'ROLE',
|
|
principalId: '$everyone',
|
|
property: 'oneMethod',
|
|
permission: 'ALLOW',
|
|
},
|
|
]});
|
|
const childRank1 = modelBuilder.define('childRank1', {}, {
|
|
base: base,
|
|
acls: [
|
|
{
|
|
principalType: 'ROLE',
|
|
principalId: '$everyone',
|
|
property: 'oneMethod',
|
|
permission: 'DENY',
|
|
},
|
|
],
|
|
});
|
|
|
|
const expectedSettings = {
|
|
acls: [
|
|
{
|
|
principalType: 'ROLE',
|
|
principalId: '$everyone',
|
|
property: 'oneMethod',
|
|
permission: 'ALLOW',
|
|
__rank: 1,
|
|
},
|
|
{
|
|
principalType: 'ROLE',
|
|
principalId: '$everyone',
|
|
property: 'oneMethod',
|
|
permission: 'DENY',
|
|
__rank: 2,
|
|
},
|
|
],
|
|
};
|
|
should.deepEqual(childRank1.settings.acls, expectedSettings.acls);
|
|
});
|
|
|
|
it('replaces baseClass relations with matching subClass relations', function() {
|
|
// merge policy of settings.relations is {patch: true}
|
|
const modelBuilder = memory.modelBuilder;
|
|
const base = modelBuilder.define('base', {}, {
|
|
relations: {
|
|
user: {
|
|
type: 'belongsTo',
|
|
model: 'User',
|
|
foreignKey: 'userId',
|
|
},
|
|
},
|
|
});
|
|
const child = base.extend('child', {}, {
|
|
relations: {
|
|
user: {
|
|
type: 'belongsTo',
|
|
idName: 'id',
|
|
polymorphic: {
|
|
idType: 'string',
|
|
foreignKey: 'userId',
|
|
discriminator: 'principalType',
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const expectedSettings = {
|
|
relations: {
|
|
user: {
|
|
type: 'belongsTo',
|
|
idName: 'id',
|
|
polymorphic: {
|
|
idType: 'string',
|
|
foreignKey: 'userId',
|
|
discriminator: 'principalType',
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
should.deepEqual(child.settings.relations, expectedSettings.relations);
|
|
});
|
|
});
|
|
|
|
describe('Merge policy WITH flag `configureModelMerge: true`', function() {
|
|
it('`{__delete: null}` allows deleting base model settings by assigning ' +
|
|
'null value at sub model level', function() {
|
|
const modelBuilder = memory.modelBuilder;
|
|
const base = modelBuilder.define('base', {}, {
|
|
anyParam: {oneKey: 'this should be removed'},
|
|
});
|
|
const child = base.extend('child', {}, {
|
|
anyParam: null,
|
|
configureModelMerge: true,
|
|
});
|
|
|
|
const expectedSettings = {};
|
|
|
|
should.deepEqual(child.settings.description, expectedSettings.description);
|
|
});
|
|
|
|
it('`{rank: true}` defines rank of array elements ' +
|
|
'according to model\'s inheritance rank', function() {
|
|
const modelBuilder = memory.modelBuilder;
|
|
const base = modelBuilder.define('base', {}, {acls: [
|
|
{
|
|
principalType: 'ROLE',
|
|
principalId: '$everyone',
|
|
property: 'oneMethod',
|
|
permission: 'ALLOW',
|
|
},
|
|
]});
|
|
const childRank1 = modelBuilder.define('childRank1', {}, {
|
|
base: base,
|
|
acls: [
|
|
{
|
|
principalType: 'ROLE',
|
|
principalId: '$owner',
|
|
property: 'anotherMethod',
|
|
permission: 'ALLOW',
|
|
},
|
|
],
|
|
configureModelMerge: true,
|
|
});
|
|
const childRank2 = childRank1.extend('childRank2', {}, {});
|
|
const childRank3 = childRank2.extend('childRank3', {}, {
|
|
acls: [
|
|
{
|
|
principalType: 'ROLE',
|
|
principalId: '$everyone',
|
|
property: 'oneMethod',
|
|
permission: 'DENY',
|
|
},
|
|
],
|
|
configureModelMerge: true,
|
|
});
|
|
|
|
const expectedSettings = {
|
|
acls: [
|
|
{
|
|
principalType: 'ROLE',
|
|
principalId: '$everyone',
|
|
property: 'oneMethod',
|
|
permission: 'ALLOW',
|
|
__rank: 1,
|
|
},
|
|
{
|
|
principalType: 'ROLE',
|
|
principalId: '$owner',
|
|
property: 'anotherMethod',
|
|
permission: 'ALLOW',
|
|
__rank: 2,
|
|
},
|
|
{
|
|
principalType: 'ROLE',
|
|
principalId: '$everyone',
|
|
property: 'oneMethod',
|
|
permission: 'DENY',
|
|
__rank: 4,
|
|
},
|
|
],
|
|
};
|
|
should.deepEqual(childRank3.settings.acls, expectedSettings.acls);
|
|
});
|
|
|
|
it('`{replace: true}` replaces base model array with sub model matching ' +
|
|
'array', function() {
|
|
// merge policy of settings.description is {replace: true}
|
|
const modelBuilder = memory.modelBuilder;
|
|
const base = modelBuilder.define('base', {}, {
|
|
description: ['base', 'model', 'description'],
|
|
});
|
|
const child = base.extend('child', {}, {
|
|
description: ['this', 'is', 'child', 'model', 'description'],
|
|
configureModelMerge: true,
|
|
});
|
|
|
|
const expectedSettings = {
|
|
description: ['this', 'is', 'child', 'model', 'description'],
|
|
};
|
|
|
|
should.deepEqual(child.settings.description, expectedSettings.description);
|
|
});
|
|
|
|
it('`{replace:true}` is applied on array parameters not defined in merge policy', function() {
|
|
const modelBuilder = memory.modelBuilder;
|
|
const base = modelBuilder.define('base', {}, {
|
|
unknownArrayParam: ['this', 'should', 'be', 'replaced'],
|
|
});
|
|
const child = base.extend('child', {}, {
|
|
unknownArrayParam: ['this', 'should', 'remain', 'after', 'merge'],
|
|
configureModelMerge: true,
|
|
});
|
|
|
|
const expectedSettings = {
|
|
unknownArrayParam: ['this', 'should', 'remain', 'after', 'merge'],
|
|
};
|
|
|
|
should.deepEqual(child.settings.description, expectedSettings.description);
|
|
});
|
|
|
|
it('`{replace:true}` is applied on object {} parameters not defined in mergePolicy', function() {
|
|
const modelBuilder = memory.modelBuilder;
|
|
const base = modelBuilder.define('base', {}, {
|
|
unknownObjectParam: {oneKey: 'this should be replaced'},
|
|
});
|
|
const child = base.extend('child', {}, {
|
|
unknownObjectParam: {anotherKey: 'this should remain after merge'},
|
|
configureModelMerge: true,
|
|
});
|
|
|
|
const expectedSettings = {
|
|
unknownObjectParam: {anotherKey: 'this should remain after merge'},
|
|
};
|
|
|
|
should.deepEqual(child.settings.description, expectedSettings.description);
|
|
});
|
|
|
|
it('`{replace: false}` adds distinct members of matching arrays from ' +
|
|
'base model and sub model', function() {
|
|
// merge policy of settings.hidden is {replace: false}
|
|
const modelBuilder = memory.modelBuilder;
|
|
const base = modelBuilder.define('base', {}, {
|
|
hidden: ['firstProperty', 'secondProperty'],
|
|
});
|
|
const child = base.extend('child', {}, {
|
|
hidden: ['secondProperty', 'thirdProperty'],
|
|
configureModelMerge: true,
|
|
});
|
|
|
|
const expectedSettings = {
|
|
hidden: ['firstProperty', 'secondProperty', 'thirdProperty'],
|
|
};
|
|
|
|
should.deepEqual(child.settings.hidden, expectedSettings.hidden);
|
|
});
|
|
|
|
it('`{patch: true}` adds distinct inner properties of matching objects ' +
|
|
'from base model and sub model', function() {
|
|
// merge policy of settings.relations is {patch: true}
|
|
const modelBuilder = memory.modelBuilder;
|
|
const base = modelBuilder.define('base', {}, {
|
|
relations: {
|
|
someOtherRelation: {
|
|
type: 'hasMany',
|
|
model: 'someOtherModel',
|
|
foreignKey: 'otherModelId',
|
|
},
|
|
},
|
|
});
|
|
const child = base.extend('child', {}, {
|
|
relations: {
|
|
someRelation: {
|
|
type: 'belongsTo',
|
|
model: 'someModel',
|
|
foreignKey: 'modelId',
|
|
},
|
|
},
|
|
configureModelMerge: true,
|
|
});
|
|
|
|
const expectedSettings = {
|
|
relations: {
|
|
someRelation: {
|
|
type: 'belongsTo',
|
|
model: 'someModel',
|
|
foreignKey: 'modelId',
|
|
},
|
|
someOtherRelation: {
|
|
type: 'hasMany',
|
|
model: 'someOtherModel',
|
|
foreignKey: 'otherModelId',
|
|
},
|
|
},
|
|
};
|
|
|
|
should.deepEqual(child.settings.relations, expectedSettings.relations);
|
|
});
|
|
|
|
it('`{patch: true}` replaces baseClass inner properties with matching ' +
|
|
'subClass inner properties', function() {
|
|
// merge policy of settings.relations is {patch: true}
|
|
const modelBuilder = memory.modelBuilder;
|
|
const base = modelBuilder.define('base', {}, {
|
|
relations: {
|
|
user: {
|
|
type: 'belongsTo',
|
|
model: 'User',
|
|
foreignKey: 'userId',
|
|
},
|
|
},
|
|
});
|
|
const child = base.extend('child', {}, {
|
|
relations: {
|
|
user: {
|
|
type: 'belongsTo',
|
|
idName: 'id',
|
|
polymorphic: {
|
|
idType: 'string',
|
|
foreignKey: 'userId',
|
|
discriminator: 'principalType',
|
|
},
|
|
},
|
|
},
|
|
configureModelMerge: true,
|
|
});
|
|
|
|
const expectedSettings = {
|
|
relations: {
|
|
user: {
|
|
type: 'belongsTo',
|
|
idName: 'id',
|
|
polymorphic: {
|
|
idType: 'string',
|
|
foreignKey: 'userId',
|
|
discriminator: 'principalType',
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
should.deepEqual(child.settings.relations, expectedSettings.relations);
|
|
});
|
|
});
|
|
});
|