From 715805f0de46c957c14c8cd18bf80da4b2e7ff13 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Wed, 30 Sep 2020 10:08:24 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20API=20support=20fo?= =?UTF-8?q?r=20user=20accounts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/api/addon/models/user.js | 81 ++++++++++++- addons/api/addon/serializers/user.js | 33 ++++++ addons/api/app/serializers/user.js | 1 + addons/api/tests/unit/models/user-test.js | 111 +++++++++++++++++- .../unit/serializers/application-test.js | 6 +- .../api/tests/unit/serializers/user-test.js | 77 ++++++++++++ 6 files changed, 303 insertions(+), 6 deletions(-) create mode 100644 addons/api/addon/serializers/user.js create mode 100644 addons/api/app/serializers/user.js create mode 100644 addons/api/tests/unit/serializers/user-test.js diff --git a/addons/api/addon/models/user.js b/addons/api/addon/models/user.js index 51053788f..c2be5cfb0 100644 --- a/addons/api/addon/models/user.js +++ b/addons/api/addon/models/user.js @@ -1,3 +1,82 @@ import GeneratedUserModel from '../generated/models/user'; +import { attr } from '@ember-data/model'; +import { A } from '@ember/array'; -export default class UserModel extends GeneratedUserModel {} +export default class UserModel extends GeneratedUserModel { + + // =attributes + + /** + * Accounts is read-only under normal circumstances. But accounts can + * be persisted via calls to `addAccounts()` or `removeAccounts()`. + */ + @attr({ + readOnly: true, + defaultValue: () => A(), + emptyArrayIfMissing: true, + }) + account_ids; + + // =methods + + /** + * Adds accounts via the `add-accounts` method. + * See serializer and adapter for more information. + * @param {[string]} accountIDs + * @param {object} options + * @param {object} options.adapterOptions + * @return {Promise} + */ + addAccounts(accountIDs, options = { adapterOptions: {} }) { + const defaultAdapterOptions = { + method: 'add-accounts', + accountIDs, + }; + // 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, + }, + }); + } + + /** + * Remove accounts via the `remove-accounts` method. + * See serializer and adapter for more information. + * @param {[string]} accountIDs + * @param {object} options + * @param {object} options.adapterOptions + * @return {Promise} + */ + removeAccounts(accountIDs, options = { adapterOptions: {} }) { + const defaultAdapterOptions = { + method: 'remove-accounts', + accountIDs, + }; + // 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, + }, + }); + } + + /** + * Remove a single account via the `remove-accounts` method. + * @param {number} accountID + * @param {object} options + * @return {Promise} + */ + removeAccount(accountID, options) { + return this.removeAccounts([accountID], options); + } + +} diff --git a/addons/api/addon/serializers/user.js b/addons/api/addon/serializers/user.js new file mode 100644 index 000000000..9c4a47194 --- /dev/null +++ b/addons/api/addon/serializers/user.js @@ -0,0 +1,33 @@ +import ApplicationSerializer from './application'; + +export default class UserSerializer extends ApplicationSerializer { + // =methods + + /** + * If `adapterOptions.accountIDs` is set (to an array of user IDs), + * then the payload is serialized via `serializewithAccounts`. + * @override + * @param {Snapshot} snapshot + * @return {object} + */ + serialize(snapshot) { + let serialized = super.serialize(...arguments); + const accountIDs = snapshot?.adapterOptions?.accountIDs; + if (accountIDs) serialized = + this.serializewithAccounts(snapshot, accountIDs); + return serialized; + } + + /** + * Returns a payload containing only the `account_ids` array (and version). + * @param {Snapshot} snapshot + * @param {[string]} account_ids + * @return {object} + */ + serializewithAccounts(snapshot, account_ids) { + return { + version: snapshot.attr('version'), + account_ids, + }; + } +} diff --git a/addons/api/app/serializers/user.js b/addons/api/app/serializers/user.js new file mode 100644 index 000000000..5c5a92643 --- /dev/null +++ b/addons/api/app/serializers/user.js @@ -0,0 +1 @@ +export { default } from 'api/serializers/user'; diff --git a/addons/api/tests/unit/models/user-test.js b/addons/api/tests/unit/models/user-test.js index df69a6501..75a8f0916 100644 --- a/addons/api/tests/unit/models/user-test.js +++ b/addons/api/tests/unit/models/user-test.js @@ -1,12 +1,115 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; module('Unit | Model | user', function (hooks) { setupTest(hooks); + setupMirage(hooks); - test('it exists', function (assert) { - let store = this.owner.lookup('service:store'); - let model = store.createRecord('user', {}); - assert.ok(model); + test('it has an `addAccounts` method that users a specific POST API endpoint and serialization', async function (assert) { + assert.expect(1); + this.server.post('/v1/users/123abc:add-accounts', (schema, request) => { + const body = JSON.parse(request.requestBody); + assert.deepEqual(body, { + account_ids: ['123_abc', 'foobar'], + version: 1, + }); + }); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '123abc', + type: 'user', + attributes: { + name: 'User', + description: 'Description', + account_ids: ['1', '2'], + version: 1, + scope: { + scope_id: 'o_1', + type: 'scope', + }, + }, + }, + }); + const model = store.peekRecord('user', '123abc'); + await model.addAccounts(['123_abc', 'foobar']); + }); + + test('it has a `removeAccounts` method that users a specific POST API endpoint and serialization', async function (assert) { + assert.expect(1); + this.server.post('/v1/users/123abc:remove-accounts', (schema, request) => { + const body = JSON.parse(request.requestBody); + assert.deepEqual(body, { + account_ids: ['1', '3'], + version: 1, + }); + }); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '123abc', + type: 'user', + attributes: { + name: 'user', + description: 'Description', + account_ids: ['4', '5'], + version: 1, + scope: { + scope_id: 'o_1', + type: 'scope', + }, + }, + }, + }); + const model = store.peekRecord('user', '123abc'); + await model.removeAccounts(['1', '3']); + }); + + test('it has a `removeAccount` method that deletes a single account set using `removeAccounts` method', async function (assert) { + assert.expect(1); + this.server.post('/v1/users/123abc:remove-accounts', (schema, request) => { + const body = JSON.parse(request.requestBody); + assert.deepEqual(body, { + account_ids: ['3'], + version: 1, + }); + }); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '123abc', + type: 'user', + attributes: { + name: 'user', + description: 'Description', + account_ids: ['1', '3'], + version: 1, + scope: { + scope_id: 'o_1', + type: 'scope', + }, + }, + }, + }); + const model = store.peekRecord('user', '123abc'); + await model.removeAccount('3'); + }); + + test('it defaults `accounts_ids` to an empty array when model instance does not define it', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: 'user_123', + type: 'user', + }, + }); + const user = store.peekRecord('user', 'user_123'); + assert.equal( + user.account_ids.length, + 0, + 'User has empty account_ids by default' + ); }); }); diff --git a/addons/api/tests/unit/serializers/application-test.js b/addons/api/tests/unit/serializers/application-test.js index 5c2c07a20..10de9d4fb 100644 --- a/addons/api/tests/unit/serializers/application-test.js +++ b/addons/api/tests/unit/serializers/application-test.js @@ -154,7 +154,11 @@ module('Unit | Serializer | application', function (hooks) { data: { id: '1', type: 'user', - attributes: { name: 'User 1', scope: { scope_id: 'o_123' } }, + attributes: { + name: 'User 1', + account_ids: [], + scope: { scope_id: 'o_123' } + }, relationships: {}, }, }); diff --git a/addons/api/tests/unit/serializers/user-test.js b/addons/api/tests/unit/serializers/user-test.js new file mode 100644 index 000000000..c0d2bd061 --- /dev/null +++ b/addons/api/tests/unit/serializers/user-test.js @@ -0,0 +1,77 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Serializer | user', function (hooks) { + setupTest(hooks); + + test('it serializes users normally, without accounts', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('user'); + const record = store.createRecord('user', { + name: 'user', + description: 'Description', + account_ids: ['1', '2'], + version: 1, + }); + const snapshot = record._createSnapshot(); + snapshot.adapterOptions = {}; + const serializedRecord = serializer.serialize(snapshot); + assert.deepEqual(serializedRecord, { + name: 'user', + description: 'Description', + version: 1, + }); + }); + + test('it serializes only host sets and version when an `adapterOptions.accountIDs` array is passed', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('user'); + const record = store.createRecord('user', { + name: 'user', + description: 'Description', + account_ids: ['1', '2'], + version: 1, + }); + const snapshot = record._createSnapshot(); + snapshot.adapterOptions = { + accountIDs: ['4', '5'], + }; + const serializedRecord = serializer.serialize(snapshot); + assert.deepEqual(serializedRecord, { + account_ids: ['4', '5'], + version: 1, + }); + }); + + test('it normalizes missing account_ids to empty array', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('user'); + const user = store.createRecord('user').constructor; + const payload = { + id: '1', + name: 'user 1', + scope: { id: 'o_123' }, + }; + const normalized = serializer.normalizeSingleResponse( + store, + user, + payload + ); + assert.deepEqual(normalized, { + included: [], + data: { + id: '1', + type: 'user', + attributes: { + name: 'user 1', + scope: { scope_id: 'o_123' }, + account_ids: [], + }, + relationships: {}, + }, + }); + }); +});