Last active
January 26, 2017 09:49
-
-
Save fragsalat/006be655298820e60db9d6a7001035ed to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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