Skip to content

Commit

Permalink
feat: support bulk create
Browse files Browse the repository at this point in the history
  • Loading branch information
alexlafroscia committed Mar 19, 2020
1 parent 2dab199 commit df4f143
Show file tree
Hide file tree
Showing 14 changed files with 574 additions and 18 deletions.
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
ember-data-json-api-bulk-ext
==============================================================================
# ember-data-json-api-bulk-ext

![.github/workflows/verify.yml](https://github.com/movableink/ember-data-json-api-bulk-ext/workflows/.github/workflows/verify.yml/badge.svg)

[Short description of the addon.]
==============================================================================

Decorator to add support to Ember Data for the [JSON:API Bulk Extension](https://github.com/json-api/json-api/blob/9c7a03dbc37f80f6ca81b16d444c960e96dd7a57/extensions/bulk/index.md).

Compatibility
------------------------------------------------------------------------------
Expand All @@ -19,12 +21,37 @@ Installation
ember install ember-data-json-api-bulk-ext
```

If you have not yet created a subclass of the Ember Data Store, do so now. You will want to import and apply the decorator to this class.

```javascript
// app/services/store.js
import Store from '@ember-data/store';
import { withBulkActions } from 'ember-data-json-api-bulk-ext';

@withBulkActions
export default class CustomStore extends Store {}
```

Usage
------------------------------------------------------------------------------

[Longer description of how to use the addon in apps.]
With the decorator applied to your Store subclass, you'll have new methods on the store available to you for dealing with bulk API actions.

```javascript
const first = this.store.createRecord('post', { title: 'First Post' });
const second = this.store.createRecord('post', { title: 'Second Post' });

await this.store.bulkCreate([first, second]);

assert.ok(first.id, 'First record was given an ID');
assert.ok(second.id, 'First record was given an ID');
```

Note the following limitations:

* The models being operated on _must_ use the `JSONAPIAdapter` and `JSONAPISerializer`
* All records must be of the same type (for now)
* Records can only be created in bulk (for now)

Contributing
------------------------------------------------------------------------------
Expand Down
42 changes: 42 additions & 0 deletions addon/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { assert } from '@ember/debug';

export function withBulkActions(StoreClass) {
return class StoreWithBulkActions extends StoreClass {
async bulkCreate(records) {
assert(
'All records must be new',
records.every(record => record.isNew)
);

const serializedRecords = records.map(record => record.serialize());

assert(
'All records must be the same type',
serializedRecords.every(record => record.data.type === serializedRecords[0].data.type)
);

const { modelName } = records[0]._internalModel.createSnapshot();
const adapter = this.adapterFor(modelName);

const url = adapter.urlForCreateRecord(modelName);
const payload = {
data: serializedRecords.map(record => record.data)
};

records.forEach(record => {
record._internalModel.adapterWillCommit();
});

const response = await adapter.ajax(url, 'POST', { data: payload });
const responseData = response.data;

records.forEach((record, index) => {
const { data } = this.normalize(modelName, responseData[index]);

this.didSaveRecord(record._internalModel, { data }, 'createRecord');
});

return records;
}
};
}
8 changes: 7 additions & 1 deletion ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ const EmberAddon = require('ember-cli/lib/broccoli/ember-addon');

module.exports = function(defaults) {
let app = new EmberAddon(defaults, {
// Add options here
autoImport: {
webpack: {
node: {
global: true
}
}
}
});

/*
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"ember-cli-inject-live-reload": "^2.0.2",
"ember-cli-sri": "^2.1.1",
"ember-cli-uglify": "^3.0.0",
"ember-data": "^3.17.0",
"ember-disable-prototype-extensions": "^1.1.3",
"ember-export-application-global": "^2.0.1",
"ember-load-initializers": "^2.1.1",
Expand All @@ -54,8 +55,12 @@
"eslint-plugin-ember": "^7.10.1",
"eslint-plugin-node": "^11.0.0",
"loader.js": "^4.7.0",
"lodash-es": "^4.17.15",
"npm-run-all": "^4.1.5",
"qunit-dom": "^1.1.0"
"pretender": "^3.3.1",
"qunit-dom": "^1.1.0",
"testdouble": "^3.13.1",
"testdouble-qunit": "^2.1.1"
},
"engines": {
"node": "10.* || >= 12"
Expand Down
5 changes: 5 additions & 0 deletions tests/assertions/verify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import td from 'testdouble';
import QUnit from 'qunit';
import installAssertion from 'testdouble-qunit';

installAssertion(QUnit, td);
3 changes: 3 additions & 0 deletions tests/dummy/app/adapters/application.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import JSONAPIAdapter from '@ember-data/adapter/json-api';

export default class ApplicationAdapter extends JSONAPIAdapter {}
5 changes: 5 additions & 0 deletions tests/dummy/app/models/post.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Model, { attr } from '@ember-data/model';

export default class PostModel extends Model {
@attr('string') title;
}
3 changes: 3 additions & 0 deletions tests/dummy/app/serializers/application.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import JSONAPISerializer from '@ember-data/serializer/json-api';

export default class ApplicationSerializer extends JSONAPISerializer {}
5 changes: 5 additions & 0 deletions tests/dummy/app/services/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Store from '@ember-data/store';
import { withBulkActions } from 'ember-data-json-api-bulk-ext';

@withBulkActions
export default class CustomStore extends Store {}
11 changes: 11 additions & 0 deletions tests/helpers/setup-pretender.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Pretender from 'pretender';

export default function setupPretender(hooks) {
hooks.beforeEach(function() {
this.server = new Pretender();
});

hooks.afterEach(function() {
this.server.shutdown();
});
}
11 changes: 11 additions & 0 deletions tests/matchers/pretender.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import td from 'testdouble';
import { isEqual } from 'lodash-es';

export const payload = td.matchers.create({
name: 'payload',
matches([payload], { requestBody }) {
const body = typeof requestBody === 'string' ? JSON.parse(requestBody) : requestBody;

return isEqual(payload, body);
}
});
2 changes: 2 additions & 0 deletions tests/test-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import config from '../config/environment';
import { setApplication } from '@ember/test-helpers';
import { start } from 'ember-qunit';

import './assertions/verify';

setApplication(Application.create(config.APP));

start();
92 changes: 92 additions & 0 deletions tests/unit/services/store-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import td from 'testdouble';
import setupPretender from '../../helpers/setup-pretender';
import { payload as payloadMatches } from '../../matchers/pretender';

module('Unit | Service | store', function(hooks) {
setupTest(hooks);
setupPretender(hooks);

hooks.beforeEach(function() {
this.store = this.owner.lookup('service:store');

this.postsHandler = td.function();
this.server.post('/posts', this.postsHandler);
});

test('it does not interfere with normal creation', async function(assert) {
td.when(
this.postsHandler(
payloadMatches({ data: { type: 'posts', attributes: { title: 'First Post' } } })
)
).thenReturn([
201,
{},
JSON.stringify({
data: {
type: 'posts',
id: 1,
attributes: {
title: 'First Post'
}
}
})
]);

const first = this.store.createRecord('post', { title: 'First Post' });

await first.save();

assert.equal(first.id, 1, 'Recieved an ID from the API');
});

test('it can create multiple models at once', async function(assert) {
td.when(
this.postsHandler(
payloadMatches({
data: [
{ type: 'posts', attributes: { title: 'First Post' } },
{ type: 'posts', attributes: { title: 'Second Post' } }
]
})
)
).thenReturn([
201,
{},
JSON.stringify({
data: [
{
type: 'posts',
id: 1,
attributes: {
title: 'First Post'
}
},
{
type: 'posts',
id: 2,
attributes: {
title: 'Second Post'
}
}
]
})
]);

const first = this.store.createRecord('post', { title: 'First Post' });
const second = this.store.createRecord('post', { title: 'Second Post' });

const result = await this.store.bulkCreate([first, second]);

assert.equal(first.id, 1, 'The first record is updated with an ID');
assert.equal(second.id, 2, 'The second record is updated with an ID');
assert.deepEqual(result, [first, second], 'Returns the created records');

assert.equal(
this.store.peekAll('post').length,
2,
'It does not add additional records to the Ember Data store'
);
});
});
Loading

0 comments on commit df4f143

Please sign in to comment.