From e51db9ed2b3e7c522d8bf10964b7653051b0ce0b Mon Sep 17 00:00:00 2001 From: kao Date: Sat, 20 May 2023 23:53:50 -0600 Subject: [PATCH 1/5] Sort area children by leftRightIndex --- src/graphql/resolvers.ts | 8 ++++---- src/model/AreaDataSource.ts | 4 ++-- src/utils/helpers.ts | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index 58f2c8d4..8981d9fb 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -20,7 +20,7 @@ import { OrganizationMutations, OrganizationQueries } from './organization/index import { TickMutations, TickQueries } from './tick/index.js' import { UserQueries, UserMutations, UserResolvers } from './user/index.js' import { getAuthorMetadataFromBaseNode } from '../db/utils/index.js' -import { geojsonPointToLatitude, geojsonPointToLongitude } from '../utils/helpers.js' +import { compareAreaLeftRightIndex, compareClimbLeftRightIndex, geojsonPointToLatitude, geojsonPointToLongitude } from '../utils/helpers.js' /** * It takes a file name as an argument, reads the file, and returns a GraphQL DocumentNode. @@ -202,7 +202,7 @@ const resolvers = { children: async (parent: AreaType, _, { dataSources: { areas } }: Context) => { if (parent.children.length > 0) { - return await areas.findManyByIds(parent.children) + return (await areas.findManyByIds(parent.children)).sort(compareAreaLeftRightIndex) } return [] }, @@ -221,11 +221,11 @@ const resolvers = { // Test to see if we have actual climb object returned from findOneAreaByUUID() const isClimbTypeArray = (x: any[]): x is ClimbType[] => x[0].name != null if (isClimbTypeArray(result)) { - return result + return result.sort(compareClimbLeftRightIndex) } // List of IDs, we need to convert them into actual climbs - return await areas.findManyClimbsByUuids(result as MUUID[]) + return (await areas.findManyClimbsByUuids(result as MUUID[])).sort(compareClimbLeftRightIndex) }, metadata: (node: AreaType) => { diff --git a/src/model/AreaDataSource.ts b/src/model/AreaDataSource.ts index 1e9e8c00..07c4ccb3 100644 --- a/src/model/AreaDataSource.ts +++ b/src/model/AreaDataSource.ts @@ -7,7 +7,7 @@ import { getAreaModel, getMediaModel, getMediaObjectModel } from '../db/index.js import { AreaType } from '../db/AreaTypes' import { GQLFilter, AreaFilterParams, PathTokenParams, LeafStatusParams, ComparisonFilterParams, StatisticsType, CragsNear, BBoxType } from '../types' import { getClimbModel } from '../db/ClimbSchema.js' -import { ClimbGQLQueryType } from '../db/ClimbTypes.js' +import { ClimbGQLQueryType, ClimbType } from '../db/ClimbTypes.js' import { logger } from '../logger.js' export default class AreaDataSource extends MongoDataSource { @@ -117,7 +117,7 @@ export default class AreaDataSource extends MongoDataSource { throw new Error(`Area ${uuid.toUUID().toString()} not found.`) } - async findManyClimbsByUuids (uuidList: muuid.MUUID[]): Promise { + async findManyClimbsByUuids (uuidList: muuid.MUUID[]): Promise { const rs = await this.climbModel.find().where('_id').in(uuidList) return rs } diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 55ecd369..e74d0c1b 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,5 +1,7 @@ import { MUUID } from 'uuid-mongodb' import { Point } from '@turf/helpers' +import { AreaType } from '../db/AreaTypes' +import { ClimbType } from '../db/ClimbTypes' export const muuidToString = (m: MUUID): string => m.toUUID().toString() @@ -21,3 +23,17 @@ export function exhaustiveCheck (_value: never): never { export const geojsonPointToLongitude = (point: Point): number => point.coordinates[0] export const geojsonPointToLatitude = (point: Point): number => point.coordinates[1] + +export function compareAreaLeftRightIndex (a: AreaType, b: AreaType): number { + if (a.metadata.leftRightIndex == null || b.metadata.leftRightIndex == null) { + return 0 // Preserve order if any element is missing leftRightIndex + } + return (a.metadata.leftRightIndex - b.metadata.leftRightIndex) +} + +export function compareClimbLeftRightIndex (a: ClimbType, b: ClimbType): number { + if (a.metadata.left_right_index == null || b.metadata.left_right_index == null) { + return 0 // Preserve order if any element is missing left_right_index + } + return (a.metadata.left_right_index - b.metadata.left_right_index) +} From 127faf5b711a4caf1e3cffb8f05a37cba71d9ce2 Mon Sep 17 00:00:00 2001 From: kao Date: Sun, 21 May 2023 00:59:42 -0600 Subject: [PATCH 2/5] Add integration test --- src/__tests__/areas.ts | 106 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/src/__tests__/areas.ts b/src/__tests__/areas.ts index dcce766d..5058a7f2 100644 --- a/src/__tests__/areas.ts +++ b/src/__tests__/areas.ts @@ -2,8 +2,10 @@ import { ApolloServer } from 'apollo-server' import muuid from 'uuid-mongodb' import { jest } from '@jest/globals' import MutableAreaDataSource, { createInstance as createAreaInstance } from '../model/MutableAreaDataSource.js' +import { createInstance as createClimbInstance } from '../model/MutableClimbDataSource.js' import MutableOrganizationDataSource, { createInstance as createOrgInstance } from '../model/MutableOrganizationDataSource.js' import { AreaType } from '../db/AreaTypes.js' +import { ClimbChangeInputType } from '../db/ClimbTypes.js' import { OrgType, OrganizationType, OrganizationEditableFieldsType } from '../db/OrganizationTypes.js' import { queryAPI, setUpServer } from '../utils/testUtils.js' import { muuidToString } from '../utils/helpers.js' @@ -22,6 +24,7 @@ describe('areas API', () => { let usa: AreaType let ca: AreaType let wa: AreaType + let ak: AreaType beforeAll(async () => { ({ server, inMemoryDB } = await setUpServer()) @@ -38,6 +41,7 @@ describe('areas API', () => { usa = await areas.addCountry('usa') ca = await areas.addArea(user, 'CA', usa.metadata.area_id) wa = await areas.addArea(user, 'WA', usa.metadata.area_id) + ak = await areas.addArea(user, 'AK', usa.metadata.area_id) }) afterAll(async () => { @@ -45,6 +49,57 @@ describe('areas API', () => { await inMemoryDB.close() }) + describe('mutations', () => { + it('updates sorting order of subareas and queries returns them in order', async () => { + const updateSortingOrderQuery = ` + mutation ($input: [AreaSortingInput]) { + updateAreasSortingOrder(input: $input) + } + ` + const updateResponse = await queryAPI({ + query: updateSortingOrderQuery, + variables: { + input: [ + { areaId: wa.metadata.area_id, leftRightIndex: 3 }, + { areaId: ca.metadata.area_id, leftRightIndex: 0 }, + { areaId: ak.metadata.area_id, leftRightIndex: 10 } + ] + }, + userUuid + }) + + expect(updateResponse.statusCode).toBe(200) + const sortingOrderResult = updateResponse.body.data.updateAreasSortingOrder + expect(sortingOrderResult).toHaveLength(3) + + const areaChildrenQuery = ` + query area($input: ID) { + area(uuid: $input) { + children { + uuid + metadata { + leftRightIndex + } + } + } + } + ` + + const areaChildrenResponse = await queryAPI({ + query: areaChildrenQuery, + variables: { input: usa.metadata.area_id }, + userUuid + }) + + expect(areaChildrenResponse.statusCode).toBe(200) + const areaResult = areaChildrenResponse.body.data.area + // In leftRightIndex order + expect(areaResult.children[0]).toMatchObject({ uuid: muuidToString(ca.metadata.area_id), metadata: { leftRightIndex: 0 } }) + expect(areaResult.children[1]).toMatchObject({ uuid: muuidToString(wa.metadata.area_id), metadata: { leftRightIndex: 3 } }) + expect(areaResult.children[2]).toMatchObject({ uuid: muuidToString(ak.metadata.area_id), metadata: { leftRightIndex: 10 } }) + }) + }) + describe('queries', () => { const areaQuery = ` query area($input: ID) { @@ -101,5 +156,56 @@ describe('areas API', () => { // ca and so should not be listed. expect(areaResult.organizations).toHaveLength(0) }) + + it('returns climbs in leftRightIndex order', async () => { + const climbs = createClimbInstance() + const leftRoute: ClimbChangeInputType = { + name: 'left', + disciplines: { sport: true }, + description: 'Leftmost route on the wall', + leftRightIndex: 0 + } + const middleRoute: ClimbChangeInputType = { + name: 'middle', + disciplines: { sport: true }, + description: 'Middle route on the wall', + leftRightIndex: 1 + } + const rightRoute: ClimbChangeInputType = { + name: 'right', + disciplines: { sport: true }, + description: 'Rightmost route on the wall', + leftRightIndex: 2 + } + await climbs.addOrUpdateClimbs( + user, + ca.metadata.area_id, + [middleRoute, leftRoute, rightRoute] + ) + + const areaClimbsQuery = ` + query area($input: ID) { + area(uuid: $input) { + climbs { + name + metadata { + leftRightIndex + } + } + } + } + ` + const areaClimbsResponse = await queryAPI({ + query: areaClimbsQuery, + variables: { input: ca.metadata.area_id }, + userUuid + }) + expect(areaClimbsResponse.statusCode).toBe(200) + const areaResult = areaClimbsResponse.body.data.area + // In leftRightIndex order + expect(areaResult.climbs[0]).toMatchObject({ name: 'left', metadata: { leftRightIndex: 0 } }) + expect(areaResult.climbs[1]).toMatchObject({ name: 'middle', metadata: { leftRightIndex: 1 } }) + expect(areaResult.climbs[2]).toMatchObject({ name: 'right', metadata: { leftRightIndex: 2 } }) + }) }) }) From cc0e38d0bf1257da0d60e588b58468552bae63e0 Mon Sep 17 00:00:00 2001 From: kao Date: Mon, 22 May 2023 14:36:10 -0600 Subject: [PATCH 3/5] Sort area children in Mongo --- src/__tests__/areas.ts | 1 + src/graphql/resolvers.ts | 8 ++++---- src/model/AreaDataSource.ts | 6 ++++++ src/utils/helpers.ts | 16 ---------------- 4 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/__tests__/areas.ts b/src/__tests__/areas.ts index 5058a7f2..7596b4bc 100644 --- a/src/__tests__/areas.ts +++ b/src/__tests__/areas.ts @@ -202,6 +202,7 @@ describe('areas API', () => { }) expect(areaClimbsResponse.statusCode).toBe(200) const areaResult = areaClimbsResponse.body.data.area + console.log(areaClimbsResponse.body) // In leftRightIndex order expect(areaResult.climbs[0]).toMatchObject({ name: 'left', metadata: { leftRightIndex: 0 } }) expect(areaResult.climbs[1]).toMatchObject({ name: 'middle', metadata: { leftRightIndex: 1 } }) diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index 8981d9fb..58f2c8d4 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -20,7 +20,7 @@ import { OrganizationMutations, OrganizationQueries } from './organization/index import { TickMutations, TickQueries } from './tick/index.js' import { UserQueries, UserMutations, UserResolvers } from './user/index.js' import { getAuthorMetadataFromBaseNode } from '../db/utils/index.js' -import { compareAreaLeftRightIndex, compareClimbLeftRightIndex, geojsonPointToLatitude, geojsonPointToLongitude } from '../utils/helpers.js' +import { geojsonPointToLatitude, geojsonPointToLongitude } from '../utils/helpers.js' /** * It takes a file name as an argument, reads the file, and returns a GraphQL DocumentNode. @@ -202,7 +202,7 @@ const resolvers = { children: async (parent: AreaType, _, { dataSources: { areas } }: Context) => { if (parent.children.length > 0) { - return (await areas.findManyByIds(parent.children)).sort(compareAreaLeftRightIndex) + return await areas.findManyByIds(parent.children) } return [] }, @@ -221,11 +221,11 @@ const resolvers = { // Test to see if we have actual climb object returned from findOneAreaByUUID() const isClimbTypeArray = (x: any[]): x is ClimbType[] => x[0].name != null if (isClimbTypeArray(result)) { - return result.sort(compareClimbLeftRightIndex) + return result } // List of IDs, we need to convert them into actual climbs - return (await areas.findManyClimbsByUuids(result as MUUID[])).sort(compareClimbLeftRightIndex) + return await areas.findManyClimbsByUuids(result as MUUID[]) }, metadata: (node: AreaType) => { diff --git a/src/model/AreaDataSource.ts b/src/model/AreaDataSource.ts index 07c4ccb3..56e09a73 100644 --- a/src/model/AreaDataSource.ts +++ b/src/model/AreaDataSource.ts @@ -108,6 +108,12 @@ export default class AreaDataSource extends MongoDataSource { $set: { 'climbs.gradeContext': '$gradeContext' // manually set area's grade context to climb } + }, + { + $set: { + climbs: { $sortArray: { input: '$climbs', sortBy: { 'metadata.left_right_index': 1 } } }, + children: { $sortArray: { input: '$children', sortBy: { 'metadata.leftRightIndex': 1 } } } + } } ]) diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index e74d0c1b..55ecd369 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,7 +1,5 @@ import { MUUID } from 'uuid-mongodb' import { Point } from '@turf/helpers' -import { AreaType } from '../db/AreaTypes' -import { ClimbType } from '../db/ClimbTypes' export const muuidToString = (m: MUUID): string => m.toUUID().toString() @@ -23,17 +21,3 @@ export function exhaustiveCheck (_value: never): never { export const geojsonPointToLongitude = (point: Point): number => point.coordinates[0] export const geojsonPointToLatitude = (point: Point): number => point.coordinates[1] - -export function compareAreaLeftRightIndex (a: AreaType, b: AreaType): number { - if (a.metadata.leftRightIndex == null || b.metadata.leftRightIndex == null) { - return 0 // Preserve order if any element is missing leftRightIndex - } - return (a.metadata.leftRightIndex - b.metadata.leftRightIndex) -} - -export function compareClimbLeftRightIndex (a: ClimbType, b: ClimbType): number { - if (a.metadata.left_right_index == null || b.metadata.left_right_index == null) { - return 0 // Preserve order if any element is missing left_right_index - } - return (a.metadata.left_right_index - b.metadata.left_right_index) -} From 666fa4c6cb92afdf6c735e5befe8904d22f9143b Mon Sep 17 00:00:00 2001 From: kao Date: Tue, 23 May 2023 14:52:42 -0600 Subject: [PATCH 4/5] New area datasource getChildren endpoint --- src/graphql/resolvers.ts | 2 +- src/model/AreaDataSource.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index 58f2c8d4..186a1b34 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -202,7 +202,7 @@ const resolvers = { children: async (parent: AreaType, _, { dataSources: { areas } }: Context) => { if (parent.children.length > 0) { - return await areas.findManyByIds(parent.children) + return await areas.findChildren(parent.children) } return [] }, diff --git a/src/model/AreaDataSource.ts b/src/model/AreaDataSource.ts index 56e09a73..abeec9b4 100644 --- a/src/model/AreaDataSource.ts +++ b/src/model/AreaDataSource.ts @@ -2,6 +2,7 @@ import { MongoDataSource } from 'apollo-datasource-mongodb' import { Filter } from 'mongodb' import muuid from 'uuid-mongodb' import bboxPolygon from '@turf/bbox-polygon' +import { Types as mongooseTypes } from 'mongoose' import { getAreaModel, getMediaModel, getMediaObjectModel } from '../db/index.js' import { AreaType } from '../db/AreaTypes' @@ -111,8 +112,7 @@ export default class AreaDataSource extends MongoDataSource { }, { $set: { - climbs: { $sortArray: { input: '$climbs', sortBy: { 'metadata.left_right_index': 1 } } }, - children: { $sortArray: { input: '$children', sortBy: { 'metadata.leftRightIndex': 1 } } } + climbs: { $sortArray: { input: '$climbs', sortBy: { 'metadata.left_right_index': 1 } } } } } ]) @@ -123,6 +123,11 @@ export default class AreaDataSource extends MongoDataSource { throw new Error(`Area ${uuid.toUUID().toString()} not found.`) } + async findChildren (children: mongooseTypes.ObjectId[]): Promise { + return await this.areaModel.find().where('_id').in(children) + .sort({ 'metadata.leftRightIndex': 1 }).lean() + } + async findManyClimbsByUuids (uuidList: muuid.MUUID[]): Promise { const rs = await this.climbModel.find().where('_id').in(uuidList) return rs From 38515ff2ef28146e76af661da5babcad685b5d24 Mon Sep 17 00:00:00 2001 From: kao Date: Tue, 23 May 2023 15:07:18 -0600 Subject: [PATCH 5/5] Use $lookup to populate area children --- src/__tests__/areas.ts | 1 - src/graphql/resolvers.ts | 7 ------- src/model/AreaDataSource.ts | 16 ++++++++++------ 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/__tests__/areas.ts b/src/__tests__/areas.ts index 7596b4bc..5058a7f2 100644 --- a/src/__tests__/areas.ts +++ b/src/__tests__/areas.ts @@ -202,7 +202,6 @@ describe('areas API', () => { }) expect(areaClimbsResponse.statusCode).toBe(200) const areaResult = areaClimbsResponse.body.data.area - console.log(areaClimbsResponse.body) // In leftRightIndex order expect(areaResult.climbs[0]).toMatchObject({ name: 'left', metadata: { leftRightIndex: 0 } }) expect(areaResult.climbs[1]).toMatchObject({ name: 'middle', metadata: { leftRightIndex: 1 } }) diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index 186a1b34..2e065d0c 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -200,13 +200,6 @@ const resolvers = { // New camel case field areaName: async (node: AreaType) => node.area_name, - children: async (parent: AreaType, _, { dataSources: { areas } }: Context) => { - if (parent.children.length > 0) { - return await areas.findChildren(parent.children) - } - return [] - }, - aggregate: async (node: AreaType) => { return node.aggregate }, diff --git a/src/model/AreaDataSource.ts b/src/model/AreaDataSource.ts index abeec9b4..537157e5 100644 --- a/src/model/AreaDataSource.ts +++ b/src/model/AreaDataSource.ts @@ -105,6 +105,14 @@ export default class AreaDataSource extends MongoDataSource { as: 'climbs' // clobber array of climb IDs with climb objects } }, + { // Self-join to populate children areas. + $lookup: { + from: 'areas', + localField: 'children', + foreignField: '_id', + as: 'children' + } + }, { $set: { 'climbs.gradeContext': '$gradeContext' // manually set area's grade context to climb @@ -112,7 +120,8 @@ export default class AreaDataSource extends MongoDataSource { }, { $set: { - climbs: { $sortArray: { input: '$climbs', sortBy: { 'metadata.left_right_index': 1 } } } + climbs: { $sortArray: { input: '$climbs', sortBy: { 'metadata.left_right_index': 1 } } }, + children: { $sortArray: { input: '$children', sortBy: { 'metadata.leftRightIndex': 1 } } } } } ]) @@ -123,11 +132,6 @@ export default class AreaDataSource extends MongoDataSource { throw new Error(`Area ${uuid.toUUID().toString()} not found.`) } - async findChildren (children: mongooseTypes.ObjectId[]): Promise { - return await this.areaModel.find().where('_id').in(children) - .sort({ 'metadata.leftRightIndex': 1 }).lean() - } - async findManyClimbsByUuids (uuidList: muuid.MUUID[]): Promise { const rs = await this.climbModel.find().where('_id').in(uuidList) return rs