Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

mobx-rest

Package Overview
Dependencies
Maintainers
1
Versions
71
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mobx-rest - npm Package Compare versions

Comparing version 2.0.4 to 2.1.1

.eslintrc.js

6

__tests__/Collection.spec.js

@@ -1,2 +0,2 @@

import { Collection, apiClient } from '../src'
import { Collection, apiClient, Request } from '../src'
import MockApi from './mocks/api'

@@ -64,3 +64,3 @@

it('returns false if the label does not match', () => {
collection.request = { label: 'creating' }
collection.request = new Request('creating', null, 0)
expect(collection.isRequest('fetching')).toBe(false)

@@ -70,3 +70,3 @@ })

it('returns true otherwie', () => {
collection.request = { label: 'fetching' }
collection.request = new Request('fetching', null, 0)
expect(collection.isRequest('fetching')).toBe(true)

@@ -73,0 +73,0 @@ })

@@ -1,2 +0,2 @@

import { Collection, Model, apiClient } from '../src'
import { Collection, Model, apiClient, Request } from '../src'
import MockApi from './mocks/api'

@@ -44,2 +44,21 @@

describe('isRequest', () => {
it('returns false if there is no request', () => {
const newModel = new MyModel({})
expect(newModel.isRequest('fetching')).toBe(false)
})
it('return false if the request is something different', () => {
const newModel = new MyModel({})
newModel.request = new Request('creating', null, 0)
expect(newModel.isRequest('fetching')).toBe(false)
})
it('return true if the request is matching', () => {
const newModel = new MyModel({})
newModel.request = new Request('fetching', null, 0)
expect(newModel.isRequest('fetching')).toBe(true)
})
})
describe('isNew', () => {

@@ -46,0 +65,0 @@ it('returns true if it does not have an id', () => {

@@ -263,3 +263,3 @@ 'use strict';

/**
* Sets the models into the collection.
* Sets the resources into the collection.
*

@@ -271,3 +271,3 @@ * You can disable adding, changing or removing.

key: 'set',
value: function set(models) {
value: function set(resources) {
var _this3 = this;

@@ -284,4 +284,4 @@

if (remove) {
var ids = models.map(function (d) {
return d.id;
var ids = resources.map(function (r) {
return r.id;
});

@@ -292,7 +292,7 @@ var toRemove = (0, _lodash.difference)(this._ids(), ids);

models.forEach(function (attributes) {
var model = _this3.get(attributes.id);
resources.forEach(function (resource) {
var model = _this3.get(resource.id);
if (model && change) model.set(attributes);
if (!model && add) _this3.add([attributes]);
if (model && change) model.set(resource);
if (!model && add) _this3.add([resource]);
});

@@ -352,3 +352,5 @@ }

}, 300);
_apiClient$post = (0, _apiClient2.default)().post(this.url(), attributes, { onProgress: onProgress }), abort = _apiClient$post.abort, promise = _apiClient$post.promise;
_apiClient$post = (0, _apiClient2.default)().post(this.url(), attributes, {
onProgress: onProgress
}), abort = _apiClient$post.abort, promise = _apiClient$post.promise;

@@ -355,0 +357,0 @@

@@ -6,3 +6,3 @@ 'use strict';

});
exports.apiClient = exports.Model = exports.Collection = undefined;
exports.Request = exports.apiClient = exports.Model = exports.Collection = undefined;

@@ -17,2 +17,6 @@ var _Collection = require('./Collection');

var _Request = require('./Request');
var _Request2 = _interopRequireDefault(_Request);
var _apiClient = require('./apiClient');

@@ -26,2 +30,3 @@

exports.Model = _Model2.default;
exports.apiClient = _apiClient2.default;
exports.apiClient = _apiClient2.default;
exports.Request = _Request2.default;

@@ -110,4 +110,4 @@ 'use strict';

/**
* Return the base url used in
* the `url` method
* Determine what attribute do you use
* as a primary id
*

@@ -119,2 +119,10 @@ * @abstract

key: 'urlRoot',
/**
* Return the base url used in
* the `url` method
*
* @abstract
*/
value: function urlRoot() {

@@ -146,3 +154,3 @@ throw new Error('`url` method not implemented');

} else {
return urlRoot + '/' + this.get('id');
return urlRoot + '/' + this.get(this.primaryKey);
}

@@ -152,6 +160,19 @@ }

/**
* Questions whether the request exists
* and matches a certain label
*/
}, {
key: 'isRequest',
value: function isRequest(label) {
if (!this.request) return false;
return this.request.label === label;
}
/**
* Wether the resource is new or not
*
* We determine this asking if it contains
* the `id` attribute (set by the server).
* the `primaryKey` attribute (set by the server).
*/

@@ -276,3 +297,3 @@

*
* If the item has an `id` it updates it,
* If the item has a `primaryKey` it updates it,
* otherwise it creates the new resource.

@@ -303,3 +324,3 @@ *

case 0:
if (this.has('id')) {
if (this.has(this.primaryKey)) {
_context2.next = 7;

@@ -424,3 +445,5 @@ break;

}, 300);
_apiClient$post = (0, _apiClient2.default)().post(this.url(), attributes, { onProgress: onProgress }), abort = _apiClient$post.abort, promise = _apiClient$post.promise;
_apiClient$post = (0, _apiClient2.default)().post(this.url(), attributes, {
onProgress: onProgress
}), abort = _apiClient$post.abort, promise = _apiClient$post.promise;

@@ -499,3 +522,3 @@

case 0:
if (!(!this.has('id') && this.collection)) {
if (!(!this.has(this.primaryKey) && this.collection)) {
_context4.next = 3;

@@ -634,5 +657,10 @@ break;

}, {
key: 'primaryKey',
get: function get() {
return 'id';
}
}, {
key: 'isNew',
get: function get() {
return !this.has('id');
return !this.has(this.primaryKey);
}

@@ -642,3 +670,3 @@ }, {

get: function get() {
return this.has('id') ? this.get('id') : this.optimisticId;
return this.has(this.primaryKey) ? this.get(this.primaryKey) : this.optimisticId;
}

@@ -645,0 +673,0 @@

{
"name": "mobx-rest",
"version": "2.0.4",
"version": "2.1.1",
"description": "REST conventions for mobx.",

@@ -11,7 +11,18 @@ "repository": {

"jest": {
"testRegex": "/__tests__/.*\\.spec\\.js$"
"collectCoverage": true,
"testRegex": "/__tests__/.*\\.spec\\.js$",
"collectCoverageFrom": [
"src/**/*.js"
]
},
"standard": {
"parser": "babel-eslint",
"globals": [ "it", "describe", "beforeEach", "expect", "Class", "jest" ]
"globals": [
"it",
"describe",
"beforeEach",
"expect",
"Class",
"jest"
]
},

@@ -27,2 +38,3 @@ "dependencies": {

"babel-jest": "^19.0.0",
"babel-plugin-transform-async-to-generator": "^6.22.0",
"babel-plugin-transform-decorators-legacy": "^1.3.4",

@@ -34,16 +46,34 @@ "babel-plugin-transform-flow-strip-types": "^6.22.0",

"babel-register": "^6.23.0",
"eslint": "^3.18.0",
"eslint-config-standard": "^7.1.0",
"eslint-plugin-flowtype": "2.30.3",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-node": "^4.2.1",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^2.1.1",
"flow-bin": "^0.41.0",
"husky": "^0.13.2",
"jest": "^19.0.2",
"snazzy": "^6.0.0",
"standard": "^9.0.1"
"lint-staged": "^3.4.0",
"prettier-standard": "^1.0.6"
},
"main": "lib",
"scripts": {
"compile": "./node_modules/.bin/babel src --out-dir lib",
"compile": "babel src --out-dir lib",
"prepublish": "npm run compile",
"jest": "BABEL_ENV=test NODE_PATH=src jest --no-cache",
"lint": "standard --verbose | snazzy",
"lint": "eslint src __tests__",
"flow": "flow",
"test": "npm run flow && npm run lint && npm run jest"
"test": "npm run flow && npm run lint && npm run jest",
"format": "prettier-standard --print-width 60 'src/**/*.js'",
"prepush": "npm test",
"lint-staged": {
"linters": {
"src/**/*.js": [
"prettier-standard",
"git add"
]
}
}
}
}

@@ -1,2 +0,2 @@

# REST Mobx
# mobx-rest

@@ -6,2 +6,3 @@ REST conventions for mobx.

[![Build Status](https://travis-ci.org/masylum/mobx-rest.svg?branch=master)](https://travis-ci.org/masylum/mobx-rest)
[![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](http://standardjs.com)

@@ -18,11 +19,383 @@ ![](https://media.giphy.com/media/b9QBHfcNpvqDK/giphy.gif)

MobX is great to represent RESTful resources. Each resource can be represented
with a store which will the expected REST actions (`create`, `fetch`, `save`, `destroy`, ...).
An application state is usually divided into three realms:
Instead of writing hundreds of boilerplate lines we can leverage REST conventions
to deal with your API interactions.
- **Component state**: Each state can have their own state, like a button
being pressed, a text input value, etc.
- **Application state**: Sometimes we need components to share state between them and
they are too far away to actually make them talk each other through props.
- **Resources state**: Other times, state is persisted in the server. We syncronize
that state through APIs that consume *resources*. One way to syncronize this state
is through REST.
## Example
MobX is an excellent state management choice to deal with those three realms:
It allows you to represent your state as a graph while other solutions,
like Redux for instance, force you to represent your state as a tree.
With `mobx-rest` resources are implemented with all their REST
actions built in (`create`, `fetch`, `save`, `destroy`, ...) so instead
of writing, over and over, hundreds of lines of boilerplate we can leverage
REST conventions to minimize the code needed for your API interactions.
## Documentation
`mobx-rest` is very simple and its source code can be read in 5 minutes.
### `Model`
A `Model` represents one resource. It's identified by a primary key (mandatory) and holds
its attributes. You can create, update and destroy models in the client and then sync
them with the server. A part from its attributes, a `Model` also holds the state of
the interactions with the server so you can react to those easily (showing loading states
for instance).
#### `attributes: ObservableMap`
An `ObservableMap` that holds the attributes of the model.
#### `collection: ?Collection`
A pointer to a `Collection`. By having models
"belong to" a collection you can take the most out
of `mobx-rest`.
#### `request`
A `Request` object that represents the state of the ongoing request, if any.
#### `error`
An `Error` object that represents the state of the failed request, if any.
#### `constructor(attributes: Object)`
Initialize the model with the given attributes.
#### `toJS(): Object`
Return the object version of the attributes.
#### `primaryKey: string`
Implement this abstract method so `mobx-rest` knows what to
use as a primary key. It defaults to `'id'` but if you use
something like mongodb you can change it to `'_id'`.
#### `urlRoot(): string`
Implement this abstract method so `mobx-rest` knows where
its API points to. If the model belongs to a `Collection`
(setting the `collection` attribute) this method does
not need to be implemented.
#### `url(): string`
Return the url for that given resource. Will leverage the
collection's base url (if any) or `urlRoot`. It uses the
primary id since that's REST convention.
Example: `tasks.get(34).url() // => "/tasks/34"`
#### `isRequest(label: string): boolean`
Helper method that asks the model whether there is an ongoing
request with the given label.
Example: `file.isRequest('saving')`
#### `isNew: boolean`
Return whether that model has been syncronized with the server or not.
Resources created in the client side (optimistically) don't have
and `id` attribute yet (that's given by the server)
Example:
```js
const user = new User({ name : 'Pau' })
user.isNew // => true
user.save()
user.isNew // => false
user.get('id') // => 1
```
#### `get(attribute: string): any`
Get the given attribute. If the attribute does not exist, it will throw.
#### `has(attribute: string): boolean`
Check that the given attribute exists.
#### `set(data: Object): void`
Update the attributes in the client.
Example:
```js
const folder = new Folder({ name : 'Trash' })
folder.get('name') // => 'Trash'
folder.set({ name: 'Rubbish' })
folder.get('name') // => 'Rubbish'
```
#### `fetch(options): Promise`
Request this resource's data from the server. It tracks the state
of the request using the label `fetching` and updates the resource when
the data is back from the API.
Example:
```js
const task = new Task({ id: 3 })
const promise = task.fetch()
task.isRequest('fetching') // => true
await promise
task.get('name') // => 'Do the laundry'
```
#### `save(attributes: Object, options: Object): Promise`
The opposite of `fetch`. It takes the resource from the client and
persists it in the server through the API. It accepts some attributes
as the first argument so you can use it as a `set` + `save`.
It tracks the state of the request using the label `saving`.
Options:
- `optimistic = true` Whether we want to update the resource in the client
first or wait for the server's response.
- `patch = true` Whether we want to use the `PATCH` verb and semantics, sending
only the changed attributes instead of the whole resource down the wire.
Example:
```js
const company = new Company({ name: 'Teambox' })
const promise = company.save({ name: 'Redbooth' }, { optimstic: false })
company.isRequest('saving') // => true
company.get('name') // => 'Teambox'
await promise
company.get('name') // => 'Redbooth'
```
#### `destroy(options: Object): Promise`
Tells the API to destroy this resource.
Options:
- `optimistic = true` Whether we want to delete the resource in the client
first or wait for the server's response.
#### `rpc(method: 'string', body: {}): Promise`
When dealing with REST there are always cases when we have some actions beyond
the conventions. Those are represented as `rpc` calls and are not opinionated.
Example:
```js
const response = await task.rpc('resolveSubtasks', { all: true })
if (response.ok) {
task.subTasks.fetch()
}
```
### `Collection`
A `Collection` represents a group of resources. Each element of a `Collection` is a `Model`.
Likewise, a collection tracks also the state of the interactions with the server so you
can react accordingly.
#### `models: ObservableArray`
An `ObservableArray` that holds the collection of models.
#### `request: ?Request`
A `Request` object that represents the state of the ongoing request, if any.
#### `error: ?ErrorObject`
An `Error` object that represents the state of the failed request, if any.
#### `constructor(data: Array<Object>)`
Initializes the collection with the given resources.
#### `url(): string`
Abstract method that must be implemented if you want your collection
and it's models to be able to interact with the API.
#### `model(): Model`
Abstract method that tells which kind of `Model` objects this collection
holds. This is used, for instance, when doing a `collection.create` so
we know which object to instantiate.
#### `toJS(): Array<Object>`
Return a plain data structure representing the collection of resources
without all the observable layer.
#### `toArray(): Array<ObservableMap>`
Return an array with the observable resources.
#### `isRequest(label: string): boolean`
Helper method that asks the collection whether there is an ongoing
request with the given label.
Example:
```js
filesCollection.isRequest('saving')
```
#### `isEmpty(): boolean`
Helper method that asks the collection whether there is any
model in it.
Example:
```js
const promise = usersCollection.fetch()
usersCollection.isEmpty() // => true
await promise
usersCollection.isEmpty() // => false
usersCollection.models.length // => 10
```
#### `at(index: number): ?Model`
Find a model at the given position.
#### `get(id: number): ?Model`
Find a model (or not) with the given id.
#### `filter(query: Object): Array<Model>`
Helper method that filters the collection by the given conditions represented
as a key value.
Example:
```js
const resolvedTasks = tasksCollection.filter({ resolved: true })
resolvedTasks.length // => 3
```
#### `find(query: Object): ?Model`
Same as `filter` but it will halt and return when the first model matches
the conditions.
Example:
```js
const pau = usersCollection.find({ name: 'pau' })
pau.get('name') // => 'pau'
```
#### `add(data: Object): Array<Model>`
Add a model with the given attributes.
#### `remove(ids: Array<number>): void`
Remove any model with the given ids.
Example:
```js
usersCollection.remove([1, 2, 3])
```
#### `set(models: Array<Object>, options: Object): void`
Merge the given models smartly the current ones in the collection.
It detects what to add, remove and change.
Options:
- `add = true` Change to disable adding models
- `change = true` Change to disable updating models
- `remove = true` Change to disable removing models
```js
const companiesCollection = new CompaniesCollection([
{ id: 1, name: 'Teambox' }
{ id: 3, name: 'Zpeaker' }
])
companiesCollection.set([
{ id: 1, name: 'Redbooth' },
{ id: 2, name: 'Factorial' }
])
companiesCollection.get(1).get('name') // => 'Redbooth'
companiesCollection.get(2).get('name') // => 'Factorial'
companiesCollection.get(3) // => null
```
#### `build(attributes: Object): Model`
Instantiates and links a model to the current collection.
```js
const factorial = companiesCollection.build({ name: 'Factorial' })
factorial.collection === companiesCollection // => true
factorial.get('name') // 'Factorial'
```
#### `create(target: Object | Model, options: Object)`
Add and save to the server the given model. If attributes are given,
also it builds the model for you. It tracks the state of the request
using the label `creating`.
Options:
- `optimistic = true` Whether we want to create the resource in the client
first or wait for the server's response.
```js
const promise = tasksCollection.create({ name: 'Do laundry' })
tasksCollection.isRequest('creating') // => true
await promise
tasksCollection.at(0).get('name') // => 'Do laundry'
```
#### `fetch(options: Object)`
Fetch the date from the server and then calls `set` to update the current
models. Accepts any option from the `set` method.
```js
const promise = tasksCollection.fetch()
tasksCollection.isEmpty() // => true
tasksCollection.isRequest('fetching') // => true
await promise
tasksCollection.isEmpty() // => false
```
#### `rpc(method: 'string', body: {}): Promise`
Exactly the same as the model one, but at the collection level.
### `apiClient`
This is the object that is going to make the `xhr` requests to interact with your API.
There is an example implementation for jQuery in the `mobx-rest-jquery-adapter` package.
## Full Example
A collection looks like this:
```js
// TasksCollection.js
const apiPath = '/api'

@@ -32,3 +405,3 @@ import jqueryAdapter from 'mobx-rest-jquery-adapter'

// Set the adapter
// We will use the jQuery adapter to make the `xhr` calls
apiClient(jqueryAdapter, { apiPath })

@@ -38,31 +411,58 @@

class Tasks extends Collection {
url () {
return `/tasks`
}
model () {
return Task
}
url () { return `/tasks` }
model () { return Task }
}
const tasks = new Tasks()
// We instantiate the collection and export it as a singleton
export default new Tasks()
```
And here an example of how to use React with it:
```js
import tasksCollection from './TasksCollection'
import { computed } from 'mobx'
import { observer } from 'mobx-react'
@observer
class Companies extends React.Component {
class Task extends React.Component {
onClick () {
this.props.task.save({ resolved: true })
}
render () {
return (
<li key={task.id}>
<button onClick={this.onClick.bind(this)}>
resolve
</button>
{this.props.task.get('name')}
</li>
)
}
}
@observer
class Tasks extends React.Component {
componentWillMount () {
tasks.fetch()
// This will call `/api/tasks?all=true`
tasksCollection.fetch({ data: { all: true } })
}
renderTask (task, i) {
return <li key={i}><Task task={task} /></li>
@computed
get activeTasks () {
return tasksCollection.filter({ resolved: false })
}
render () {
if (tasks.isRequest('fetching')) {
if (tasksCollection.isRequest('fetching')) {
return <span>Fetching tasks...</span>
}
return <ul>{tasks.models.map(this.renderTask.bind(this))}</ul>
return (
<div>
<span>{this.activeTasks.length} tasks</span>
<ul>{activeTasks.map((task) => <Task task={task} />)}</ul>
</div>
)
}

@@ -73,28 +473,17 @@ }

## Tree schema
## State shape
Your tree will have the following schema:
Your collections and models will have the following state shape:
### Collection
```js
models: [
{ // Information at the resource level
optimisticId: String, // Client side id. Used for optimistic updates
request: { // An ongoing request
label: String, // Examples: 'updating', 'creating', 'fetching', 'destroying' ...
abort: Function, // A method to abort the ongoing request
},
error: { // A failed request
label: String, // Examples: 'updating', 'creating', 'fetching', 'destroying' ...
body: String, // A string representing the error
},
attributes: Object // The resource attributes
}
] // Information at the collection level
models: Array<Model> // This is where the models live
request: { // An ongoing request
label: String, // Examples: 'updating', 'creating', 'fetching', 'destroying' ...
abort: Function, // A method to abort the ongoing request
label: string, // Examples: 'updating', 'creating', 'fetching', 'destroying' ...
abort: () => void, // A method to abort the ongoing request
progress: number // If uploading a file, represents the progress
},
error: { // A failed request
label: String, // Examples: 'updating', 'creating', 'fetching', 'destroying' ...
label: string, // Examples: 'updating', 'creating', 'fetching', 'destroying' ...
body: Object, // A string representing the error

@@ -104,2 +493,44 @@ }

### Model
```js
attributes: Object // The resource attributes
optimisticId: string, // Client side id. Used for optimistic updates
request: { // An ongoing request
label: string, // Examples: 'updating', 'creating', 'fetching', 'destroying' ...
abort: () => void, // A method to abort the ongoing request
},
error: { // A failed request
label: string, // Examples: 'updating', 'creating', 'fetching', 'destroying' ...
body: string, // A string representing the error
},
```
## FAQ
### How do I create relations between the models?
This is something that mobx makes really easy to achieve:
```js
import usersCollection from './UsersCollections'
import { computed } from 'mobx'
class Task extends Model {
@computed
author () {
const userId = this.get('userId')
return usersCollection.get(userId) ||
usersCollection.nullObject()
}
}
```
I recommend to always fallback with a null object which will facilitate
a ton to write code like `task.author.get('name')`.
## Where is it used?
Developed and battle tested in production in [Factorial](https://factorialhr.com)
## License

@@ -106,0 +537,0 @@

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc