Skip to content

Commit

Permalink
Merge pull request #4 from movableink/use-ext-mime-type
Browse files Browse the repository at this point in the history
Add option to use extension's MIME type
  • Loading branch information
alexlafroscia authored Mar 20, 2020
2 parents 94b7f75 + 68beaa7 commit 524fac1
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 72 deletions.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ If you have not yet created a subclass of the Ember Data Store, do so now. You w
import Store from '@ember-data/store';
import { withBulkActions } from 'ember-data-json-api-bulk-ext';

@withBulkActions
export default class CustomStore extends Store {}
@withBulkActions()
class CustomStore extends Store {}

export default CustomStore;
```

## Usage
Expand All @@ -47,6 +49,15 @@ Note the following limitations:
- All records must be of the same type (for now)
- Records can only be created in bulk (for now)

### Using the extension MIME type

The bulk extension for JSON:API describes a custom MIME type for your requests. To override the default JSON:API MIME type and use the one from the extension, pass the following option to the `withBulkActions` decorator:

```javascript
@withBulkActions({ useExtensionMimeType: true })
class CustomStore extends Store {
```
## Contributing
See the [Contributing](CONTRIBUTING.md) guide for details.
Expand Down
1 change: 1 addition & 0 deletions addon/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MIME_TYPE = 'application/vnd.api+json; ext=bulk';
31 changes: 29 additions & 2 deletions addon/with-bulk-actions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Store from '@ember-data/store';
import { assert } from '@ember/debug';
import { MIME_TYPE } from './constants';

export function withBulkActions(StoreClass) {
function extendStore(StoreClass, { useExtensionMimeType = false } = {}) {
return class StoreWithBulkActions extends StoreClass {
async bulkCreate(records) {
assert(
Expand All @@ -27,7 +29,16 @@ export function withBulkActions(StoreClass) {
record._internalModel.adapterWillCommit();
});

const response = await adapter.ajax(url, 'POST', { data: payload });
const requestOptions = useExtensionMimeType
? {
headers: {
Accept: MIME_TYPE
},
contentType: MIME_TYPE
}
: {};

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

records.forEach((record, index) => {
Expand All @@ -40,3 +51,19 @@ export function withBulkActions(StoreClass) {
}
};
}

export function withBulkActions(arg) {
// Decorator is not called as a function
if (arg && arg.prototype instanceof Store) {
return extendStore(arg);
}

return function(StoreClass) {
assert(
'Decorator must be applied to the Ember Data Store',
StoreClass.prototype instanceof Store
);

return extendStore(StoreClass, arg);
};
}
5 changes: 0 additions & 5 deletions tests/dummy/app/services/store.js

This file was deleted.

13 changes: 12 additions & 1 deletion tests/matchers/pretender.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import td from 'testdouble';
import { isEqual } from 'lodash-es';
import { isEqual, mapKeys } from 'lodash-es';

function normalizedHeaders(headers) {
return mapKeys(headers, (value, key) => key.toLowerCase());
}

export const payload = td.matchers.create({
name: 'payload',
Expand All @@ -9,3 +13,10 @@ export const payload = td.matchers.create({
return isEqual(payload, body);
}
});

export const headers = td.matchers.create({
name: 'headers',
matches([headers], { requestHeaders }) {
return isEqual(normalizedHeaders(headers), normalizedHeaders(requestHeaders));
}
});
255 changes: 193 additions & 62 deletions tests/unit/with-bulk-actions-test.js
Original file line number Diff line number Diff line change
@@ -1,92 +1,223 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import td from 'testdouble';
import Store from '@ember-data/store';
import { withBulkActions } from 'ember-data-json-api-bulk-ext';
import { MIME_TYPE } from 'ember-data-json-api-bulk-ext/constants';
import setupPretender from '../helpers/setup-pretender';
import { payload as payloadMatches } from '../matchers/pretender';
import { headers as headersMatch, payload as payloadMatches } from '../matchers/pretender';

module('Unit | withBulkActions', function(hooks) {
setupTest(hooks);
setupPretender(hooks);
@withBulkActions()
class StoreWithoutHeaderOptions extends Store {}

@withBulkActions({ useExtensionMimeType: true })
class StoreWithHeaderOptions extends Store {}

function setupStore(hooks, StoreClass = StoreWithoutHeaderOptions) {
hooks.beforeEach(function() {
this.owner.unregister('service:store');
this.owner.register('service:store', StoreClass);

this.store = this.owner.lookup('service:store');
});
}

function setupPostHandler(hooks) {
setupPretender(hooks);

hooks.beforeEach(function() {
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'
}
}
})
]);
module('Unit | withBulkActions', function(hooks) {
setupTest(hooks);

const first = this.store.createRecord('post', { title: 'First Post' });
module('mime type options', function() {
module('when enabled', function(hooks) {
setupStore(hooks, StoreWithHeaderOptions);
setupPostHandler(hooks);

await first.save();
test('it sends the headers to the API', async function(assert) {
td.when(
this.postsHandler(headersMatch({ Accept: MIME_TYPE, 'Content-Type': MIME_TYPE }))
).thenReturn([
201,
{},
JSON.stringify({
data: [
{
type: 'posts',
id: 1,
attributes: {
title: 'My Post'
}
}
]
})
]);

assert.equal(first.id, 1, 'Recieved an ID from the API');
const record = this.store.createRecord('post', { title: 'My Post' });

await this.store.bulkCreate([record]);

assert.equal(record.id, 1, 'Creation was preformed successfully');
});
});

module('when disabled', function(hooks) {
setupStore(hooks, StoreWithoutHeaderOptions);
setupPostHandler(hooks);

test('it does not send the headers to the API', async function(assert) {
td.when(
this.postsHandler(
headersMatch({
Accept: 'application/vnd.api+json',
'Content-Type': 'application/vnd.api+json'
})
)
).thenReturn([
201,
{},
JSON.stringify({
data: [
{
type: 'posts',
id: 1,
attributes: {
title: 'My Post'
}
}
]
})
]);

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

await this.store.bulkCreate([record]);

assert.equal(record.id, 1, 'Creation was preformed successfully');
});
});
});

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: [
{
module('bulkCreate', function(hooks) {
setupStore(hooks);
setupPostHandler(hooks);

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'
}
},
{
type: 'posts',
id: 2,
attributes: {
title: 'Second 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'
);
});
});

module('using the decorator without calling it as a function', function(hooks) {
@withBulkActions
class CustomStore extends Store {}

setupStore(hooks, CustomStore);

test('the additional methods are injected into the store', function(assert) {
assert.ok(this.store.bulkCreate, 'The extension was applied successfully');
});
});

module('validating the class the decorator is applied to', function() {
test('does not work when applied to a non-Ember Data Store class', function(assert) {
assert.throws(
function() {
@withBulkActions()
class FooBar {} // eslint-disable-line no-unused-vars
},
/Decorator must be applied to the Ember Data Store/,
'Asserts that the class applied to is the Ember Data Store'
);
});

test('works with a subclass of the Ember Data Store', function(assert) {
class StoreSubclass extends Store {}

const first = this.store.createRecord('post', { title: 'First Post' });
const second = this.store.createRecord('post', { title: 'Second Post' });
@withBulkActions()
class SubclassWithExtension extends StoreSubclass {}

const result = await this.store.bulkCreate([first, second]);
this.owner.unregister('service:store');
this.owner.register('service:store', SubclassWithExtension);

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');
const instance = this.owner.lookup('service:store');

assert.equal(
this.store.peekAll('post').length,
2,
'It does not add additional records to the Ember Data store'
);
assert.ok(instance.bulkCreate, 'Custom subclass was extended');
});
});
});

0 comments on commit 524fac1

Please sign in to comment.