Skip to content
This repository

Add a readOnly option to DS.attr #303

Closed
wants to merge 2 commits into from
Bradley Priest
Collaborator

This adds a readOnly option to DS.attr which causes the attribute not to be serialized to json.
I'm not sure if readOnly is the right name because it doesn't stop you changing the value in any way it just makes sure the changes aren't persisted.

My main use case is for created_at and updated_at fields. I'm currently filtering them out of the json in my Rails controllers at the server end. But it would be nice to not send them at all.

Peter Wagenet
Owner

@bradleypriest I like this idea, but it looks like the PR doesn't merge anymore.

Bradley Priest
Collaborator

@wagenet rebased and pushed

Peter Wagenet
Owner

@bradleypriest Do you think it makes sense to warn if a user tries to set a readOnly attribute?

Peter Wagenet wagenet referenced this pull request July 12, 2012
Closed

read-only attributes #107

Bradley Priest
Collaborator

Yeah, I think that'd be a good idea. To be honest, this feature was just called excludeFromJson right up until I created the PR :)

Pepe Cano

@wagenet , @bradleypriest. One specific case of a similar "readOnly" feature, require the following implementation:

  • excludeFromJson
  • doNotDirty: when user changes property, its value is updated, but setProperty is skipped, so the record is not on dirty state and won't be committed to the server.

This allows the user changes the value to provide a more responsiveness UI, but the value will only be really updated by the server.

Bradley Priest
Collaborator

@ppcano That seems like a pretty specific situation to me. Would you mind giving a specific example of where that would be useful

Pepe Cano

hasLiked property defines if the current authorized/logged user "likes" the following produc and cannot be updated with the product REST API, even though the info can be redundant/unnecesary, it has been defined to improve UIs.

Yn.Product = DS.Model.extend({

  hasLiked: DS.attr('boolean', {key: 'has_voted', doNotDirty: true, exclude:true} )

})

When the user fires the like button, the property will be updated immediately ( i think, this a common approach to manage async UIs ), and it will be updated based on server action response.

product.set('hasLiked', true);

var like = Yn.Like.createRecord({ user: currentUser, product: product};);
like.on('didCreate', function(item) {
  // refresh product if necessary
});

like.on('didCreateError', function(item) {
  product.set('hasLiked', false);
  //show error ui
});

App.store.commit();
Le Wang

How does the PR relate to the relationship-improvements branch? Which AFAIU is the future of Ember-data.

Bradley Priest
Collaborator

It doesn't yet, I'll update once that branch gets merged, relationship-improvements is still too buggy to recommend that anyone uses.

Peter Wagenet
Owner

@bradleypriest Can you revisit this now?

Bradley Priest
Collaborator

Have got it working locally, but it's broken a handful of unrelated tests that were naively stubbing the attributes property. Will push it up once I've sorted

Kevin Ansfield

Would this also cover the use case where you need to define a BelongsTo on a model so that it can be tied to a user but you don't want the user_id to be sent back to the server because it should only ever be set to the logged-in user and so is protected against mass assignment?

Pepe Cano

@kevinansfield, i already mentioned this case, doNotDirty + exclude.

@wagenet, @wycats, Wasn't this feature mentioned in other cases?

Bradley Priest
Collaborator
Bradley Priest
Collaborator
Bradley Priest
Collaborator

From the talk done at the Addepari meetup 2 weeks ago, I think this is being handled in the core of Ember-Data now?
Should I leave it? /cc @wycats

Mark Westra

It would be great to have this feature.

My current use case is that I have a list of items in a table, which I can sort by clicking on the header (this changes the sortProperties on the controller). In the first column I have a select box, so I can select numerous items, and then perform a common action on them, such as set a property. To be able to do this, I have added an 'isSelected' property to the item model. However, I don't want this (local) selection to be persisted to the server.

I cannot use views for this, as the views get recreated when the sortProperties is changed and the table is rebuild.

any ideas are welcome!

Spencer P

@mtwestra — I have a nearly identical implementation for what you're describing (Table of data with sort options; checkboxes to check multiple rows, etc).

In my Model, the 'isSelected' property is set to false (instead of being set to DS.attr("boolean") )...it behaves as expected and is excluded when serializing toData / toJSON.

Not sure if this is how it's supposed to work...but haven't had any issues yet. ;-)

Mark Westra

