Skip to content
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
21 changes: 21 additions & 0 deletions src/__tests__/areas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,27 @@ describe('areas API', () => {
})
})

it('retrieves an area and its cumulative media weight', async () => {
const response = await queryAPI({
query: `
query area($input: ID) {
area(uuid: $input) {
uuid
imageByteSum
}
}
`,
operationName: 'area',
variables: { input: ca.metadata.area_id },
userUuid,
app
})
expect(response.statusCode).toBe(200)
const areaResult = response.body.data.area
expect(areaResult.uuid).toBe(muuidToString(ca.metadata.area_id))
expect(areaResult.imageByteSum).toBe(0)
})

it('retrieves an area omitting organizations that exclude it', async () => {
const response = await queryAPI({
query: areaQuery,
Expand Down
7 changes: 4 additions & 3 deletions src/graphql/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,10 @@ const resolvers = {
return []
},

aggregate: async (node: AreaType) => {
return node.aggregate
},
aggregate: async (node: AreaType) => node.aggregate,

imageByteSum: async (node: AreaType, _, { dataSources: { areas } }: GQLContext) =>
await areas.computeImageByteSum(node.metadata.area_id),

ancestors: async (parent) => parent.ancestors?.split(',') ?? [],

Expand Down
9 changes: 9 additions & 0 deletions src/graphql/schema/Area.gql
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ type Area {
authorMetadata: AuthorMetadata!
"Organizations associated with this area or its parent areas"
organizations: [Organization]

"""
If you were to sum all approximate image sizes for this area, you get
a kind of picture of what the cost to cache all of the media in a given
area might be. You could use this to indicate a recommended compression
ratio or maybe know ahead of time if a given cache exercise would exceed
current storage capacity.
"""
imageByteSum: Int!
}

"""
Expand Down
24 changes: 24 additions & 0 deletions src/model/AreaDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,30 @@ export default class AreaDataSource extends MongoDataSource<AreaType> {
return rs
}

async computeImageByteSum (uuid: muuid.MUUID): Promise<number> {
const descendantUuids = await this.areaModel.find({
ancestors: { $regex: new RegExp(`(?:^|,)${uuid.toString()}(?:,|$)`) }
}, { 'metadata.area_id': 1, climbs: 1 })

return await this.mediaObjectModal.aggregate([
{
$match: {
'entityTags.targetId': {
$in: [...descendantUuids.map(i => i.metadata.area_id),
...descendantUuids.reduce((prev, curr) => [...prev, ...curr.climbs], [])
]
}
}
},
{
$group: {
_id: null,
totalSize: { $sum: '$size' }
}
}
]).then(d => d[0]?.totalSize) ?? 0
}

/**
* Find a climb by uuid. Also return the parent area object (crag or boulder).
*
Expand Down
124 changes: 124 additions & 0 deletions src/model/__tests__/AreaDataSource.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { getAreaModel, createIndexes } from "../../db"
import inMemoryDB from "../../utils/inMemoryDB"
import MutableAreaDataSource from "../MutableAreaDataSource"
import muid, { MUUID } from 'uuid-mongodb'
import { AreaType } from "../../db/AreaTypes"
import AreaDataSource from "../AreaDataSource"
import MutableMediaDataSource from "../MutableMediaDataSource"
import { MediaObjectGQLInput } from "../../db/MediaObjectTypes"
import MutableClimbDataSource from "../MutableClimbDataSource"
import muuid from 'uuid-mongodb'


function mediaInput(val: number) {
return {
userUuid: 'a2eb6353-65d1-445f-912c-53c6301404bd',
mediaUrl: `/u/a2eb6353-65d1-445f-912c-53c6301404bd/photo${val}.jpg`,
width: 800,
height: 600,
format: 'jpeg',
size: 45000 + Math.floor(Math.random() * 100)
} satisfies MediaObjectGQLInput}

describe("Test area data source", () => {
let areas: AreaDataSource
let rootCountry: AreaType
let areaCounter = 0
const testUser = muid.v4()

async function addArea(name?: string, extra?: Partial<{ leaf: boolean, boulder: boolean, parent: MUUID | AreaType}>) {
function isArea(x: any): x is AreaType {
return typeof x.metadata?.area_id !== 'undefined'
}

areaCounter += 1
if (name === undefined || name === 'test') {
name = process.uptime().toString() + '-' + areaCounter.toString()
}

let parent: MUUID | undefined = undefined
if (extra?.parent) {
if (isArea(extra.parent)) {
parent = extra.parent.metadata?.area_id
} else {
parent = extra.parent
}
}

return MutableAreaDataSource.getInstance().addArea(
testUser,
name,
parent ?? rootCountry.metadata.area_id,
undefined,
undefined,
extra?.leaf,
extra?.boulder
)
}

beforeAll(async () => {
await inMemoryDB.connect()
await getAreaModel().collection.drop()
await createIndexes()
areas = MutableAreaDataSource.getInstance()
// We need a root country, and it is beyond the scope of these tests
rootCountry = await MutableAreaDataSource.getInstance().addCountry("USA")
})

afterAll(inMemoryDB.close)

describe("Image size summing", () => {
test("Area image size summing should not produce false counts", async () => {
const area = await addArea()
const val = await areas.computeImageByteSum(area.metadata.area_id)
expect(val).toBe(0)
})

test("Area image size summing should work for direct tags", async () => {
const area = await addArea()
const media = MutableMediaDataSource.getInstance()
const [object] = await media.addMediaObjects([mediaInput(0)])
media.upsertEntityTag({ entityType: 1, entityUuid: area.metadata.area_id, mediaId: object._id })
const val = await areas.computeImageByteSum(area.metadata.area_id)
expect(val).toBe(object.size)
})

test("Area image size summing should work for direct tags to children", async () => {
const media = MutableMediaDataSource.getInstance()
const area = await addArea()
let child = area
let sizeAccumulator = 0
for (const idx of Array.from({ length: 10}).map((_, idx) => idx)) {
child = await addArea(undefined, { parent: child.metadata.area_id})
const [object] = await media.addMediaObjects([mediaInput((idx + 1) * 10)])
media.upsertEntityTag({ entityType: 1, entityUuid: child.metadata.area_id, mediaId: object._id })

// We always query the top level
expect(await areas.computeImageByteSum(area.metadata.area_id).then(d => {
sizeAccumulator += d
return sizeAccumulator
})).toBe(sizeAccumulator)
// equally, we expect the child to not get reverse-polluted
expect(await areas.computeImageByteSum(child.metadata.area_id)).toBe(object.size)
}
})

test("Area image size summing should work for direct tags to climbs", async () => {
const area = await addArea()
const media = MutableMediaDataSource.getInstance()
const climbs = MutableClimbDataSource.getInstance()
const child = await addArea(undefined, { parent: area.metadata.area_id})
expect(await areas.computeImageByteSum(area.metadata.area_id)).toBe(0)
expect(await areas.computeImageByteSum(child.metadata.area_id)).toBe(0)

const [object] = await media.addMediaObjects([mediaInput(2 * 100)])
const [climb] = await climbs.addOrUpdateClimbs(object.userUuid, child.metadata.area_id, [{
name: "climb",
grade: "6c+"
}])

media.upsertEntityTag({ entityType: 0, entityUuid: muuid.from(climb), mediaId: object._id })
expect(await areas.computeImageByteSum(area.metadata.area_id)).toBe(object.size)
})
})
})
Loading