import Serializer from './http'; import { set } from '@ember/object'; import { HEADERS_SYMBOL as HTTP_HEADERS_SYMBOL, HEADERS_INDEX as HTTP_HEADERS_INDEX, HEADERS_DATACENTER as HTTP_HEADERS_DATACENTER, HEADERS_NAMESPACE as HTTP_HEADERS_NAMESPACE, } from 'consul-ui/utils/http/consul'; import { CACHE_CONTROL as HTTP_HEADERS_CACHE_CONTROL } from 'consul-ui/utils/http/headers'; import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc'; import { NSPACE_KEY } from 'consul-ui/models/nspace'; import createFingerprinter from 'consul-ui/utils/create-fingerprinter'; const DEFAULT_NSPACE = 'default'; const map = function(obj, cb) { if (!Array.isArray(obj)) { return [obj].map(cb)[0]; } return obj.map(cb); }; const attachHeaders = function(headers, body, query = {}) { // lowercase everything incase we get browser inconsistencies const lower = {}; Object.keys(headers).forEach(function(key) { lower[key.toLowerCase()] = headers[key]; }); // Add a 'pretend' Datacenter/Nspace header, they are not headers // the come from the request but we add them here so we can use them later // for store reconciliation if (typeof query.dc !== 'undefined') { lower[HTTP_HEADERS_DATACENTER.toLowerCase()] = query.dc; } lower[HTTP_HEADERS_NAMESPACE.toLowerCase()] = typeof query.ns !== 'undefined' ? query.ns : DEFAULT_NSPACE; // body[HTTP_HEADERS_SYMBOL] = lower; return body; }; export default Serializer.extend({ attachHeaders: attachHeaders, fingerprint: createFingerprinter(DATACENTER_KEY, NSPACE_KEY), respondForQuery: function(respond, query) { return respond((headers, body) => attachHeaders( headers, map(body, this.fingerprint(this.primaryKey, this.slugKey, query.dc)), query ) ); }, respondForQueryRecord: function(respond, query) { return respond((headers, body) => attachHeaders(headers, this.fingerprint(this.primaryKey, this.slugKey, query.dc)(body), query) ); }, respondForCreateRecord: function(respond, serialized, data) { const slugKey = this.slugKey; const primaryKey = this.primaryKey; return respond((headers, body) => { // If creates are true use the info we already have if (body === true) { body = data; } // Creates need a primaryKey adding return this.fingerprint(primaryKey, slugKey, data[DATACENTER_KEY])(body); }); }, respondForUpdateRecord: function(respond, serialized, data) { const slugKey = this.slugKey; const primaryKey = this.primaryKey; return respond((headers, body) => { // If updates are true use the info we already have // TODO: We may aswell avoid re-fingerprinting here if we are just // going to reuse data then its already fingerprinted and as the response // is true we don't have anything changed so the old fingerprint stays the same // as long as nothing in the fingerprint has been edited (the namespace?) if (body === true) { body = data; } return this.fingerprint(primaryKey, slugKey, data[DATACENTER_KEY])(body); }); }, respondForDeleteRecord: function(respond, serialized, data) { const slugKey = this.slugKey; const primaryKey = this.primaryKey; return respond((headers, body) => { // Deletes only need the primaryKey/uid returning // and they need the slug key AND potential namespace in order to // create the correct uid/fingerprint return { [primaryKey]: this.fingerprint( primaryKey, slugKey, data[DATACENTER_KEY] )({ [slugKey]: data[slugKey], [NSPACE_KEY]: data[NSPACE_KEY], })[primaryKey], }; }); }, // this could get confusing if you tried to override // say `normalizeQueryResponse` // TODO: consider creating a method for each one of the `normalize...Response` family normalizeResponse: function(store, modelClass, payload, id, requestType) { // Pick the meta/headers back off the payload and cleanup // before we go through serializing const headers = payload[HTTP_HEADERS_SYMBOL] || {}; delete payload[HTTP_HEADERS_SYMBOL]; const normalizedPayload = this.normalizePayload(payload, id, requestType); // put the meta onto the response, here this is ok // as JSON-API allows this and our specific data is now in // response[primaryModelClass.modelName] // so we aren't in danger of overwriting anything // (which was the reason for the Symbol-like property earlier) // use a method modelled on ember-data methods so we have the opportunity to // do this on a per-model level const meta = this.normalizeMeta(store, modelClass, headers, normalizedPayload, id, requestType); if (requestType !== 'query') { normalizedPayload.meta = meta; } const res = this._super( store, modelClass, { meta: meta, [modelClass.modelName]: normalizedPayload, }, id, requestType ); // If the result of the super normalizeResponse is undefined // its because the JSONSerializer (which REST inherits from) // doesn't recognise the requestType, in this case its likely to be an 'action' // request rather than a specific 'load me some data' one. // Therefore its ok to bypass the store here for the moment // we currently use this for self, but it also would affect any custom // methods that use a serializer in our custom service/store if (typeof res === 'undefined') { return payload; } return res; }, timestamp: function() { return new Date().getTime(); }, normalizeMeta: function(store, modelClass, headers, payload, id, requestType) { const meta = { cacheControl: headers[HTTP_HEADERS_CACHE_CONTROL.toLowerCase()], cursor: headers[HTTP_HEADERS_INDEX.toLowerCase()], dc: headers[HTTP_HEADERS_DATACENTER.toLowerCase()], nspace: headers[HTTP_HEADERS_NAMESPACE.toLowerCase()], }; if (typeof headers['x-range'] !== 'undefined') { meta.range = headers['x-range']; } if (requestType === 'query') { meta.date = this.timestamp(); payload.forEach(function(item) { set(item, 'SyncTime', meta.date); }); } return meta; }, normalizePayload: function(payload, id, requestType) { return payload; }, });