• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CenterForOpenScience / ember-osf-web / 14961817356

12 May 2025 01:24AM UTC coverage: 67.414%. First build
14961817356

Pull #2559

github

web-flow
Merge 91fa8fad3 into a28878b48
Pull Request #2559: [ENG-7575][ENG-7782]Link service project workflow

3241 of 5292 branches covered (61.24%)

Branch coverage included in aggregate %.

18 of 50 new or added lines in 6 files covered. (36.0%)

8485 of 12102 relevant lines covered (70.11%)

184.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

55.91
/app/packages/addons-service/provider.ts
1
import { getOwner, setOwner } from '@ember/application';
2
import EmberArray from '@ember/array';
3
import { inject as service } from '@ember/service';
4
import { waitFor } from '@ember/test-waiters';
5
import Store from '@ember-data/store';
6
import { tracked } from '@glimmer/tracking';
7
import { Task, task } from 'ember-concurrency';
8
import { taskFor } from 'ember-concurrency-ts';
9
import Intl from 'ember-intl/services/intl';
10
import Toast from 'ember-toastr/services/toast';
11

12
import NodeModel from 'ember-osf-web/models/node';
13
import CurrentUserService from 'ember-osf-web/services/current-user';
14
import UserReferenceModel from 'ember-osf-web/models/user-reference';
15
import ResourceReferenceModel from 'ember-osf-web/models/resource-reference';
16
import ConfiguredStorageAddonModel from 'ember-osf-web/models/configured-storage-addon';
17
import ConfiguredCitationAddonModel from 'ember-osf-web/models/configured-citation-addon';
18
import ConfiguredComputingAddonModel from 'ember-osf-web/models/configured-computing-addon';
19
import { AccountCreationArgs } from 'ember-osf-web/models/authorized-account';
20
import AuthorizedStorageAccountModel from 'ember-osf-web/models/authorized-storage-account';
21
import AuthorizedCitationAccountModel from 'ember-osf-web/models/authorized-citation-account';
22
import AuthorizedComputingAccountModel from 'ember-osf-web/models/authorized-computing-account';
23
import ExternalStorageServiceModel from 'ember-osf-web/models/external-storage-service';
24
import ExternalComputingServiceModel from 'ember-osf-web/models/external-computing-service';
25
import ExternalCitationServiceModel from 'ember-osf-web/models/external-citation-service';
26
import { notifyPropertyChange } from '@ember/object';
27
import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception';
28
import ExternalLinkServiceModel from 'ember-osf-web/models/external-link-service';
29
import AuthorizedLinkAccountModel from 'ember-osf-web/models/authorized-link-account';
30
import ConfiguredLinkAddonModel from 'ember-osf-web/models/configured-link-addon';
31

32
export type AllProviderTypes =
33
    ExternalStorageServiceModel |
34
    ExternalComputingServiceModel |
35
    ExternalCitationServiceModel |
36
    ExternalLinkServiceModel;
37
export type AllAuthorizedAccountTypes =
38
    AuthorizedStorageAccountModel |
39
    AuthorizedCitationAccountModel |
40
    AuthorizedComputingAccountModel |
41
    AuthorizedLinkAccountModel;
42
export type AllConfiguredAddonTypes =
43
    ConfiguredStorageAddonModel |
44
    ConfiguredCitationAddonModel |
45
    ConfiguredComputingAddonModel |
46
    ConfiguredLinkAddonModel;
47

48
interface ProviderTypeMapper {
49
    getAuthorizedAccounts: Task<any, any>;
50
    createAuthorizedAccount: Task<any, any>;
51
    createConfiguredAddon: Task<any, any>;
52
}
53

54

