From 40a43fd9bee4963fcd05cf3f8f0d9ae50d90202a Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Tue, 1 Sep 2020 10:42:16 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20group=20custom=20m?= =?UTF-8?q?ethods=20for=20add/remove=20members?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/api/addon/models/group.js | 88 +++++++++++- addons/api/addon/serializers/group.js | 34 +++++ addons/api/app/serializers/group.js | 1 + addons/api/tests/unit/models/group-test.js | 129 +++++++++++++++++- .../api/tests/unit/serializers/group-test.js | 48 +++++++ 5 files changed, 295 insertions(+), 5 deletions(-) create mode 100644 addons/api/addon/serializers/group.js create mode 100644 addons/api/app/serializers/group.js create mode 100644 addons/api/tests/unit/serializers/group-test.js diff --git a/addons/api/addon/models/group.js b/addons/api/addon/models/group.js index 899c71d0c..64e9b2c9d 100644 --- a/addons/api/addon/models/group.js +++ b/addons/api/addon/models/group.js @@ -1,3 +1,89 @@ import GeneratedGroupModel from '../generated/models/group'; +import { attr } from '@ember-data/model'; +import { computed } from '@ember/object'; -export default class GroupModel extends GeneratedGroupModel {} +export default class GroupModel extends GeneratedGroupModel { + + // =attributes + + /** + * Members is read-only under normal circumstances. But members can + * be persisted via calls to `addMembers()` or `removeMembers()`. + */ + @attr( {readOnly: true}) member_ids; + + /** + * An array of resolved user instances. + * Unresolvable instances are excluded from the array. + * @type {[UserModel]} + */ + @computed('member_ids.[]') + get members() { + return this.member_ids + .map(id => this.store.peekRecord('user', id)) + .filter(Boolean); + } + + // =methods + + /** + * Adds members via the `add-members` method. + * See serializer and adapter for more information. + * @param {[string]} memberIDs + * @param {object} options + * @param {object} options.adapterOptions + * @return {Promise} + */ + addMembers(memberIDs, options={ adapterOptions: {} }) { + const defaultAdapterOptions = { + method: 'add-members', + memberIDs + }; + // There is no "deep merge" in ES. + // All of this nonsense is here to ensure we get + // a decent merge of `adapterOptions`. + return this.save({ + ...options, + adapterOptions: { + ...defaultAdapterOptions, + ...options.adapterOptions + } + }); + } + + /** + * Delete members via the `remove-members` method. + * See serializer and adapter for more information. + * @param {[string]} memberIDs + * @param {object} options + * @param {object} options.adapterOptions + * @return {Promise} + */ + removeMembers(memberIDs, options={ adapterOptions: {} }) { + const defaultAdapterOptions = { + method: 'remove-members', + memberIDs + }; + // There is no "deep merge" in ES. + // All of this nonsense is here to ensure we get + // a decent merge of `adapterOptions`. + return this.save({ + ...options, + adapterOptions: { + ...defaultAdapterOptions, + ...options.adapterOptions + } + }); + } + + /** + * Delete a single member via the `remove-members` method. + * @param {number} memberID + * @param {object} options + * @return {Promise} + */ + removeMember(memberID, options) { + return this.removeMembers([memberID], options); + } + +} diff --git a/addons/api/addon/serializers/group.js b/addons/api/addon/serializers/group.js new file mode 100644 index 000000000..bbccc068f --- /dev/null +++ b/addons/api/addon/serializers/group.js @@ -0,0 +1,34 @@ +import ApplicationSerializer from './application'; + +export default class GroupSerializer extends ApplicationSerializer { + + // =methods + + /** + * If `adapterOptions.memberIDs` is set (to an array of user IDs), + * then the payload is serialized via `serializewithMembers`. + * @override + * @param {Snapshot} snapshot + * @return {object} + */ + serialize(snapshot) { + let serialized = super.serialize(...arguments); + const memberIDs = snapshot?.adapterOptions?.memberIDs; + if (memberIDs) serialized = this.serializewithMembers(snapshot, memberIDs); + return serialized; + } + + /** + * Returns a payload containing only the `member_ids` array (and version). + * @param {Snapshot} snapshot + * @param {[string]} memberIDs + * @return {object} + */ + serializewithMembers(snapshot, memberIDs) { + return { + version: snapshot.attr('version'), + member_ids: memberIDs + }; + } + +} diff --git a/addons/api/app/serializers/group.js b/addons/api/app/serializers/group.js new file mode 100644 index 000000000..d469741cc --- /dev/null +++ b/addons/api/app/serializers/group.js @@ -0,0 +1 @@ +export { default } from 'api/serializers/group'; diff --git a/addons/api/tests/unit/models/group-test.js b/addons/api/tests/unit/models/group-test.js index 4ab06e07a..75216bf6f 100644 --- a/addons/api/tests/unit/models/group-test.js +++ b/addons/api/tests/unit/models/group-test.js @@ -1,12 +1,133 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; module('Unit | Model | group', function(hooks) { setupTest(hooks); + setupMirage(hooks); - test('it exists', function(assert) { - let store = this.owner.lookup('service:store'); - let model = store.createRecord('group', {}); - assert.ok(model); + test('it has a `members` array of resolved model instances (if those instances are already in the store)', function(assert) { + assert.expect(4); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: 'group_123', + type: 'group', + attributes: { + member_ids: [ '1', '2', ] + } + } + }); + const group = store.peekRecord('group', 'group_123'); + assert.equal(group.member_ids.length, 2, 'Group has two entires in member_ids'); + assert.equal(group.members.length, 0, 'Group has no resolved members because they are not loaded yet'); + store.push({ + data: { + id: '1', + type: 'user', + attributes: {} + } + }); + store.push({ + data: { + id: '2', + type: 'user', + attributes: {} + } + }); + // eslint-disable-next-line no-self-assign + group.member_ids = [...group.member_ids]; + assert.equal(group.member_ids.length, 2, 'Group has two entires in member_ids'); + assert.equal(group.members.length, 2, 'Group has two resolved members'); + }); + + test('it has an `addMembers` method that groups a specific POST API endpoint and serialization', async function (assert) { + assert.expect(1); + this.server.post('/v1/scopes/o_1/groups/123abc:add-members', (schema, request) => { + const body = JSON.parse(request.requestBody); + assert.deepEqual(body, { + member_ids: ['123_abc', 'foobar'], + version: 1 + }); + }); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '123abc', + type: 'group', + attributes: { + name: 'Group', + description: 'Description', + member_ids: [ '1', '2' ], + version: 1, + scope: { + scope_id: 'o_1', + type: 'scope' + } + } + } + }); + const model = store.peekRecord('group', '123abc'); + await model.addMembers(['123_abc', 'foobar']); + }); + + test('it has a `removeMembers` method that groups a specific POST API endpoint and serialization', async function (assert) { + assert.expect(1); + this.server.post('/v1/scopes/o_1/groups/123abc:remove-members', (schema, request) => { + const body = JSON.parse(request.requestBody); + assert.deepEqual(body, { + member_ids: ['1', '3'], + version: 1 + }); + }); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '123abc', + type: 'group', + attributes: { + name: 'group', + description: 'Description', + member_ids: [ '4', '5' ], + version: 1, + scope: { + scope_id: 'o_1', + type: 'scope' + } + } + } + }); + const model = store.peekRecord('group', '123abc'); + await model.removeMembers(['1', '3']); + }); + + test('it has a `removeMember` method that deletes a single member set using `removeMembers` method', async function (assert) { + assert.expect(1); + this.server.post('/v1/scopes/o_1/groups/123abc:remove-members', (schema, request) => { + const body = JSON.parse(request.requestBody); + assert.deepEqual(body, { + member_ids: ['3'], + version: 1 + }); + }); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '123abc', + type: 'group', + attributes: { + name: 'group', + description: 'Description', + member_ids: [ '1', '3' ], + version: 1, + scope: { + scope_id: 'o_1', + type: 'scope' + } + } + } + }); + const model = store.peekRecord('group', '123abc'); + await model.removeMember('3'); }); }); diff --git a/addons/api/tests/unit/serializers/group-test.js b/addons/api/tests/unit/serializers/group-test.js new file mode 100644 index 000000000..2a7da2b06 --- /dev/null +++ b/addons/api/tests/unit/serializers/group-test.js @@ -0,0 +1,48 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Serializer | group', function(hooks) { + setupTest(hooks); + + test('it serializes groups normally, without members', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('group'); + const record = store.createRecord('group', { + name: 'Group', + description: 'Description', + member_ids: [ '1', '2', ], + version: 1 + }); + const snapshot = record._createSnapshot(); + snapshot.adapterOptions = {}; + const serializedRecord = serializer.serialize(snapshot); + assert.deepEqual(serializedRecord, { + name: 'Group', + description: 'Description', + version: 1 + }); + }); + + test('it serializes only host sets and version when an `adapterOptions.memberIDs` array is passed', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('group'); + const record = store.createRecord('group', { + name: 'Group', + description: 'Description', + member_ids: [ '1', '2', ], + version: 1 + }); + const snapshot = record._createSnapshot(); + snapshot.adapterOptions = { + memberIDs: ['4', '5'] + }; + const serializedRecord = serializer.serialize(snapshot); + assert.deepEqual(serializedRecord, { + member_ids: ['4', '5'], + version: 1 + }); + }); + +});