Skip to content

Commit

Permalink
feat: add multipitch support (#350)
Browse files Browse the repository at this point in the history
* feat: add multipitch support
  • Loading branch information
l4u532 committed Oct 16, 2023
1 parent 3ecbd17 commit 9a08eec
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 11 deletions.
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,10 @@ build
tmp
.env.local

# Intellij
# Intellij and VSCode
.idea/
*.iml
.vscode/settings.json

/export/
/openbeta-export/
/openbeta-export/
23 changes: 23 additions & 0 deletions src/db/ClimbSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,28 @@ const GradeTypeSchema = new Schema<GradeScalesTypes>({
UIAA: { type: Schema.Types.String, required: false }
}, { _id: false })

const PitchSchema = new mongoose.Schema({
_id: {
type: 'object',
value: { type: 'Buffer' },
default: () => muuid.v4()
},
parentId: {
type: Schema.Types.Mixed,
ref: 'climbs',
required: true
},
pitchNumber: { type: Number, required: true },
grades: { type: GradeTypeSchema },
type: { type: mongoose.Schema.Types.Mixed },
length: { type: Number },
boltsCount: { type: Number },
description: { type: String }
}, {
_id: false,
timestamps: true
})

export const ClimbSchema = new Schema<ClimbType>({
_id: {
type: 'object',
Expand All @@ -86,6 +108,7 @@ export const ClimbSchema = new Schema<ClimbType>({
required: true
},
boltsCount: { type: Schema.Types.Number, required: false },
pitches: { type: [PitchSchema], default: undefined, required: false },
metadata: MetadataSchema,
content: ContentSchema,
_deleting: { type: Date },
Expand Down
37 changes: 32 additions & 5 deletions src/db/ClimbTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,35 @@ export type ClimbGQLQueryType = ClimbType & {
* Clinbs have a number of fields that may be expected to appear within their documents.
*/
export type ClimbType = IClimbProps & {
pitches?: IPitch[]
metadata: IClimbMetadata
content?: IClimbContent
}

/* Models a single pitch of a multi-pitch route */
export interface IPitch {
_id: MUUID
parentId: MUUID
pitchNumber: number
grades?: Partial<Record<GradeScalesTypes, string>>
type?: DisciplineType
length?: number
boltsCount?: number
description?: string
}

export interface IClimbProps {
_id: MUUID
name: string
/** First ascent, if known. Who was the first person to climb this route? */
fa?: string
yds?: string

/** Total length in metersif known. We will support individual pitch lenth in the future. */
/** Total length in meters, if known */
length?: number
/** Total number of bolts (fixed anchors) */
boltsCount?: number
/* Array of Pitch objects representing the individual pitches of the climb */
pitches?: IPitch[] | undefined
/**
* Grades appear within as an I18n-safe format.
* We achieve this via a larger data encapsulation, and perform interpretation and comparison
Expand Down Expand Up @@ -127,8 +141,8 @@ export interface IClimbMetadata {
/** mountainProject ID (if this climb was sourced from mountainproject) */
mp_id?: string
/**
* If this climb was sourced from mountianproject, we expect a parent ID
* for its crag to also be Available
* If this climb was sourced from mountainproject, we expect a parent ID
* for its crag to also be available
*/
mp_crag_id?: string
/** the parent Area in which this climb appears */
Expand All @@ -150,6 +164,17 @@ export interface IClimbContent {

export type ClimbGradeContextType = Record<keyof DisciplineType, GradeScalesTypes>

export interface PitchChangeInputType {
id?: string
parentId?: string
pitchNumber?: number
grades?: Partial<Record<GradeScalesTypes, string>>
type?: DisciplineType
length?: number
boltsCount?: number
description?: string
}

export interface ClimbChangeInputType {
id?: string
name?: string
Expand All @@ -160,6 +185,7 @@ export interface ClimbChangeInputType {
location?: string
protection?: string
boltsCount?: number
pitches?: PitchChangeInputType[] | undefined
fa?: string
length?: number
experimentalAuthor?: {
Expand All @@ -168,7 +194,8 @@ export interface ClimbChangeInputType {
}
}

type UpdatableClimbFieldsType = Pick<ClimbType, 'fa' | 'name' | 'type' | 'gradeContext' | 'grades' | 'content' | 'length' | 'boltsCount'>
type UpdatableClimbFieldsType = Pick<ClimbType, 'fa' | 'name' | 'type' | 'gradeContext' | 'grades' | 'content' | 'length' | 'boltsCount' | 'pitches'>

/**
* Minimum required fields when adding a new climb or boulder problem
*/
Expand Down
11 changes: 11 additions & 0 deletions src/graphql/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,17 @@ const resolvers = {

boltsCount: (node: ClimbGQLQueryType) => node.boltsCount ?? -1,

pitches: (node: ClimbGQLQueryType) => node.pitches != null
? node.pitches.map(pitch => {
const { parentId, ...otherPitchProps } = pitch
return {
id: pitch._id?.toUUID().toString(),
parentId: node._id?.toUUID().toString(),
...otherPitchProps
}
})
: null,

grades: (node: ClimbGQLQueryType) => node.grades ?? null,

metadata: (node: ClimbGQLQueryType) => {
Expand Down
16 changes: 15 additions & 1 deletion src/graphql/schema/Climb.gql
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ type Climb {
length: Int!

"Number of bolts/permanent anchors, if known (-1 otherwise)"
boltsCount: Int!
boltsCount: Int

"List of Pitch objects representing individual pitches of a multi-pitch climb"
pitches: [Pitch]

"The grade(s) assigned to this climb. See GradeType documentation"
grades: GradeType
Expand Down Expand Up @@ -191,3 +194,14 @@ enum SafetyEnum {
"No protection and overall the route is extremely dangerous."
X
}

type Pitch {
id: ID!
parentId: ID!
pitchNumber: Int!
grades: GradeType
type: ClimbType
length: Int
boltsCount: Int
description: String
}
25 changes: 22 additions & 3 deletions src/model/MutableClimbDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import muid, { MUUID } from 'uuid-mongodb'
import { UserInputError } from 'apollo-server'
import { ClientSession } from 'mongoose'

import { ClimbChangeDocType, ClimbChangeInputType, ClimbEditOperationType } from '../db/ClimbTypes.js'
import { ClimbChangeDocType, ClimbChangeInputType, ClimbEditOperationType, IPitch } from '../db/ClimbTypes.js'
import ClimbDataSource from './ClimbDataSource.js'
import { createInstance as createExperimentalUserDataSource } from './ExperimentalUserDataSource.js'
import { sanitizeDisciplines, gradeContextToGradeScales, createGradeObject } from '../GradeUtils.js'
Expand Down Expand Up @@ -107,7 +107,7 @@ export default class MutableClimbDataSource extends ClimbDataSource {
for (let i = 0; i < userInput.length; i++) {
// when adding new climbs we require name and disciplines
if (!idList[i].existed && userInput[i].name == null) {
throw new UserInputError(`Can't add new climbs without name. (Index[index=${i}])`)
throw new UserInputError(`Can't add new climbs without name. (Index[index=${i}])`)
}

// See https://github.com/OpenBeta/openbeta-graphql/issues/244
Expand All @@ -125,6 +125,24 @@ export default class MutableClimbDataSource extends ClimbDataSource {
? createGradeObject(grade, typeSafeDisciplines, cragGradeScales)
: null

const pitches = userInput[i].pitches

const newPitchesWithIDs = pitches != null
? pitches.map((pitch): IPitch => {
const { id, ...partialPitch } = pitch // separate 'id' input and rest of the pitch properties to avoid duplicate id and _id
if (partialPitch.pitchNumber === undefined) {
throw new UserInputError('Each pitch in a multi-pitch climb must have a pitchNumber representing its sequence in the climb. Please ensure that every pitch is numbered.')
}
return {
_id: muid.from(id ?? muid.v4()), // populate _id
// feed rest of pitch data
...partialPitch,
parentId: muid.from(partialPitch.parentId ?? newClimbIds[i]),
pitchNumber: partialPitch.pitchNumber
}
})
: null

const { description, location, protection, name, fa, length, boltsCount } = userInput[i]

// Make sure we don't update content = {}
Expand All @@ -151,7 +169,8 @@ export default class MutableClimbDataSource extends ClimbDataSource {
gradeContext: parent.gradeContext,
...fa != null && { fa },
...length != null && length > 0 && { length },
...boltsCount != null && boltsCount > 0 && { boltsCount },
...boltsCount != null && boltsCount >= 0 && { boltsCount }, // Include 'boltsCount' if it's defined and its value is 0 (no bolts) or greater
...newPitchesWithIDs != null && { pitches: newPitchesWithIDs },
...Object.keys(content).length > 0 && { content },
metadata: {
areaRef: parent.metadata.area_id,
Expand Down
Loading

0 comments on commit 9a08eec

Please sign in to comment.