diff --git a/src/cdn/cdnClient.ts b/src/cdn/cdnClient.ts new file mode 100644 index 00000000..8d1b11c3 --- /dev/null +++ b/src/cdn/cdnClient.ts @@ -0,0 +1,29 @@ +import { Storage } from '@google-cloud/storage' + +const storage = new Storage({ + projectId: 'openbeta', + credentials: { + type: 'service-account', + client_email: 'photo-bucket-admin@openbeta.iam.gserviceaccount.com', + private_key: '' + } +}) + +/** + * Get a user's media files. May not need this. + * @param uuidStr user id in uuid v4 format + * @returns a list media objects including associated tags + */ +export const getUserMedia = async (uuidStr: string): Promise => { + const [files] = await storage.bucket('openbeta-staging').getFiles({ + autoPaginate: false, + prefix: `u/${uuidStr}/`, // + delimiter: '/' + }) + return files.reduce((acc, curr) => { + if (!curr.name.endsWith('.json')) { + acc.push(`/${curr.name}`) + } + return acc + }, []) +} diff --git a/src/db/MediaTypes.ts b/src/db/MediaTypes.ts index e279064e..bb121f8f 100644 --- a/src/db/MediaTypes.ts +++ b/src/db/MediaTypes.ts @@ -43,6 +43,15 @@ export interface MediaListByAuthorType { tagList: TagType[] } +export interface SimpleTag { + id: MUUID + name: string +} +export interface UserMediaWithTags extends MediaMetaType { + climbTags: SimpleTag[] + areaTags: SimpleTag[] +} + export interface MediaInputType { mediaUuid: MUUID mediaUrl: string diff --git a/src/graphql/media/MediaResolvers.ts b/src/graphql/media/MediaResolvers.ts index 52c02870..94c27d89 100644 --- a/src/graphql/media/MediaResolvers.ts +++ b/src/graphql/media/MediaResolvers.ts @@ -1,4 +1,4 @@ -import { CompleteAreaTag, CompleteClimbTag, MediaListByAuthorType, RefModelType, TagEntryResultType, TagType, BaseTagType } from '../../db/MediaTypes.js' +import { CompleteAreaTag, CompleteClimbTag, MediaListByAuthorType, RefModelType, TagEntryResultType, TagType, BaseTagType, SimpleTag } from '../../db/MediaTypes.js' import AreaDataSource from '../../model/AreaDataSource.js' import { getUserNickFromMediaDir } from '../../utils/helpers.js' @@ -63,20 +63,14 @@ const MediaResolvers = { } }, + SimpleTag: { + id: async (node: SimpleTag) => node.id.toUUID().toString() + }, + MediaListByAuthorType: { authorUuid: (node: MediaListByAuthorType) => node._id }, - // ClimbTag: { - // id: (node: ClimbTagType) => node._id.toString(), - // mediaUuid: (node: ClimbTagType) => node.mediaUuid.toUUID().toString() - // }, - - // AreaTag: { - // id: (node: AreaTagType) => node._id.toString(), - // mediaUuid: (node: AreaTagType) => node.mediaUuid.toUUID().toString() - // }, - DeleteTagResult: { // nothing to override } diff --git a/src/graphql/media/queries.ts b/src/graphql/media/queries.ts index 3ec8369b..cc0929ad 100644 --- a/src/graphql/media/queries.ts +++ b/src/graphql/media/queries.ts @@ -1,4 +1,4 @@ -import { MediaListByAuthorType, TagsLeaderboardType } from '../../db/MediaTypes.js' +import { MediaListByAuthorType, UserMediaWithTags, TagsLeaderboardType } from '../../db/MediaTypes.js' import { DataSourcesType } from '../../types.js' const MediaQueries = { @@ -19,6 +19,14 @@ const MediaQueries = { return await media.getRecentTags(userLimit) }, + /** + * Return most recent tags + */ + getUserMedia: async (_, { userUuid, limit = 1000 }: { limit: number | undefined, userUuid: string }, { dataSources }): Promise => { + const { media }: DataSourcesType = dataSources + return await media.getUserPhotos(userUuid, limit) + }, + getTagsLeaderboard: async (_, { limit = 30 }: { limit: number }, { dataSources }): Promise => { const { media }: DataSourcesType = dataSources return await media.getTagsLeaderboard(limit) diff --git a/src/graphql/schema/Media.gql b/src/graphql/schema/Media.gql index d029e408..e17a9799 100644 --- a/src/graphql/schema/Media.gql +++ b/src/graphql/schema/Media.gql @@ -14,6 +14,10 @@ type Query { getRecentTags(userLimit: Int): [MediaListByAuthorType] } +type Query { + getUserMedia(userUuid: ID!): [MediaWithTags] +} + type Query { """ Get a list of users and their tagged photo count @@ -26,7 +30,17 @@ type TagsLeaderboardType { total: Int! } -"Core attributes of a tag" +"Media metadata" +interface IMediaMetadata { + width: Int! + height: Int! + format: String! + mtime: Date! + birthTime: Date! + size: Int! +} + +"Attributes of a tag" interface ITag { id: ID! username: String @@ -35,16 +49,10 @@ interface ITag { mediaType: Int! destination: ID! destType: Int! - width: Int! - height: Int! - format: String! - mtime: Date! - birthTime: Date! - size: Int! } "An area tag" -type AreaTag implements ITag { +type AreaTag implements ITag & IMediaMetadata { id: ID! username: String mediaUuid: ID! @@ -62,7 +70,7 @@ type AreaTag implements ITag { } "A climb tag" -type ClimbTag implements ITag { +type ClimbTag implements ITag & IMediaMetadata{ id: ID! username: String mediaUuid: ID! @@ -82,7 +90,7 @@ type ClimbTag implements ITag { union MediaTag = AreaTag | ClimbTag "Representing a tag without the tagged climb/area. Used by climb & area queries." -type BaseTag implements ITag { +type BaseTag implements ITag & IMediaMetadata { id: ID! username: String mediaUuid: ID! @@ -104,6 +112,23 @@ type MediaListByAuthorType { tagList: [MediaTag] } +type SimpleTag { + id: ID! + name: String! +} + +type MediaWithTags implements IMediaMetadata { + name: String! + width: Int! + height: Int! + format: String! + mtime: Date! + birthTime: Date! + size: Int! + climbTags: [SimpleTag] + areaTags: [SimpleTag] +} + union TagEntryResult = ClimbTag | AreaTag "Result of a delete tag operation" diff --git a/src/model/AreaDataSource.ts b/src/model/AreaDataSource.ts index 545ee71a..86492011 100644 --- a/src/model/AreaDataSource.ts +++ b/src/model/AreaDataSource.ts @@ -10,6 +10,7 @@ import { getClimbModel } from '../db/ClimbSchema.js' import { ClimbGQLQueryType } from '../db/ClimbTypes.js' import { logger } from '../logger.js' import { BaseTagType } from '../db/MediaTypes.js' +import { joiningTagWithMediaObject } from './MediaDataSource.js' export default class AreaDataSource extends MongoDataSource { areaModel = getAreaModel() @@ -134,7 +135,7 @@ export default class AreaDataSource extends MongoDataSource { * Find all area tags whose ancestors and children have 'areaId' */ const taggedAreas = await getMediaModel().aggregate([ - ...this.joiningTagWithMediaObject, + ...joiningTagWithMediaObject, { // SELECT * // FROM media @@ -187,7 +188,7 @@ export default class AreaDataSource extends MongoDataSource { */ const taggeClimbs = await getMediaModel() .aggregate([ - ...this.joiningTagWithMediaObject, + ...joiningTagWithMediaObject, { // SELECT * // FROM media @@ -278,37 +279,6 @@ export default class AreaDataSource extends MongoDataSource { return null } - /** - * A reusable aggregation pipeline for 'joining' tag collection and media object collection. - * - * ``` - * select * - * from media, media_objects - * where media.mediaUrl == media_objects.name - * ``` - */ - joiningTagWithMediaObject = [ - { - $lookup: { - localField: 'mediaUrl', - from: this.mediaObjectModal.modelName, // Foreign collection name - foreignField: 'name', - as: 'meta' // add a new parent field - } - }, - { $unwind: '$meta' }, - { - $unset: ['meta.name', 'meta._id', 'meta.createdAt', 'meta.updatedAt'] - }, - { - $replaceWith: { - $mergeObjects: ['$$ROOT', '$meta'] - } - }, - { - $unset: ['meta'] - }] - /** * Find tags for a given climb id. * @@ -325,7 +295,7 @@ export default class AreaDataSource extends MongoDataSource { const rs = await this.tagModel .aggregate([ { $match: { destinationId: climbId } }, - ...this.joiningTagWithMediaObject + ...joiningTagWithMediaObject ]) return rs diff --git a/src/model/MediaDataSource.ts b/src/model/MediaDataSource.ts index d5e12870..1fcff403 100644 --- a/src/model/MediaDataSource.ts +++ b/src/model/MediaDataSource.ts @@ -2,7 +2,7 @@ import { MongoDataSource } from 'apollo-datasource-mongodb' import muid from 'uuid-mongodb' import { getMediaModel, getMediaObjectModel } from '../db/index.js' -import { MediaType, MediaListByAuthorType, TagsLeaderboardType } from '../db/MediaTypes.js' +import { MediaType, MediaListByAuthorType, TagsLeaderboardType, UserMediaWithTags } from '../db/MediaTypes.js' export default class MediaDataSource extends MongoDataSource { tagModel = getMediaModel() @@ -82,6 +82,92 @@ export default class MediaDataSource extends MongoDataSource { return rs } + /** + * Get all photos for a user + * @param userLimit + */ + async getUserPhotos (uuidStr: string, userLimit: number = 10): Promise { + // const list = await getUserMedia(uuidStr) + // console.log('#list', list) + + const rs = await getMediaObjectModel().aggregate([ + { + $match: { + $expr: { + $eq: [{ $substr: ['$name', 3, 36] }, uuidStr] + } + } + }, + { + $lookup: { + localField: 'name', + from: 'media', // Foreign collection name + foreignField: 'mediaUrl', + as: 'climbTags', // add a new parent field + pipeline: [ + { + $lookup: { + from: 'climbs', // other collection name + foreignField: '_id', // climb._id + localField: 'destinationId', + as: 'taggedClimbs' + } + + }, + { + $unwind: '$taggedClimbs' + }, + { + $set: { + 'climb.id': '$taggedClimbs._id', + 'climb.name': '$taggedClimbs.name' + } + }, + { + $unset: 'taggedClimbs' + }, + { $replaceRoot: { newRoot: '$climb' } } + ] + } + }, + { + $lookup: { + localField: 'name', + from: 'media', // Foreign collection name + foreignField: 'mediaUrl', + as: 'areaTags', // add a new parent field + pipeline: [ + { + $lookup: { + from: 'areas', // other collection name + foreignField: 'metadata.area_id', // climb._id + localField: 'destinationId', + as: 'taggedAreas' + } + + }, + { + $unwind: '$taggedAreas' + }, + { + $set: { + 'area.id': '$taggedAreas.metadata.area_id', + 'area.name': '$taggedAreas.area_name' + } + }, + { + $unset: 'taggedAreas' + }, + { $replaceRoot: { newRoot: '$area' } } + ] + } + } + + ] + ) + return rs + } + /** * Get a list of users and their tagged photo count * @param limit how many entries @@ -117,3 +203,36 @@ export default class MediaDataSource extends MongoDataSource { return rs } } + +/** + * A reusable Mongo aggregation snippet for 'joining' tag collection and media object collection. + * Ideally we should just embed tags as an array inside 'media_objects' collection to eliminate + * this extra join. + * + * ``` + * select * + * from media, media_objects + * where media.mediaUrl == media_objects.name + * ``` + */ +export const joiningTagWithMediaObject = [ + { + $lookup: { + localField: 'mediaUrl', + from: 'media_objects', // Foreign collection name + foreignField: 'name', + as: 'meta' // add a new parent field + } + }, + { $unwind: '$meta' }, + { + $unset: ['meta.name', 'meta._id', 'meta.createdAt', 'meta.updatedAt'] + }, + { + $replaceWith: { + $mergeObjects: ['$$ROOT', '$meta'] + } + }, + { + $unset: ['meta'] + }]