@@ -3,7 +3,7 @@ import muid, { MUUID } from 'uuid-mongodb'
33import mongoose from 'mongoose'
44import { logger } from '../logger.js'
55import { getMediaObjectModel } from '../db/index.js'
6- import { TagsLeaderboardType , UserMediaQueryInput , AllTimeTagStats , MediaByUsers , MediaForFeedInput , MediaObject , UserMedia } from '../db/MediaObjectTypes.js'
6+ import { TagsLeaderboardType , UserMediaQueryInput , AreaMediaQueryInput , ClimbMediaQueryInput , AllTimeTagStats , MediaByUsers , MediaForFeedInput , MediaObject , UserMedia , AreaMedia , ClimbMedia } from '../db/MediaObjectTypes.js'
77
88const HARD_MAX_FILES = 1000
99const HARD_MAX_USERS = 100
@@ -127,71 +127,64 @@ export default class MediaDataSource extends MongoDataSource<MediaObject> {
127127 */
128128 async getOneUserMediaPagination ( input : UserMediaQueryInput ) : Promise < UserMedia > {
129129 const { userUuid, first = 6 , after } = input
130- let nextCreatedDate : number
131- let nextId : mongoose . Types . ObjectId
132- let filters : any
133- if ( after != null ) {
134- const d = after . split ( '_' )
135- nextCreatedDate = Number . parseInt ( d [ 0 ] )
136- nextId = new mongoose . Types . ObjectId ( d [ 1 ] )
137- filters = {
138- $match : {
139- $and : [
140- { userUuid } ,
141- {
142- $or : [ {
143- createdAt : { $lt : new Date ( nextCreatedDate ) }
144- } ,
145- {
146- // If the created date is an exact match, we need a tiebreaker,
147- // so we use the _id field from the cursor.
148- createdAt : new Date ( nextCreatedDate ) ,
149- _id : { $lt : nextId }
150- }
151- ]
152- }
153- ]
154- }
155- }
156- } else {
157- filters = { $match : { userUuid } }
130+ const filters = this . mediaFilters ( after , userUuid , 'user' )
131+ const filteredMedia = await this . aggregateMedia ( filters , first )
132+ const itemCount = await this . mediaObjectModel . countDocuments ( { userUuid } )
133+ let hasNextPage = false
134+ if ( filteredMedia . length > first ) {
135+ // ok there's a next page. remove the extra item.
136+ filteredMedia . pop ( )
137+ hasNextPage = true
158138 }
159139
160- const rs = await this . mediaObjectModel . aggregate < MediaObject > ( [
161- filters ,
162- {
163- $sort : { createdAt : - 1 , _id : - 1 }
164- } ,
165- {
166- $limit : first + 1 // fetch 1 extra to see if there's a next page
167- }
168- ] )
140+ return {
141+ userUuid : userUuid . toUUID ( ) . toString ( ) ,
142+ mediaConnection : this . getMediaConnection ( filteredMedia , itemCount , hasNextPage )
143+ }
144+ }
169145
170- const itemCount = await this . mediaObjectModel . countDocuments ( { userUuid } )
146+ /**
147+ * Get Area media by page.
148+ *
149+ * See
150+ * - https://engage.so/blog/a-deep-dive-into-offset-and-cursor-based-pagination-in-mongodb/#what-is-cursor-based-pagination
151+ * - https://www.mixmax.com/engineering/api-paging-built-the-right-way
152+ * - https://graphql.org/learn/pagination/
153+ * @param input
154+ * @returns
155+ */
171156
157+ async getOneAreaMediaPagination ( input : AreaMediaQueryInput ) : Promise < AreaMedia > {
158+ const { areaUuid, first = 6 , after } = input
159+ const filters = this . mediaFilters ( after , areaUuid , 'area' )
160+ const filteredMedia = await this . aggregateMedia ( filters , first )
161+ const itemCount = await this . mediaObjectModel . countDocuments ( { 'entityTags.ancestors' : { $regex : areaUuid . toUUID ( ) . toString ( ) } } )
172162 let hasNextPage = false
173- if ( rs . length > first ) {
174- // ok there's a next page. remove the extra item.
175- rs . pop ( )
163+ if ( filteredMedia . length > first ) {
164+ filteredMedia . pop ( )
176165 hasNextPage = true
177166 }
178167
179168 return {
180- userUuid : userUuid . toUUID ( ) . toString ( ) ,
181- mediaConnection : {
182- edges : rs . map ( node => (
183- {
184- node,
185- cursor : `${ node . createdAt . getTime ( ) } _${ node . _id . toString ( ) } `
186- }
187- ) ) ,
188- pageInfo : {
189- hasNextPage,
190- totalItems : itemCount ,
191- endCursor : null
192- }
169+ areaUuid : areaUuid . toUUID ( ) . toString ( ) ,
170+ mediaConnection : this . getMediaConnection ( filteredMedia , itemCount , hasNextPage )
171+ }
172+ }
193173
194- }
174+ async getOneClimbMediaPagination ( input : ClimbMediaQueryInput ) : Promise < ClimbMedia > {
175+ const { climbUuid, first = 6 , after } = input
176+ const filters = this . mediaFilters ( after , climbUuid , 'climb' )
177+ const filteredMedia = await this . aggregateMedia ( filters , first )
178+ const itemCount = await this . mediaObjectModel . countDocuments ( { 'entityTags.targetId' : muid . from ( climbUuid ) } )
179+ let hasNextPage = false
180+ if ( filteredMedia . length > first ) {
181+ filteredMedia . pop ( )
182+ hasNextPage = true
183+ }
184+
185+ return {
186+ climbUuid : climbUuid . toUUID ( ) . toString ( ) ,
187+ mediaConnection : this . getMediaConnection ( filteredMedia , itemCount , hasNextPage )
195188 }
196189 }
197190
@@ -270,7 +263,7 @@ export default class MediaDataSource extends MongoDataSource<MediaObject> {
270263 *
271264 * @param areaId
272265 * @param ancestors
273- * @returns `UserMediaWithTags ` array
266+ * @returns `MediaWithTags ` array
274267 */
275268 async findMediaByAreaId ( areaId : MUUID , projection : any , shouldConvertUuidToString = false ) : Promise < MediaObject [ ] > {
276269 const transformFn = ( doc : MediaObject ) : any => {
@@ -304,4 +297,82 @@ export default class MediaDataSource extends MongoDataSource<MediaObject> {
304297 const doc = await this . mediaObjectModel . find ( { _id, userUuid } , { _id : 1 } ) . lean ( )
305298 return doc != null
306299 }
300+
301+ /**
302+ * helper functions for quering media objects by user, area, or climb. Each query has slightly different syntax,
303+ * so these private functions return the appropriate filter for the query.
304+ * @param entityUuid area, climb or user uuid
305+ * @param entityType 'area', 'climb', or 'user'
306+ * @returns
307+ */
308+
309+ private mediaFilters ( after : string | undefined , entityUuid : MUUID , entityType : string ) : any {
310+ let nextCreatedDate : number
311+ let nextId : mongoose . Types . ObjectId
312+ let filters : any
313+ const matchClause = this . getMatchClause ( entityUuid , entityType )
314+
315+ if ( after != null ) {
316+ const d = after . split ( '_' )
317+ nextCreatedDate = Number . parseInt ( d [ 0 ] )
318+ nextId = new mongoose . Types . ObjectId ( d [ 1 ] )
319+ filters = {
320+ $match : {
321+ $and : [
322+ matchClause ,
323+ {
324+ $or : [ {
325+ createdAt : { $lt : new Date ( nextCreatedDate ) }
326+ } ,
327+ {
328+ // If the created date is an exact match, we need a tiebreaker,
329+ // so we use the _id field from the cursor.
330+ createdAt : new Date ( nextCreatedDate ) ,
331+ _id : { $lt : nextId }
332+ }
333+ ]
334+ }
335+ ]
336+ }
337+ }
338+ } else {
339+ filters = { $match : matchClause }
340+ }
341+ return filters
342+ }
343+
344+ private getMatchClause ( entityUuid : MUUID , entityType : string ) : any {
345+ if ( entityType === 'user' ) {
346+ return { userUuid : entityUuid }
347+ } else if ( entityType === 'area' ) {
348+ return { 'entityTags.ancestors' : { $regex : entityUuid . toUUID ( ) . toString ( ) } }
349+ } else if ( entityType === 'climb' ) {
350+ return { 'entityTags.targetId' : entityUuid }
351+ }
352+ throw new Error ( `Unexpected entity type: ${ entityType } ` )
353+ }
354+
355+ private async aggregateMedia ( filters : any , first : number ) : Promise < MediaObject [ ] > {
356+ return await this . mediaObjectModel . aggregate < MediaObject > ( [
357+ filters ,
358+ { $sort : { createdAt : - 1 , _id : - 1 } } ,
359+ { $limit : first + 1 } // fetch 1 extra to see if there's a next page
360+ ] )
361+ }
362+
363+ private getMediaConnection ( filteredMedia : MediaObject [ ] , itemCount : number , hasNextPage : boolean ) : any {
364+ return {
365+ edges : filteredMedia . map ( node => (
366+ {
367+ node,
368+ cursor : `${ node . createdAt . getTime ( ) } _${ node . _id . toString ( ) } `
369+ }
370+ ) ) ,
371+ pageInfo : {
372+ hasNextPage,
373+ totalItems : itemCount ,
374+ endCursor : null
375+ }
376+ }
377+ }
307378}
0 commit comments