UI - dynamic related capabilities (#7013)
* lay groundwork for application serializer to setup capabilities relationships * add api path util and tests, and attach-capabilites fn * make attach-capabilities work with array responses, add tests
This commit is contained in:
parent
2fcac90052
commit
887e2febf8
|
@ -38,7 +38,12 @@ export default ApplicationAdapter.extend({
|
|||
},
|
||||
|
||||
query(store, type, query) {
|
||||
return this.ajax(this.urlForQuery(query, type.modelName), 'GET');
|
||||
return this.ajax(this.urlForQuery(query, type.modelName), 'GET').then(resp => {
|
||||
// remove pagination query items here
|
||||
const { size, page, responsePath, pageFilter, ...modelAttrs } = query;
|
||||
resp._requestQuery = modelAttrs;
|
||||
return resp;
|
||||
});
|
||||
},
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import DS from 'ember-data';
|
||||
import { assert, debug } from '@ember/debug';
|
||||
import { typeOf } from '@ember/utils';
|
||||
import { isArray } from '@ember/array';
|
||||
const { belongsTo } = DS;
|
||||
|
||||
/*
|
||||
*
|
||||
* attachCapabilities
|
||||
*
|
||||
* @param modelClass = An Ember Data model class
|
||||
* @param capabilities - an Object whose keys will added to the model class as related 'capabilities' models
|
||||
* and whose values should be functions that return the id of the related capabilites model
|
||||
*
|
||||
* definition of capabilities be done shorthand with the apiPath tagged template funtion
|
||||
*
|
||||
*
|
||||
* @usage
|
||||
*
|
||||
* let Model = DS.Model.extend({
|
||||
* backend: attr(),
|
||||
* scope: attr(),
|
||||
* });
|
||||
*
|
||||
* export default attachCapabilities(Model, {
|
||||
* updatePath: apiPath`${'backend'}/scope/${'scope'}/role/${'id'}`,
|
||||
* });
|
||||
*
|
||||
*/
|
||||
export default function attachCapabilities(modelClass, capabilities) {
|
||||
let capabilityKeys = Object.keys(capabilities);
|
||||
let newRelationships = capabilityKeys.reduce((ret, key) => {
|
||||
ret[key] = belongsTo('capabilities');
|
||||
return ret;
|
||||
}, {});
|
||||
|
||||
debug(`adding new relationships: ${capabilityKeys.join(', ')} to ${modelClass.toString()}`);
|
||||
modelClass.reopen(newRelationships);
|
||||
modelClass.reopenClass({
|
||||
// relatedCapabilities is called in the application serializer's
|
||||
// normalizeResponse hook to add the capabilities relationships to the
|
||||
// JSON-API document used by Ember Data
|
||||
relatedCapabilities(jsonAPIDoc) {
|
||||
let { data, included } = jsonAPIDoc;
|
||||
if (!data) {
|
||||
data = jsonAPIDoc;
|
||||
}
|
||||
if (isArray(data)) {
|
||||
let newData = data.map(this.relatedCapabilities);
|
||||
return {
|
||||
data: newData,
|
||||
included,
|
||||
};
|
||||
}
|
||||
let context = {
|
||||
id: data.id,
|
||||
...data.attributes,
|
||||
};
|
||||
for (let newCapability of capabilityKeys) {
|
||||
let templateFn = capabilities[newCapability];
|
||||
let type = typeOf(templateFn);
|
||||
assert(`expected value of ${newCapability} to be a function but found ${type}.`, type === 'function');
|
||||
data.relationships[newCapability] = {
|
||||
data: {
|
||||
type: 'capabilities',
|
||||
id: templateFn(context),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (included) {
|
||||
return {
|
||||
data,
|
||||
included,
|
||||
};
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
},
|
||||
});
|
||||
return modelClass;
|
||||
}
|
|
@ -2,9 +2,11 @@ import DS from 'ember-data';
|
|||
import { computed } from '@ember/object';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
import fieldToAttrs from 'vault/utils/field-to-attrs';
|
||||
import apiPath from 'vault/utils/api-path';
|
||||
import attachCapabilities from 'vault/lib/attach-capabilities';
|
||||
|
||||
const { attr } = DS;
|
||||
export default DS.Model.extend({
|
||||
const Model = DS.Model.extend({
|
||||
useOpenAPI: true,
|
||||
backend: attr({ readOnly: true }),
|
||||
scope: attr({ readOnly: true }),
|
||||
|
@ -24,3 +26,7 @@ export default DS.Model.extend({
|
|||
return expandAttributeMeta(this, fields);
|
||||
}),
|
||||
});
|
||||
|
||||
export default attachCapabilities(Model, {
|
||||
updatePath: apiPath`${'backend'}/scope/${'scope'}/role/${'id'}`,
|
||||
});
|
||||
|
|
|
@ -15,7 +15,14 @@ export default DS.JSONSerializer.extend({
|
|||
return key;
|
||||
}
|
||||
let pk = this.get('primaryKey') || 'id';
|
||||
return { [pk]: key };
|
||||
let model = { [pk]: key };
|
||||
// if we've added a in the adapter, we want
|
||||
// attach it to the individual models
|
||||
if (payload._requestQuery) {
|
||||
model = { ...model, ...payload._requestQuery };
|
||||
delete payload._requestQuery;
|
||||
}
|
||||
return model;
|
||||
});
|
||||
return models;
|
||||
}
|
||||
|
@ -40,7 +47,11 @@ export default DS.JSONSerializer.extend({
|
|||
if (id && !responseJSON.id) {
|
||||
responseJSON.id = id;
|
||||
}
|
||||
return this._super(store, primaryModelClass, responseJSON, id, requestType);
|
||||
let jsonAPIRepresentation = this._super(store, primaryModelClass, responseJSON, id, requestType);
|
||||
if (primaryModelClass.relatedCapabilities) {
|
||||
jsonAPIRepresentation = primaryModelClass.relatedCapabilities(jsonAPIRepresentation);
|
||||
}
|
||||
return jsonAPIRepresentation;
|
||||
},
|
||||
|
||||
serializeAttribute(snapshot, json, key, attributes) {
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { assert } from '@ember/debug';
|
||||
|
||||
// This is a tagged template function that will
|
||||
// replace placeholders in the form of 'id' with the value from the passed context
|
||||
//
|
||||
// usage:
|
||||
// let fn = apiPath`foo/bar/${'id'}`;
|
||||
// let output = fn({id: 'an-id'});
|
||||
// output will result in 'foo/bar/an-id';
|
||||
|
||||
export default function apiPath(strings, ...keys) {
|
||||
return function(data) {
|
||||
let dict = data || {};
|
||||
let result = [strings[0]];
|
||||
assert(
|
||||
`Expected ${keys.length} keys in apiPath context, only recieved ${Object.keys(data).length}`,
|
||||
keys.length === Object.keys(data).length
|
||||
);
|
||||
keys.forEach((key, i) => {
|
||||
result.push(dict[key], strings[i + 1]);
|
||||
});
|
||||
return result.join('');
|
||||
};
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import attachCapabilities from 'vault/lib/attach-capabilities';
|
||||
import apiPath from 'vault/utils/api-path';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
let MODEL_TYPE = 'test-form-model';
|
||||
|
||||
module('Unit | lib | attach capabilities', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it attaches passed capabilities', function(assert) {
|
||||
let mc = this.owner.lookup('service:store').modelFor(MODEL_TYPE);
|
||||
mc = attachCapabilities(mc, {
|
||||
updatePath: apiPath`update/{'id'}`,
|
||||
deletePath: apiPath`delete/{'id'}`,
|
||||
});
|
||||
let relationship = get(mc, 'relationshipsByName').get('updatePath');
|
||||
|
||||
assert.equal(relationship.key, 'updatePath', 'has updatePath relationship');
|
||||
assert.equal(relationship.kind, 'belongsTo', 'kind of relationship is belongsTo');
|
||||
assert.equal(relationship.type, 'capabilities', 'updatePath is a related capabilities model');
|
||||
|
||||
relationship = get(mc, 'relationshipsByName').get('deletePath');
|
||||
assert.equal(relationship.key, 'deletePath', 'has deletePath relationship');
|
||||
assert.equal(relationship.kind, 'belongsTo', 'kind of relationship is belongsTo');
|
||||
assert.equal(relationship.type, 'capabilities', 'deletePath is a related capabilities model');
|
||||
});
|
||||
|
||||
test('it adds a static method to the model class', function(assert) {
|
||||
let mc = this.owner.lookup('service:store').modelFor(MODEL_TYPE);
|
||||
mc = attachCapabilities(mc, {
|
||||
updatePath: apiPath`update/{'id'}`,
|
||||
deletePath: apiPath`delete/{'id'}`,
|
||||
});
|
||||
assert.ok(
|
||||
!!mc.relatedCapabilities && typeof mc.relatedCapabilities === 'function',
|
||||
'model class now has a relatedCapabilities static function'
|
||||
);
|
||||
});
|
||||
|
||||
test('calling static method with single response JSON-API document adds expected relationships', function(assert) {
|
||||
let mc = this.owner.lookup('service:store').modelFor(MODEL_TYPE);
|
||||
mc = attachCapabilities(mc, {
|
||||
updatePath: apiPath`update/${'id'}`,
|
||||
deletePath: apiPath`delete/${'id'}`,
|
||||
});
|
||||
let jsonAPIDocSingle = {
|
||||
data: {
|
||||
id: 'test',
|
||||
type: MODEL_TYPE,
|
||||
attributes: {},
|
||||
relationships: {},
|
||||
},
|
||||
included: [],
|
||||
};
|
||||
|
||||
let expected = {
|
||||
data: {
|
||||
id: 'test',
|
||||
type: MODEL_TYPE,
|
||||
attributes: {},
|
||||
relationships: {
|
||||
updatePath: {
|
||||
data: {
|
||||
type: 'capabilities',
|
||||
id: 'update/test',
|
||||
},
|
||||
},
|
||||
deletePath: {
|
||||
data: {
|
||||
type: 'capabilities',
|
||||
id: 'delete/test',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
included: [],
|
||||
};
|
||||
|
||||
mc.relatedCapabilities(jsonAPIDocSingle);
|
||||
|
||||
assert.equal(
|
||||
Object.keys(jsonAPIDocSingle.data.relationships).length,
|
||||
2,
|
||||
'document now has 2 relationships'
|
||||
);
|
||||
assert.deepEqual(jsonAPIDocSingle, expected, 'has the exected new document structure');
|
||||
});
|
||||
|
||||
test('calling static method with an arrary response JSON-API document adds expected relationships', function(assert) {
|
||||
let mc = this.owner.lookup('service:store').modelFor(MODEL_TYPE);
|
||||
mc = attachCapabilities(mc, {
|
||||
updatePath: apiPath`update/${'id'}`,
|
||||
deletePath: apiPath`delete/${'id'}`,
|
||||
});
|
||||
let jsonAPIDocSingle = {
|
||||
data: [
|
||||
{
|
||||
id: 'test',
|
||||
type: MODEL_TYPE,
|
||||
attributes: {},
|
||||
relationships: {},
|
||||
},
|
||||
{
|
||||
id: 'foo',
|
||||
type: MODEL_TYPE,
|
||||
attributes: {},
|
||||
relationships: {},
|
||||
},
|
||||
],
|
||||
included: [],
|
||||
};
|
||||
|
||||
let expected = {
|
||||
data: [
|
||||
{
|
||||
id: 'test',
|
||||
type: MODEL_TYPE,
|
||||
attributes: {},
|
||||
relationships: {
|
||||
updatePath: {
|
||||
data: {
|
||||
type: 'capabilities',
|
||||
id: 'update/test',
|
||||
},
|
||||
},
|
||||
deletePath: {
|
||||
data: {
|
||||
type: 'capabilities',
|
||||
id: 'delete/test',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'foo',
|
||||
type: MODEL_TYPE,
|
||||
attributes: {},
|
||||
relationships: {
|
||||
updatePath: {
|
||||
data: {
|
||||
type: 'capabilities',
|
||||
id: 'update/foo',
|
||||
},
|
||||
},
|
||||
deletePath: {
|
||||
data: {
|
||||
type: 'capabilities',
|
||||
id: 'delete/foo',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
included: [],
|
||||
};
|
||||
mc.relatedCapabilities(jsonAPIDocSingle);
|
||||
assert.deepEqual(jsonAPIDocSingle, expected, 'has the exected new document structure');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
import apiPath from 'vault/utils/api-path';
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
module('Unit | Util | api path', function() {
|
||||
test('it returns a function', function(assert) {
|
||||
let ret = apiPath`foo`;
|
||||
assert.ok(typeof ret === 'function');
|
||||
});
|
||||
|
||||
test('it iterpolates strings from passed context object', function(assert) {
|
||||
let ret = apiPath`foo/${'one'}/${'two'}`;
|
||||
let result = ret({ one: 1, two: 2 });
|
||||
|
||||
assert.equal(result, 'foo/1/2', 'returns the expected string');
|
||||
});
|
||||
|
||||
test('it throws when the key is not found in the context', function(assert) {
|
||||
let ret = apiPath`foo/${'one'}/${'two'}`;
|
||||
assert.throws(() => {
|
||||
ret({ one: 1 });
|
||||
}, /Error: Assertion Failed: Expected 2 keys in apiPath context, only recieved 1/);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue