Skip to content

Instantly share code, notes, and snippets.

@fragsalat
Last active January 26, 2017 09:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fragsalat/006be655298820e60db9d6a7001035ed to your computer and use it in GitHub Desktop.
Save fragsalat/006be655298820e60db9d6a7001035ed to your computer and use it in GitHub Desktop.
import {HttpClient} from 'aurelia-http-client';
import {Container} from 'aurelia-dependency-injection';
import {Rest} from 'app/util/rest';
import {Page} from 'app/model/page';
export class AbstractModel {
/**
* @type {Array<String>} list of excluded fields
*/
static excludedFields = ['id', '_links'];
/**
* @type {String}
*/
static route = '';
/**
* @param data {Object}
*/
constructor(data) {
Object.assign(this, data);
if (data.etag) {
Object.defineProperty(this, 'etag', Object.getOwnPropertyDescriptor(data, 'etag'));
}
}
/**
* @return {String}
*/
getId() {
return this.id;
}
/**
* @returns {Object}
*/
getETag() {
return this.etag || {value: this.last_modified};
}
/**
* @param args {*} Possible list of arguments (Used on Quantity model)
* @returns {Rest}
*/
static getRest(...args) {
if (!this._rest) {
let http = Container.instance.get(HttpClient);
this._rest = new Rest(http, this.route);
}
return this._rest;
}
/**
* Gets single entity by it's id
*
* @param id {String} The id of the model
* @param args {*} Can hold arguments for custom getRest fn
* @returns {Promise}
*/
static get(id, ...args) {
return this.getRest(...args).get(id).then(data => {
return new this(data);
});
}
/**
* Search for entries by given parameters
*
* @param params {Object} Set of key value pairs
* @param args {*} Can hold arguments for custom getRest fn
* @returns {Promise}
*/
static find(params, ...args) {
return this.getRest(...args).list(params).then(response => {
return Page.fromResponse(response, this);
});
}
/**
* Call POST or PUT API depending on id is in place or not
*
* @param args {*} Can hold arguments for custom getRest fn
* @returns {Promise}
*/
save(...args) {
// Create data object from current instance
let data = Object.assign({}, this);
// Remove obsolete fields to reduce load
this.constructor.excludedFields.forEach(field => delete data[field]);
// Check if id is set
let id = this.getId();
if (id) {
data.etag = this.getETag();
return this.constructor.getRest(...args).put(id, data);
}
return this.constructor.getRest(...args).post(this);
}
/**
* Call API delete endpoint
*
* @param args {*} Can hold arguments for custom getRest fn
* @returns {Promise}
*/
delete(...args) {
return this.constructor.getRest(...args).delete(this.getId());
}
}
import {AbstractModel} from 'app/model/abstract-model';
export class Order extends AbstractModel {
static route = '/shopping-basket/order';
static excludedFields = ['id', 'products', '_links'];
/** @type {Number} */
id;
/** @type {String} */
created;
/** @type {Number} */
total_value;
/** @type {Number} */
total_products;
/** @type {Array<Product>} */
products;
}
export class Page extends Array {
total = 0;
number = 0;
pages = 0;
/**
* Parses an hateoas API response into a page object
*
* @param response {{_embedded: Array, page: {number: Number, total_pages: Number, total_elements: Number}}}
* @param Clazz {Function} Class constructor of target object type
* @returns {Page}
*/
static fromResponse(response, Clazz) {
let page = new this();
let entities = response._embedded || {};
let keys = Object.keys(entities);
// Check if there is a child which hold a list of objects
if (keys.length && Array.isArray(entities[keys[0]])) {
entities[keys[0]].forEach(entity => {
page.push(new Clazz(entity));
});
}
// Get pagination parameters
if (response.page) {
page.number = response.page.number;
page.pages = response.page.total_pages;
page.total = response.page.total_elements;
}
return page;
}
}
import {AbstractModel} from 'app/model/abstract-model';
export class Product extends AbstractModel {
static route = '/shopping-basket/order/{{0}}/products';
static excludedFields = ['id', '_links'];
/** @type {Number} */
id;
/** @type {Number} */
order_id;
/** @type {String} */
name;
/** @type {Number} */
price;
/** @type {Number} */
quantity;
/** @type {Number} */
discount;
}
import {buildQueryString} from 'aurelia-path';
/**
* Rest client to handle list, get, post, put and delete requests
* On Get requests it checks if a etag header is available and set it to the response as non enumerable property
*/
export class Rest {
static NO_CONTENT = 204;
/**
* @param http {HttpClient}
* @param path {String}
*/
constructor(http, path) {
if (!http) {
throw new Error('No http client set');
}
this._path = path;
this._http = http;
}
/**
* Set base uri to http client
*
* @param baseUrl {String} The base url to the api endpoint
*/
withBaseUrl(baseUrl) {
this._http.configure(config => config.withBaseUrl(baseUrl));
}
/**
* Execute a api request to list nodes
*
* @param params {Object} Optional get parameters
* @returns {Promise}
*/
list(params) {
return this.request(this._path, 'GET', params).then(response => {
return response.content;
});
}
/**
* Execute a api request to retrieve a specific resource
*
* @param id {String|Number} The identifier of the resource
* @returns {Promise}
*/
get(id) {
return this.request(`${this._path}/${id}`).then(response => {
let data = response.content;
// Set etag on response object if available
if (response.headers && response.headers.has('ETag')) {
// Checking with two cases because firesux converts header names
// and aurelia's implementation isn't case insensitive yet
let etag = (response.headers.get('ETag') || response.headers.get('Etag')).match('(W/)?"?([^"]+)"?');
Object.defineProperty(data, 'etag', {
value: {value: etag[2], weak: !!etag[1], toString: () => etag[0]},
enumerable: false,
writable: false,
configurable: false
});
}
return data;
});
}
/**
* Execute api request to update a resource.
*
* @param id {String} Identifier of the resource
* @param data {Object} The new resource. (The whole object not just the updated part)
* @returns {Promise}
*/
put(id, data) {
return this.request(`${this._path}/${id}`, 'PUT', data).then(response => {
if (response.statusCode !== Rest.NO_CONTENT) {
return response.response && response.content;
}
});
}
/**
* Execute api request to create a new resource.
*
* @param data {Object} The new resource. (The whole object not just the updated part)
* @returns {Promise}
*/
post(data) {
return this.request(`${this._path}`, 'POST', data).then(response => {
return response.response && response.content;
});
}
/**
* Execute a api request to delete a specific resource
*
* @param id {string} The identifier of the resource
* @returns {Promise}
*/
delete(id) {
return this.request(`${this._path}/${id}`, 'DELETE');
}
/**
* Helper function to do a api request
*
* @param url {String} The url to the endpoint
* @param method {String} Http method. Can be GET|POST|PUT|DELETE
* @param data {Object} Optional data to send with the body (POST|PUT) or url query (GET).
* @returns {Promise}
*/
request(url, method = 'GET', data = null) {
let fn = method.substr(0, 1).toUpperCase() + method.substr(1).toLowerCase();
let query = method === 'GET' && data ? '?' + decodeURIComponent(buildQueryString(data, true)) : '';
let builder = this._http.createRequest(url + query);
builder.withHeader('Content-Type', 'application/json');
builder.withHeader('cache', 'false');
if (data && data.etag) {
builder.withHeader('If-Match', data.etag.value);
}
if (['POST', 'PUT'].indexOf(method) !== -1 && data) {
builder.withContent(data);
}
return builder['as' + fn]().send().then(response => {
if (response.statusCode >= 400) {
let error = response.content && (response.content.detail || response.content.message) || response.content || 'unknown';
throw new Error(`Request failed: ${method} ${url} with status ${response.statusCode} because "${error}"`);
}
return response;
});
}
}
import {Order} from 'app/model/order';
import {Product} from 'app/model/order';
/**
* Example for get and list method
*/
Order.get(1234).then(order => {
console.log(`We have ${total_products} products on this order`);
Product.find({order_id: order.getId()}).then(products => {
for (let product of products) {
console.log(`${product.name} ${product.price} ${product.quantity}`);
}
});
});
/**
* Get data on aurelia activate livecycle and return promise for routing
* Expect that product properties are bound two-way or the data gets changed durin events
*/
activate() {
return Product.get(1234).then(product => this.product = product);
}
/**
* You could use aurelia-validation plugin and call save on success
*/
onSubmit() {
// Do some validations
if (product.name === null) {
// ...
}
this.product.save().then(() => {
// Show some success message and redirect back to product view
});
}
/**
* Delete the product
*/
onDelete() {
if (confirm('Are you sure?')) {
this.product.delete();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment