Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

compago-ajax

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

compago-ajax - npm Package Compare versions

Comparing version 1.0.1 to 2.0.0

README.md

69

package.json
{
"name": "compago-ajax",
"version": "1.0.1",
"description": "An AJAX storage engine for the Compago framework.",
"version": "2.0.0",
"description": "An Ajax storage engine for the Compago framework.",
"main": "dist/node/index.js",

@@ -26,12 +26,23 @@ "keywords": [

"license": "MIT",
"jspm": {
"main": "index",
"format": "es6",
"jspmNodeConversion": false,
"directories": {
"lib": "src"
},
"dependencies": {
"compago-listener": "npm:compago-listener@^1.0.4",
"compago-model": "npm:compago-model@^1.0.1"
"eslintConfig": {
"extends": "airbnb-base",
"rules": {
"no-underscore-dangle": 0,
"no-nested-ternary": 1,
"no-plusplus": 0,
"no-bitwise": 1,
"eqeqeq": [
2,
"smart"
],
"valid-jsdoc": [
2,
{
"prefer": {
"return": "returns"
},
"requireReturnDescription": false,
"requireParamDescription": false
}
]
}

@@ -41,29 +52,21 @@ },

"presets": [
"node5"
"node6"
]
},
"eslintConfig": {
"extends": "airbnb",
"rules": {
"no-nested-ternary": 1,
"eqeqeq": [2, "smart"]
}
"dependencies": {
"compago-listener": "^1.0.5"
},
"devDependencies": {
"babel-cli": "^6.3.17",
"babel-preset-node5": "^10.1.2",
"babel-cli": "^6.16.0",
"babel-preset-node6": "^11.0.0",
"codecov.io": "^0.1.6",
"compago-view": "^1.0.0",
"eslint": "^1.10.3",
"eslint-config-airbnb": "^2.1.1",
"eslint-plugin-react": "^3.13.1",
"expect": "^1.13.4",
"istanbul": "^1.0.0-alpha.2",
"mocha": "^2.3.4",
"sinon": "^1.17.2"
},
"dependencies": {
"compago-listener": "^1.0.4",
"compago-model": "^1.0.1"
"eslint": "^3.7.1",
"eslint-config-airbnb-base": "^8.0.0",
"eslint-plugin-import": "^2.0.1",
"expect": "^1.20.2",
"istanbul": "^1.1.0-alpha.1",
"mkdirp": "^0.5.1",
"mocha": "^3.1.2",
"rimraf": "^2.5.4"
}
}

@@ -1,6 +0,10 @@

import { Listener } from 'compago-listener';
import Model from 'compago-model';
/* eslint-env browser */
import Listener from 'compago-listener';
/** Used as a source of default options for methods to avoid creating new objects on every call. */
const _opt = Object.seal(Object.create(null));
/**
* Facilitates interaction with a REST server through the XMLHttpRequest API.
* Facilitates interaction with a REST server through the Fetch API.
*

@@ -13,7 +17,10 @@ * @mixes Listener

* @param {Object} [options]
* @param {string} [options.url] the URL for requests, by default uses the window's origin
* @param {string} [options.url] the base URL for requests, by default uses the window's origin
* @param {Object} [options.init] an options object for custom settings
* to use as the `init` parameter in calls to the global fetch()
*/
constructor(options = {}) {
constructor({ url = window.location.origin, init } = _opt) {
Object.assign(this, Listener);
this.url = options.url || window.location.origin;
this.url = url;
this.init = init || { headers: { 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'include' };
}

@@ -25,3 +32,3 @@

* @param {Model} model the model to be checked
* @return {boolean} True if the model is already stored on the server
* @returns {boolean} True if the model is already stored on the server
*/

@@ -36,67 +43,50 @@ static isStored(model) {

* @param {string} method a method name to execute.
* Internal method names are mapped to ajax methods in `Ajax.methods`.
* Internal method names are mapped to HTTP methods in `Ajax.methods`.
* @param {(Model|Collection)} model a model or a collection to by synchronized
* @param {Object} options
* @param {Function} options.success the function called if the request succeeds
* @param {Function} options.error the function called if the server returns an error
* @param {string} [options.url] a specific url for the request, in case it's different from the default url of the storage
* @param {boolean} [options.silent] whether to avoid firing any events
* @param {Boolean} [options.patch] whether to send only changed attributes (if present) using `PATCH` method
* @return {XMLHttpRequest} an request object
* @param {Boolean} [options.patch] whether to send only changed attributes (if present)
* using the `PATCH` method
* @param {string} [options.url] a specific url for the request,
* in case it's different from the default url of the storage
* @param {Object} [options.init] an options object for custom settings
* to use as the `init` parameter in calls to the global fetch()
* @returns {Promise}
*/
sync(method, model, options) {
sync(method, model, { silent, patch, url = this.url, init = this.init } = _opt) {
const options = Object.assign({}, init);
const methods = this.constructor.methods;
let type = methods[method];
if (!type) return false;
options.method = methods[method];
if (!options.method) return Promise.reject(new Error('Method is not found.'));
const { patch, silent, success, error } = options;
let { url = this.url } = options;
const xhr = new XMLHttpRequest();
const isStored = this.constructor.isStored(model);
const self = this;
const changes = (patch && model.changes) ? model.changes : false;
if (isStored) {
url += '/' + model.id;
if (method === 'write') type = changes ? methods.patch : methods.update;
} else {
if (method === 'erase') {
success();
return false;
}
if (method === 'read' && (model instanceof Model)) {
error('Model has not been saved yet.');
return false;
}
url += `/${model.id}`;
if (method === 'write') options.method = changes ? methods.patch : methods.update;
}
options.xhr = xhr;
options.model = model;
xhr.open(type, url);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
let data;
if (method === 'write') {
xhr.setRequestHeader('Content-Type', 'application/json');
data = changes ? changes : model.toJSON();
options.headers['Content-Type'] = 'application/json';
options.body = JSON.stringify(changes || model.toJSON());
}
xhr.onreadystatechange = () => {
const status = xhr.status;
if (xhr.readyState === 4) {
if (!silent) self.emit('response', options);
const response = Ajax._isJSONString(xhr.responseText) ? JSON.parse(xhr.responseText) : xhr.responseText;
if ((status >= 200) && (status < 300 || status === 304)) {
success(response);
if (!silent) this.emit('request', { model, options });
return fetch(url, options)
.then((response) => {
if (!silent) this.emit('response', { model, options, response });
if (response.ok || response.status === 304) {
const contentType = response.headers.get('content-type');
if (contentType && ~contentType.indexOf('application/json')) return response.json();
} else {
error(response);
const error = new Error(response.status);
error.response = response;
throw error;
}
}
};
if (!silent) this.emit('before:request', options);
xhr.send(JSON.stringify(data));
if (!silent) this.emit('request', options);
return xhr;
})
.catch((error) => {
throw error;
});
}

@@ -109,3 +99,3 @@

* @param {boolean} [options.silent] whether to avoid emitting the `dispose` event.
* @return {this}
* @returns {this}
*/

@@ -117,17 +107,2 @@ dispose(options = {}) {

}
/**
* Checks whether the provided value is a valid JSON string
*
* @param {*} val the value to be checked
* @return {Boolean} True if the value is a valid JSON string
*/
static _isJSONString(val) {
try {
JSON.parse(val);
} catch (e) {
return false;
}
return true;
}
}

@@ -139,9 +114,9 @@

Ajax.methods = {
'write': 'POST',
'erase': 'DELETE',
'read': 'GET',
'update': 'PUT',
'patch': 'PATCH',
write: 'POST',
erase: 'DELETE',
read: 'GET',
update: 'PUT',
patch: 'PATCH',
};
export default Ajax;
/* eslint-env node, mocha */
import expect from 'expect';
import Ajax from '../src/index';
import Model from 'compago-model';
import expect from 'expect';
import sinon from 'sinon';
class MockResponse {
constructor(status, headers, body) {
this.status = status;
this.headers = new Map();
Object.keys(headers).forEach((key) => {
this.headers.set(key, headers[key]);
});
this.body = body;
}
get ok() {
return (this.status >= 200) && (this.status < 300);
}
json() {
return this.body ? Promise.resolve(this.body) : Promise.reject();
}
}
describe('Ajax', () => {

@@ -31,164 +47,145 @@ let storage;

describe('sync', () => {
let xhr;
let model;
let options;
let requests;
let response;
beforeEach(() => {
xhr = sinon.useFakeXMLHttpRequest();
global.XMLHttpRequest = xhr;
requests = [];
/* eslint no-shadow: 1*/
xhr.onCreate = (xhr) => {
requests.push(xhr);
};
model = { id: 42 };
model.toJSON = () => {
return model;
};
options = {};
options.success = expect.createSpy();
options.error = expect.createSpy();
model.toJSON = () => model;
response = new MockResponse(200, { 'content-type': 'application/json' }, {});
});
afterEach(() => {
xhr.restore();
it('rejects if an invalid method is used', () => {
return storage.sync().catch((error) => {
expect(error.message).toBe('Method is not found.');
});
});
it('returns `false` if no valid operation type is provided', () => {
expect(storage.sync()).toBe(false);
expect(requests.length).toBe(0);
it('reads a model', () => {
const result = { name: 'Arthur' };
response.body = result;
global.fetch = expect.createSpy().andReturn(Promise.resolve(response));
return storage.sync('read', model).then((data) => {
expect(global.fetch.calls.length).toBe(1);
expect(global.fetch.calls[0].arguments).toEqual(['http://example.com/posts/42', {
method: 'GET',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
credentials: 'include',
}]);
expect(data).toEqual(result);
});
});
it('parses JSON responses', () => {
storage.sync('read', model, options);
requests[0].respond(200, { 'Content-Type': 'application/json' }, '{"name":"Arthur"}');
expect(options.success.calls[0].arguments).toEqual([{ name: 'Arthur' }]);
expect(options.error).toNotHaveBeenCalled();
});
it('retrieves a model', () => {
storage.sync('read', model, options);
expect(requests.length).toBe(1);
expect(requests[0].method).toBe('GET');
expect(options.success).toNotHaveBeenCalled();
requests[0].respond(200, { 'Content-Type': 'text/plain' }, 'Ok');
expect(options.success).toHaveBeenCalledWith('Ok');
expect(options.error).toNotHaveBeenCalled();
});
it('invokes `option.error` when attempts to retrieve an unsaved model', () => {
const m = new Model();
storage.sync('read', m, options);
expect(options.error).toHaveBeenCalled();
expect(requests[0].readyState).toBe(0);
});
it('retrieves all models in a collection', () => {
it('reads all models in a collection', () => {
const collection = {};
storage.sync('read', collection, options);
expect(requests[0].url).toBe('http://example.com/posts');
expect(requests[0].method).toBe('GET');
expect(options.success).toNotHaveBeenCalled();
requests[0].respond(200, { 'Content-Type': 'text/plain' }, 'Ok');
expect(options.success).toHaveBeenCalledWith('Ok');
expect(options.error).toNotHaveBeenCalled();
response.headers.set('content-type', 'text/plain');
global.fetch = expect.createSpy().andReturn(Promise.resolve(response));
return storage.sync('read', collection).then(() => {
expect(global.fetch.calls.length).toBe(1);
expect(global.fetch.calls[0].arguments).toEqual(['http://example.com/posts', {
method: 'GET',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
credentials: 'include',
}]);
});
});
it('saves a model', () => {
it('creates a model', () => {
delete model.id;
model.name = 'Arthur';
storage.sync('write', model, options);
expect(requests[0].url).toBe('http://example.com/posts');
expect(requests[0].method).toBe('POST');
expect(requests[0].requestBody).toEqual('{"name":"Arthur"}');
const result = { name: 'Arthur', id: 1 };
response.body = result;
global.fetch = expect.createSpy().andReturn(Promise.resolve(response));
return storage.sync('write', model).then((data) => {
expect(global.fetch.calls.length).toBe(1);
expect(global.fetch.calls[0].arguments).toEqual(['http://example.com/posts', {
method: 'POST',
body: JSON.stringify(model),
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json' },
credentials: 'include',
}]);
expect(data).toEqual(result);
});
});
it('updates a model', () => {
storage.sync('write', model, options);
expect(requests[0].url).toBe('http://example.com/posts/42');
expect(requests[0].method).toBe('PUT');
expect(requests[0].requestBody).toEqual('{"id":42}');
global.fetch = expect.createSpy().andReturn(Promise.resolve(response));
return storage.sync('write', model).then(() => {
expect(global.fetch.calls.length).toBe(1);
expect(global.fetch.calls[0].arguments).toEqual(['http://example.com/posts/42', {
method: 'PUT',
body: JSON.stringify(model),
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json' },
credentials: 'include',
}]);
});
});
it('sends only changes if `patch:true`', () => {
it('patches a model if `patch:true` and model.changes are present', () => {
model.changes = { name: 'Arthur' };
options.patch = true;
storage.sync('write', model, options);
expect(requests[0].url).toBe('http://example.com/posts/42');
expect(requests[0].method).toBe('PATCH');
expect(requests[0].requestBody).toEqual('{"name":"Arthur"}');
global.fetch = expect.createSpy().andReturn(Promise.resolve(response));
return storage.sync('write', model, { patch: true }).then(() => {
expect(global.fetch.calls.length).toBe(1);
expect(global.fetch.calls[0].arguments).toEqual(['http://example.com/posts/42', {
method: 'PATCH',
body: JSON.stringify(model.changes),
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json' },
credentials: 'include',
}]);
});
});
it('sends all attributes if no changes found despite `patch:true`', () => {
options.patch = true;
storage.sync('write', model, options);
expect(requests[0].url).toBe('http://example.com/posts/42');
expect(requests[0].method).toBe('PUT');
expect(requests[0].requestBody).toEqual('{"id":42}');
model.changes = false;
it('deletes a model', () => {
global.fetch = expect.createSpy().andReturn(Promise.resolve(response));
return storage.sync('erase', model).then(() => {
expect(global.fetch.calls.length).toBe(1);
expect(global.fetch.calls[0].arguments).toEqual(['http://example.com/posts/42', {
method: 'DELETE',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
credentials: 'include',
}]);
});
});
it('deletes a single model', () => {
storage.sync('erase', model, options);
expect(requests[0].url).toBe('http://example.com/posts/42');
expect(requests[0].method).toBe('DELETE');
it('rejects if server returns an error', () => {
response.status = 404;
response.headers.set('content-type', 'text/plain');
global.fetch = expect.createSpy().andReturn(Promise.resolve(response));
return storage.sync('read', model).catch((error) => {
expect(global.fetch.calls.length).toBe(1);
expect(global.fetch.calls[0].arguments).toEqual(['http://example.com/posts/42', {
method: 'GET',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
credentials: 'include',
}]);
expect(error.message).toBe('404');
});
});
it('invokes `option.success` when attempts to delete an unsaved model', () => {
delete model.id;
storage.sync('erase', model, options);
expect(options.success).toHaveBeenCalled();
expect(requests[0].readyState).toBe(0);
});
it('invokes `options.error` if a error is returned by the server', () => {
storage.sync('read', model, options);
expect(requests[0].url).toBe('http://example.com/posts/42');
expect(requests[0].method).toBe('GET');
expect(options.success).toNotHaveBeenCalled();
requests[0].respond(404, { 'Content-Type': 'text/plain' }, 'Not Ok');
expect(options.success).toNotHaveBeenCalled();
expect(options.error).toHaveBeenCalledWith('Not Ok');
});
it('fires `before:request`, `request`, and `response` events unless `silent:true`', () => {
it('fires `request` and `response` events unless `silent:true`', () => {
storage.someMethod = expect.createSpy();
storage.otherMethod = expect.createSpy();
storage.anotherMethod = expect.createSpy();
storage.on(storage, 'before:request', storage.someMethod);
storage.on(storage, 'request', storage.otherMethod);
storage.on(storage, 'response', storage.anotherMethod);
storage.sync('read', model, options);
requests[0].respond(200, { 'Content-Type': 'text/plain' }, 'Not Ok');
expect(storage.someMethod).toHaveBeenCalled();
expect(storage.otherMethod).toHaveBeenCalled();
expect(storage.anotherMethod).toHaveBeenCalled();
storage.on(storage, 'request', storage.someMethod);
storage.on(storage, 'response', storage.otherMethod);
global.fetch = expect.createSpy().andReturn(Promise.resolve(response));
return storage.sync('read', model).then(() => {
expect(storage.someMethod).toHaveBeenCalled();
expect(storage.otherMethod).toHaveBeenCalled();
});
});
it('does not fire events if `silent:true`', () => {
it('does not fire any events if `silent:true`', () => {
storage.someMethod = expect.createSpy();
storage.otherMethod = expect.createSpy();
storage.anotherMethod = expect.createSpy();
storage.on(storage, 'before:request', storage.someMethod);
storage.on(storage, 'request', storage.otherMethod);
storage.on(storage, 'response', storage.anotherMethod);
options.silent = true;
storage.sync('read', model, options);
requests[0].respond(200, { 'Content-Type': 'text/plain' }, 'Not Ok');
expect(storage.someMethod).toNotHaveBeenCalled();
expect(storage.otherMethod).toNotHaveBeenCalled();
expect(storage.anotherMethod).toNotHaveBeenCalled();
storage.on(storage, 'request', storage.someMethod);
storage.on(storage, 'response', storage.otherMethod);
global.fetch = expect.createSpy().andReturn(Promise.resolve(response));
return storage.sync('read', model, { silent: true }).then(() => {
expect(storage.someMethod).toNotHaveBeenCalled();
expect(storage.otherMethod).toNotHaveBeenCalled();
});
});
});
describe('_isJSONString', () => {
it('checks whether the passed argument is a JSON string or not', () => {
expect(Ajax._isJSONString('{"name":"Arthur"}')).toBe(true);
expect(Ajax._isJSONString('Arthur')).toBe(false);
});
});
describe('dispose', () => {
it('prepares the storage controller to be disposed', () => {
it('prepares the storage to be disposed', () => {
storage.on(storage, 'dispose', () => {

@@ -202,2 +199,3 @@ });

});
it('fires `dispose` event unless `silent:true`', () => {

@@ -204,0 +202,0 @@ storage.someMethod = expect.createSpy();

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc