Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support adding/updating topo data #401

Merged
merged 1 commit into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading