@@ -15,7 +15,12 @@ import CurrentUser from 'ember-osf-web/services/current-user';
1515import getHref from 'ember-osf-web/utils/get-href' ;
1616import getRelatedHref from 'ember-osf-web/utils/get-related-href' ;
1717import getSelfHref from 'ember-osf-web/utils/get-self-href' ;
18- import pathJoin from 'ember-osf-web/utils/path-join' ;
18+ import {
19+ buildFieldsParam ,
20+ parseSparseResource ,
21+ SparseFieldset ,
22+ SparseModel ,
23+ } from 'ember-osf-web/utils/sparse-fieldsets' ;
1924
2025import {
2126 BaseMeta ,
@@ -41,6 +46,17 @@ export interface QueryHasManyResult<T> extends Array<T> {
4146 links ?: Links | PaginationLinks ;
4247}
4348
49+ export interface RequestOptions {
50+ queryParams ?: object ;
51+ ajaxOptions ?: object ;
52+ }
53+
54+ export interface SparseHasManyResult {
55+ sparseModels : SparseModel [ ] ;
56+ meta : PaginatedMeta ;
57+ links ?: Links | PaginationLinks ;
58+ }
59+
4460export interface PaginatedQueryOptions {
4561 'page[size]' : number ;
4662 page : number ;
@@ -70,6 +86,21 @@ export default class OsfModel extends Model {
7086 return pluralize ( underscore ( this . modelName ) ) ;
7187 }
7288
89+ getHasManyLink < T extends OsfModel , R extends RelationshipsFor < T > > (
90+ this : T ,
91+ relationshipName : R ,
92+ ) : string {
93+ const reference = this . hasMany ( relationshipName ) ;
94+
95+ // HACK: ember-data discards/ignores the link if an object on the belongsTo side
96+ // came first. In that case, grab the link where we expect it from OSF's API
97+ const url = reference . link ( ) || getRelatedHref ( this . relationshipLinks [ relationshipName ] ) ;
98+ if ( ! url ) {
99+ throw new Error ( `Could not find a link for '${ relationshipName } ' relationship` ) ;
100+ }
101+ return url ;
102+ }
103+
73104 /*
74105 * Query a hasMany relationship with query params
75106 *
@@ -89,18 +120,8 @@ export default class OsfModel extends Model {
89120 queryParams ?: object ,
90121 ajaxOptions ?: object ,
91122 ) : Promise < QueryHasManyResult < RT > > {
92- const reference = this . hasMany ( propertyName ) ;
93-
94- // HACK: ember-data discards/ignores the link if an object on the belongsTo side
95- // came first. In that case, grab the link where we expect it from OSF's API
96- const url = reference . link ( ) || getRelatedHref ( this . relationshipLinks [ propertyName ] ) ;
97-
98- if ( ! url ) {
99- throw new Error ( `Could not find a link for '${ propertyName } ' relationship` ) ;
100- }
101-
102123 const options : object = {
103- url,
124+ url : this . getHasManyLink ( propertyName ) ,
104125 data : queryParams ,
105126 ...ajaxOptions ,
106127 } ;
@@ -186,20 +207,15 @@ export default class OsfModel extends Model {
186207 relatedModel : OsfModel ,
187208 ) {
188209 const apiRelationshipName = underscore ( relationshipName ) ;
189- let url = getSelfHref ( this . relationshipLinks [ apiRelationshipName ] ) ;
210+ const url = getSelfHref ( this . relationshipLinks [ apiRelationshipName ] ) ;
190211
191- let data = JSON . stringify ( {
212+ const data = JSON . stringify ( {
192213 data : [ {
193214 id : relatedModel . id ,
194215 type : relatedModel . apiType ,
195216 } ] ,
196217 } ) ;
197218
198- if ( url && action === 'delete' ) {
199- data = '' ;
200- url = pathJoin ( url , relatedModel . id ) ;
201- }
202-
203219 if ( ! url ) {
204220 throw new Error ( `Couldn't find self link for ${ apiRelationshipName } relationship` ) ;
205221 }
@@ -271,4 +287,86 @@ export default class OsfModel extends Model {
271287 throw new Error ( `Unexpected response ${ errorContext } ` ) ;
272288 }
273289 }
290+
291+ /**
292+ * Fetch one page of a has-many relationship using sparse fieldsets.
293+ * See https://developer.osf.io/#tag/Sparse-Fieldsets
294+ *
295+ * The API response includes only the specified fields.
296+ * This is useful for potentially long lists that require rendering only a few fields.
297+ *
298+ * Does NOT use ember-data. This means a few things:
299+ * - Attributes don't pass through the transforms (e.g. 'fixstring') set with `DS.attr`, they remain as
300+ * represented in JSON. In particular, date fields are not deserialized to `Date` objects.
301+ * - Sparse models aren't put in the store. This means no potential interactions with code that does use
302+ * the store, but if you want caching you'll have to do it yourself.
303+ *
304+ * Example:
305+ * ```ts
306+ * const contributors = await node.sparseHasMany(
307+ * 'contributors',
308+ * { contributor: ['users'], user: ['fullName'] },
309+ * { queryParams: { 'page[size]': 100 } },
310+ * });
311+ *
312+ * contributors.sparseModels.forEach(contrib => {
313+ * console.log(contrib.users.fullName);
314+ * );
315+ * ```
316+ */
317+ async sparseHasMany < T extends OsfModel > (
318+ this : T ,
319+ relationshipName : RelationshipsFor < T > ,
320+ fieldset : SparseFieldset ,
321+ options : RequestOptions = { } ,
322+ ) : Promise < SparseHasManyResult > {
323+ const response : ResourceCollectionDocument = await this . currentUser . authenticatedAJAX ( {
324+ url : this . getHasManyLink ( relationshipName ) ,
325+ data : {
326+ fields : buildFieldsParam ( fieldset ) ,
327+ ...options . queryParams ,
328+ } ,
329+ ...options . ajaxOptions ,
330+ } ) ;
331+
332+ const { data, meta, links } = response ;
333+
334+ return {
335+ sparseModels : data . map ( parseSparseResource ) ,
336+ meta,
337+ ...( links ? { links } : { } ) ,
338+ } ;
339+ }
340+
341+ /**
342+ * Fetch the entirety of a has-many relationship using sparse fieldsets.
343+ * See `sparseHasMany` above.
344+ */
345+ async sparseLoadAll < T extends OsfModel > (
346+ this : T ,
347+ relationshipName : RelationshipsFor < T > ,
348+ fieldset : SparseFieldset ,
349+ options : RequestOptions = { } ,
350+ ) : Promise < SparseModel [ ] > {
351+ const sparseModels : SparseModel [ ] = [ ] ;
352+ let page = 1 ;
353+ let totalPages = 0 ;
354+
355+ do { // eslint-disable-next-line no-await-in-loop
356+ const response = await this . sparseHasMany ( relationshipName , fieldset , {
357+ ...options ,
358+ queryParams : {
359+ ...options . queryParams ,
360+ page,
361+ 'page[size]' : 100 ,
362+ } ,
363+ } ) ;
364+
365+ sparseModels . push ( ...response . sparseModels ) ;
366+ totalPages = Math . ceil ( response . meta . total / response . meta . per_page ) ;
367+ page ++ ;
368+ } while ( page <= totalPages ) ;
369+
370+ return sparseModels ;
371+ }
274372}
0 commit comments