Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adapterOptions #510

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 18 additions & 3 deletions addon/utils/build-url.ts
Expand Up @@ -48,6 +48,7 @@ export function buildOperationUrl<M extends Model>(
record: M,
opPath: string,
urlType: EmberDataRequestType,
adapterOptions: any,
instance = true
) {
const modelClass = _getModelClass(record);
Expand All @@ -56,17 +57,31 @@ export function buildOperationUrl<M extends Model>(
const adapter = store.adapterFor(modelName);
const path = opPath;
const snapshot = snapshotFromRecord(record);
snapshot.adapterOptions = adapterOptions;
const baseUrl = adapter.buildURL(modelName, instance ? record.get('id') : null, snapshot, urlType);

if (!path) {
return baseUrl;
}

if (baseUrl.charAt(baseUrl.length - 1) === '/') {
return `${baseUrl}${path}`;
let url;
const [baseUrlNoQueries, baseQueries] = baseUrl.split('?');
const [pathNoQueries, pathQueries] = path.split('?');

if (baseUrlNoQueries.charAt(baseUrl.length - 1) === '/') {
url = `${baseUrlNoQueries}${pathNoQueries}`;
} else {
return `${baseUrl}/${path}`;
url = `${baseUrlNoQueries}/${pathNoQueries}`;
}

if (baseQueries || pathQueries) {
const baseSearchParams = new URLSearchParams(baseQueries);
const pathSearchParams = new URLSearchParams(pathQueries);
for (const [k, v] of pathSearchParams) { baseSearchParams.append(k, v) };
url = `${url}?${baseSearchParams.toString()}`;
}

return url;
}

export default buildOperationUrl;
8 changes: 6 additions & 2 deletions addon/utils/collection-action.ts
Expand Up @@ -14,15 +14,19 @@ export interface CollectionOperationOptions<IN, OUT> {
}

export default function collectionOp<IN = any, OUT = any>(options: CollectionOperationOptions<IN, OUT>) {
return function runCollectionOp(this: Model, payload: IN): Promise<OUT> {
return function runCollectionOp(
this: Model,
payload: IN,
collectionOptions: any = {}
): Promise<OUT> {
const model: Model = this;
const recordClass = _getModelClass(model);
const modelName = _getModelName(recordClass);
const store = _getStoreFromRecord(model);
const requestType: HTTPVerb = strictifyHttpVerb(options.type || 'put');
const urlType: EmberDataRequestType = options.urlType || 'updateRecord';
const adapter = store.adapterFor(modelName);
const fullUrl = buildOperationUrl(model, options.path, urlType, false);
const fullUrl = buildOperationUrl(model, options.path, urlType, collectionOptions.adapterOptions, false);
const data = (options.before && options.before.call(model, payload)) || payload;
return adapter
.ajax(fullUrl, requestType, assign(options.ajaxOptions || {}, { data }))
Expand Down
8 changes: 6 additions & 2 deletions addon/utils/member-action.ts
Expand Up @@ -14,14 +14,18 @@ export interface InstanceOperationOptions<IN, OUT> {
}

export default function instanceOp<IN = any, OUT = any>(options: InstanceOperationOptions<IN, OUT>) {
return function runInstanceOp(this: Model, payload: IN): Promise<OUT> {
return function runInstanceOp(
this: Model,
payload: IN,
instanceOptions:any = {}
): Promise<OUT> {
const recordClass = _getModelClass(this);
const modelName = _getModelName(recordClass);
const store = _getStoreFromRecord(this);
const { ajaxOptions, path, before, after, type = 'put', urlType = 'updateRecord' } = options;
const requestType: HTTPVerb = strictifyHttpVerb(type);
const adapter = store.adapterFor(modelName);
const fullUrl = buildOperationUrl(this, path, urlType);
const fullUrl = buildOperationUrl(this, path, urlType, instanceOptions.adapterOptions);
const data = (before && before.call(this, payload)) || payload;
return adapter.ajax(fullUrl, requestType, assign(ajaxOptions || {}, { data })).then((response: JSONValue) => {
if (after && !this.isDestroyed) {
Expand Down
66 changes: 66 additions & 0 deletions tests/acceptance/index-test.ts
Expand Up @@ -118,4 +118,70 @@ module('Acceptance | index2', hooks => {

(assert as any).dom(`[data-test-fruit-name="Completely Eaten apple"]`).exists();
});

test('query params', async function(assert) {
await visit('/');
assert.expect(9);

this.server.get('/vegatables/:id/info', (request) => {
assert.equal(request.params.id, '1', 'request made to the right URL');
assert.equal(request.queryParams.vegatableId, '1', 'request made with the right query params');
assert.equal(request.queryParams.vegatable, 'potato', 'request made with the right buildURL query params');
return [200, {}, '{"status": "ok"}'];
});

this.server.get('/vegatables/:id/moreInfo', (request) => {
assert.equal(request.params.id, '2', 'request made to the right URL');
assert.equal(request.queryParams.vegatableId, '2', 'request made with the right query params');
assert.equal(request.queryParams.vegatable, 'carrot', 'request made with the right buildURL query params');
assert.equal(request.queryParams.more, 'true', 'request made with the right path query params');
return [200, {}, '{"status": "ok"}'];
});

this.server.get('/vegatables/allInfo', (request) => {
assert.equal(request.queryParams.vegatable, 'potato', 'request made with the right buildURL query params');
assert.equal(request.queryParams.less, 'false', 'request made with the right path query params');
return [200, {}, '{"status": "ok"}'];
});

await click('#potato .info-instance-button');

await click('#carrot .more-info-instance-button');

await click('.all-vegatables .all-info-button');
});

test('adapterOptions', async function(assert) {
await visit('/');
assert.expect(11);

this.server.get('/vegatables/:id/moreInfo', (request) => {
assert.equal(request.params.id, '1', 'request made to the right URL');
assert.equal(request.queryParams.vegatableId, '1', 'request made with the right query params');
assert.equal(request.queryParams.vegatable, 'potato', 'request made with the right buildURL query params');
assert.equal(request.queryParams.extra, 'false', 'request made with the right adapterOptions query params');
return [200, {}, '{"status": "ok"}'];
});

await click('#potato .options-instance-button');

this.server.get('/vegatables/:id/moreInfo', (request) => {
assert.equal(request.params.id, '2', 'request made to the right URL');
assert.equal(request.queryParams.vegatableId, '2', 'request made with the right query params');
assert.equal(request.queryParams.vegatable, 'carrot', 'request made with the right buildURL query params');
assert.equal(request.queryParams.more, 'true', 'request made with the right path query params');
assert.equal(request.queryParams.extra, 'false', 'request made with the right adapterOptions query params');
return [200, {}, '{"status": "ok"}'];
});

await click('#carrot .options-instance-button');

this.server.get('/vegatables/allInfo', (request) => {
assert.equal(request.queryParams.vegatable, 'potato', 'request made with the right buildURL query params');
assert.equal(request.queryParams.extra, 'false', 'request made with the right adapterOptions query params');
return [200, {}, '{"status": "ok"}'];
});

await click('.all-vegatables .all-info-button');
});
});
16 changes: 16 additions & 0 deletions tests/dummy/app/adapters/vegatable.ts
@@ -0,0 +1,16 @@
import ApplicationAdapter from './application';

export default class VegatableAdapter extends ApplicationAdapter {
public buildURL(modelName: string, id: string|[string]|object, snapshot: object, requestType: string, query: object) {
const urlStr = super.buildURL(modelName, id, snapshot, requestType, query);
const [path, searchStr] = urlStr.split('?');
const searchParams = new URLSearchParams(searchStr);
// for testing buildURL queryParams
searchParams.append(modelName, snapshot.attr('name'));
// for testing adapterOptions
for (const [k, v] of Object.entries(snapshot.adapterOptions || {})) {
searchParams.append(k, v.toString());
}
return `${path}?${searchParams.toString()}`;
}
}
12 changes: 12 additions & 0 deletions tests/dummy/app/controllers/index.js
Expand Up @@ -31,6 +31,18 @@ export default Controller.extend({
},
juiceAllFruit(fruit) {
Fruit.juiceAll({ was_eaten: true });
},
vegatableInfo(vegatable) {
vegatable.info({ vegatableId: vegatable.id });
},
vegatableMoreInfo(vegatable) {
vegatable.moreInfo({ vegatableId: vegatable.id });
},
vegatableOptions(vegatable) {
vegatable.moreInfo({ vegatableId: vegatable.id }, { adapterOptions: { extra: 'false' }});
},
allInfoVegatables(vegatable) {
vegatable.allInfo({}, { adapterOptions: { extra: 'false' }});
}
}
// END-SNIPPET
Expand Down
15 changes: 15 additions & 0 deletions tests/dummy/app/models/vegatable.js
@@ -0,0 +1,15 @@
// BEGIN-SNIPPET vegatable-model
import DS from 'ember-data';
import { collectionAction, memberAction } from 'ember-api-actions';

const { attr, Model } = DS;

const Vegatable = Model.extend({
name: attr('string'),
info: memberAction({ path: 'info', type: 'get' }),
moreInfo: memberAction({ path: 'moreInfo?more=true', type: 'get' }),
allInfo: collectionAction({ path: 'allInfo?less=false', type: 'get' }),
});

export default Vegatable;
// END-SNIPPET
54 changes: 47 additions & 7 deletions tests/dummy/app/routes/index.ts
Expand Up @@ -5,7 +5,7 @@ import Pretender from 'pretender';

const { testing } = Ember;

const LEGACY_PAYLOAD = {
const LEGACY_FRUIT_PAYLOAD = {
fruit: [
{
id: 1,
Expand All @@ -26,7 +26,20 @@ const LEGACY_PAYLOAD = {
]
};

const PAYLOAD = {
const LEGACY_VEGATABLE_PAYLOAD = {
vegatable: [
{
id: 1,
name: 'potato',
},
{
id: 2,
name: 'carrot',
}
]
}

const FRUIT_PAYLOAD = {
data: [
{
type: 'fruit',
Expand Down Expand Up @@ -55,23 +68,50 @@ const PAYLOAD = {
attributes: {
name: 'grape'
}
},
]
};

const VEGATABLE_PAYLOAD = {
data: [
{
type: 'vegatable',
id: 1,
attributes: {
name: 'potato'
}
},
{
type: 'vegatable',
id: 2,
attributes: {
name: 'carrot'
}
}
]
};




export default Route.extend({
server: undefined as any,
requests: [] as any[],
currentModel: undefined as any,
model() {
let arr: any = [];
this.store.pushPayload('fruit', !this.store.peekAll ? LEGACY_PAYLOAD : PAYLOAD);
let fruitArr: any = [];
let vegatableArr: any = [];
this.store.pushPayload('fruit', !this.store.peekAll ? LEGACY_FRUIT_PAYLOAD : FRUIT_PAYLOAD);
this.store.pushPayload('vegatable', !this.store.peekAll ? LEGACY_VEGATABLE_PAYLOAD : VEGATABLE_PAYLOAD);
if (!this.store.peekAll) {
arr = [1, 2, 3, 4].map(id => (this.store as any).getById('fruit', id));
fruitArr = [1, 2, 3, 4].map(id => (this.store as any).getById('fruit', id));
vegatableArr = [1, 2].map(id => (this.store as any).getById('vegatable', id));
} else {
arr = this.store.peekAll('fruit');
fruitArr = this.store.peekAll('fruit');
vegatableArr = this.store.peekAll('vegatable');
}
return A(arr);

return { fruit: A(fruitArr), vegatable: A(vegatableArr) };
},

beforeModel() {
Expand Down
58 changes: 51 additions & 7 deletions tests/dummy/app/templates/index.hbs
@@ -1,14 +1,16 @@
<div class="row">
<div class="col m6 s12">
<h4>API actions on an individual resource</h4>
{{#each model as |fruit|}}

<h5>Fruit</h5>
{{#each model.fruit as |fruit|}}
<p class="fruit-thing" id={{fruit.name}}>
{{#x-btn
class="ripen-instance-button yellow"
click=(action "ripenFruit" fruit)
}}
Ripen
{{/x-btn}}
{{/x-btn}}
{{#x-btn
class="info-instance-button indigo white-text"
click=(action "fruitInfo" fruit)
Expand All @@ -23,34 +25,76 @@
{{#x-btn class="eat-instance-button yellow" click=(action "eatFruit" fruit)}}Eat{{/x-btn}}
</p>
{{/each}}

<h5>Vegatable</h5>
{{#each model.vegatable as |vegatable|}}
<p class="vegatable-thing" id={{vegatable.name}}>
{{#x-btn
class="info-instance-button indigo white-text"
click=(action "vegatableInfo" vegatable)
}}
Info
{{/x-btn}}
</p>

<p class="vegatable-thing" id={{vegatable.name}}>
{{#x-btn
class="more-info-instance-button yellow white-text"
click=(action "vegatableMoreInfo" vegatable)
}}
More Info
{{/x-btn}}
</p>

<p class="vegatable-thing" id={{vegatable.name}}>
{{#x-btn
class="options-instance-button yellow white-text"
click=(action "vegatableOptions" vegatable)
}}
Options
{{/x-btn}}
</p>
{{/each}}

<h4>API action on a collection of resources</h4>
<p class="all-vegatables">
{{content.vegatable.constructor.modelName}}
{{#x-btn
class="all-info-button indigo white-text"
click=(action "allInfoVegatables"
(first-in-array (or content.vegatable model.vegatable)))
}}
All Info
{{/x-btn}}
</p>

<p class="all-fruit">
{{content.constructor.modelName}}
{{content.fruit.constructor.modelName}}
{{#x-btn
class="ripen-type-button yellow"
click=(action "ripenAllFruit" (first-in-array (or content model)))
click=(action "ripenAllFruit" (first-in-array (or content.fruit model.fruit)))
}}
Ripen All
{{/x-btn}}
{{#x-btn
class="fresh-type-button indigo white-text"
click=(action "getAllFreshFruit"
(first-in-array (or content model)))
(first-in-array (or content.fruit model.fruit)))
}}
Get Fresh
{{/x-btn}}
{{#x-btn
class="fresh-type-button indigo white-text"
click=(action "juiceAllFruit"
(first-in-array (or content model)))
(first-in-array (or content.fruit model.fruit)))
}}
Juice Everything
{{/x-btn}}

{{#x-btn
class="eat-all-button indigo white-text"
click=(action "eatAll"
(first-in-array (or content model)))
(first-in-array (or content.fruit model.fruit)))
}}
Eat all
{{/x-btn}}
Expand Down