diff --git a/README.md b/README.md index fe3528b..2b3cb07 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ your model config. } ``` -Attempting to update a ReadOnly model will reult in a 403 error. +Attempting to update a ReadOnly model will reult in a 403 error for this entire model. OPTIONS ============= @@ -98,7 +98,8 @@ OPTIONS The specific fields that are to be marked as readonly can be set by passing an object to the mixin options. -In this example we mark the `status` and `role` fields as readonly. +In this example we mark the `status` field as readonly for all actions, but the +`role` field can be set on create, but not for updates (it is marked `"createOnly"`). ```json { @@ -117,7 +118,7 @@ In this example we mark the `status` and `role` fields as readonly. "mixins": { "ReadOnly" : { "status" : true, - "role" : true + "role" : "createOnly" } } } @@ -125,6 +126,8 @@ In this example we mark the `status` and `role` fields as readonly. Any data set by a REST client in ReadOnly properties will be stripped out on the way to the server and will not be saved on the updated model instance. +However, any method calls from within the server application can still +update any of this data. TESTING ============= diff --git a/lib/read-only.js b/lib/read-only.js index a8c6797..e3d85d2 100644 --- a/lib/read-only.js +++ b/lib/read-only.js @@ -15,8 +15,11 @@ module.exports = function(Model, options) { if (properties) { debug('Creating %s : Read only properties are %j', Model.modelName, properties); Object.keys(properties).forEach(function(key) { - debug('The \'%s\' property is read only, removing incoming data', key); - delete body[key]; + if (properties[key] === true || + (properties[key].toString().toLowerCase() === 'createonly' && ctx.instance)) { + debug('The \'%s\' property is read only, removing incoming data', key); + delete body[key]; + } }); next(); } else { diff --git a/test/fixtures/simple-app/common/models/person.json b/test/fixtures/simple-app/common/models/person.json index 3ea177b..2f3e154 100644 --- a/test/fixtures/simple-app/common/models/person.json +++ b/test/fixtures/simple-app/common/models/person.json @@ -22,7 +22,7 @@ "mixins": { "ReadOnly": { "status": true, - "role": true + "role": "createOnly" } } } diff --git a/test/test-mixinsources.js b/test/test-mixinsources.js index c68050c..4b8ec6e 100644 --- a/test/test-mixinsources.js +++ b/test/test-mixinsources.js @@ -36,6 +36,11 @@ describe('loopback datasource readonly property (mixin sources.js)', function() describe('when called remotely', function() { lt.beforeEach.givenModel('Product', {name: 'some book', type: 'book', status: 'pending'}, 'product'); + lt.beforeEach.givenModel('Person', {name: 'Tom', status: 'disabled', role: 'user'}, 'Person'); + lt.beforeEach.givenModel('AuditTrail', {event: 'edit', user: 'tom'}, 'audittrail'); + lt.beforeEach.givenModel('Product', {name: 'book 1', type: 'book', status: 'disabled'}, 'book1', 'product'); + lt.beforeEach.givenModel('Product', {name: 'book 12', type: 'book', status: 'pending'}, 'book2', 'product'); + it('should not save readonly properties on create.', function(done) { var product = this.product; this.post('/api/products') @@ -52,7 +57,24 @@ describe('loopback datasource readonly property (mixin sources.js)', function() }); }); - lt.beforeEach.givenModel('Product', {name: 'some book', type: 'book', status: 'pending'}, 'product'); + it('should save createOnly properties on create.', function(done) { + var Person = this.Person; + this.post('/api/people') + .send({ + name: 'John', + status: 'active', + role: 'other user' + }) + .expect(200) + .end(function(err, res) { + expect(err).to.not.exist; + expect(res.body.name).to.equal('John'); + expect(res.body.status).to.not.exist; + expect(res.body.role).to.equal('other user'); + done(); + }); + }); + it('should not change readonly properties on update (single readonly property)', function(done) { var product = this.product; this.put('/api/products/' + product.id) @@ -69,14 +91,13 @@ describe('loopback datasource readonly property (mixin sources.js)', function() }); }); - lt.beforeEach.givenModel('Person', {name: 'Tom', status: 'disabled', role: 'user'}, 'Person'); it('should not change readonly properties on update (multiple readonly properties)', function(done) { var Person = this.Person; this.put('/api/people/' + Person.id) .send({ name: 'Tom (edited)', status: 'active', - role: 'user' + role: 'user (edited)' }) .expect(200) .end(function(err, res) { @@ -88,7 +109,6 @@ describe('loopback datasource readonly property (mixin sources.js)', function() }); }); - lt.beforeEach.givenModel('AuditTrail', {event: 'edit', user: 'tom'}, 'audittrail'); it('should not change readonly properties on update (full read only model)', function(done) { var audittrail = this.audittrail; this.put('/api/audittrails/' + audittrail.id) @@ -100,8 +120,6 @@ describe('loopback datasource readonly property (mixin sources.js)', function() .end(done); }); - lt.beforeEach.givenModel('Product', {name: 'book 1', type: 'book', status: 'disabled'}, 'book1', 'product'); - lt.beforeEach.givenModel('Product', {name: 'book 12', type: 'book', status: 'pending'}, 'book2', 'product'); it('should not change readonly properties with bulk updates', function(done) { var self = this; var data = { status: 'disabled' }; diff --git a/test/test-serverjs.js b/test/test-serverjs.js index 3c25171..47e4538 100644 --- a/test/test-serverjs.js +++ b/test/test-serverjs.js @@ -23,7 +23,7 @@ describe('loopback datasource readonly property (server.js)', function() { // A model with 2 readonly properties. var Person = this.Person = loopback.PersistedModel.extend('person', { name: String, status: String, role: String }, - { mixins: { ReadOnly: { status: true, role: true } } } + { mixins: { ReadOnly: { status: true, role: "createOnly" } } } ); Person.attachTo(loopback.memory()); app.model(Person); @@ -68,7 +68,12 @@ describe('loopback datasource readonly property (server.js)', function() { }); describe('when called remotely', function() { - + lt.beforeEach.givenModel('product', {name: 'some book', type: 'book', status: 'pending'}); + lt.beforeEach.givenModel('person', {name: 'Tom', status: 'disabled', role: 'user'}); + lt.beforeEach.givenModel('audittrail', {event: 'edit', user: 'tom'}); + lt.beforeEach.givenModel('product', {name: 'book 1', type: 'book', status: 'disabled'}, 'book1'); + lt.beforeEach.givenModel('product', {name: 'book 12', type: 'book', status: 'pending'}, 'book2'); + it('should not save readonly properties on create.', function(done) { var product = this.product; this.post('/products') @@ -85,7 +90,6 @@ describe('loopback datasource readonly property (server.js)', function() { }); }); - lt.beforeEach.givenModel('product', {name: 'some book', type: 'book', status: 'pending'}); it('should not change readonly properties on update (single readonly property)', function(done) { var product = this.product; this.put('/products/' + product.id) @@ -102,7 +106,6 @@ describe('loopback datasource readonly property (server.js)', function() { }); }); - lt.beforeEach.givenModel('person', {name: 'Tom', status: 'disabled', role: 'user'}); it('should not change readonly properties on update (multiple readonly properties)', function(done) { var person = this.person; this.put('/people/' + person.id) @@ -121,7 +124,6 @@ describe('loopback datasource readonly property (server.js)', function() { }); }); - lt.beforeEach.givenModel('audittrail', {event: 'edit', user: 'tom'}); it('should not change readonly properties on update (full read only model)', function(done) { var audittrail = this.audittrail; this.put('/audittrails/' + audittrail.id) @@ -133,8 +135,6 @@ describe('loopback datasource readonly property (server.js)', function() { .end(done); }); - lt.beforeEach.givenModel('product', {name: 'book 1', type: 'book', status: 'disabled'}, 'book1'); - lt.beforeEach.givenModel('product', {name: 'book 12', type: 'book', status: 'pending'}, 'book2'); it('should not change readonly properties with bulk updates', function(done) { var self = this; var data = { 'status': 'disabled' };