Git Product home page Git Product logo

ampersand-model's People

Contributors

bear avatar cdaringe avatar dhritzkiv avatar dminkovsky avatar francisfuzz avatar gingermusketeer avatar henrikjoreteg avatar jakehp avatar janpaul123 avatar jrmyio avatar kasaimagi avatar latentflip avatar lukekarrys avatar meandavejustice avatar pgilad avatar redclov3r avatar remko avatar samhashemi avatar sorensen avatar spencerbyw avatar stephenlacy avatar stevelacy avatar svnlto avatar timwis avatar wraithgar avatar zearin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ampersand-model's Issues

model.destroy() returning opts.success() with no DELETE xhr created

The model is removed from the collection when category.destroy() is invoked, yet no xhr request is made.

// categories.js
import Collection from 'ampersand-rest-collection'
import xhr from 'xhr'

import Category from './category'
import config   from '../config'


export default Collection.extend({
  url: config.apiUrl + '/categories',
  model: Category,
  mainIndex: '_id',

  create(category) {
    xhr({
      uri: this.url,
      json: category,
      method: 'post',
    }, (err, res, body) => {
      if (err) return alert(err)
      if (body && body.success === false) return alert(body.error)

      category._id = body._id
      this.add(category)
    })
  },

  fetchWithTags() {
    xhr({
      uri: this.url + '?tags=true',
      method: 'get',
      json: true,
    }, (err, res, body) => {
      if (err) return alert(err)
      if (body && body.success === false) return alert(body.error)

      this.set(body)
    })
  },
})
// category.js
import Model from 'ampersand-model'

import cfg from '../config'
import Categories from './categories'


export default Model.extend({
  // urlRoot: cfg.apiUrl + '/categories',

  // url() {
  //   return cfg.apiUrl + '/categories/' + this._id
  // },

  props: {
    _id:  'string',
    name: 'string',
    tags: 'array',
  },
})
// pages/categories.jsx
import app   from 'ampersand-app'
import AmpMixin from 'ampersand-react-mixin'
import React from 'react'
import xhr   from 'xhr'

import { preAuth } from '../util/helpers'
import cfg      from '../config'
import Category from '../models/category'
import Topbar   from '../components/topbar.jsx'
import CategoryItem from '../components/category-item.jsx'


export default React.createClass({
  displayName: 'CategoriesPage',
  mixins: [AmpMixin],

  getInitialState() {
    return {
      categoryInput: '',
    }
  },

  categoryInputChanged(e) {
    this.setState({ categoryInput: e.target.value })
  },

  addCategory(e) {
    e.preventDefault()

    if (this.state.categoryInput.trim() == '') return

    preAuth(() => {
      app.categories.create({ name: this.state.categoryInput })
    })
  },

  deleteCategory(category) {

    console.log('category', category)

    category.destroy({
      success(model, res, opts) {
        console.log('category destroyed!')
        console.log('model, res, opts', model, res, opts)
      },
      error(model, res, opts) {
        console.log('There was an error destroying the category')
        console.log('model, res, opts', model, res, opts)
      },
    })

  },

  render() {
    var {categoryInput} = this.state
    var {categories}    = this.props

    var content = categories
      ? (<ul>
          {categories.map((cat) => {
            if (cat.name) return <CategoryItem key={cat._id} deleteCat={this.deleteCategory} cat={cat}></CategoryItem>
          })}
        </ul>)
      : <h4>Loading categories...</h4>


    return (
      <div className='form-wrap'>
        <header>
          <h1>Categories</h1>

          <form onSubmit={this.addCategory}>
            <input onChange={this.categoryInputChanged} value={categoryInput} type='text' placeholder='Add category'/>
            <button type='submit' className='commit'>Add</button>
          </form>
        </header>

        {content}
      </div>
    )
  },
})

Here's the console output, res is undefined
screen shot 2015-09-15 at 8 00 24 pm

Fetch successful even when 404

We are using ampersand-models for a project and noticed that when the model called .fetch() it would return a success call even when the url route was a 404 (returned html not json or null).
This creates a redundant item without any data.

I created a test repo to explain the issue: https://github.com/stevelacy/ampersand-fetch-test

Could a param be added to determine what type of data was needed to be returned, and if not throw an error? Or check the body type to make sure it is json or null. (ampersand-sync)

