John Cowen b16a6fa033
ui: Adds Partitions to the HTTP layer (#10447)
This PR mainly adds partition to our HTTP adapter. Additionally and perhaps most importantly, we've also taken the opportunity to move our 'conditional namespaces' deeper into the app.

The reason for doing this was, we like that namespaces should be thought of as required instead of conditional, 'special' things and would like the same thinking to be applied to partitions.

Now, instead of using code throughout the app throughout the adapters to add/remove namespaces or partitions depending on whether they are enabled or not. As a UI engineer you just pretend that namespaces and partitions are always enabled, and we remove them for you deeper in the app, out of the way of you forgetting to treat these properties as a special case.

Notes:

Added a PartitionAbility while we were there (not used as yet)
Started to remove the CONSTANT variables we had just for property names. I prefer that our adapters are as readable and straightforwards as possible, it just looks like HTTP.
We'll probably remove our formatDatacenter method we use also at some point, it was mainly too make it look the same as our previous formatNspace, but now we don't have that, it instead now looks different!
We enable parsing of partition in the UIs URL, but this is feature flagged so still does nothing just yet.
All of the test changes were related to the fact that we were treating client.url as a function rather than a method, and now that we reference this in client.url (etc) it needs binding to client.
2021-09-15 18:09:55 +01:00

201 lines
6.2 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 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];
});
//
body[HTTP_HEADERS_SYMBOL] = lower;
return body;
};
export default class ApplicationSerializer extends Serializer {
attachHeaders = attachHeaders;
fingerprint = createFingerprinter(DATACENTER_KEY, NSPACE_KEY);
respondForQuery(respond, query) {
return respond((headers, body) =>
attachHeaders(
headers,
map(
body,
this.fingerprint(this.primaryKey, this.slugKey, query.dc, headers[HTTP_HEADERS_NAMESPACE])
),
query
)
);
}
respondForQueryRecord(respond, query) {
return respond((headers, body) =>
attachHeaders(
headers,
this.fingerprint(
this.primaryKey,
this.slugKey,
query.dc,
headers[HTTP_HEADERS_NAMESPACE]
)(body),
query
)
);
}
respondForCreateRecord(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],
headers[HTTP_HEADERS_NAMESPACE]
)(body);
});
}
respondForUpdateRecord(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],
headers[HTTP_HEADERS_NAMESPACE]
)(body);
});
}
respondForDeleteRecord(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],
headers[HTTP_HEADERS_NAMESPACE]
)({
[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(store, modelClass, payload, id, requestType) {
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, normalizedPayload, id, requestType);
if (requestType !== 'query') {
normalizedPayload.meta = meta;
}
const res = super.normalizeResponse(
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() {
return new Date().getTime();
}
normalizeMeta(store, modelClass, payload, id, requestType) {
// Pick the meta/headers back off the payload and cleanup
const headers = payload[HTTP_HEADERS_SYMBOL] || {};
delete payload[HTTP_HEADERS_SYMBOL];
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 (typeof headers['refresh'] !== 'undefined') {
meta.interval = headers['refresh'] * 1000;
}
if (requestType === 'query') {
meta.date = this.timestamp();
payload.forEach(function(item) {
set(item, 'SyncTime', meta.date);
});
}
return meta;
}
normalizePayload(payload, id, requestType) {
return payload;
}
}