@spencer516, have tried it, and it works like a charm. Thanks for the quick reaction!
For local object state this makes a lot of sense: have attributes for things the server cares about, and simple properties for local state, such as 'selected'.

Mark Przepiora

Here is my short-and-sweet solution to this problem. (Sorry about the CoffeeScript)

DS.RESTAdapter.map 'App.Post'
  readOnlyKeys: ['createdAt']

FilteredSerializer = DS.RESTSerializer.extend
  addAttributes: (data, record) ->
    record.eachAttribute ((name, attribute) ->
      isReadOnly = @mappings.get(record.constructor)?['readOnlyKeys']?.indexOf(name) >= 0
      @_addAttribute data, record, name, attribute.type unless isReadOnly
    ), this

App.store = DS.Store.create
  revision: 10
  adapter: DS.RESTAdapter.create
    serializer: FilteredSerializer

In this example, when you commit an App.Post object, the createdAt attribute will be ignored.

The only downside is that you will not be prevented from performing aPost.set('createdAt', ...), but I'm sure this could easily be done as well.

Wesley Workman

+1 . We've implemented this in our app in a very simular fashion. I'd definitely appreciate it being a native piece of functionality.

I understand the desire to warn users about changing a read-only property, but it would be ideal if we could turn that off conditionally or maybe via an ENV setting. Our use case is we have a noSQL backend. We can't do joins and other things to produce complex counts. So we have stat jobs that de-normalizes this data for us asynchronously. When a count is updated we use PubNub to push that down to our clients. Lastly, the adapter then updates those counts on the effected models. Those count attributes are stored read-only because they would never be sent up to the server from the client.

Blah, that was a long description. Sorry for that, I thought it was necessary to provide a valid use case.

Stanley

@markprzepiora your solution worked great, thanks for sharing.

@workmanw I'm in a similar situation as you. Would be nice to have support for this natively.

Bradley Priest
Collaborator

Here's the slides from the talk I mentioned where Tom mentions readOnly attrs. https://speakerdeck.com/tomdale/30

Tom Dale
Owner

This is definitely on the roadmap. We want to this feature to have runtime checking, so that changing a read-only attribute immediately raises an exception.

That being said, I would be willing to accept a "lightweight" PR that implements some of the API read-only proposal (without the runtime checking), so long as it was not too intrusive. The API proposal is here: https://gist.github.com/4263171

Carl Taylor

Here is my CoffeeScript fix for this:

# This is oddly an INSTANCE, not an extendable class... Hence, reopen
DS.RESTSerializer.reopen
  #####
  # This filters attributes so that properties with read_only:true options
  # are not sent back on the wire:
  # App.User = DS.Model.extend
  #   created_at: DS.attr('date', read_only:true )
  addAttribute: (hash, record, attributeName, attributeType)->
    unless record.constructor.metaForProperty(attributeName).options.read_only
      @_super hash, record, attributeName, attributeType
Carl Taylor