55
export default class Provider {
56
    @service toast!: Toast;
57
    @service intl!: Intl;
58

59
    @tracked node?: NodeModel;
60
    @tracked serviceNode?: ResourceReferenceModel | null;
61

62
    currentUser: CurrentUserService;
63
    @tracked userReference!: UserReferenceModel;
64
    provider: AllProviderTypes;
65
    private providerMap?: ProviderTypeMapper;
66

67
    get displayName() {
68
        return this.provider.displayName;
60✔
69
    }
70

71
    get id() {
72
        return this.provider.id;
5✔
73
    }
74

75
    providerTypeMapper: Record<string, ProviderTypeMapper>  = {
77✔
76
        externalStorageService: {
77
            getAuthorizedAccounts: taskFor(this.getAuthorizedStorageAccounts),
78
            createAuthorizedAccount: taskFor(this.createAuthorizedStorageAccount),
79
            createConfiguredAddon: taskFor(this.createConfiguredStorageAddon),
80
        },
81
        externalComputingService: {
82
            getAuthorizedAccounts: taskFor(this.getAuthorizedComputingAccounts),
83
            createAuthorizedAccount: taskFor(this.createAuthorizedComputingAccount),
84
            createConfiguredAddon: taskFor(this.createConfiguredComputingAddon),
85
        },
86
        externalCitationService: {
87
            getAuthorizedAccounts: taskFor(this.getAuthorizedCitationAccounts),
88
            createAuthorizedAccount: taskFor(this.createAuthorizedCitationAccount),
89
            createConfiguredAddon: taskFor(this.createConfiguredCitationAddon),
90
        },
91
        externalLinkService: {
92
            getAuthorizedAccounts: taskFor(this.getAuthorizedLinkAccounts),
93
            createAuthorizedAccount: taskFor(this.createAuthorizedLinkAccount),
94
            createConfiguredAddon: taskFor(this.createConfiguredLinkAddon),
95
        },
96
    };
97

98
    @tracked configuredAddon?: AllConfiguredAddonTypes;
99
    @tracked configuredAddons?: AllConfiguredAddonTypes[];
100
    @tracked authorizedAccount?: AllAuthorizedAccountTypes;
101
    @tracked authorizedAccounts?: AllAuthorizedAccountTypes[];
102

103
    @service store!: Store;
104

105
    get isConfigured() {
106
        return Boolean(this.configuredAddons?.length);
102✔
107
    }
108

109
    get isOwned() {
110
        if (this.node?.userHasAdminPermission) {
64!
111
            return true;
64✔
112
        }
113
        if (!this.configuredAddons || this.configuredAddons.length === 0) {
×
114
            return true;
×
115
        }
116
        if (!this.userReference) {
×
117
            return false;
×
118
        }
119
        return this.configuredAddons?.any(
×
120
            addon => addon.currentUserIsOwner,
×
121
        );
122
    }
123

124
    constructor(
125
        provider: any,
126
        currentUser: CurrentUserService,
127
        node?: NodeModel,
128
        allConfiguredAddons?: EmberArray<AllConfiguredAddonTypes>,
129
        resourceReference?: ResourceReferenceModel | null,
130
        userReference?: UserReferenceModel,
131
    ) {
132
        setOwner(this, getOwner(provider));
77✔
133
        this.node = node;
77✔
134
        this.currentUser = currentUser;
77✔
135
        this.provider = provider;
77✔
136
        this.configuredAddons = allConfiguredAddons?.filter(addon => addon.externalServiceId === this.provider.id);
77✔
137
        this.serviceNode = resourceReference;
77✔
138
        if (userReference) {
77✔
139
            this.userReference = userReference;
75✔
140
        }
141

142
        if (provider instanceof ExternalStorageServiceModel) {
77✔
143
            this.providerMap = this.providerTypeMapper.externalStorageService;
70✔
144
        } else if (provider instanceof ExternalComputingServiceModel) {
7!
145
            this.providerMap = this.providerTypeMapper.externalComputingService;
×
146
        } else if (provider instanceof ExternalCitationServiceModel) {
7!
147
            this.providerMap = this.providerTypeMapper.externalCitationService;
7✔
NEW
148
        } else if (provider instanceof ExternalLinkServiceModel) {
×
NEW
149
            this.providerMap = this.providerTypeMapper.externalLinkService;
×
150
        }
151
        taskFor(this.initialize).perform();
77✔
152
    }
153

154
    @task
155
    @waitFor
156
    async initialize() {
157
        await taskFor(this.getUserReference).perform();
77✔
158
        await taskFor(this.getResourceReference).perform();
77✔
159
    }
160

161
    @task
162
    @waitFor
163
    async removeConfiguredAddon(selectedConfiguration: AllConfiguredAddonTypes) {
164
        const errorMessage = this.intl.t('addons.provider.remove-configured-addon-error');
1✔
165
        const successMessage = this.intl.t('addons.provider.remove-configured-addon-success');
1✔
166
        try {
1✔
167
            await selectedConfiguration?.destroyRecord();
1✔
168
            this.configuredAddons?.removeObject(selectedConfiguration);
1✔
169
            notifyPropertyChange(this, 'configuredAddons');
1✔
170
            this.toast.success(successMessage);
1✔
171
        }  catch (e) {
172
            captureException(e, { errorMessage });
×
173
            this.toast.error(getApiErrorMessage(e), errorMessage);
×
174
            return;
×
175
        }
176
    }
177

178
    @task
179
    @waitFor
180
    async getUserReference() {
181
        if (this.userReference){
77✔
182
            return;
75✔
183
        }
184
        const { user } = this.currentUser;
2✔
185
        const userReferences = await this.store.query('user-reference', {
2✔
186
            filter: {user_uri: user?.links.iri?.toString()},
187
        });
188
        this.userReference = userReferences.firstObject;
2✔
189
    }
190

191

192
    @task
193
    @waitFor
194
    async getResourceReference() {
195
        if (this.node && this.serviceNode === undefined) {
77✔
196
            const resourceRefs = await this.store.query('resource-reference', {
2✔
197
                filter: {resource_uri: this.node.links.iri?.toString()},
198
            });
199
            this.serviceNode = resourceRefs.firstObject;
2✔
200
        }
201
    }
202

203
    @task
204
    @waitFor
205
    async getAuthorizedStorageAccounts() {
206
        const authorizedStorageAccounts = await this.userReference.authorizedStorageAccounts;
1✔
207
        this.authorizedAccounts = authorizedStorageAccounts
1✔
208
            .filterBy('externalStorageService.id', this.provider.id).toArray();
209
    }
210

211
    @task
212
    @waitFor
213
    async getAuthorizedCitationAccounts() {
214
        const authorizedCitationAccounts = await this.userReference.authorizedCitationAccounts;
×
215
        this.authorizedAccounts = authorizedCitationAccounts
×
216
            .filterBy('externalCitationService.id', this.provider.id).toArray();
217
    }
218

219
    @task
220
    @waitFor
221
    async getAuthorizedComputingAccounts() {
222
        const authorizedComputingAccounts = await this.userReference.authorizedComputingAccounts;
×
223
        this.authorizedAccounts = authorizedComputingAccounts
×
224
            .filterBy('externalComputingService.id', this.provider.id).toArray();
225
    }
226

227
    @task
228
    @waitFor
229
    async getAuthorizedLinkAccounts() {
NEW
230
        const authorizedLinkAccounts = await this.userReference.authorizedLinkAccounts;
×
NEW
231
        this.authorizedAccounts = authorizedLinkAccounts
×
232
            .filterBy('externalLinkService.id', this.provider.id).toArray();
233
    }
234

235
    @task
236
    @waitFor
237
    async getAuthorizedAccounts() {
238
        await this.providerMap?.getAuthorizedAccounts.perform();
1✔
239
    }
240

241
    async userAddonAccounts() {
242
        return await this.userReference.authorizedStorageAccounts;
×
243
    }
244

245

246
    @task
247
    @waitFor
248
    private async createAuthorizedStorageAccount(arg: AccountCreationArgs) {
249
        const { credentials, apiBaseUrl, displayName, initiateOauth } = arg;
3✔
250
        const newAccount = this.store.createRecord('authorized-storage-account', {
3✔
251
            credentials,
252
            initiateOauth,
253
            apiBaseUrl,
254
            externalUserId: this.currentUser.user?.id,
255
            authorizedCapabilities: ['ACCESS', 'UPDATE'],
256
            externalStorageService: this.provider,
257
            displayName,
258
            accountOwner: this.userReference,
259
        });
260
        await newAccount.save();
3✔
261
        return newAccount;
3✔
262
    }
263

264
    @task
265
    @waitFor
266
    private async createAuthorizedCitationAccount(arg: AccountCreationArgs) {
267
        const { credentials, apiBaseUrl, displayName, initiateOauth } = arg;
×
268
        const newAccount = this.store.createRecord('authorized-citation-account', {
×
269
            credentials,
270
            apiBaseUrl,
271
            initiateOauth,
272
            externalUserId: this.currentUser.user?.id,
273
            authorizedCapabilities: ['ACCESS', 'UPDATE'],
274
            scopes: [],
275
            externalCitationService: this.provider,
276
            accountOwner: this.userReference,
277
            displayName,
278
        });
279
        await newAccount.save();
×
280
        return newAccount;
×
281
    }
282

283
    @task
284
    @waitFor
285
    private async createAuthorizedComputingAccount(arg: AccountCreationArgs) {
286
        const { credentials, apiBaseUrl, displayName, initiateOauth } = arg;
×
287
        const newAccount = this.store.createRecord('authorized-computing-account', {
×
288
            credentials,
289
            apiBaseUrl,
290
            initiateOauth,
291
            externalUserId: this.currentUser.user?.id,
292
            authorizedCapabilities: ['ACCESS', 'UPDATE'],
293
            scopes: [],
294
            externalComputingService: this.provider,
295
            accountOwner: this.userReference,
296
            displayName,
297
        });
298
        await newAccount.save();
×
299
        return newAccount;
×
300
    }
301

302
    @task
303
    @waitFor
304
    private async createAuthorizedLinkAccount(arg: AccountCreationArgs) {
NEW
305
        const { credentials, apiBaseUrl, displayName, initiateOauth } = arg;
×
NEW
306
        const newAccount = this.store.createRecord('authorized-link-account', {
×
307
            credentials,
308
            apiBaseUrl,
309
            initiateOauth,
310
            externalUserId: this.currentUser.user?.id,
311
            authorizedCapabilities: ['ACCESS', 'UPDATE'],
312
            scopes: [],
313
            externalLinkService: this.provider,
314
            accountOwner: this.userReference,
315
            displayName,
316
        });
NEW
317
        await newAccount.save();
×
NEW
318
        return newAccount;
×
319
    }
320

321
    @task
322
    @waitFor
323
    public async createAuthorizedAccount(arg: AccountCreationArgs) {
324
        return await taskFor(this.providerMap!.createAuthorizedAccount)
3✔
325
            .perform(arg);
326
    }
327

328
    @task
329
    @waitFor
330
    public async reconnectAuthorizedAccount(args: AccountCreationArgs, account: AllAuthorizedAccountTypes) {
331
        const { credentials, apiBaseUrl, displayName } = args;
1✔
332
        account.credentials = credentials;
1✔
333
        account.apiBaseUrl = apiBaseUrl;
1✔
334
        account.displayName = displayName;
1✔
335
        await account.save();
1✔
336
        await account.reload();
1✔
337
    }
338

339
    @task
340
    @waitFor
341
    private async createConfiguredStorageAddon(account: AuthorizedStorageAccountModel) {
342
        const configuredStorageAddon = this.store.createRecord('configured-storage-addon', {
1✔
343
            rootFolder: '',
344
            externalStorageService: this.provider,
345
            accountOwner: this.userReference,
346
            authorizedResourceUri: this.node!.links.iri,
347
            baseAccount: account,
348
            connectedCapabilities: ['ACCESS', 'UPDATE'],
349
        });
350
        return await configuredStorageAddon.save();
1✔
351
    }
352

353
    @task
354
    @waitFor
355
    private async createConfiguredCitationAddon(account: AuthorizedCitationAccountModel) {
356
        const configuredCitationAddon = this.store.createRecord('configured-citation-addon', {
×
357
            rootFolder: '',
358
            externalCitationService: this.provider,
359
            accountOwner: this.userReference,
360
            authorizedResourceUri: this.node!.links.iri,
361
            baseAccount: account,
362
            connectedCapabilities: ['ACCESS', 'UPDATE'],
363
        });
364
        return await configuredCitationAddon.save();
×
365
    }
366

367
    @task
368
    @waitFor
369
    private async createConfiguredComputingAddon(account: AuthorizedComputingAccountModel) {
370
        const configuredComputingAddon = this.store.createRecord('configured-computing-addon', {
×
371
            // rootFolder: '',
372
            externalComputingService: this.provider,
373
            accountOwner: this.userReference,
374
            // authorizedResource: this.serviceNode,
375
            authorizedResourceUri: this.node!.links.iri,
376
            baseAccount: account,
377
            connectedCapabilities: ['ACCESS', 'UPDATE'],
378
        });
379
        return await configuredComputingAddon.save();
×
380
    }
381

382
    @task
383
    @waitFor
384
    private async createConfiguredLinkAddon(account: AuthorizedComputingAccountModel) {
NEW
385
        const configuredLinkAddon = this.store.createRecord('configured-link-addon', {
×
386
            externalLinkService: this.provider,
387
            accountOwner: this.userReference,
388
            authorizedResourceUri: this.node!.links.iri,
389
            baseAccount: account,
390
            connectedCapabilities: ['ACCESS', 'UPDATE'],
391
        });
NEW
392
        return await configuredLinkAddon.save();
×
393
    }
394

395

396
    @task
397
    @waitFor
398
    public async createConfiguredAddon(account: AllAuthorizedAccountTypes) {
399
        const newConfiguredAddon = await taskFor(this.providerMap!.createConfiguredAddon).perform(account);
1✔
400
        this.configuredAddons!.pushObject(newConfiguredAddon);
1✔
401
        return newConfiguredAddon;
1✔
402
    }
403

404
    @task
405
    @waitFor
406
    async disableProjectAddon() {
407
        if (this.configuredAddon) {
1!
408
            await this.configuredAddon.destroyRecord();
×
409
            this.configuredAddon = undefined;
×
410
        }
411
    }
412

413
    @task
414
    @waitFor
415
    async setRootFolder(newRootFolder: string) {
416
        if (this.configuredAddon) {
×
417
            (this.configuredAddon as ConfiguredStorageAddonModel).rootFolder = newRootFolder;
×
418
            await this.configuredAddon.save();
×
419
        }
420
    }
421
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc