Skip to content

Commit

Permalink
feat: support adding/updating topo data (#401)
Browse files Browse the repository at this point in the history
  • Loading branch information
vnugent authored May 3, 2024
1 parent 468f6bf commit 98d54fc
Show file tree
Hide file tree
Showing 10 changed files with 76 additions and 34 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"graphql": "^16.8.1",
"graphql-middleware": "^6.1.31",
"graphql-shield": "^7.5.0",
"graphql-type-json": "^0.3.2",
"i18n-iso-countries": "^7.5.0",
"immer": "^9.0.15",
"jsonwebtoken": "^8.5.1",
Expand Down
3 changes: 2 additions & 1 deletion src/db/MediaObjectSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const EntitySchema = new Schema<EntityTag>({
type: PointSchema,
index: '2dsphere',
required: false
}
},
topoData: { type: Schema.Types.Mixed }
}, { _id: true, toObject: { versionKey: false } })

const schema = new Schema<MediaObject>({
Expand Down
4 changes: 3 additions & 1 deletion src/db/MediaObjectTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface EntityTag {
climbName?: string
areaName: string
lnglat?: Point
topoData?: object
}

export interface MediaByUsers {
Expand Down Expand Up @@ -99,12 +100,13 @@ export interface AddEntityTagGQLInput {
mediaId: string
entityId: string
entityType: number
topoData?: object
}

/**
* Formal input type for addEntityTag function
*/
export type AddTagEntityInput = Pick<AddEntityTagGQLInput, 'entityType'> & {
export type AddTagEntityInput = Pick<AddEntityTagGQLInput, 'entityType' | 'topoData'> & {
mediaId: mongoose.Types.ObjectId
entityUuid: MUUID
}
Expand Down
3 changes: 2 additions & 1 deletion src/graphql/media/MediaResolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ const MediaResolvers = {
id: (node: EntityTag) => node._id,
targetId: (node: EntityTag) => node.targetId.toUUID().toString(),
lat: (node: EntityTag) => geojsonPointToLatitude(node.lnglat),
lng: (node: EntityTag) => geojsonPointToLongitude(node.lnglat)
lng: (node: EntityTag) => geojsonPointToLongitude(node.lnglat),
topoData: (node: EntityTag) => node?.topoData
},

DeleteTagResult: {
Expand Down
12 changes: 9 additions & 3 deletions src/graphql/media/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ const MediaMutations = {
addEntityTag: async (_: any, args, { dataSources }: Context): Promise<EntityTag> => {
const { media } = dataSources
const { input }: { input: AddEntityTagGQLInput } = args
const { mediaId, entityId, entityType } = input
return await media.addEntityTag({
const { mediaId, entityId, entityType, topoData } = input
return await media.upsertEntityTag({
mediaId: new mongoose.Types.ObjectId(mediaId),
entityUuid: muid.from(entityId),
entityType
entityType,
topoData
})
},

Expand All @@ -36,6 +37,11 @@ const MediaMutations = {
tagId: new mongoose.Types.ObjectId(tagId)
})
}

// updateTopoData: async (_: any, args, { dataSources }: Context): Promise<EntityTag> => {
// const { media } = dataSources
// const { input }: { input: AddEntityTagGQLInput } = args
// const { mediaId, entityId, entityType
}

export default MediaMutations
2 changes: 2 additions & 0 deletions src/graphql/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import muid, { MUUID } from 'uuid-mongodb'
import fs from 'fs'
import { gql } from 'apollo-server-express'
import { DocumentNode } from 'graphql'
import { GraphQLJSONObject } from 'graphql-type-json'

import { CommonResolvers, CommonTypeDef } from './common/index.js'
import { HistoryFieldResolvers, HistoryQueries } from '../graphql/history/index.js'
Expand Down Expand Up @@ -128,6 +129,7 @@ const resolvers = {
...PostResolvers,
...XMediaResolvers,
...UserResolvers,
JSONObject: GraphQLJSONObject,

Climb: {
id: (node: ClimbGQLQueryType) => node._id.toUUID().toString(),
Expand Down
10 changes: 9 additions & 1 deletion src/graphql/schema/Media.gql
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
scalar JSONObject

type Mutation {
"""
Add one or more media objects. Each media object may contain one tag.
Expand All @@ -11,7 +13,8 @@ type Mutation {


"""
Add an entity tag to a media.
Add an entity tag to a media. Calling this function with the same
mediaId, entityUuid, and entityType will update the topo data.
"""
addEntityTag(input: MediaEntityTagInput): EntityTag!

Expand Down Expand Up @@ -150,6 +153,9 @@ type EntityTag {

"Latitude"
lat: Float!

"Topo data"
topoData: JSONObject
}

"Represent a media object"
Expand Down Expand Up @@ -195,6 +201,8 @@ input MediaEntityTagInput {
entityId: ID!
"0: climb, 1: area"
entityType: Int!
"Optional topo data"
topoData: JSONObject
}

"Input parameters for deleting a tag"
Expand Down
52 changes: 34 additions & 18 deletions src/model/MutableMediaDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,30 +61,46 @@ export default class MutableMediaDataSource extends MediaDataSource {
}

/**
* Add a new entity tag (a climb or area) to a media object.
* @returns new EntityTag . 'null' if the entity already exists.
* Add a new entity tag to a media object. `mediaId`, `entityUuid`, `entityType`
* together uniquely identify the entity tag. Providing the same 3 IDs with a
* different `topoData` to update the existing entity tag.
* @returns the new EntityTag or the one being updated.
*/
async addEntityTag ({ mediaId, entityUuid, entityType }: AddTagEntityInput): Promise<EntityTag> {
async upsertEntityTag ({ mediaId, entityUuid, entityType, topoData }: AddTagEntityInput): Promise<EntityTag> {
// Find the entity we want to tag
const newEntityTagDoc = await this.getEntityDoc({ entityUuid, entityType })

// We treat 'entityTags' like a Set - can't tag the same climb/area id twice.
// See https://stackoverflow.com/questions/33576223/using-mongoose-mongodb-addtoset-functionality-on-array-of-objects
const filter = {
_id: new mongoose.Types.ObjectId(mediaId),
'entityTags.targetId': { $ne: entityUuid }
}

await this.mediaObjectModel
.updateOne(
filter,
{
newEntityTagDoc.topoData = topoData

// Use `bulkWrite` because we can't upsert an array element in a document.
// See https://www.mongodb.com/community/forums/t/how-to-update-nested-array-using-arrayfilters-but-if-it-doesnt-find-a-match-it-should-insert-new-values/245505
const bulkOperations: any [] = [{
updateOne: {
filter: {
_id: new mongoose.Types.ObjectId(mediaId)
},
update: {
$pull: {
entityTags: { targetId: entityUuid }
}
}
}
}, {
// We treat 'entityTags' like a Set - can't add a new tag the same climb/area id twice.
// See https://stackoverflow.com/questions/33576223/using-mongoose-mongodb-addtoset-functionality-on-array-of-objects
updateOne: {
filter: {
_id: new mongoose.Types.ObjectId(mediaId),
'entityTags.targetId': { $ne: entityUuid }
},
update: {
$push: {
entityTags: newEntityTagDoc
}
})
.orFail(new UserInputError('Media not found or tag already exists.'))
.lean()
}
}
}]

await this.mediaObjectModel.bulkWrite(bulkOperations, { ordered: true })

return newEntityTagDoc
}
Expand Down
18 changes: 9 additions & 9 deletions src/model/__tests__/MediaDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ describe('MediaDataSource', () => {
areaTag2 = {
mediaId: testMediaObject._id,
entityType: 1,
entityUuid: areaForTagging2.metadata.area_id
entityUuid: areaForTagging2.metadata.area_id,
topoData: { name: 'AA', value: '1234' }
}

climbTag = {
Expand All @@ -102,7 +103,7 @@ describe('MediaDataSource', () => {
entityType: 1,
entityUuid: muuid.v4() // some random area
}
await expect(media.addEntityTag(badAreaTag)).rejects.toThrow(/area .* not found/i)
await expect(media.upsertEntityTag(badAreaTag)).rejects.toThrow(/area .* not found/i)
})

it('should not tag a nonexistent *climb*', async () => {
Expand All @@ -111,7 +112,7 @@ describe('MediaDataSource', () => {
entityType: 0,
entityUuid: muuid.v4() // some random climb
}
await expect(media.addEntityTag(badClimbTag)).rejects.toThrow(/climb .* not found/i)
await expect(media.upsertEntityTag(badClimbTag)).rejects.toThrow(/climb .* not found/i)
})

it('should tag & remove an area tag', async () => {
Expand All @@ -122,10 +123,10 @@ describe('MediaDataSource', () => {
expect(mediaObjects[0].entityTags).toHaveLength(0)

// add 1st tag
await media.addEntityTag(areaTag1)
await media.upsertEntityTag(areaTag1)

// add 2nd tag
const tag = await media.addEntityTag(climbTag)
const tag = await media.upsertEntityTag(climbTag)

expect(tag).toMatchObject<Partial<EntityTag>>({
targetId: climbTag.entityUuid,
Expand Down Expand Up @@ -165,11 +166,10 @@ describe('MediaDataSource', () => {
})

it('should not add a duplicate tag', async () => {
const newTag = await media.addEntityTag(areaTag2)
const updating = { ...areaTag2, topoData: { name: 'ZZ' } }
const newTag = await media.upsertEntityTag(updating)
expect(newTag.targetId).toEqual(areaTag2.entityUuid)

// Insert the same tag again
await expect(media.addEntityTag(areaTag2)).rejects.toThrowError(/tag already exists/i)
expect(newTag.topoData).toEqual(updating.topoData)
})

it('should not add media with the same url', async () => {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4362,6 +4362,11 @@ graphql-tag@^2.11.0:
dependencies:
tslib "^2.1.0"

graphql-type-json@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/graphql-type-json/-/graphql-type-json-0.3.2.tgz#f53a851dbfe07bd1c8157d24150064baab41e115"
integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==

graphql@^16.8.1:
version "16.8.1"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
Expand Down

0 comments on commit 98d54fc

Please sign in to comment.