And if you really care about assertions on setting read only data, then you can add an assert under this existing one:

Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('<type>')` from " + this.toString(), key !== 'id');
Ember.assert("Cannot set `" + key + "` because it is declared read_only on your model", options.read_only);

Unfortunately, this must be modified within your ember-data.js, because it's pretty much hidden by closures.

Carl Taylor

It is currently impossible for addAttribute to do what I just mentioned because the context getting the property's meta was removed:

https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/serializer.js#L759

At this point, you'd have to reopen and overwrite _addAttribute.

Stephan Behnke

Anyone have a functioning workaround for this?

Bradley Priest
Collaborator

@stephanos I'm currently using this

Serializer = DS.RESTSerializer.extend
  _addAttribute: (data, record, attributeName, attributeType) ->
    options = Ember.get(record, "constructor.attributes").get(attributeName).options
    if options.readOnly
      return
    else
      key = @_keyForAttributeName(record.constructor, attributeName)
      value = get(record, attributeName)
      @addAttribute(data, key, @serializeValue(value, attributeType))
kdemanawa

@stephanos I override addAttributes:

App.RESTSerializer = DS.RESTSerializer.extend({
 addAttributes: function(data, record) {
    record.eachAttribute(function(name, attribute) {
      if (!attribute.options.readOnly) this._addAttribute(data, record, name, attribute.type);
    }, this);
  }
});

and in my model, App.Post = DS.Model.extend({createdAt: DS.attr('date', {readOnly: true})});

Steven Lindberg

@stephanos This is what the addAttributes hook looks like if you want to use mappings ala @tomdale's proposal:

CustomSerializer = DS.RESTSerializer.extend({
  addAttributes: function(data, record) {
    record.eachAttribute(function(name, attribute) {
      // Skip serializing the attribute if 'readOnly' is true in its mapping
      if (!this.mappingOption(record.constructor, name, 'readOnly')) {
        this._addAttribute(data, record, name, attribute.type);
      }
    }, this);
  }
};

And to add the mapping:

DS.RESTAdapter.map('App.SomeModel', {
  someField: { readOnly: true }
});
Tom Dale
Owner
tomdale commented May 10, 2013

What is the status of this?

Bradley Priest
Collaborator

@tomdale is https://gist.github.com/tomdale/4263171 still a good doc to use for API preference.
Currently everyone has seems to be using their own custom hacks to stop returning attributes in the JSON, I can't say I've seen any implementations of the other features yet.
I'll start looking at getting this step reimplemented on top of master

Steven Soroka

+1

Stas Sușcov
stas commented July 12, 2013

Looking forward to see this merged. Thanks.

Peter Wagenet
Owner
Bradley Priest
Collaborator

Ok, I'm taking a look at this now that 1.0beta is out.

Tom's proposal is obviously quite out-of-date, but I've tried to see how it translates
Correct me if I'm wrong, but it looks like the configure and map APIs have disappeared.

At first glance, there's a couple of basic API choices.

Firstly, an updated version of the proposal.

App.Post = DS.Model.extend({
  published: attr("boolean")
})
App.PostSerializer = DS.JSONSerializer.extend({
  attrs: {
     published: { readOnly: true }
  }
})

This one is based on the commented out tests used for embedded records here. Currently the attrs hash is used for customizing keys, but I haven't seen any documentation about it.

Although this makes sense for serializing, it doesn't really make sense with the warning step. The model would need to ask it's serializer if it can set a value?

Secondly, declaratively on the Model. This allows warning when setting readOnly attributes, but is putting serialization knowledge on the model itself.

App.Post = DS.Model.extend({
  published: attr("boolean", { readOnly: true })
})

I have basic versions of both working locally, would love to get some feedback.

Also @tomdale is all of the previous feature set on the wishlist within the new lighter weight ED?

  1. Read-Only Records
  2. Read-Only Attributes
  3. Read-Only attributes with warning when calling set
  4. Read-Only Associations
Yehuda Katz
Owner

A simpler implementation based on more recent Ember might just be to mark the computed property as .readOnly.

@bradleypriest want to open another PR with that implementation against 1.0?

Yehuda Katz wycats closed this September 03, 2013
Bradley Priest
Collaborator

@wycats Will do, however IMO the much more important use-case of readonly attributes is excluding them from the JSON.
Thoughts on adding the config to the model vs the serializer?

Kevin Ansfield

Is there a canonical example of excluding attributes from the JSON returned to the server? Perhaps it would make a good cookbook example. I'm finding I have to write a lot of workarounds server-side to prevent mass-assignment errors.

Bradley Priest
Collaborator

With ED 1.0+ the basic override to exclude from JSON is:

App.ApplicationSerializer = DS.RESTSerializer.extend({
  serializeAttribute: function(record, json, key, attribute) {
    if (!attribute.options.readOnly) {
      return this._super(record, json, key, attribute);
    }
  }
});

App.User = DS.Model.create({
  admin: DS.attr('boolean', { readOnly: true })
})
Mitch Lloyd

@bradleypriest Could we reopen this issue? I was working with someone new to Ember and this was a stumbling point. He needed a way to exclude an attribute from the JSON response as you're describing and overriding a serializer was a little intimidating.

I'd be happy to take a shot at another implementation that works with ED 1.0 beta, but I'm not sure where this functionality should go. Would it be appropriate to include in the serializeAttribute function of the JSONSerializer?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
21  packages/ember-data/lib/system/model/attributes.js
@@ -37,6 +37,22 @@ function getAttr(record, options, key) {
37 37
   return value;
38 38
 }
39 39
 
  40
+  /**
  41
+    Defines an attribute on a DS.Model of a specified type.
  42
+    By default, data ships with four attribute types:
  43
+      'string', 'number', 'boolean' and 'date'.
  44
+    You can define your own transform by appending to DS.attr.transforms.
  45
+
  46
+    DS.attr takes an optional hash as a second parameter, currently
  47
+    supported options are:
  48
+      'defaultValue': Sets the attribute to a default if none is supplied by the user.
  49
+      'key': Use a custom key in the JSON.
  50
+      'readOnly': Do not include this attribute in the JSON representation.
  51
+
  52
+    @param {String} type the attribute type
  53
+    @param {Object} options a hash of options
  54
+  */
  55
+
40 56
 DS.attr = function(type, options) {
41 57
   var transform = DS.attr.transforms[type];
42 58
   Ember.assert("Could not find model attribute of type " + type, !!transform);
@@ -67,6 +83,11 @@ DS.attr = function(type, options) {
67 83
     if (arguments.length === 2) {
68 84
       value = transformTo(value);
69 85
 
  86
+      if(meta.options.readOnly){
  87
+        Ember.warn("You can't set the read-only attribute: " + key);
  88
+        value = getAttr(this, options, key);
  89
+      }
  90
+
70 91
       if (value !== getAttr(this, options, key)) {
71 92
         this.setProperty(key, value);
72 93
       }
4  packages/ember-data/lib/system/model/model.js
@@ -66,6 +66,8 @@ DS.Model = Ember.Object.extend(Ember.Evented, {
66 66
     The default implementation gets the current value of each
67 67
     attribute from the `data`, and uses a `defaultValue` if
68 68
     specified in the `DS.attr` definition.
  69
+    Attributes can be skipped by setting readOnly to true in
  70
+    the 'DS.attr' definition.
69 71
 
70 72
     @param {Object} json the JSON hash being build
71 73
     @param {Ember.Map} attributes a Map of attributes
@@ -73,6 +75,8 @@ DS.Model = Ember.Object.extend(Ember.Evented, {
73 75
   */
74 76
   addAttributesToJSON: function(json, attributes, data) {
75 77
     attributes.forEach(function(name, meta) {
  78
+      if(meta.options.readOnly){ return; }
  79
+
76 80
       var key = meta.key(this.constructor),
77 81
           value = get(data, key);
78 82
 
22  packages/ember-data/tests/unit/model_test.js
@@ -35,6 +35,28 @@ test("setting a property on a record that has not changed does not cause it to b
35 35
   equal(person.get('isDirty'), false, "record does not become dirty after setting property to old value");
36 36
 });
37 37
 
  38
+test("does not set a readOnly Property", function() {
  39
+  Person.reopen({
  40
+    foobar: DS.attr('string', {readOnly: true})
  41
+  });
  42
+
  43
+  var record = store.createRecord(Person);
  44
+  set(record, 'foobar', 'bar');
  45
+
  46
+  equal(get(record, 'foobar'), null, "readOnly property was not set on the record");
  47
+});
  48
+
  49
+test("sets readOnly properties using store.load", function() {
  50
+  Person.reopen({
  51
+    foobar: DS.attr('string', {readOnly: true})
  52
+  });
  53
+
  54
+  var record = store.load(Person, { id: 1, foobar: 'bar'});
  55
+  set(record, 'foobar', 'bar');
  56
+
  57
+  equal(get(record, 'foobar'), 'bar', "readOnly property was loaded on the record");
  58
+});
  59
+
38 60
 test("a record reports its unique id via the `id` property", function() {
39 61
   store.load(Person, { id: 1 });
40 62
 
22  packages/ember-data/tests/unit/to_json_test.js
@@ -296,3 +296,25 @@ test("custom belongsTo keys are applied", function() {
296 296
   equal(json.related_id, 1, "applied standard key to JSON");
297 297
   equal(json.my_custom_key, 1, "applied custom key to JSON");
298 298
 });
  299
+
  300
+test("toJSON respects readOnly meta", function() {
  301
+  var store = DS.Store.create();
  302
+
  303
+  var Model = DS.Model.extend({
  304
+    name: DS.attr('string'),
  305
+    readOnlyName: DS.attr('string', {readOnly: true})
  306
+  });
  307
+
  308
+  store.load(Model, {
  309
+    id: 1,
  310
+    name: "John Doe",
  311
+    readOnlyName: 'Jane Doe'
  312
+  });
  313
+
  314
+  var record = store.find(Model, 1);
  315
+
  316
+  var json = record.toJSON();
  317
+
  318
+  equal(json.name, "John Doe", "serialized attribute correctly");
  319
+  ok(!json.hasOwnProperty('readOnlyName'), "ignored readOnly attribute");
  320
+});
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.