Skip to content

Commit

Permalink
Enable creating a resource without being in a collection
Browse files Browse the repository at this point in the history
  • Loading branch information
masylum committed Feb 13, 2017
1 parent 48bc0a5 commit 8ced2ae
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 30 deletions.
14 changes: 0 additions & 14 deletions __tests__/Collection.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,20 +212,6 @@ describe('Collection', () => {
expect(collection.at(1).get('name')).toBe('bob')
})
})

describe('given a successful non-array response', () => {
beforeEach(() => {
resolve({ name: 'bob' })()
})

it('throws an error', async () => {
collection.fetch().then((result) => {
throw new Error('expected promise to fail but it succeeded')
}, (err) => {
expect(err.message).toBe('expected an array response')
})
})
})
})

describe('set', () => {
Expand Down
103 changes: 98 additions & 5 deletions __tests__/Model.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ class MyCollection extends Collection {
url () {
return '/resources'
}

model () {
return MyModel
}
}

class MyModel extends Model {
url () {
const id = this.has('id') && this.get('id')
if (id) {
return `/resources/${id}`
} else {
return '/resources'
}
}
}

describe('Model', () => {
Expand Down Expand Up @@ -81,10 +96,88 @@ describe('Model', () => {
})

describe('and it does not have a collection', () => {
it('throws an error', () => {
const newModel = new Model()
newModel.save(item).catch((s) => {
expect(s).toBeTruthy()
beforeEach(() => {
model.collection = null
})

describe('if its optimistic (default)', () => {
it('it sets model straight away', () => {
model.save({ name })
expect(model.get('name')).toBe('dylan')
expect(model.get('album')).toBe(item.album)
expect(model.request.label).toBe('creating')
})

describe('when it fails', () => {
beforeEach(reject)

it('sets the error', () => {
return model.save({ name }).catch(() => {
expect(model.error.label).toBe('creating')
expect(model.error.body).toBe(error)
})
})

it('nullifies the request', () => {
return model.save({ name }).catch(() => {
expect(model.request).toBe(null)
})
})
})

describe('when it succeeds', () => {
beforeEach(() => {
resolve({ id: 1, name: 'coltrane' })()
})

it('updates the data from the server', () => {
return model.save({ name }).then(() => {
expect(model.get('name')).toBe('coltrane')
})
})

it('nullifies the request', () => {
return model.save({ name }).then(() => {
expect(model.request).toBe(null)
})
})
})
})

describe('if its pessimistic', () => {
describe('when it fails', () => {
beforeEach(reject)

it('sets the error', () => {
return model.save({ name }, { optimistic: false }).catch(() => {
expect(model.error.label).toBe('creating')
expect(model.error.body).toBe(error)
})
})

it('nullifies the request', () => {
return model.save({ name }).catch(() => {
expect(model.request).toBe(null)
})
})
})

describe('when it succeeds', () => {
beforeEach(() => {
resolve({ id: 2, name: 'dylan' })()
})

it('adds data from the server', () => {
return model.save({ name }, { optimistic: false }).then(() => {
expect(model.get('name')).toBe('dylan')
})
})

it('nullifies the request', () => {
return model.save({ name }).then(() => {
expect(model.request).toBe(null)
})
})
})
})
})
Expand Down Expand Up @@ -153,7 +246,7 @@ describe('Model', () => {
})
})

describe('if its pessimistic (default)', () => {
describe('if its pessimistic', () => {
describe('when it fails', () => {
beforeEach(reject)

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mobx-rest",
"version": "1.0.1",
"version": "1.0.2",
"description": "REST conventions for mobx.",
"repository": {
"type": "git",
Expand Down
13 changes: 11 additions & 2 deletions src/Collection.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
// @flow
import { observable, action, asReference, IObservableArray, runInAction } from 'mobx'
import Model from './Model'
import { isEmpty, filter, isMatch, find, difference, debounce, last } from 'lodash'
import {
isEmpty,
filter,
isMatch,
find,
difference,
debounce,
map,
last
} from 'lodash'
import apiClient from './apiClient'
import type { Label, CreateOptions, ErrorType, Request, SetOptions, Id } from './types'

Expand Down Expand Up @@ -51,7 +60,7 @@ export default class Collection<T: Model> {
* Gets the ids of all the items in the collection
*/
_ids (): Array<Id> {
return this.models.map((item) => item.id)
return map(this.models, (item) => item.id)
.filter(Boolean)
}

Expand Down
77 changes: 69 additions & 8 deletions src/Model.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
// @flow
import { observable, asMap, asFlat, action, asReference, ObservableMap, runInAction } from 'mobx'
import Collection from './Collection'
import { uniqueId, isString } from 'lodash'
import { uniqueId, isString, debounce } from 'lodash'
import apiClient from './apiClient'
import type { OptimisticId, ErrorType, Request, Id, Label, DestroyOptions, SaveOptions } from './types'
import type {
OptimisticId,
ErrorType,
Request,
Id,
Label,
DestroyOptions,
SaveOptions,
CreateOptions
} from './types'

export default class Model {
@observable request: ?Request = null
Expand Down Expand Up @@ -121,26 +130,27 @@ export default class Model {
* otherwise it creates the new resource.
*
* It supports optimistic and patch updates.
*
* TODO: Add progress
*/
@action
async save (
attributes: {},
{ optimistic = true, patch = true }: SaveOptions = {}
): Promise<*> {
const originalAttributes = this.attributes.toJS()
let newAttributes
let data

if (!this.has('id')) {
this.set(Object.assign({}, attributes))
if (this.collection) {
return this.collection.create(this, { optimistic })
} else {
return this._create(attributes, { optimistic })
}

throw new Error('This model does not have a collection defined')
}

let newAttributes
let data
const label: Label = 'updating'
const originalAttributes = this.attributes.toJS()

if (patch) {
newAttributes = Object.assign({}, originalAttributes, attributes)
Expand Down Expand Up @@ -186,6 +196,57 @@ export default class Model {
return response
}

/**
* Internal method that takes care of creating a model that does
* not belong to a collection
*/
async _create (
attributes: {},
{ optimistic = true }: CreateOptions = {}
): Promise<*> {
const label: Label = 'creating'

const onProgress = debounce(function onProgress (progress) {
if (optimistic && this.request) {
this.request.progress = progress
}
}, 300)

const { abort, promise } = apiClient().post(
this.url(),
attributes,
{ onProgress }
)

if (optimistic) {
this.request = {
label,
abort: asReference(abort),
progress: 0
}
}

let data: {}

try {
data = await promise
} catch (body) {
runInAction('create-error', () => {
this.error = { label, body }
this.request = null
})

throw body
}

runInAction('create-done', () => {
this.set(data)
this.request = null
})

return data
}

/**
* Destroys the resurce on the client and
* requests the backend to delete it there
Expand Down
1 change: 1 addition & 0 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type DestroyOptions = {
export type SaveOptions = {
optimistic?: boolean;
patch?: boolean;
onProgress?: () => mixed;
}

export type ErrorType = {
Expand Down

0 comments on commit 8ced2ae

Please sign in to comment.