Skip to content
Open
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
13 changes: 13 additions & 0 deletions packages/backend/src/db/queries/climbs/create-climb-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
// This eliminates the need for a JOIN on product_sizes in the main query
// MoonBoard climbs have NULL edge values (single fixed size), so skip edge filtering
const sizeConditions: SQL[] = params.board_name === 'moonboard' ? [] : [
sql`${tables.climbs.edgeLeft} > ${sizeEdges.edgeLeft}`,

Check failure on line 77 in packages/backend/src/db/queries/climbs/create-climb-filters.ts

View workflow job for this annotation

GitHub Actions / test

src/__tests__/climb-queries.test.ts > Climb Query Functions > countClimbs > should respect filters in count

TypeError: Cannot read properties of null (reading 'edgeLeft') ❯ createClimbFilters src/db/queries/climbs/create-climb-filters.ts:77:49 ❯ countClimbs src/db/queries/climbs/count-climbs.ts:27:19 ❯ src/__tests__/climb-queries.test.ts:221:35

Check failure on line 77 in packages/backend/src/db/queries/climbs/create-climb-filters.ts

View workflow job for this annotation

GitHub Actions / test

src/__tests__/climb-queries.test.ts > Climb Query Functions > countClimbs > should return accurate total count

TypeError: Cannot read properties of null (reading 'edgeLeft') ❯ createClimbFilters src/db/queries/climbs/create-climb-filters.ts:77:49 ❯ countClimbs src/db/queries/climbs/count-climbs.ts:27:19 ❯ src/__tests__/climb-queries.test.ts:202:27
sql`${tables.climbs.edgeRight} < ${sizeEdges.edgeRight}`,
sql`${tables.climbs.edgeBottom} > ${sizeEdges.edgeBottom}`,
sql`${tables.climbs.edgeTop} < ${sizeEdges.edgeTop}`,
Expand Down Expand Up @@ -251,6 +251,19 @@
eq(tables.climbStats.angle, params.angle),
],

// For use in getHoldHeatmapData - joins climbStats via climbHolds
getHoldHeatmapClimbStatsConditions: () => [
eq(tables.climbStats.climbUuid, tables.climbHolds.climbUuid),
eq(tables.climbStats.boardType, params.board_name),
eq(tables.climbStats.angle, params.angle),
],

// For use when joining climbHolds to climbs
getClimbHoldsJoinConditions: () => [
eq(tables.climbHolds.climbUuid, tables.climbs.uuid),
eq(tables.climbHolds.boardType, params.board_name),
],

// User-specific logbook data selectors
getUserLogbookSelects,

Expand Down
162 changes: 162 additions & 0 deletions packages/backend/src/graphql/resolvers/data-queries/mutations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { eq, and } from 'drizzle-orm';
import type { ConnectionContext } from '@boardsesh/shared-schema';
import { db } from '../../../db/client';
import * as dbSchema from '@boardsesh/db/schema';
import { requireAuthenticated, validateInput } from '../shared/helpers';
import {
SaveHoldClassificationInputSchema,
SaveUserBoardMappingInputSchema,
} from '../../../validation/schemas';

export const dataQueryMutations = {
/**
* Save or update a hold classification.
* Requires authentication.
*/
saveHoldClassification: async (
_: unknown,
{ input }: { input: {
boardType: string;
layoutId: number;
sizeId: number;
holdId: number;
holdType?: string | null;
handRating?: number | null;
footRating?: number | null;
pullDirection?: number | null;
}},
ctx: ConnectionContext,
) => {
requireAuthenticated(ctx);
const validatedInput = validateInput(SaveHoldClassificationInputSchema, input, 'input');
const userId = ctx.userId!;

// Check if a classification already exists
const existing = await db
.select()
.from(dbSchema.userHoldClassifications)
.where(
and(
eq(dbSchema.userHoldClassifications.userId, userId),
eq(dbSchema.userHoldClassifications.boardType, validatedInput.boardType),
eq(dbSchema.userHoldClassifications.layoutId, validatedInput.layoutId),
eq(dbSchema.userHoldClassifications.sizeId, validatedInput.sizeId),
eq(dbSchema.userHoldClassifications.holdId, validatedInput.holdId),
),
)
.limit(1);

const now = new Date().toISOString();

if (existing.length > 0) {
// Update existing classification
await db
.update(dbSchema.userHoldClassifications)
.set({
holdType: validatedInput.holdType ?? null,
handRating: validatedInput.handRating ?? null,
footRating: validatedInput.footRating ?? null,
pullDirection: validatedInput.pullDirection ?? null,
updatedAt: now,
})
.where(eq(dbSchema.userHoldClassifications.id, existing[0].id));

return {
id: existing[0].id.toString(),
userId,
boardType: validatedInput.boardType,
layoutId: validatedInput.layoutId,
sizeId: validatedInput.sizeId,
holdId: validatedInput.holdId,
holdType: validatedInput.holdType ?? null,
handRating: validatedInput.handRating ?? null,
footRating: validatedInput.footRating ?? null,
pullDirection: validatedInput.pullDirection ?? null,
createdAt: existing[0].createdAt,
updatedAt: now,
};
} else {
// Create new classification
const [result] = await db
.insert(dbSchema.userHoldClassifications)
.values({
userId,
boardType: validatedInput.boardType,
layoutId: validatedInput.layoutId,
sizeId: validatedInput.sizeId,
holdId: validatedInput.holdId,
holdType: validatedInput.holdType ?? null,
handRating: validatedInput.handRating ?? null,
footRating: validatedInput.footRating ?? null,
pullDirection: validatedInput.pullDirection ?? null,
createdAt: now,
updatedAt: now,
})
.returning();

return {
id: result.id.toString(),
userId,
boardType: validatedInput.boardType,
layoutId: validatedInput.layoutId,
sizeId: validatedInput.sizeId,
holdId: validatedInput.holdId,
holdType: validatedInput.holdType ?? null,
handRating: validatedInput.handRating ?? null,
footRating: validatedInput.footRating ?? null,
pullDirection: validatedInput.pullDirection ?? null,
createdAt: now,
updatedAt: now,
};
}
},

/**
* Save a user board mapping.
* Requires authentication.
*/
saveUserBoardMapping: async (
_: unknown,
{ input }: { input: { boardType: string; boardUserId: number; boardUsername?: string | null } },
ctx: ConnectionContext,
) => {
requireAuthenticated(ctx);
const validatedInput = validateInput(SaveUserBoardMappingInputSchema, input, 'input');
const userId = ctx.userId!;

// Upsert: check if mapping already exists
const existing = await db
.select()
.from(dbSchema.userBoardMappings)
.where(
and(
eq(dbSchema.userBoardMappings.userId, userId),
eq(dbSchema.userBoardMappings.boardType, validatedInput.boardType),
eq(dbSchema.userBoardMappings.boardUserId, validatedInput.boardUserId),
),
)
.limit(1);

if (existing.length > 0) {
// Update existing mapping
await db
.update(dbSchema.userBoardMappings)
.set({
boardUsername: validatedInput.boardUsername ?? null,
})
.where(eq(dbSchema.userBoardMappings.id, existing[0].id));
} else {
// Create new mapping
await db
.insert(dbSchema.userBoardMappings)
.values({
userId,
boardType: validatedInput.boardType,
boardUserId: validatedInput.boardUserId,
boardUsername: validatedInput.boardUsername ?? null,
});
}

return true;
},
};
Loading
Loading