local data

Hello,

My app have an offline mode and all of data is saved in cdvfile://localhost/persistent/...
When offline mode is activate the urls of models data change for json local url.
On ios its ok but on android the status of the json files is 'pending' on google dev tools network when the app is starting and the app is blocked.

Any idea?

Relative urlRoot

In the upcoming release of ampsersand-sync, xhr2.js will be used. There is already a bug and pull request tracking this AmpersandJS/ampersand-sync#32 This gives us a great new feature we didn't previously have: we can test the interfaces outside of the browser, using Mocha. It also raises a question..

var model = BaseModel.extend({
    urlRoot: "/login"
});

What this does inside the browser is logical: it's a simple relative URL and all of it is handled behind the scenes. However, we'll have to hack ampersand-model.url() https://github.com/AmpersandJS/ampersand-model/blob/master/ampersand-model.js#L128 so we can reference some kind of global root path or something. If we don't have a reference to a global root path, or some way to emulate this functionality transparently everywhere ampersand-models are instantiated then we can we either

  • Have all urlRoots absolute urls.
  • Lose the ability to run them headless under Node.js.

Suggested Fix

Add a computed lazy property that emulates something like baseURI, we'll use baseURL as an example here.

*. Default baseURL property with
1. window.document.location.href (if this exists)
2. if it doesn't exist default it to global.document.location.href
3. throw fatal error.

This would permit us to always calculate the base for the current urlRoot.

*. Add a convenience function, isRelativeURL to check whether or not urlRoot is relative.
*. Add a conditional here using the new isRelativeURL. If the root is relative, use a URL library to make the urlRoot relative to the baseURL.

Suggestions? Ideas? One consideration we may have is the ambiguity in names. Under the current scheme urlRoot is often not the root at all. It's often relative to the implicit url root of document.window.location.href. This doesn't fix this bad property name.

.destroy is only successful with a 200 HTTP status code

I've noticed that destroy is only successful when the server responds with a 200 status code (vs. an arguably more appropriate 204).

Wouldn't it be simple for the code to test if the status code is >= 200 && < 300 and determine success thusly?

Pull out sync related methods and move them to separate repository

We are planning to use ampersand-model on the server, with the requirement that different users need different authentication tokens added to the headers without using some magic to propagate them down to all models that need to know how to fetch themselves.

To solve this problem, the idea was to remove a model's ability to fetch itself, and use a different component that knows everything about how to fetch models.
The idea is similar to this:

var syncManager = require('sync-manager')({
    authToken: 1243
});
var Model = require('our-model');

var m = new Model({
    id: 123,
    urlRoot: '/foo'
});

var fetched = syncManager.fetch(m);
var changed = fetched.then(function (model) {
    model.set('foo', 'bar');
    return model;
});
var saved = changed.then(syncManager.save);

Our model implementation will not contain any of the save, fetch and destroy methods.
That way we enforce that updates need to go through the syncManager and can slowly work our way towards single-directional data flow. We also don't need to propagate user data down into randomly required models and collections, something quite painful to do in event-loop based async programming on node. We currently use https://github.com/othiym23/node-continuation-local-storage instead to magically create something similar to thread locals which allow us to basically treat the auth data as globals over the lifetime of the request.

Of course we do not want to copy-paste all the code from ampersand-model over and create maintainence and compatibility overhead. At first I wanted to delegate through to ampersand-model's prototype method, but there's one annoying problem: sync aka ampersand-sync is accessed from the closure scope in ampersand-model, making it necessary to create a proxy object passing through all this.XXX and model.XXX data so we can have our models be clean of the sync related methods.

I would propose instead to turn the code into a mixin factory which you can then put on the prototype of Model and we can use in our sync-manager.

// ampersand-model.js
var syncMethodFactory = require('ampersand-sync-methods');
var sync = require('ampersand-sync');

...

_.extend(Model.prototype, syncMethodFactory(sync));

// sync-manager.js
var syncMethodFactory = require('ampersand-sync-methods');
var sync = require('our-isomorphic-sync');

var syncManager = function (options) {
    var customizedSync = sync({
        authToken: options.authToken
    });

    var syncMethods = syncMethodFactory(customizedSync);

    return {
        save: function (model, key, val, options) {
            return syncMethods.save.call(model, key, val, options);
        }
    };
};

