open-consul/ui-v2/app/serializers/application.js
John Cowen 52b1a73d3b
ui: Use X-Range header as a signal as to whether to reconcile the ember-data store (#8384)
* ui: Use `X-Range` header/meta to decide whether to reconcile or not

Previously we used a `shouldReconcile` method in order to decide whether
a response should trigger a reconciliation of the frontend ember-data
'source of truth' or not. It's a lot nicer/clearer if this 'flag' can be set
alongside the HTTP request information, moreover we almost have the same
functionality in `If-Range`/`Partial Content` HTTP functionality.

Here we partly follow this HTTP semantics but use a custom `X-Range` header
instead.
2020-07-29 10:16:09 +02:00

173 lines
6.3 KiB
JavaScript

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;
},
});