From f8b33f171b6f205265e29eafed0fa06d3b913dcc Mon Sep 17 00:00:00 2001 From: "Brian J. Geiger" Date: Mon, 13 Nov 2023 18:16:09 -0500 Subject: [PATCH] [ENG-4681] Add mirage for v2 api (#2051) ## Purpose Make the v2 endpoints work with mirage. This includes a lot of normalization of the API but not the extra features such as extended attributes for providers nor getting the folder lists. ## Summary of Changes 1. Add mirage 2. Adjust models --- app/models/external-accounts.ts | 11 ++- app/models/node-addon.ts | 12 +++- app/models/node.ts | 4 ++ app/models/user-addon.ts | 10 ++- app/models/user.ts | 4 ++ mirage/config.ts | 14 ++++ mirage/factories/external-account.ts | 24 +++++++ mirage/factories/node-addon.ts | 22 ++++++ mirage/factories/user-addon.ts | 19 ++++++ mirage/fixtures/addons.ts | 93 ++++++++++++++++++++++++++ mirage/scenarios/dashboard.ts | 26 +++++++ mirage/scenarios/default.ts | 1 + mirage/serializers/addon.ts | 6 ++ mirage/serializers/external-account.ts | 16 +++++ mirage/serializers/node-addon.ts | 29 ++++++++ mirage/serializers/node.ts | 7 ++ mirage/serializers/user-addon.ts | 43 ++++++++++++ mirage/serializers/user.ts | 7 ++ mirage/views/external-account.ts | 33 +++++++++ mirage/views/node-addon.ts | 34 ++++++++++ 20 files changed, 410 insertions(+), 5 deletions(-) create mode 100644 mirage/factories/external-account.ts create mode 100644 mirage/factories/node-addon.ts create mode 100644 mirage/factories/user-addon.ts create mode 100644 mirage/fixtures/addons.ts create mode 100644 mirage/serializers/addon.ts create mode 100644 mirage/serializers/external-account.ts create mode 100644 mirage/serializers/node-addon.ts create mode 100644 mirage/serializers/user-addon.ts create mode 100644 mirage/views/external-account.ts create mode 100644 mirage/views/node-addon.ts diff --git a/app/models/external-accounts.ts b/app/models/external-accounts.ts index eb31df7eec9..2f5cafec353 100644 --- a/app/models/external-accounts.ts +++ b/app/models/external-accounts.ts @@ -1,11 +1,18 @@ -import { attr } from '@ember-data/model'; +import { AsyncBelongsTo, attr, belongsTo } from '@ember-data/model'; +import AddonModel from 'ember-osf-web/models/addon'; +import UserModel from 'ember-osf-web/models/user'; import OsfModel from './osf-model'; export default class ExternalAccountsModel extends OsfModel { - @attr('string') provider!: string; @attr('fixstring') profileUrl?: string; @attr('fixstring') displayName!: string; + + @belongsTo('addon', { inverse: null }) + provider!: AsyncBelongsTo & AddonModel; + + @belongsTo('user', { inverse: null} ) + user!: AsyncBelongsTo & UserModel; } declare module 'ember-data/types/registries/model' { diff --git a/app/models/node-addon.ts b/app/models/node-addon.ts index 6da8c330800..3408e8dfee5 100644 --- a/app/models/node-addon.ts +++ b/app/models/node-addon.ts @@ -1,13 +1,21 @@ -import { attr } from '@ember-data/model'; +import { AsyncBelongsTo, attr, belongsTo } from '@ember-data/model'; + +import ExternalAccountsModel from 'ember-osf-web/models/external-accounts'; +import NodeModel from 'ember-osf-web/models/node'; import OsfModel from './osf-model'; export default class NodeAddonModel extends OsfModel { @attr('boolean') nodeHasAuth!: boolean; @attr('boolean') configured!: boolean; - @attr('string') externalAccountId!: string; @attr('string') folderId?: string; @attr('string') folderPath?: string; + + @belongsTo('node', { inverse: 'nodeAddons' }) + node!: AsyncBelongsTo & NodeModel; + + @belongsTo('external-account', { inverse: null }) + externalAccount!: AsyncBelongsTo & ExternalAccountsModel; } declare module 'ember-data/types/registries/model' { diff --git a/app/models/node.ts b/app/models/node.ts index 66193720316..d11f924aa16 100644 --- a/app/models/node.ts +++ b/app/models/node.ts @@ -10,6 +10,7 @@ import Intl from 'ember-intl/services/intl'; import getRelatedHref from 'ember-osf-web/utils/get-related-href'; import AbstractNodeModel from 'ember-osf-web/models/abstract-node'; +import NodeAddonModel from 'ember-osf-web/models/node-addon'; import CitationModel from './citation'; import CommentModel from './comment'; import ContributorModel from './contributor'; @@ -177,6 +178,9 @@ export default class NodeModel extends AbstractNodeModel.extend(Validations, Col @hasMany('subject', { inverse: null, async: false }) subjects!: SubjectModel[]; + @hasMany('node-addon', { inverse: 'node' }) + nodeAddons!: AsyncHasMany; + // These are only computeds because maintaining separate flag values on // different classes would be a headache TODO: Improve. diff --git a/app/models/user-addon.ts b/app/models/user-addon.ts index e308c632253..030cd6f95a6 100644 --- a/app/models/user-addon.ts +++ b/app/models/user-addon.ts @@ -1,6 +1,8 @@ -import { AsyncHasMany, attr, hasMany } from '@ember-data/model'; +import { AsyncBelongsTo, AsyncHasMany, attr, belongsTo, hasMany } from '@ember-data/model'; +import AddonModel from 'ember-osf-web/models/addon'; import ExternalAccountsModel from 'ember-osf-web/models/external-accounts'; +import UserModel from 'ember-osf-web/models/user'; import OsfModel from './osf-model'; @@ -10,6 +12,12 @@ export default class UserAddonModel extends OsfModel { @hasMany('external-accounts', { inverse: null }) externalAccounts!: AsyncHasMany & ExternalAccountsModel[]; + + @belongsTo('user', { inverse: 'userAddons' }) + user!: AsyncBelongsTo & UserModel; + + @belongsTo('addon', { inverse: null }) + addon!: AsyncBelongsTo & AddonModel; } declare module 'ember-data/types/registries/model' { diff --git a/app/models/user.ts b/app/models/user.ts index b02912bacfe..215bf6e767a 100644 --- a/app/models/user.ts +++ b/app/models/user.ts @@ -6,6 +6,7 @@ import { Link } from 'jsonapi-typescript'; import PreprintModel from 'ember-osf-web/models/preprint'; import SparseNodeModel from 'ember-osf-web/models/sparse-node'; +import UserAddonModel from 'ember-osf-web/models/user-addon'; import ContributorModel from './contributor'; import DraftRegistrationModel from './draft-registration'; import InstitutionModel from './institution'; @@ -127,6 +128,9 @@ export default class UserModel extends OsfModel.extend(Validations) { @hasMany('sparse-node', { inverse: null }) sparseNodes!: AsyncHasMany; + @hasMany('user-addon', { inverse: 'user' }) + userAddons!: AsyncHasMany; + // Calculated fields @alias('links.html') profileURL!: string; @alias('links.profile_image') profileImage!: string; diff --git a/mirage/config.ts b/mirage/config.ts index 8f1437b91d2..085436b8a15 100644 --- a/mirage/config.ts +++ b/mirage/config.ts @@ -1,6 +1,8 @@ import { Server } from 'ember-cli-mirage'; import config from 'ember-osf-web/config/environment'; +import { externalAccountDetail, externalAccountList } from 'ember-osf-web/mirage/views/external-account'; +import { nodeAddonDetail, nodeAddonList } from 'ember-osf-web/mirage/views/node-addon'; import { createReviewAction } from 'ember-osf-web/mirage/views/review-action'; import { createResource, updateResource } from 'ember-osf-web/mirage/views/resource'; import { getCitation } from './views/citation'; @@ -72,10 +74,13 @@ export default function(this: Server) { this.get('/', rootDetail); + osfResource(this, 'addon', { only: ['index']}); osfResource(this, 'developer-app', { path: 'applications', except: ['create', 'update'] }); this.post('/applications', createDeveloperApp); this.patch('/applications/:id', updateDeveloperApp); + osfResource(this, 'external-account', {only: ['index', 'show', 'create']}); + osfResource(this, 'file', { only: ['show', 'update'] }); this.get('/guids/:id', guidDetail); @@ -110,6 +115,9 @@ export default function(this: Server) { defaultSortKey: 'index', onCreate: createBibliographicContributor, }); + this.get('/nodes/:parentID/addons/:id', nodeAddonDetail); + this.get('/nodes/:parentID/addons/', nodeAddonList); + osfNestedResource(this, 'node', 'nodeAddons', {except: [ 'index', 'show' ]}); this.get('/nodes/:parentID/files', nodeFileProviderList); // Node file providers list this.get('/nodes/:parentID/files/:fileProviderId', nodeFilesListForProvider); // Node files list for file provider @@ -280,6 +288,12 @@ export default function(this: Server) { this.post('/users/:id/settings/export', userSettings.requestExport); this.post('/users/:parentID/settings/password/', updatePassword); this.post('/users/:parentID/claim/', claimUnregisteredUser); + osfNestedResource(this, 'user', 'userAddons', { + path: '/users/:parentID/addons/', + relatedModelName: 'user-addon', + }); + this.get('/users/:userID/addons/:addonID/accounts', externalAccountList); + this.get('/users/:userID/addons/:addonID/accounts/:accountID', externalAccountDetail); osfResource(this, 'external-identity', { path: '/users/me/settings/identities', diff --git a/mirage/factories/external-account.ts b/mirage/factories/external-account.ts new file mode 100644 index 00000000000..b4099713c6b --- /dev/null +++ b/mirage/factories/external-account.ts @@ -0,0 +1,24 @@ +import { Factory } from 'ember-cli-mirage'; +import faker from 'faker'; + +import ExternalAccountsModel from 'ember-osf-web/models/external-accounts'; + +export default Factory.extend({ + profileUrl: faker.internet.url, + + displayName() { + return faker.name.findName(); + }, +}); + +declare module 'ember-cli-mirage/types/registries/model' { + export default interface MirageModelRegistry { + 'external-account': ExternalAccountsModel; + } // eslint-disable-line semi +} + +declare module 'ember-cli-mirage/types/registries/schema' { + export default interface MirageSchemaRegistry { + externalAccounts: ExternalAccountsModel; + } // eslint-disable-line semi +} diff --git a/mirage/factories/node-addon.ts b/mirage/factories/node-addon.ts new file mode 100644 index 00000000000..66aede17fab --- /dev/null +++ b/mirage/factories/node-addon.ts @@ -0,0 +1,22 @@ +import { Factory } from 'ember-cli-mirage'; + +import NodeAddonModel from 'ember-osf-web/models/node-addon'; + +export default Factory.extend({ + nodeHasAuth: false, + configured: false, + folderId: null, + folderPath: null, +}); + +declare module 'ember-cli-mirage/types/registries/model' { + export default interface MirageModelRegistry { + 'node-addon': NodeAddonModel; + } // eslint-disable-line semi +} + +declare module 'ember-cli-mirage/types/registries/schema' { + export default interface MirageSchemaRegistry { + nodeAddons: NodeAddonModel; + } // eslint-disable-line semi +} diff --git a/mirage/factories/user-addon.ts b/mirage/factories/user-addon.ts new file mode 100644 index 00000000000..3cd87d40582 --- /dev/null +++ b/mirage/factories/user-addon.ts @@ -0,0 +1,19 @@ +import { Factory } from 'ember-cli-mirage'; + +import UserAddonModel from 'ember-osf-web/models/user-addon'; + +export default Factory.extend({ + userHasAuth: true, +}); + +declare module 'ember-cli-mirage/types/registries/model' { + export default interface MirageModelRegistry { + 'user-addon': UserAddonModel; + } // eslint-disable-line semi +} + +declare module 'ember-cli-mirage/types/registries/schema' { + export default interface MirageSchemaRegistry { + userAddons: UserAddonModel; + } // eslint-disable-line semi +} diff --git a/mirage/fixtures/addons.ts b/mirage/fixtures/addons.ts new file mode 100644 index 00000000000..f69ed177809 --- /dev/null +++ b/mirage/fixtures/addons.ts @@ -0,0 +1,93 @@ +export default [ + { + id: 'box', + name: 'Box', + categories: [ + 'storage', + ], + }, + { + id: 'dataverse', + name: 'Dataverse', + categories: [ + 'storage', + ], + }, + { + id: 'dropbox', + name: 'Dropbox', + categories: [ + 'storage', + ], + }, + { + id: 'figshare', + name: 'figshare', + categories: [ + 'storage', + ], + }, + { + id: 'github', + name: 'GitHub', + categories: [ + 'storage', + ], + }, + { + id: 'gitlab', + name: 'GitLab', + categories: [ + 'storage', + ], + }, + { + id: 'mendeley', + name: 'Mendeley', + categories: [ + 'citations', + ], + }, + { + id: 'zotero', + name: 'Zotero', + categories: [ + 'citations', + ], + }, + { + id: 'owncloud', + name: 'ownCloud', + categories: [ + 'storage', + ], + }, + { + id: 'onedrive', + name: 'OneDrive', + categories: [ + 'storage', + ], + }, + { + id: 's3', + name: 'Amazon S3', + categories: [ + 'storage', + ], + }, + { + id: 'googledrive', + name: 'Google Drive', + categories: [ + 'storage', + ], + }, + { + id: 'bitbucket', + name: 'Bitbucket', + categories: [ + 'storage', + ], + }, +]; diff --git a/mirage/scenarios/dashboard.ts b/mirage/scenarios/dashboard.ts index cfd51f3f8e4..85e934583dc 100644 --- a/mirage/scenarios/dashboard.ts +++ b/mirage/scenarios/dashboard.ts @@ -1,5 +1,6 @@ import { ModelInstance, Server } from 'ember-cli-mirage'; import config from 'ember-osf-web/config/environment'; +import AddonModel from 'ember-osf-web/models/addon'; import { Permission } from 'ember-osf-web/models/osf-model'; import User from 'ember-osf-web/models/user'; @@ -57,6 +58,31 @@ export function dashboardScenario(server: Server, currentUser: ModelInstance; + const dropboxAccount = server.create('external-account', { + displayName: 'Bugs Bunny', + provider: dropbox, + }); + const dropboxAccountTwo = server.create('external-account', { + displayName: 'Daffy Duck', + provider: dropbox, + }); + server.create('user-addon', { + id: 'dropbox', + externalAccounts: [ dropboxAccount, dropboxAccountTwo ], + userHasAuth: true, + user: currentUser, + addon: dropbox, + }); + server.create('node-addon', { + nodeHasAuth: true, + folderId: '/', + folderPath: '/', + externalAccount: dropboxAccount, + node: filesNode, + }); + // NOTE: Some institutions are already created by this point server.createList('institution', 20); // Create a specific institution to test institutional dashboard with; should be ID 29 at this point diff --git a/mirage/scenarios/default.ts b/mirage/scenarios/default.ts index 3aeb1e86dcc..53ae240163b 100644 --- a/mirage/scenarios/default.ts +++ b/mirage/scenarios/default.ts @@ -22,6 +22,7 @@ export default function(server: Server) { server.loadFixtures('regions'); server.loadFixtures('preprint-providers'); server.loadFixtures('licenses'); + server.loadFixtures('addons'); // server.loadFixtures('registration-providers'); const userTraits = !mirageScenarios.includes('loggedIn') ? [] diff --git a/mirage/serializers/addon.ts b/mirage/serializers/addon.ts new file mode 100644 index 00000000000..354097f9c50 --- /dev/null +++ b/mirage/serializers/addon.ts @@ -0,0 +1,6 @@ +import AddonModel from 'ember-osf-web/models/addon'; + +import ApplicationSerializer from './application'; + +export default class AddonSerializer extends ApplicationSerializer { +} diff --git a/mirage/serializers/external-account.ts b/mirage/serializers/external-account.ts new file mode 100644 index 00000000000..be8cc3180fa --- /dev/null +++ b/mirage/serializers/external-account.ts @@ -0,0 +1,16 @@ +import { ModelInstance } from 'ember-cli-mirage'; +import config from 'ember-osf-web/config/environment'; + +import ExternalAccountsModel from 'ember-osf-web/models/external-accounts'; + +import ApplicationSerializer from './application'; + +const { OSF: { apiUrl } } = config; + +export default class ExternalAccountSerializer extends ApplicationSerializer { + buildNormalLinks(model: ModelInstance) { + return { + self: `${apiUrl}/v2/users/me/addons/${model.provider}/accounts/${model.id}`, + }; + } +} diff --git a/mirage/serializers/node-addon.ts b/mirage/serializers/node-addon.ts new file mode 100644 index 00000000000..af5e7b7e4ad --- /dev/null +++ b/mirage/serializers/node-addon.ts @@ -0,0 +1,29 @@ +import { ModelInstance } from 'ember-cli-mirage'; +import config from 'ember-osf-web/config/environment'; + +import NodeAddonModel from 'ember-osf-web/models/node-addon'; + +import ApplicationSerializer from './application'; + +const { OSF: { apiUrl } } = config; + +export default class NodeAddonSerializer extends ApplicationSerializer { + buildRelationships(model: ModelInstance) { + return { + node: { + links: { + related: { + href: `${apiUrl}/v2/nodes/${model.node.id}`, + meta: this.buildRelatedLinkMeta(model, 'node'), + }, + }, + }, + }; + } + + buildNormalLinks(model: ModelInstance) { + return { + self: `${apiUrl}/v2/nodes/${model.node.id}/addons/${model.id}`, + }; + } +} diff --git a/mirage/serializers/node.ts b/mirage/serializers/node.ts index 4e93202c0ad..4be5591accd 100644 --- a/mirage/serializers/node.ts +++ b/mirage/serializers/node.ts @@ -133,6 +133,13 @@ export default class NodeSerializer extends ApplicationSerializer { }, }, }, + nodeAddons: { + links: { + related: { + href: `${apiUrl}/v2/nodes/${model.id}/addons/`, + }, + }, + }, }; if (model.attrs.parentId !== null) { const { parentId } = model.attrs; diff --git a/mirage/serializers/user-addon.ts b/mirage/serializers/user-addon.ts new file mode 100644 index 00000000000..fc3e74ba847 --- /dev/null +++ b/mirage/serializers/user-addon.ts @@ -0,0 +1,43 @@ +import { ModelInstance } from 'ember-cli-mirage'; +import config from 'ember-osf-web/config/environment'; + +import UserAddonModel from 'ember-osf-web/models/user-addon'; + +import ApplicationSerializer from './application'; + +const { OSF: { apiUrl } } = config; + +export default class UserAddonSerializer extends ApplicationSerializer { + buildRelationships(model: ModelInstance) { + return { + user: { + links: { + related: { + href: `${apiUrl}/v2/users/${model.user.id}`, + meta: this.buildRelatedLinkMeta(model, 'user'), + }, + }, + }, + externalAccounts: { + links: { + related: { + href: `${apiUrl}/v2/users/${model.user.id}/addons/${model.id}/accounts/`, + }, + }, + }, + addon: { + links: { + related: { + href: `${apiUrl}/v2/addons/${model.addon.id}`, + }, + }, + }, + }; + } + + buildNormalLinks(model: ModelInstance) { + return { + self: `${apiUrl}/v2/users/${model.user.id}/addons/${model.id}`, + }; + } +} diff --git a/mirage/serializers/user.ts b/mirage/serializers/user.ts index fef5af248b8..179d86de68e 100644 --- a/mirage/serializers/user.ts +++ b/mirage/serializers/user.ts @@ -66,6 +66,13 @@ export default class UserSerializer extends ApplicationSerializer { }, }, }, + userAddons: { + links: { + related: { + href: `${apiUrl}/v2/users/${model.id}/addons/`, + }, + }, + }, }; if (model.defaultRegion) { serializedRelationships.defaultRegion = { diff --git a/mirage/views/external-account.ts b/mirage/views/external-account.ts new file mode 100644 index 00000000000..fd438ecf6b9 --- /dev/null +++ b/mirage/views/external-account.ts @@ -0,0 +1,33 @@ +import { HandlerContext, ModelInstance, Request, Schema } from 'ember-cli-mirage'; + +import ExternalAccountsModel from 'ember-osf-web/models/external-accounts'; +import { process } from './utils'; + +export function externalAccountDetail(this: HandlerContext, schema: Schema, request: Request) { + const { accountID } = request.params; + const externalAccount = schema.externalAccounts.find(accountID); + + const serializedExternalAccount = this.serialize(externalAccount); + const { data } = process(schema, request, this, [serializedExternalAccount.data]); + + return { + data: data[0], + meta: serializedExternalAccount.meta, + }; +} + +export function externalAccountList(this: HandlerContext, schema: Schema, request: Request) { + const userAddons = schema.users + .find(request.params.userID)['userAddons'] + .models; + const externalAccounts = userAddons.length ? userAddons[0].externalAccounts.models : null; + if (externalAccounts) { + externalAccounts + .filter( (item: ModelInstance) => item.provider.id === request.params.addonID ) + .map((model: ModelInstance) => this.serialize(model).data); + + const json = process(schema, request, this, externalAccounts, { defaultSortKey: 'id' }); + return json; + } + return {}; +} diff --git a/mirage/views/node-addon.ts b/mirage/views/node-addon.ts new file mode 100644 index 00000000000..3b6b555c092 --- /dev/null +++ b/mirage/views/node-addon.ts @@ -0,0 +1,34 @@ +import { HandlerContext, ModelInstance, Request, Schema } from 'ember-cli-mirage'; + +import NodeAddonModel from 'ember-osf-web/models/node-addon'; +import { filter, process } from './utils'; + +export function nodeAddonDetail(this: HandlerContext, schema: Schema, request: Request) { + const { id } = request.params; + const nodeAddon = schema.nodeAddons.find(id); + + nodeAddon.configured = Boolean(nodeAddon.folderPath); + + const serializedNodeAddon = this.serialize(nodeAddon); + const { data } = process(schema, request, this, [serializedNodeAddon.data]); + + return { + data: data[0], + meta: serializedNodeAddon.meta, + }; +} + +export function nodeAddonList(this: HandlerContext, schema: Schema, request: Request) { + const nodeAddons = schema.nodes + .find(request.params.parentID)['nodeAddons'] + .models + .filter((m: ModelInstance) => filter(m, request)) + .map((model: ModelInstance) => { + model.configured = Boolean(model.folderPath); + return model; + }) + .map((model: ModelInstance) => this.serialize(model).data); + + const json = process(schema, request, this, nodeAddons, { defaultSortKey: 'last_logged' }); + return json; +}