This way it's very easy to create customized model classes that still rely on the sync code maintained by the project, and also allowing developers to replace sync inside of ampersand-model without having to rely on monkey patching the prototype.

Incompatibility with Webpack?

Using ampersand-model with webpack creates an invalid bundle.
The error require is not defined occurs on line 14654 (caused by module.exports = require("net");).

Below is a snippet that reproduces the problem.
Surprisingly, the exact same config works perfectly when using only ampersand-view.

Bonus point: this way, the (broken) bundle is made of 54756 lines. With browserify it's a working bundle made of 8406 lines. Any idea on the huge difference of file weight?

app.js

var Model = require('ampersand-model');
module.exports = Model.extend({});

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>

    </body>
    <script src="js/bundle.js"></script>
</html>

package.json

{
    "dependencies": {
        "ampersand-view": "6.x",
        "ampersand-model": "6.x"
    },
    "devDependencies": {
        "webpack": "x",
        "webpack-build": "x",
        "babel-core": "x",
        "babel-loader": "x",
        "babel-preset-es2015": "x",
        "handlebars": "x",
        "handlebars-loader": "x",
        "json-loader": "x",

        "hbsfy": "x",
        "watchify": "x",
        "browserify": "x"
    },
    "scripts": {
        "failing": "webpack",
        "working": "browserify -t [hbsfy -e hbs] js/app.js -o js/bundle.js"
    }
}

webpack.config.js

var webpack = require('webpack');

module.exports = {
    context: __dirname + "/js",
    entry: {
        "bundle" : "./app"
    },
    output: {
        path: __dirname + "/js",
        filename: "[name].js"
    },
    module: {
        loaders: [
            { test: /\.json$/, loader: 'json-loader'},
            {
                test: /\.js$/,
                loader: 'babel-loader',
                query: {
                    compact: false,
                    presets: ['es2015']
                }
            },
            {
                test: /\.hbs$/,
                loader: 'handlebars-loader'
            }
        ]
    },
    plugins: [
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': '"development"',
            'global': {} // bizarre lodash(?) webpack workaround
        })
    ],
    target: 'node',
    node: {
        __dirname: false,
        __filename: false,
    }
};

cannot unset property of type 'date'

var Model = AmpersandModel.extend({ props: { timestamp: 'date' }})
model = new Model()
model.timestamp = new Date()
model.unset('timestamp')
// TypeError: Property 'timestamp' must be of type date. Tried to set NaN

I'd have expxted the TypeError to only be thrown when timestamp property is required

Missing attributes from server are not removed

Missing attributes from server are not removed in model:

const model = new Model();
model.save({name: 'Paul', age: 18});
// server replies {name: 'Paul'}
// model still has is age attributes equal to 18

// server replies {name: 'Paul', age: 20}
// model is correctly updated

configurable Sync strategies for models + collections

looking to create alternative sync implementations (localstorage, firebase etc) and need ability and a consistent strategy. I think this merits some discussion since it spans models & collections and there are varying layers of abstraction

  • ampersand-model encapsulates fetch, destroy, sync like backbone but it's sync implementation is hard-coded - need to be able to inject a sync strategy
  • ampersand-collection does not have fetch etc, this exists rather in collection-rest-mixin but it also hard-codes it's sync strategy (will open an issue on collection)

Using 'url' as a property name in a model

When you create a model and try to use url in the list of props, an error is thrown when you try to save it.

Error: A "url" property or function must be specified

If you set the url it of coarse changes the url in which the model is saved.

model.save({ url: 'http://google.com' });
XMLHttpRequest cannot load http://google.com/.

Since url is a common field name used in databases I expected to be able to use it in an ampersand model.

Here is a simplified example: http://requirebin.com/?gist=2372c9322f4d2f5d5c3a

When using ampersand-model I cannot save a prop called `url`

Hi,

In my application, I have an API endpoint that accepts URLs (and other parameters) in POST / PUT requests, and also returns them back in the GET request. Currently, the URL field has the name "url" in the API, however, and it seems there's a conflict with the model's url() method.

When I try to add "url" to the props, like:

var MyModel = AmpersandModel.extend({
  props: {
    url: 'String'
  }
  ...
});

I see that when I call .save() to POST the data ({"url": "my-url.com"}), the URL that we hit is:

/my-url.com

But I really want to hit /api/urls with the data.

Any recommendations?

Adopt ampersand-sync v4.0

I have released the 4.0.0 version of ampersand-sync.
README.md now has docs too.

The biggest change for you is that ajaxConfig can do much more and response object is not the XMLHttpRequest instance anymore. Also, ampersand-sync now works in nodejs, and ampersand-model should be able to do that too.

Error when using latest ampersand-state

I'm getting errors of functions being undefined when I use the latest version of ampersand-state and the latest version of ampersand-model in the same project. This is due to the usage of an old ampersand-state version (4.6.0) as a dependency in ampersand-model. When is this going to be updated?

Attributes being marked as "not changed" before persisting to server

I have a model with properties as such:

var Profile = BaseModel.extend({
  props: {
    firstName: 'string',
    lastName: 'string',
    avatar: 'string'
  }
});

My model initialization should denote that all attributes start as null or undefined:

var profile = new Profile();

From a React view, I am then updating the avatar to a local file URI (for when user captures photo or selects a photo in the context of a Cordova app), e.g.:

function onPhotoChanged: function (uri) {
  this.props.profile.set('avatar', uri);
}

Later, when the user clicks the "Save" button, I actually persist to the server:

function onSubmit: function (event) {
  this.props.profile.save({
    firstName: this.refs.firstName.value(),
    lastName: this.refs.lastName.value(),
    avatar: this.refs.avatar.value()
  }, {
    success: this.onSuccess.bind(this),
    error: this.onError.bind(this)
  })
}

