Skip to content

Commit

Permalink
Fixes #14, Use PATCH by default insteadof PUT
Browse files Browse the repository at this point in the history
  • Loading branch information
BlairJ authored and BlairJ committed Jul 1, 2016
1 parent ea6b036 commit a6f1472
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 36 deletions.
2 changes: 2 additions & 0 deletions scripts/typings/js-data/DSUtil.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
contains: (obj1: Object, obj2: Object) => boolean;
keys: (obj: Object) => any[];

fillIn: <T>(target:T, obj:any) => T;


}
}
14 changes: 13 additions & 1 deletion scripts/typings/js-data/JsonApiAdapter.d.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@

declare module JsonApiAdapter {

export interface DSJsonApiOptions {

// JsonApi does not support PUT semantics, so use PATCH by default
usePATCH?: boolean;

// Do not set globally, used to override the url for a resource
// This is set internally using the self link of objects or relationships
jsonApiPath?: string;
}

export interface DSJsonApiAdapterOptions extends JSData.DSHttpAdapterOptions {
log?: (message?: any, ...optionalParams: any[]) => void;
error?: (message?: any, ...optionalParams: any[]) => void;

// DSHTTPSpecific Options
http?: any;
headers?: any;
method?: string;

// If required pass json api specific options on here
jsonApi?: any,
jsonApi?: DSJsonApiOptions,

// We can pass in an existing adapter rather than creating internally.
// This can work better with js-data-angular
Expand Down
64 changes: 46 additions & 18 deletions src/JsonApiAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class JsonApiAdapter implements JSData.IDSAdapter {
private deserialize: (resourceConfig: JSData.DSResourceDefinition<any>, data: JSData.DSHttpAdapterPromiseResolveType) => any;

constructor(options?: JsonApiAdapter.DSJsonApiAdapterOptions) {
var httpAdapter: typeof DSHttpAdapter = JSDataHttp;

this.DSUtils = JSDataLib['DSUtils'];

//No longer use options
Expand All @@ -45,11 +45,16 @@ export class JsonApiAdapter implements JSData.IDSAdapter {
//}

// Create base adapter
if (options) {
if (options && options.adapter) {
this.adapter = <JSData.DSHttpAdapterExtended>(options.adapter);
} else {
var httpAdapter: typeof DSHttpAdapter = JSDataHttp;
this.adapter = <JSData.DSHttpAdapterExtended>(new httpAdapter(options));
}

this.adapter = this.adapter || <JSData.DSHttpAdapterExtended>(new httpAdapter(options));
// Apply defaults
this.defaults.jsonApi = options.jsonApi || {};
this.DSUtils.fillIn(this.defaults.jsonApi, { usePATCH: true });

// Override default get path implementation
this.adapterGetPath = this.adapter.getPath;
Expand Down Expand Up @@ -141,7 +146,7 @@ export class JsonApiAdapter implements JSData.IDSAdapter {

var jsonApiPath = this.DSUtils.get(options, 'jsonApi.jsonApiPath');
if (jsonApiPath) {
// Discard any additional parameters
// Discard any additional parameters as we have the path recorded from a JsonApi response!
(<any>options).params = {};
} else {

Expand Down Expand Up @@ -178,6 +183,13 @@ export class JsonApiAdapter implements JSData.IDSAdapter {
}
}
}
} else {

////TODO : for updates use self link!!
//var metaData = Helper.MetaData.TryGetMetaData(item);
//if (metaData && metaData.selfLink) {
// jsonApiPath = metaData.selfLink;
//}
}
}

Expand All @@ -200,39 +212,43 @@ export class JsonApiAdapter implements JSData.IDSAdapter {
* @returns {object} options copy of options with serializers configured for jsonapi
* @memberOf JsonApiAdapter
*/
configureSerializers(options: JSData.DSConfiguration): any {
options = this.DSUtils.copy(options) || {};
options['headers'] = options['headers'] || {};
configureSerializers(options?: JSData.DSConfiguration): JsonApiAdapter.DSJsonApiAdapterOptions {
var callOptions: JsonApiAdapter.DSJsonApiAdapterOptions = <JsonApiAdapter.DSJsonApiAdapterOptions>(this.DSUtils.copy(options) || {});
callOptions.jsonApi = callOptions.jsonApi || {};

// Passed options take priority over defaults
this.DSUtils.fillIn(callOptions.jsonApi, this.defaults.jsonApi);

//Json Api requires accept header
Helper.JsonApiHelper.AddJsonApiAcceptHeader(options['headers']);
Helper.JsonApiHelper.AddJsonApiContentTypeHeader(options['headers']);
callOptions['headers'] = callOptions['headers'] || {};
Helper.JsonApiHelper.AddJsonApiAcceptHeader(callOptions['headers']);
Helper.JsonApiHelper.AddJsonApiContentTypeHeader(callOptions['headers']);

// Ensure that we always call the JsonApi serializer first then any other serializers
var serialize = options['serialize'] || this.defaults.serialize;
var serialize = callOptions['serialize'] || this.defaults.serialize;
if (serialize) {
options['serialize'] = (resourceConfig: JSData.DSResourceDefinition<any>, attrs: Object) => {
callOptions['serialize'] = (resourceConfig: JSData.DSResourceDefinition<any>, attrs: Object) => {
return serialize(resourceConfig, this.serialize(resourceConfig, attrs));
};
} else {
options['serialize'] = (resourceConfig: JSData.DSResourceDefinition<any>, attrs: Object) => {
callOptions['serialize'] = (resourceConfig: JSData.DSResourceDefinition<any>, attrs: Object) => {
return this.serialize(resourceConfig, attrs);
};
}

// Ensure that we always call the JsonApi deserializer first then any other deserializers
var deserialize = options['deserialize'] || this.defaults.deserialize;
var deserialize = callOptions['deserialize'] || this.defaults.deserialize;
if (deserialize) {
options['deserialize'] = (resourceConfig: JSData.DSResourceDefinition<any>, data: JSData.DSHttpAdapterPromiseResolveType) => {
callOptions['deserialize'] = (resourceConfig: JSData.DSResourceDefinition<any>, data: JSData.DSHttpAdapterPromiseResolveType) => {
return deserialize(resourceConfig, this.deserialize(resourceConfig, data));
};
} else {
options['deserialize'] = (resourceConfig: JSData.DSResourceDefinition<any>, data: JSData.DSHttpAdapterPromiseResolveType) => {
callOptions['deserialize'] = (resourceConfig: JSData.DSResourceDefinition<any>, data: JSData.DSHttpAdapterPromiseResolveType) => {
return this.deserialize(resourceConfig, data);
};
}

return options;
return callOptions;
}

// DSHttpAdapter uses axios or $http, so options are axios config objects or $http config options.
Expand Down Expand Up @@ -288,8 +304,8 @@ export class JsonApiAdapter implements JSData.IDSAdapter {
let localOptions = this.configureSerializers(options);

// Id
if (attrs[localOptions.idAtttribute]) {
attrs[localOptions.idAtttribute] = attrs[localOptions.idAtttribute].toString();
if (attrs[config.idAttribute]) {
attrs[config.idAttribute] = attrs[config.idAttribute].toString();
}

return this.adapter.create(config, attrs, localOptions).then(
Expand Down Expand Up @@ -359,7 +375,13 @@ export class JsonApiAdapter implements JSData.IDSAdapter {
} else {
attrs[config.idAttribute] = idString;
}

let localOptions = this.configureSerializers(options);
if (localOptions.jsonApi.usePATCH === true) {
// Use Jsonapi PATCH symantics
localOptions.method = 'patch';
}

return this.adapter.update(config, idString, attrs, localOptions).then(
null,
(error: any) => {
Expand All @@ -370,6 +392,12 @@ export class JsonApiAdapter implements JSData.IDSAdapter {

public updateAll(config: JSData.DSResourceDefinition<any>, attrs: Object, params?: JSData.DSFilterArg, options?: JSData.DSConfiguration): JSData.JSDataPromise<any> {
let localOptions = this.configureSerializers(options);

if (localOptions.jsonApi.usePATCH === true) {
// Use Jsonapi PATCH symantics
localOptions.method = 'patch';
}

return this.adapter.updateAll(config, attrs, params, localOptions).then(
null,
(error: any) => {
Expand Down
9 changes: 4 additions & 5 deletions src/JsonApiSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1298,13 +1298,12 @@ export class JsonApiHelper {
var parentResourceType = new SerializationOptions(resource);
var relation = parentResourceType.getChildRelationWithLocalField(relationshipMeta.type, relationName);
var childResource = parentResourceType.getResource(relation.relation);
var config = {};
config[relation.foreignKey] = this.Id;

//{ containerid: this.Id }
var params = { jsonApi: { jsonApiPath: relationshipMeta.url}};
params[relation.foreignKey] = this.Id;

var operationConfig: JSDataLib.DSAdapterOperationConfiguration = { bypassCache: true };
config['jsonApi'] = { jsonApiPath: relationshipMeta.url };
return (<JSData.DSResourceDefinition<any>>childResource.def()).findAll(config, operationConfig);
return (<JSData.DSResourceDefinition<any>>childResource.def()).findAll(params, operationConfig);
}
} else {
// Resolve promise synchronously!!
Expand Down
4 changes: 2 additions & 2 deletions test/jsonApiSerializationSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@
setTimeout(function () {
assert.equal(1, _this.requests.length);
assert.equal(_this.requests[0].url, 'api/author/1');
assert.equal(_this.requests[0].method, 'PUT');
assert.equal(_this.requests[0].method, 'PATCH');

// NOTE the
var req = new DSJsonApiAdapter.JsonApi.JsonApiRequest();
Expand Down Expand Up @@ -355,7 +355,7 @@
setTimeout(function () {
assert.equal(1, _this.requests.length);
assert.equal(_this.requests[0].url, 'api/author/1');
assert.equal(_this.requests[0].method, 'PUT');
assert.equal(_this.requests[0].method, 'PATCH');

// NOTE the
var req = new DSJsonApiAdapter.JsonApi.JsonApiRequest();
Expand Down
80 changes: 70 additions & 10 deletions test/update.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,49 @@

describe('Update Tests', function () {
describe('DSJsonAdapter.update(resourceConfig, id, attrs, options)', function () {

it('should make a PATCH request', function () {
var _this = this;

setTimeout(function () {
assert.equal(1, _this.requests.length);
assert.equal(_this.requests[0].url, 'api/posts/1');
assert.equal(_this.requests[0].method, 'PATCH');
assert.isDefined(_this.requests[0].requestHeaders);
assert.include(_this.requests[0].requestHeaders['Accept'], 'application/vnd.api+json', 'Contains json api content-type header');
assert.equal(_this.requests[0].requestBody, JSON.stringify({ data: { id: '1', type: 'posts', attributes: { author: 'John', age: 30 }, links: {}, relationships: {} } }));


_this.requests[0].respond(200, { 'Content-Type': 'application/vnd.api+json' }, JSON.stringify(p1.jsonApiData));
}, 30);

return dsHttpAdapter.update(Post, 1, { author: 'John', age: 30 }).then(function (data) {
// We are not testing meta data yet
ignoreMetaData(data);

assert.deepEqual(data, p1.model, 'post 1 should have been updated#1');

setTimeout(function () {
assert.equal(2, _this.requests.length);
assert.equal(_this.requests[1].url, 'api2/posts/1');
assert.equal(_this.requests[1].method, 'PATCH');
assert.isDefined(_this.requests[1].requestHeaders);
assert.include(_this.requests[1].requestHeaders['Accept'], 'application/vnd.api+json', 'Contains json api content-type header');
assert.equal(_this.requests[1].requestBody, JSON.stringify({ data: { id: "1", type: 'posts', attributes: { author: 'John', age: 30 }, links: {}, relationships: {} } }));

_this.requests[1].respond(200, { 'Content-Type': 'application/vnd.api+json' }, JSON.stringify(p1.jsonApiData));

}, 30);

return dsHttpAdapter.update(Post, 1, { author: 'John', age: 30 }, { basePath: 'api2' });
}).then(function (data) {
// We are not testing meta data yet
ignoreMetaData(data);

assert.deepEqual(data, p1.model, 'post 1 should have been updated#2');
assert.equal(queryTransform.callCount, 2, 'queryTransform should have been called twice');
});
});

it('should make a PUT request', function () {
var _this = this;

Expand All @@ -17,7 +59,7 @@ describe('Update Tests', function () {
_this.requests[0].respond(200, { 'Content-Type': 'application/vnd.api+json' }, JSON.stringify(p1.jsonApiData));
}, 30);

return dsHttpAdapter.update(Post, 1, { author: 'John', age: 30 }).then(function (data) {
return dsHttpAdapter.update(Post, 1, { author: 'John', age: 30 }, { jsonApi: { usePATCH: false } }).then(function (data) {
// We are not testing meta data yet
ignoreMetaData(data);

Expand All @@ -26,7 +68,7 @@ describe('Update Tests', function () {
setTimeout(function () {
assert.equal(2, _this.requests.length);
assert.equal(_this.requests[1].url, 'api2/posts/1');
assert.equal(_this.requests[1].method, 'PUT');
assert.equal(_this.requests[1].method, 'PATCH');
assert.isDefined(_this.requests[1].requestHeaders);
assert.include(_this.requests[1].requestHeaders['Accept'], 'application/vnd.api+json', 'Contains json api content-type header');
assert.equal(_this.requests[1].requestBody, JSON.stringify({ data: { id: "1", type: 'posts', attributes: { author: 'John', age: 30 }, links: {}, relationships: {} } }));
Expand Down Expand Up @@ -59,6 +101,29 @@ describe('Update Tests', function () {
p1.model[0].Id = '1';
_this.requests[0].respond(204);//{ 'Content-Type': 'application/vnd.api+json' }
}, 30);

return dsHttpAdapter.update(Post, 1, { author: 'John', age: 30, type: 'person' }, {jsonApi: {usePATCH:false}}).then(function (data) {
// We are not testing meta data yet
ignoreMetaData(data);

assert.deepEqual(data, p1.model, 'post 1 should have been updated and data returned to datastore even though server returned no content');
});
});

it('should handle server 204 NoContent reponse correctly when PATCH (update) data is stored with out any changes on the server. So servers may chose to return no content', function () {
var _this = this;

setTimeout(function () {
assert.equal(1, _this.requests.length);
assert.equal(_this.requests[0].url, 'api/posts/1');
assert.equal(_this.requests[0].method, 'PATCH');
assert.isDefined(_this.requests[0].requestHeaders);
assert.include(_this.requests[0].requestHeaders['Accept'], 'application/vnd.api+json', 'Contains json api content-type header');
assert.equal(_this.requests[0].requestBody, JSON.stringify({ data: { id: "1", type: 'posts', attributes: { author: 'John', age: 30, type: 'person' }, links: {}, relationships: {} } }));

p1.model[0].Id = '1';
_this.requests[0].respond(204);//{ 'Content-Type': 'application/vnd.api+json' }
}, 30);

return dsHttpAdapter.update(Post, 1, { author: 'John', age: 30, type: 'person' }).then(function (data) {
// We are not testing meta data yet
Expand All @@ -68,6 +133,7 @@ describe('Update Tests', function () {
});
});


it('should fail update when supplied id and object primary key differ', function () {

assert.throw(
Expand Down Expand Up @@ -117,12 +183,6 @@ describe('Update Tests', function () {
}
}
});

//var author = { id: 1, name: 'Bob', articleid: 2 };
//var article = { id: 2, title: 'js-data' }; //, authorid: 1
//article.author = author;

//testData.config.Article.inject(article);
});

it('should save data to adapter', function () {
Expand All @@ -143,7 +203,7 @@ describe('Update Tests', function () {
setTimeout(function () {
assert.equal(2, _this.requests.length);
assert.equal(_this.requests[1].url, 'author/1');
assert.equal(_this.requests[1].method, 'PUT');
assert.equal(_this.requests[1].method, 'PATCH');

_this.requests[1].respond(200, { 'Content-Type': 'application/vnd.api+json' }, _this.requests[1].requestBody);
}, 30);
Expand Down

0 comments on commit a6f1472

Please sign in to comment.