All the attributes are being propagated correctly. However, because of the set('avatar', uri) call in the onPhotoChanged handler, my Ampersand model thinks that the avatar value has NOT changed (via hasChanged()) even though it has yet to be persisted to the server up until the point that I actually call save. (FWIW, I'm calling hasChanged from my Profile#sync method prior to persisting, and I can see that all other attributes are properly marked as changed.) Looking at the previousAttributes object shows that it's not considering "avatar" null or undefined, but is instead stating that its initial value is whatever last came back when calling set('avatar', uri).

For example:

if (this.profile.hasChanged('avatar')) {
  // Separate handling logic for saving avatar image.
  // This never gets called!
}

This seems like a bug to me, is this the expected behavior?

Mixing in data for every request

Hi There,

I have some data relating to authorization that needs to be included in every ajax request. How would I include this in a base class without having to include it in every single one of my models.

For example:

client_id: 'myAppId'

should be part of every ajax request.

I had a look at ajaxConfig .. but this only lets me set headers and and xhr options, but does not seem to provide a way to include data.

I also looked at the "request" module on npm, but even there I couldn't find a neat way to always inject some data into every request.

In AngularJS I saw an example of an "authentication injector service" ... how would I achieve this with Ampersand?

Thanks,

Oliver

success handlers dont get access to the actual http response

https://github.com/AmpersandJS/ampersand-model/blob/master/ampersand-model.js#L52

The resp object is the JSON.parse(body) output, not the actual HTTP request. The success and error functions are inconsistent with one another, the arguments are in different orders and even though they are called the same arg names the data is totally different. Not sure what the thought process was behind how these were arranged but I think it would make sense to normalize them and make them work the same way.

When wait: true, serialization not applied to attributes passed to the save method.

If I have a model with a property someDate of type date, I get two different serialization results when I save with {wait: true}, depending on if I set someDate prior to invoking save, or if I pass it to the save function as follows:

model.someDate = new Date();
model.save(null, {wait: true})
// serialized to something like {"someDate": 143502072321}

model.save({someDate: new Date()}, {wait: true});
// serializes to  {"someDate": "2015-06-23T01:59:21.780Z"}

Line 67 appears to be the culprit.

Perhaps I am wrong, but something akin to the requested enhancement in ampersand-state Issue #122 seems to be needed so solve this.

Default headers for XHR

Perhaps the default headers passed to XHR should be

{
    "Accept": "application/json"
}

I think it makes sense since the model is expecting JSON. I've been otherwise having to add this to ajaxConfig for all my models.

Thoughts?

Default content type to text/plain;charset=UTF-8 with ajaxConfig

Is there a reason why body are send by default with a Content-Type to text/plain;charset=UTF-8?
This is very painful.

Ok, this is on my side too.
Using ajaxConfig is the reason of this, but I can't find why.

ajaxConfig: {
  beforeSend: function(xhr) {
    xhr.open(this.type, this.url + `?token=...`, true);
  },

patch and wait

I am noticing a potential issue in the last lines of the model's save function. When I try to make a patch and I also want to wait to make the changes such as:

model.save({ 
    /*attrs*/
}, {
    patch: true,
    wait: true
}

the attrs set in sync are set to the serialization to the model.

        if (method === 'patch') options.attrs = attrs;
        // if we're waiting we haven't actually set our attributes yet so
        // we need to do make sure we send right data
        if (options.wait) options.attrs = _.extend(model.serialize(), attrs);
        xhr = this.sync(method, this, options);

        return xhr;

if I am sending a patch I definitely don't want to send over EVERYTHING that is attached to my model but maybe I am missing a point here.

Props array type doesn't update until after .save()

I'm unsure if I'm supposed to notify the collection of a change?

The tags property is just an array of strings.

export default Model.extend({
  props: {
    _id:  'string',
    name: 'string',
    tags: 'array',
  },
})

Method in a React Class:

addTag(e) {
  e.preventDefault()
  if (this.state.tagInput.trim() == '') return

  var cat = this.props.cat

  console.log(cat.tags) // food, shopping
  if (!cat.tags) cat.tags = []
  cat.tags.push(this.state.tagInput)
  console.log(cat.tags) // food, shopping, entertainment (not updated in UI)

  this.setState({ submitted: true })

  var cat  = this.props.cat
  var self = this
  cat.save({ tags: cat.tags }, {
    success(model, res, opts) {
      self.setState({
        tagInput: '',
        submitted: false,
      })
      // Tag now displayed in UI
    },
    error(model, res, opts) {
      if (res.success === false) return alert(res.error)
    }
  })
},

A collection declared as a prop introduces errors.

I think if you have a collection and a prop by the same name, a fatal error should be thrown when one tries to override the other. My code only works as a collection..

module.exports = BaseModel.extend({        
  props: {                                 
    clientid: 'number'                     
    // , events: 'object'                     
  }                                        
  , collections: { events: NE } 

Out of order responses for `save` lose changes?

Suppose that I do the following:

model.save({foo: 1});
model.save({foo: 2});

Now suppose that the first save takes the server longer to complete than the second one. In that case, the callback to update the model with the server state will first update the model with foo: 2, and then with foo: 1, discarding the second save. Is this known? This seems to be an actual issue for me.

What's the way to handle parallel PUT operations with Ampersand? Is it at all possible?

wrapError model param is descoped by options.error model param

the code in question:

// Wrap an optional error callback with a fallback error event.
var wrapError = function (model, options) {
    var error = options.error;
    options.error = function (model, resp, options) {
        if (error) error(model, resp, options);
        model.trigger('error', model, resp, options);
    };
};

model.trigger(...) is expecting the model from wrapError arguments, but instead is model from options.error. We can see from ampersand-sync that the first argument is the response and not the model in this scenario:

    // Make the request. The callback executes functions that are compatible
    // With jQuery.ajax's syntax.
    var request = options.xhr = xhr(ajaxSettings, function (err, resp, body) {
        if (err && options.error) return options.error(resp, 'error', err.message);

`save` always updates with server-side state

The save code says:

// After a successful server-side save, the client is (optionally)
// updated with the server-side state.

From the code that follows, I was assuming this was controlled using options.parse, but it seems i'm wrong. How do you make sure the model does not update itself using the server state?

FYI, I used the following to test this:

  // Succeeds
  test("`save` updates model with server response", function (t) {
        t.plan(2);
        var model = new Backbone.Model({props: {testing: 'string'}});
        model.sync = function (method, model, options) { options.success({testing: 'bar'}); };
        model.save({testing: 'foo'}, { parse: true });
        t.equal(model.get('testing'), 'bar');
        t.ok(true);
   });

  // Fails
  test("`save` does not update model with server response", function (t) {
        t.plan(2);
        var model = new Backbone.Model({props: {testing: 'string'}});
        model.sync = function (method, model, options) { options.success({testing: 'bar'}); };
        model.save({testing: 'foo'}, { parse: false });
        t.equal(model.get('testing'), 'foo');
        t.ok(true);
   });

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.