Files
freno-dev/src/server/api/routers/nessa.ts

2645 lines
94 KiB
TypeScript

import { createTRPCRouter, nessaProcedure, publicProcedure } from "../utils";
import { z } from "zod";
import { TRPCError } from "@trpc/server";
import { jwtVerify, importJWK } from "jose";
import { NessaConnectionFactory } from "~/server/database";
import { cache } from "~/server/cache";
import { hashPassword, checkPasswordSafe } from "~/server/utils";
import { signNessaToken } from "~/server/nessa-auth";
const NESSA_CACHE_TTL_MS = 5 * 60 * 1000;
const paginatedQuerySchema = z.object({
limit: z.number().int().min(1).max(100).optional(),
offset: z.number().int().min(0).optional(),
since: z.string().optional()
});
const planIdSchema = z.object({ id: z.string().min(1) });
const userIdSchema = z.object({ id: z.string().min(1) });
const workoutIdSchema = z.object({ id: z.string().min(1) });
const exerciseIdSchema = z.object({ id: z.string().min(1) });
const planExerciseIdSchema = z.object({ id: z.string().min(1) });
const planSetIdSchema = z.object({ id: z.string().min(1) });
const routePointIdSchema = z.object({ id: z.string().min(1) });
const userInputSchema = z.object({
id: z.string().min(1),
email: z.string().email().nullable().optional(),
emailVerified: z.number().int().min(0).max(1).optional(),
firstName: z.string().nullable().optional(),
lastName: z.string().nullable().optional(),
displayName: z.string().nullable().optional(),
avatarUrl: z.string().url().nullable().optional(),
provider: z.string().nullable().optional(),
appleUserId: z.string().nullable().optional(),
status: z.string().optional()
});
const exerciseLibrarySchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
category: z.string().min(1),
muscleGroups: z.string().nullable().optional(),
equipment: z.string().nullable().optional(),
instructions: z.string().nullable().optional(),
defaultSets: z.number().int().nullable().optional(),
defaultReps: z.number().int().nullable().optional(),
defaultRestSeconds: z.number().int().nullable().optional(),
notes: z.string().nullable().optional()
});
const workoutPlanSchema = z.object({
id: z.string().min(1),
userId: z.string().min(1),
name: z.string().min(1),
description: z.string().nullable().optional(),
category: z.string().min(1),
difficulty: z.string().optional(),
durationMinutes: z.number().int().nullable().optional(),
type: z.string().min(1),
isPublic: z.number().int().min(0).max(1).optional()
});
const planExerciseSchema = z.object({
id: z.string().min(1),
planId: z.string().min(1),
exerciseId: z.string().nullable().optional(),
name: z.string().min(1),
category: z.string().min(1),
orderIndex: z.number().int(),
notes: z.string().nullable().optional()
});
const planSetSchema = z.object({
id: z.string().min(1),
planExerciseId: z.string().min(1),
setNumber: z.number().int(),
reps: z.number().int().nullable().optional(),
weight: z.number().nullable().optional(),
durationSeconds: z.number().nullable().optional(),
rpe: z.number().nullable().optional(),
restAfterSeconds: z.number().int().nullable().optional(),
isWarmup: z.number().int().min(0).max(1).optional(),
isDropset: z.number().int().min(0).max(1).optional(),
notes: z.string().nullable().optional()
});
const routePointSchema = z.object({
id: z.string().min(1),
planId: z.string().min(1),
latitude: z.number(),
longitude: z.number(),
orderIndex: z.number().int(),
isWaypoint: z.number().int().min(0).max(1).optional()
});
const workoutSchema = z.object({
id: z.string().min(1),
userId: z.string().min(1),
planId: z.string().nullable().optional(),
type: z.string().min(1),
name: z.string().nullable().optional(),
startDate: z.string().min(1),
endDate: z.string().nullable().optional(),
durationSeconds: z.number().nullable().optional(),
distanceMeters: z.number().nullable().optional(),
calories: z.number().nullable().optional(),
averageHeartRate: z.number().nullable().optional(),
maxHeartRate: z.number().nullable().optional(),
averagePace: z.number().nullable().optional(),
elevationGain: z.number().nullable().optional(),
status: z.string().min(1),
source: z.string().min(1),
healthKitUUID: z.string().nullable().optional(),
notes: z.string().nullable().optional()
});
const heartRateSchema = z.object({
id: z.string().min(1),
workoutId: z.string().min(1),
timestamp: z.string().min(1),
bpm: z.number(),
source: z.string().nullable().optional()
});
const locationSampleSchema = z.object({
id: z.string().min(1),
workoutId: z.string().min(1),
timestamp: z.string().min(1),
latitude: z.number(),
longitude: z.number(),
altitude: z.number().nullable().optional(),
horizontalAccuracy: z.number().nullable().optional(),
verticalAccuracy: z.number().nullable().optional(),
speed: z.number().nullable().optional(),
course: z.number().nullable().optional()
});
const workoutSplitSchema = z.object({
id: z.string().min(1),
workoutId: z.string().min(1),
splitNumber: z.number().int(),
distanceMeters: z.number(),
durationSeconds: z.number(),
startTimestamp: z.string().min(1),
endTimestamp: z.string().min(1),
averageHeartRate: z.number().nullable().optional(),
averagePace: z.number().nullable().optional(),
elevationGain: z.number().nullable().optional(),
elevationLoss: z.number().nullable().optional()
});
const providerSchema = z.object({
id: z.string().min(1),
userId: z.string().min(1),
provider: z.string().min(1),
providerUserId: z.string().nullable().optional(),
email: z.string().nullable().optional(),
displayName: z.string().nullable().optional(),
avatarUrl: z.string().nullable().optional()
});
const bulkSchema = z.object({
users: z.array(userInputSchema).optional(),
workoutPlans: z.array(workoutPlanSchema).optional(),
planExercises: z.array(planExerciseSchema).optional(),
planSets: z.array(planSetSchema).optional(),
routePoints: z.array(routePointSchema).optional(),
workouts: z.array(workoutSchema).optional(),
heartRateSamples: z.array(heartRateSchema).optional(),
locationSamples: z.array(locationSampleSchema).optional(),
workoutSplits: z.array(workoutSplitSchema).optional(),
exerciseLibrary: z.array(exerciseLibrarySchema).optional(),
authProviders: z.array(providerSchema).optional()
});
const registerSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
firstName: z.string().min(1),
lastName: z.string().min(1)
});
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(1)
});
const googleSignInSchema = z.object({
idToken: z.string().min(1),
email: z.string().email().optional(),
firstName: z.string().optional(),
lastName: z.string().optional()
});
const appleSignInSchema = z.object({
idToken: z.string().min(1),
email: z.string().email().optional(),
firstName: z.string().optional(),
lastName: z.string().optional(),
appleUserId: z.string().min(1)
});
interface GoogleTokenPayload {
iss: string;
azp: string;
aud: string;
sub: string;
email?: string;
email_verified?: boolean;
name?: string;
picture?: string;
given_name?: string;
family_name?: string;
iat: number;
exp: number;
}
interface AppleTokenPayload {
iss: string;
aud: string;
exp: number;
iat: number;
sub: string;
email?: string;
email_verified?: boolean | string;
is_private_email?: boolean | string;
real_user_status?: number;
}
export const nessaDbRouter = createTRPCRouter({
health: nessaProcedure.query(async () => {
try {
const conn = NessaConnectionFactory();
const result = await conn.execute("SELECT 1 as ok");
return { success: true, ok: result.rows.length > 0 };
} catch (error) {
console.error("Nessa remote DB health check failed:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Remote database health check failed"
});
}
}),
register: publicProcedure
.input(registerSchema)
.mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
const existing = await conn.execute({
sql: "SELECT id FROM users WHERE email = ?",
args: [input.email]
});
if (existing.rows.length) {
throw new TRPCError({
code: "CONFLICT",
message: "Email already registered"
});
}
const userId = crypto.randomUUID();
const passwordHash = await hashPassword(input.password);
await conn.execute({
sql: `INSERT INTO users (id, email, emailVerified, firstName, lastName, displayName, provider, status, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
args: [
userId,
input.email,
0,
input.firstName,
input.lastName,
`${input.firstName} ${input.lastName}`.trim(),
"email",
"active"
]
});
await conn.execute({
sql: "INSERT INTO authProviders (id, userId, provider, providerUserId, email, displayName, avatarUrl) VALUES (?, ?, ?, ?, ?, ?, ?)",
args: [
crypto.randomUUID(),
userId,
"email",
null,
input.email,
null,
null
]
});
await conn.execute({
sql: "INSERT INTO authProviders (id, userId, provider, providerUserId, email, displayName, avatarUrl) VALUES (?, ?, ?, ?, ?, ?, ?)",
args: [
crypto.randomUUID(),
userId,
"password",
passwordHash,
input.email,
null,
null
]
});
await conn.execute({
sql: "INSERT INTO workoutPlans (id, userId, name, category, difficulty, type, isPublic) VALUES (?, ?, ?, ?, ?, ?, ?)",
args: [
crypto.randomUUID(),
userId,
"Getting Started",
"strength",
"beginner",
"strength",
0
]
});
const token = await signNessaToken(userId);
return { success: true, token, userId };
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
console.error("Failed to register Nessa user:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to register user"
});
}
}),
login: publicProcedure.input(loginSchema).mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
const result = await conn.execute({
sql: "SELECT userId, email, provider, providerUserId FROM authProviders WHERE email = ? AND provider IN ('email', 'password')",
args: [input.email]
});
if (!result.rows.length) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid credentials"
});
}
const rows = result.rows as Array<{
userId: string;
email: string | null;
provider: string;
providerUserId: string | null;
}>;
const emailProvider = rows.find((row) => row.provider === "email");
const passwordProvider = rows.find((row) => row.provider === "password");
if (emailProvider?.userId !== passwordProvider?.userId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid credentials"
});
}
if (!emailProvider || !passwordProvider) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid credentials"
});
}
const matches = await checkPasswordSafe(
input.password,
passwordProvider.providerUserId
);
if (!matches) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid credentials"
});
}
const token = await signNessaToken(emailProvider.userId);
await conn.execute({
sql: "UPDATE users SET lastLoginAt = datetime('now'), updatedAt = datetime('now') WHERE id = ?",
args: [emailProvider.userId]
});
return { success: true, token, userId: emailProvider.userId };
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
console.error("Failed to login Nessa user:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to login"
});
}
}),
googleSignIn: publicProcedure
.input(googleSignInSchema)
.mutation(async ({ input }) => {
try {
// Verify the Google ID token
const tokenInfoResponse = await fetch(
`https://oauth2.googleapis.com/tokeninfo?id_token=${input.idToken}`
);
if (!tokenInfoResponse.ok) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid Google ID token"
});
}
const tokenPayload =
(await tokenInfoResponse.json()) as GoogleTokenPayload;
// Validate the token payload
if (
tokenPayload.iss !== "accounts.google.com" &&
tokenPayload.iss !== "https://accounts.google.com"
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid token issuer"
});
}
// Check if token is expired
const now = Math.floor(Date.now() / 1000);
if (tokenPayload.exp < now) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Token has expired"
});
}
const googleUserId = tokenPayload.sub;
const email = tokenPayload.email ?? input.email;
const firstName =
tokenPayload.given_name ?? input.firstName ?? "Google";
const lastName = tokenPayload.family_name ?? input.lastName ?? "User";
const displayName =
tokenPayload.name ?? `${firstName} ${lastName}`.trim();
const avatarUrl = tokenPayload.picture ?? null;
const conn = NessaConnectionFactory();
// Check if user exists by Google provider ID
const existingByGoogle = await conn.execute({
sql: "SELECT userId FROM authProviders WHERE provider = 'google' AND providerUserId = ?",
args: [googleUserId]
});
let userId: string;
if (existingByGoogle.rows.length > 0) {
// User exists with Google account - log them in
userId = existingByGoogle.rows[0].userId as string;
await conn.execute({
sql: "UPDATE users SET lastLoginAt = datetime('now'), updatedAt = datetime('now') WHERE id = ?",
args: [userId]
});
} else if (email) {
// Check if user exists by email
const existingByEmail = await conn.execute({
sql: "SELECT id FROM users WHERE email = ?",
args: [email]
});
if (existingByEmail.rows.length > 0) {
// User exists with email - link Google account
userId = existingByEmail.rows[0].id as string;
await conn.execute({
sql: "INSERT INTO authProviders (id, userId, provider, providerUserId, email, displayName, avatarUrl) VALUES (?, ?, ?, ?, ?, ?, ?)",
args: [
crypto.randomUUID(),
userId,
"google",
googleUserId,
email,
displayName,
avatarUrl
]
});
await conn.execute({
sql: "UPDATE users SET provider = 'google', lastLoginAt = datetime('now'), updatedAt = datetime('now') WHERE id = ?",
args: [userId]
});
} else {
// Create new user with Google account
userId = crypto.randomUUID();
await conn.execute({
sql: `INSERT INTO users (id, email, emailVerified, firstName, lastName, displayName, avatarUrl, provider, status, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
args: [
userId,
email,
tokenPayload.email_verified ? 1 : 0,
firstName,
lastName,
displayName,
avatarUrl,
"google",
"active"
]
});
await conn.execute({
sql: "INSERT INTO authProviders (id, userId, provider, providerUserId, email, displayName, avatarUrl) VALUES (?, ?, ?, ?, ?, ?, ?)",
args: [
crypto.randomUUID(),
userId,
"google",
googleUserId,
email,
displayName,
avatarUrl
]
});
// Create default workout plan for new user
await conn.execute({
sql: "INSERT INTO workoutPlans (id, userId, name, category, difficulty, type, isPublic) VALUES (?, ?, ?, ?, ?, ?, ?)",
args: [
crypto.randomUUID(),
userId,
"Getting Started",
"strength",
"beginner",
"strength",
0
]
});
}
} else {
// No email available - create user without email
userId = crypto.randomUUID();
await conn.execute({
sql: `INSERT INTO users (id, email, emailVerified, firstName, lastName, displayName, avatarUrl, provider, status, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
args: [
userId,
null,
0,
firstName,
lastName,
displayName,
avatarUrl,
"google",
"active"
]
});
await conn.execute({
sql: "INSERT INTO authProviders (id, userId, provider, providerUserId, email, displayName, avatarUrl) VALUES (?, ?, ?, ?, ?, ?, ?)",
args: [
crypto.randomUUID(),
userId,
"google",
googleUserId,
null,
displayName,
avatarUrl
]
});
// Create default workout plan for new user
await conn.execute({
sql: "INSERT INTO workoutPlans (id, userId, name, category, difficulty, type, isPublic) VALUES (?, ?, ?, ?, ?, ?, ?)",
args: [
crypto.randomUUID(),
userId,
"Getting Started",
"strength",
"beginner",
"strength",
0
]
});
}
const token = await signNessaToken(userId);
return { success: true, token, userId };
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
console.error("Failed to sign in with Google:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to sign in with Google"
});
}
}),
appleSignIn: publicProcedure
.input(appleSignInSchema)
.mutation(async ({ input }) => {
try {
// Verify the Apple ID token
// Apple's public keys for JWT verification
const appleKeysResponse = await fetch(
"https://appleid.apple.com/auth/keys"
);
if (!appleKeysResponse.ok) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch Apple public keys"
});
}
const appleKeys = (await appleKeysResponse.json()) as {
keys: Array<{
kty: string;
kid: string;
use: string;
alg: string;
n: string;
e: string;
}>;
};
// Decode the JWT header to get the key ID
const [headerB64] = input.idToken.split(".");
if (!headerB64) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid Apple ID token format"
});
}
const headerJson = Buffer.from(headerB64, "base64url").toString("utf8");
const header = JSON.parse(headerJson) as { kid: string; alg: string };
// Find the matching key
const jwk = appleKeys.keys.find((k) => k.kid === header.kid);
if (!jwk) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Apple public key not found"
});
}
// Import the Apple JWK key for signature verification
const publicKey = await importJWK(jwk, "RS256");
// Verify the Apple ID token signature and claims using jose
const jwtOptions: Parameters<typeof jwtVerify>[2] = {
algorithms: ["RS256"],
issuer: "https://appleid.apple.com"
};
if (env.APPLE_CLIENT_ID) {
jwtOptions.audience = env.APPLE_CLIENT_ID;
}
const { payload: tokenPayload } = await jwtVerify(
input.idToken,
publicKey,
jwtOptions
);
// Apple user ID from token should match the one provided
if (tokenPayload.sub !== input.appleUserId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Apple user ID mismatch"
});
}
const appleUserId = tokenPayload.sub as string;
// Apple only sends email on first sign-in, so use input.email if token doesn't have it
const email = tokenPayload.email ?? input.email;
const firstName = input.firstName ?? "Apple";
const lastName = input.lastName ?? "User";
const displayName = `${firstName} ${lastName}`.trim();
const conn = NessaConnectionFactory();
// Check if user exists by Apple provider ID
const existingByApple = await conn.execute({
sql: "SELECT userId FROM authProviders WHERE provider = 'apple' AND providerUserId = ?",
args: [appleUserId]
});
let userId: string;
if (existingByApple.rows.length > 0) {
// User exists with Apple account - log them in
userId = existingByApple.rows[0].userId as string;
await conn.execute({
sql: "UPDATE users SET lastLoginAt = datetime('now'), updatedAt = datetime('now') WHERE id = ?",
args: [userId]
});
} else if (email) {
// Check if user exists by email
const existingByEmail = await conn.execute({
sql: "SELECT id FROM users WHERE email = ?",
args: [email]
});
if (existingByEmail.rows.length > 0) {
// User exists with email - link Apple account
userId = existingByEmail.rows[0].id as string;
await conn.execute({
sql: "INSERT INTO authProviders (id, userId, provider, providerUserId, email, displayName, avatarUrl) VALUES (?, ?, ?, ?, ?, ?, ?)",
args: [
crypto.randomUUID(),
userId,
"apple",
appleUserId,
email,
displayName,
null
]
});
await conn.execute({
sql: "UPDATE users SET provider = 'apple', appleUserId = ?, lastLoginAt = datetime('now'), updatedAt = datetime('now') WHERE id = ?",
args: [appleUserId, userId]
});
} else {
// Create new user with Apple account
userId = crypto.randomUUID();
await conn.execute({
sql: `INSERT INTO users (id, email, emailVerified, firstName, lastName, displayName, avatarUrl, provider, appleUserId, status, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
args: [
userId,
email,
tokenPayload.email_verified === true ||
tokenPayload.email_verified === "true"
? 1
: 0,
firstName,
lastName,
displayName,
null,
"apple",
appleUserId,
"active"
]
});
await conn.execute({
sql: "INSERT INTO authProviders (id, userId, provider, providerUserId, email, displayName, avatarUrl) VALUES (?, ?, ?, ?, ?, ?, ?)",
args: [
crypto.randomUUID(),
userId,
"apple",
appleUserId,
email,
displayName,
null
]
});
// Create default workout plan for new user
await conn.execute({
sql: "INSERT INTO workoutPlans (id, userId, name, category, difficulty, type, isPublic) VALUES (?, ?, ?, ?, ?, ?, ?)",
args: [
crypto.randomUUID(),
userId,
"Getting Started",
"strength",
"beginner",
"strength",
0
]
});
}
} else {
// No email available - create user without email
userId = crypto.randomUUID();
await conn.execute({
sql: `INSERT INTO users (id, email, emailVerified, firstName, lastName, displayName, avatarUrl, provider, appleUserId, status, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
args: [
userId,
null,
0,
firstName,
lastName,
displayName,
null,
"apple",
appleUserId,
"active"
]
});
await conn.execute({
sql: "INSERT INTO authProviders (id, userId, provider, providerUserId, email, displayName, avatarUrl) VALUES (?, ?, ?, ?, ?, ?, ?)",
args: [
crypto.randomUUID(),
userId,
"apple",
appleUserId,
null,
displayName,
null
]
});
// Create default workout plan for new user
await conn.execute({
sql: "INSERT INTO workoutPlans (id, userId, name, category, difficulty, type, isPublic) VALUES (?, ?, ?, ?, ?, ?, ?)",
args: [
crypto.randomUUID(),
userId,
"Getting Started",
"strength",
"beginner",
"strength",
0
]
});
}
const token = await signNessaToken(userId);
return { success: true, token, userId };
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
console.error("Failed to sign in with Apple:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to sign in with Apple"
});
}
}),
getUsers: nessaProcedure
.input(paginatedQuerySchema)
.query(async ({ input, ctx }) => {
const limit = input.limit ?? 50;
const offset = input.offset ?? 0;
const cacheKey = `nessa-users:${ctx.nessaUserId}:${limit}:${offset}:${input.since ?? ""}`;
const cached = await cache.get<{
users: Array<{ id: string; email: string | null }>;
}>(cacheKey);
if (cached) {
return cached;
}
try {
const conn = NessaConnectionFactory();
const sql = input.since
? "SELECT id, email FROM users WHERE id = ? AND updatedAt > ? ORDER BY createdAt DESC LIMIT ? OFFSET ?"
: "SELECT id, email FROM users WHERE id = ? ORDER BY createdAt DESC LIMIT ? OFFSET ?";
const args = input.since
? [ctx.nessaUserId, input.since, limit, offset]
: [ctx.nessaUserId, limit, offset];
const result = await conn.execute({
sql,
args
});
const payload = {
users: result.rows as Array<{ id: string; email: string | null }>
};
await cache.set(cacheKey, payload, NESSA_CACHE_TTL_MS);
return payload;
} catch (error) {
console.error("Failed to fetch Nessa users:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch Nessa users"
});
}
}),
getPlans: nessaProcedure
.input(paginatedQuerySchema)
.query(async ({ input, ctx }) => {
const limit = input.limit ?? 50;
const offset = input.offset ?? 0;
const cacheKey = `nessa-plans:${ctx.nessaUserId}:${limit}:${offset}:${input.since ?? ""}`;
const cached = await cache.get<{
plans: Array<{ id: string; name: string; category: string }>;
}>(cacheKey);
if (cached) {
return cached;
}
try {
const conn = NessaConnectionFactory();
const sql = input.since
? "SELECT id, name, category FROM workoutPlans WHERE userId = ? AND updatedAt > ? ORDER BY createdAt DESC LIMIT ? OFFSET ?"
: "SELECT id, name, category FROM workoutPlans WHERE userId = ? ORDER BY createdAt DESC LIMIT ? OFFSET ?";
const args = input.since
? [ctx.nessaUserId, input.since, limit, offset]
: [ctx.nessaUserId, limit, offset];
const result = await conn.execute({
sql,
args
});
const payload = {
plans: result.rows as Array<{
id: string;
name: string;
category: string;
}>
};
await cache.set(cacheKey, payload, NESSA_CACHE_TTL_MS);
return payload;
} catch (error) {
console.error("Failed to fetch Nessa plans:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch Nessa plans"
});
}
}),
getPlanDetails: nessaProcedure
.input(paginatedQuerySchema)
.query(async ({ input, ctx }) => {
const limit = input.limit ?? 50;
const offset = input.offset ?? 0;
const cacheKey = `nessa-plan-details:${ctx.nessaUserId}:${limit}:${offset}:${input.since ?? ""}`;
const cached = await cache.get<{
plans: Array<{
id: string;
userId: string;
name: string;
description: string | null;
category: string;
difficulty: string | null;
durationMinutes: number | null;
type: string;
isPublic: number | null;
createdAt: string;
updatedAt: string;
}>;
planExercises: Array<{
id: string;
planId: string;
exerciseId: string | null;
name: string;
category: string;
orderIndex: number;
notes: string | null;
createdAt: string;
}>;
planSets: Array<{
id: string;
planExerciseId: string;
setNumber: number;
reps: number | null;
weight: number | null;
durationSeconds: number | null;
rpe: number | null;
restAfterSeconds: number | null;
isWarmup: number | null;
isDropset: number | null;
notes: string | null;
createdAt: string;
}>;
routePoints: Array<{
id: string;
planId: string;
latitude: number;
longitude: number;
orderIndex: number;
isWaypoint: number | null;
createdAt: string;
}>;
}>(cacheKey);
if (cached) {
return cached;
}
try {
const conn = NessaConnectionFactory();
const planSql = input.since
? "SELECT id, userId, name, description, category, difficulty, durationMinutes, type, isPublic, createdAt, updatedAt FROM workoutPlans WHERE userId = ? AND updatedAt > ? ORDER BY createdAt DESC LIMIT ? OFFSET ?"
: "SELECT id, userId, name, description, category, difficulty, durationMinutes, type, isPublic, createdAt, updatedAt FROM workoutPlans WHERE userId = ? ORDER BY createdAt DESC LIMIT ? OFFSET ?";
const planArgs = input.since
? [ctx.nessaUserId, input.since, limit, offset]
: [ctx.nessaUserId, limit, offset];
const planResult = await conn.execute({
sql: planSql,
args: planArgs
});
const planExercisesSql = input.since
? "SELECT id, planId, exerciseId, name, category, orderIndex, notes, createdAt FROM planExercises WHERE planId IN (SELECT id FROM workoutPlans WHERE userId = ? AND updatedAt > ?) ORDER BY orderIndex ASC"
: "SELECT id, planId, exerciseId, name, category, orderIndex, notes, createdAt FROM planExercises WHERE planId IN (SELECT id FROM workoutPlans WHERE userId = ?) ORDER BY orderIndex ASC";
const planExercisesArgs = input.since
? [ctx.nessaUserId, input.since]
: [ctx.nessaUserId];
const planExercisesResult = await conn.execute({
sql: planExercisesSql,
args: planExercisesArgs
});
const planSetsSql = input.since
? "SELECT id, planExerciseId, setNumber, reps, weight, durationSeconds, rpe, restAfterSeconds, isWarmup, isDropset, notes, createdAt FROM planSets WHERE planExerciseId IN (SELECT id FROM planExercises WHERE planId IN (SELECT id FROM workoutPlans WHERE userId = ? AND updatedAt > ?)) ORDER BY setNumber ASC"
: "SELECT id, planExerciseId, setNumber, reps, weight, durationSeconds, rpe, restAfterSeconds, isWarmup, isDropset, notes, createdAt FROM planSets WHERE planExerciseId IN (SELECT id FROM planExercises WHERE planId IN (SELECT id FROM workoutPlans WHERE userId = ?)) ORDER BY setNumber ASC";
const planSetsArgs = input.since
? [ctx.nessaUserId, input.since]
: [ctx.nessaUserId];
const planSetsResult = await conn.execute({
sql: planSetsSql,
args: planSetsArgs
});
const routePointsSql = input.since
? "SELECT id, planId, latitude, longitude, orderIndex, isWaypoint, createdAt FROM routePoints WHERE planId IN (SELECT id FROM workoutPlans WHERE userId = ? AND updatedAt > ?) ORDER BY orderIndex ASC"
: "SELECT id, planId, latitude, longitude, orderIndex, isWaypoint, createdAt FROM routePoints WHERE planId IN (SELECT id FROM workoutPlans WHERE userId = ?) ORDER BY orderIndex ASC";
const routePointsArgs = input.since
? [ctx.nessaUserId, input.since]
: [ctx.nessaUserId];
const routePointsResult = await conn.execute({
sql: routePointsSql,
args: routePointsArgs
});
const payload = {
plans: planResult.rows as Array<{
id: string;
userId: string;
name: string;
description: string | null;
category: string;
difficulty: string | null;
durationMinutes: number | null;
type: string;
isPublic: number | null;
createdAt: string;
updatedAt: string;
}>,
planExercises: planExercisesResult.rows as Array<{
id: string;
planId: string;
exerciseId: string | null;
name: string;
category: string;
orderIndex: number;
notes: string | null;
createdAt: string;
}>,
planSets: planSetsResult.rows as Array<{
id: string;
planExerciseId: string;
setNumber: number;
reps: number | null;
weight: number | null;
durationSeconds: number | null;
rpe: number | null;
restAfterSeconds: number | null;
isWarmup: number | null;
isDropset: number | null;
notes: string | null;
createdAt: string;
}>,
routePoints: routePointsResult.rows as Array<{
id: string;
planId: string;
latitude: number;
longitude: number;
orderIndex: number;
isWaypoint: number | null;
createdAt: string;
}>
};
await cache.set(cacheKey, payload, NESSA_CACHE_TTL_MS);
return payload;
} catch (error) {
console.error("Failed to fetch Nessa plan details:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch Nessa plan details"
});
}
}),
getWorkouts: nessaProcedure
.input(paginatedQuerySchema)
.query(async ({ input, ctx }) => {
const limit = input.limit ?? 50;
const offset = input.offset ?? 0;
const cacheKey = `nessa-workouts:${ctx.nessaUserId}:${limit}:${offset}:${input.since ?? ""}`;
const cached = await cache.get<{
workouts: Array<{ id: string; type: string; startDate: string }>;
}>(cacheKey);
if (cached) {
return cached;
}
try {
const conn = NessaConnectionFactory();
const sql = input.since
? "SELECT id, type, startDate FROM workouts WHERE userId = ? AND updatedAt > ? ORDER BY startDate DESC LIMIT ? OFFSET ?"
: "SELECT id, type, startDate FROM workouts WHERE userId = ? ORDER BY startDate DESC LIMIT ? OFFSET ?";
const args = input.since
? [ctx.nessaUserId, input.since, limit, offset]
: [ctx.nessaUserId, limit, offset];
const result = await conn.execute({
sql,
args
});
const payload = {
workouts: result.rows as Array<{
id: string;
type: string;
startDate: string;
}>
};
await cache.set(cacheKey, payload, NESSA_CACHE_TTL_MS);
return payload;
} catch (error) {
console.error("Failed to fetch Nessa workouts:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch Nessa workouts"
});
}
}),
getWorkoutDetails: nessaProcedure
.input(paginatedQuerySchema)
.query(async ({ input, ctx }) => {
const limit = input.limit ?? 50;
const offset = input.offset ?? 0;
const cacheKey = `nessa-workout-details:${ctx.nessaUserId}:${limit}:${offset}:${input.since ?? ""}`;
const cached = await cache.get<{
workouts: Array<{
id: string;
userId: string;
planId: string | null;
type: string;
name: string | null;
startDate: string;
endDate: string | null;
durationSeconds: number | null;
distanceMeters: number | null;
calories: number | null;
averageHeartRate: number | null;
maxHeartRate: number | null;
averagePace: number | null;
elevationGain: number | null;
status: string;
source: string;
healthKitUUID: string | null;
notes: string | null;
createdAt: string;
updatedAt: string;
syncedAt: string | null;
}>;
heartRateSamples: Array<{
id: string;
workoutId: string;
timestamp: string;
bpm: number;
source: string | null;
}>;
locationSamples: Array<{
id: string;
workoutId: string;
timestamp: string;
latitude: number;
longitude: number;
altitude: number | null;
horizontalAccuracy: number | null;
verticalAccuracy: number | null;
speed: number | null;
course: number | null;
}>;
workoutSplits: Array<{
id: string;
workoutId: string;
splitNumber: number;
distanceMeters: number;
durationSeconds: number;
startTimestamp: string;
endTimestamp: string;
averageHeartRate: number | null;
averagePace: number | null;
elevationGain: number | null;
elevationLoss: number | null;
}>;
}>(cacheKey);
if (cached) {
return cached;
}
try {
const conn = NessaConnectionFactory();
const workoutSql = input.since
? "SELECT id, userId, planId, type, name, startDate, endDate, durationSeconds, distanceMeters, calories, averageHeartRate, maxHeartRate, averagePace, elevationGain, status, source, healthKitUUID, notes, createdAt, updatedAt, syncedAt FROM workouts WHERE userId = ? AND updatedAt > ? ORDER BY startDate DESC LIMIT ? OFFSET ?"
: "SELECT id, userId, planId, type, name, startDate, endDate, durationSeconds, distanceMeters, calories, averageHeartRate, maxHeartRate, averagePace, elevationGain, status, source, healthKitUUID, notes, createdAt, updatedAt, syncedAt FROM workouts WHERE userId = ? ORDER BY startDate DESC LIMIT ? OFFSET ?";
const workoutArgs = input.since
? [ctx.nessaUserId, input.since, limit, offset]
: [ctx.nessaUserId, limit, offset];
const workoutResult = await conn.execute({
sql: workoutSql,
args: workoutArgs
});
const heartRateSql = input.since
? "SELECT id, workoutId, timestamp, bpm, source FROM heartRateSamples WHERE workoutId IN (SELECT id FROM workouts WHERE userId = ? AND updatedAt > ?) ORDER BY timestamp ASC"
: "SELECT id, workoutId, timestamp, bpm, source FROM heartRateSamples WHERE workoutId IN (SELECT id FROM workouts WHERE userId = ?) ORDER BY timestamp ASC";
const heartRateArgs = input.since
? [ctx.nessaUserId, input.since]
: [ctx.nessaUserId];
const heartRateResult = await conn.execute({
sql: heartRateSql,
args: heartRateArgs
});
const locationSql = input.since
? "SELECT id, workoutId, timestamp, latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy, speed, course FROM locationSamples WHERE workoutId IN (SELECT id FROM workouts WHERE userId = ? AND updatedAt > ?) ORDER BY timestamp ASC"
: "SELECT id, workoutId, timestamp, latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy, speed, course FROM locationSamples WHERE workoutId IN (SELECT id FROM workouts WHERE userId = ?) ORDER BY timestamp ASC";
const locationArgs = input.since
? [ctx.nessaUserId, input.since]
: [ctx.nessaUserId];
const locationResult = await conn.execute({
sql: locationSql,
args: locationArgs
});
const splitSql = input.since
? "SELECT id, workoutId, splitNumber, distanceMeters, durationSeconds, startTimestamp, endTimestamp, averageHeartRate, averagePace, elevationGain, elevationLoss FROM workoutSplits WHERE workoutId IN (SELECT id FROM workouts WHERE userId = ? AND updatedAt > ?) ORDER BY splitNumber ASC"
: "SELECT id, workoutId, splitNumber, distanceMeters, durationSeconds, startTimestamp, endTimestamp, averageHeartRate, averagePace, elevationGain, elevationLoss FROM workoutSplits WHERE workoutId IN (SELECT id FROM workouts WHERE userId = ?) ORDER BY splitNumber ASC";
const splitArgs = input.since
? [ctx.nessaUserId, input.since]
: [ctx.nessaUserId];
const splitResult = await conn.execute({
sql: splitSql,
args: splitArgs
});
const payload = {
workouts: workoutResult.rows as Array<{
id: string;
userId: string;
planId: string | null;
type: string;
name: string | null;
startDate: string;
endDate: string | null;
durationSeconds: number | null;
distanceMeters: number | null;
calories: number | null;
averageHeartRate: number | null;
maxHeartRate: number | null;
averagePace: number | null;
elevationGain: number | null;
status: string;
source: string;
healthKitUUID: string | null;
notes: string | null;
createdAt: string;
updatedAt: string;
syncedAt: string | null;
}>,
heartRateSamples: heartRateResult.rows as Array<{
id: string;
workoutId: string;
timestamp: string;
bpm: number;
source: string | null;
}>,
locationSamples: locationResult.rows as Array<{
id: string;
workoutId: string;
timestamp: string;
latitude: number;
longitude: number;
altitude: number | null;
horizontalAccuracy: number | null;
verticalAccuracy: number | null;
speed: number | null;
course: number | null;
}>,
workoutSplits: splitResult.rows as Array<{
id: string;
workoutId: string;
splitNumber: number;
distanceMeters: number;
durationSeconds: number;
startTimestamp: string;
endTimestamp: string;
averageHeartRate: number | null;
averagePace: number | null;
elevationGain: number | null;
elevationLoss: number | null;
}>
};
await cache.set(cacheKey, payload, NESSA_CACHE_TTL_MS);
return payload;
} catch (error) {
console.error("Failed to fetch Nessa workout details:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch Nessa workout details"
});
}
}),
createUser: nessaProcedure
.input(userInputSchema)
.mutation(async ({ input, ctx }) => {
if (input.id !== ctx.nessaUserId) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: `INSERT INTO users (id, email, emailVerified, firstName, lastName, displayName, avatarUrl, provider, appleUserId, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET email = excluded.email, emailVerified = excluded.emailVerified, firstName = excluded.firstName, lastName = excluded.lastName, displayName = excluded.displayName, avatarUrl = excluded.avatarUrl, provider = excluded.provider, appleUserId = excluded.appleUserId, status = excluded.status, updatedAt = datetime('now')`,
args: [
input.id,
input.email ?? null,
input.emailVerified ?? 0,
input.firstName ?? null,
input.lastName ?? null,
input.displayName ?? null,
input.avatarUrl ?? null,
input.provider ?? null,
input.appleUserId ?? null,
input.status ?? "active"
]
});
return { success: true };
} catch (error) {
console.error("Failed to create user:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create user"
});
}
}),
updateUser: nessaProcedure
.input(userInputSchema)
.mutation(async ({ input, ctx }) => {
if (input.id !== ctx.nessaUserId) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: `UPDATE users SET email = ?, emailVerified = ?, firstName = ?, lastName = ?, displayName = ?, avatarUrl = ?, provider = ?, appleUserId = ?, status = ?, updatedAt = datetime('now') WHERE id = ?`,
args: [
input.email ?? null,
input.emailVerified ?? 0,
input.firstName ?? null,
input.lastName ?? null,
input.displayName ?? null,
input.avatarUrl ?? null,
input.provider ?? null,
input.appleUserId ?? null,
input.status ?? "active",
input.id
]
});
return { success: true };
} catch (error) {
console.error("Failed to update user:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update user"
});
}
}),
deleteUser: nessaProcedure
.input(userIdSchema)
.mutation(async ({ input, ctx }) => {
if (input.id !== ctx.nessaUserId) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: "DELETE FROM users WHERE id = ?",
args: [input.id]
});
return { success: true };
} catch (error) {
console.error("Failed to delete user:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete user"
});
}
}),
createWorkoutPlan: nessaProcedure
.input(workoutPlanSchema)
.mutation(async ({ input, ctx }) => {
if (input.userId != ctx.nessaUserId) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: `INSERT INTO workoutPlans (id, userId, name, description, category, difficulty, durationMinutes, type, isPublic)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
args: [
input.id,
input.userId,
input.name,
input.description ?? null,
input.category,
input.difficulty ?? "intermediate",
input.durationMinutes ?? null,
input.type,
input.isPublic ?? 0
]
});
return { success: true };
} catch (error) {
console.error("Failed to create workout plan:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create workout plan"
});
}
}),
updateWorkoutPlan: nessaProcedure
.input(workoutPlanSchema)
.mutation(async ({ input, ctx }) => {
if (input.userId != ctx.nessaUserId) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: `UPDATE workoutPlans SET name = ?, description = ?, category = ?, difficulty = ?, durationMinutes = ?, type = ?, isPublic = ?, updatedAt = datetime('now') WHERE id = ?`,
args: [
input.name,
input.description ?? null,
input.category,
input.difficulty ?? "intermediate",
input.durationMinutes ?? null,
input.type,
input.isPublic ?? 0,
input.id
]
});
return { success: true };
} catch (error) {
console.error("Failed to update workout plan:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update workout plan"
});
}
}),
deleteWorkoutPlan: nessaProcedure
.input(planIdSchema)
.mutation(async ({ input, ctx }) => {
try {
const conn = NessaConnectionFactory();
const check = await conn.execute({
sql: "SELECT userId FROM workoutPlans WHERE id = ?",
args: [input.id]
});
if (!check.rows.length || check.rows[0].userId !== ctx.nessaUserId) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
await conn.execute({
sql: "DELETE FROM workoutPlans WHERE id = ?",
args: [input.id]
});
return { success: true };
} catch (error) {
console.error("Failed to delete workout plan:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete workout plan"
});
}
}),
createPlanExercise: nessaProcedure
.input(planExerciseSchema)
.mutation(async ({ input, ctx }) => {
try {
const conn = NessaConnectionFactory();
const planCheck = await conn.execute({
sql: "SELECT userId FROM workoutPlans WHERE id = ?",
args: [input.planId]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
await conn.execute({
sql: `INSERT INTO planExercises (id, planId, exerciseId, name, category, orderIndex, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
args: [
input.id,
input.planId,
input.exerciseId ?? null,
input.name,
input.category,
input.orderIndex,
input.notes ?? null
]
});
return { success: true };
} catch (error) {
console.error("Failed to create plan exercise:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create plan exercise"
});
}
}),
updatePlanExercise: nessaProcedure
.input(planExerciseSchema)
.mutation(async ({ input, ctx }) => {
try {
const conn = NessaConnectionFactory();
const planCheck = await conn.execute({
sql: "SELECT userId FROM workoutPlans WHERE id = ?",
args: [input.planId]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
await conn.execute({
sql: `UPDATE planExercises SET exerciseId = ?, name = ?, category = ?, orderIndex = ?, notes = ? WHERE id = ?`,
args: [
input.exerciseId ?? null,
input.name,
input.category,
input.orderIndex,
input.notes ?? null,
input.id
]
});
return { success: true };
} catch (error) {
console.error("Failed to update plan exercise:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update plan exercise"
});
}
}),
deletePlanExercise: nessaProcedure
.input(planExerciseIdSchema)
.mutation(async ({ input, ctx }) => {
try {
const conn = NessaConnectionFactory();
const planCheck = await conn.execute({
sql: "SELECT workoutPlans.userId FROM workoutPlans INNER JOIN planExercises ON planExercises.planId = workoutPlans.id WHERE planExercises.id = ?",
args: [input.id]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
await conn.execute({
sql: "DELETE FROM planExercises WHERE id = ?",
args: [input.id]
});
return { success: true };
} catch (error) {
console.error("Failed to delete plan exercise:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete plan exercise"
});
}
}),
createPlanSet: nessaProcedure
.input(planSetSchema)
.mutation(async ({ input, ctx }) => {
try {
const conn = NessaConnectionFactory();
const planCheck = await conn.execute({
sql: "SELECT workoutPlans.userId FROM workoutPlans INNER JOIN planExercises ON planExercises.planId = workoutPlans.id WHERE planExercises.id = ?",
args: [input.planExerciseId]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
await conn.execute({
sql: `INSERT INTO planSets (id, planExerciseId, setNumber, reps, weight, durationSeconds, rpe, restAfterSeconds, isWarmup, isDropset, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
args: [
input.id,
input.planExerciseId,
input.setNumber,
input.reps ?? null,
input.weight ?? null,
input.durationSeconds ?? null,
input.rpe ?? null,
input.restAfterSeconds ?? null,
input.isWarmup ?? 0,
input.isDropset ?? 0,
input.notes ?? null
]
});
return { success: true };
} catch (error) {
console.error("Failed to create plan set:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create plan set"
});
}
}),
updatePlanSet: nessaProcedure
.input(planSetSchema)
.mutation(async ({ input, ctx }) => {
try {
const conn = NessaConnectionFactory();
const planCheck = await conn.execute({
sql: "SELECT workoutPlans.userId FROM workoutPlans INNER JOIN planExercises ON planExercises.planId = workoutPlans.id INNER JOIN planSets ON planSets.planExerciseId = planExercises.id WHERE planSets.id = ?",
args: [input.id]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
await conn.execute({
sql: `UPDATE planSets SET setNumber = ?, reps = ?, weight = ?, durationSeconds = ?, rpe = ?, restAfterSeconds = ?, isWarmup = ?, isDropset = ?, notes = ? WHERE id = ?`,
args: [
input.setNumber,
input.reps ?? null,
input.weight ?? null,
input.durationSeconds ?? null,
input.rpe ?? null,
input.restAfterSeconds ?? null,
input.isWarmup ?? 0,
input.isDropset ?? 0,
input.notes ?? null,
input.id
]
});
return { success: true };
} catch (error) {
console.error("Failed to update plan set:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update plan set"
});
}
}),
deletePlanSet: nessaProcedure
.input(planSetIdSchema)
.mutation(async ({ input, ctx }) => {
try {
const conn = NessaConnectionFactory();
const planCheck = await conn.execute({
sql: "SELECT workoutPlans.userId FROM workoutPlans INNER JOIN planExercises ON planExercises.planId = workoutPlans.id INNER JOIN planSets ON planSets.planExerciseId = planExercises.id WHERE planSets.id = ?",
args: [input.id]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
await conn.execute({
sql: "DELETE FROM planSets WHERE id = ?",
args: [input.id]
});
return { success: true };
} catch (error) {
console.error("Failed to delete plan set:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete plan set"
});
}
}),
createRoutePoint: nessaProcedure
.input(routePointSchema)
.mutation(async ({ input, ctx }) => {
try {
const conn = NessaConnectionFactory();
const planCheck = await conn.execute({
sql: "SELECT userId FROM workoutPlans WHERE id = ?",
args: [input.planId]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
await conn.execute({
sql: `INSERT INTO routePoints (id, planId, latitude, longitude, orderIndex, isWaypoint)
VALUES (?, ?, ?, ?, ?, ?)`,
args: [
input.id,
input.planId,
input.latitude,
input.longitude,
input.orderIndex,
input.isWaypoint ?? 0
]
});
return { success: true };
} catch (error) {
console.error("Failed to create route point:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create route point"
});
}
}),
updateRoutePoint: nessaProcedure
.input(routePointSchema)
.mutation(async ({ input, ctx }) => {
try {
const conn = NessaConnectionFactory();
const planCheck = await conn.execute({
sql: "SELECT workoutPlans.userId FROM workoutPlans INNER JOIN routePoints ON routePoints.planId = workoutPlans.id WHERE routePoints.id = ?",
args: [input.id]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
await conn.execute({
sql: `UPDATE routePoints SET latitude = ?, longitude = ?, orderIndex = ?, isWaypoint = ? WHERE id = ?`,
args: [
input.latitude,
input.longitude,
input.orderIndex,
input.isWaypoint ?? 0,
input.id
]
});
return { success: true };
} catch (error) {
console.error("Failed to update route point:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update route point"
});
}
}),
deleteRoutePoint: nessaProcedure
.input(routePointIdSchema)
.mutation(async ({ input, ctx }) => {
try {
const conn = NessaConnectionFactory();
const planCheck = await conn.execute({
sql: "SELECT workoutPlans.userId FROM workoutPlans INNER JOIN routePoints ON routePoints.planId = workoutPlans.id WHERE routePoints.id = ?",
args: [input.id]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
await conn.execute({
sql: "DELETE FROM routePoints WHERE id = ?",
args: [input.id]
});
return { success: true };
} catch (error) {
console.error("Failed to delete route point:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete route point"
});
}
}),
createWorkout: nessaProcedure
.input(workoutSchema)
.mutation(async ({ input, ctx }) => {
if (input.userId != ctx.nessaUserId) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: `INSERT INTO workouts (id, userId, planId, type, name, startDate, endDate, durationSeconds, distanceMeters, calories, averageHeartRate, maxHeartRate, averagePace, elevationGain, status, source, healthKitUUID, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
args: [
input.id,
input.userId,
input.planId ?? null,
input.type,
input.name ?? null,
input.startDate,
input.endDate ?? null,
input.durationSeconds ?? null,
input.distanceMeters ?? null,
input.calories ?? null,
input.averageHeartRate ?? null,
input.maxHeartRate ?? null,
input.averagePace ?? null,
input.elevationGain ?? null,
input.status,
input.source,
input.healthKitUUID ?? null,
input.notes ?? null
]
});
return { success: true };
} catch (error) {
console.error("Failed to create workout:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create workout"
});
}
}),
updateWorkout: nessaProcedure
.input(workoutSchema)
.mutation(async ({ input, ctx }) => {
if (input.userId != ctx.nessaUserId) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: `UPDATE workouts SET planId = ?, type = ?, name = ?, startDate = ?, endDate = ?, durationSeconds = ?, distanceMeters = ?, calories = ?, averageHeartRate = ?, maxHeartRate = ?, averagePace = ?, elevationGain = ?, status = ?, source = ?, healthKitUUID = ?, notes = ?, updatedAt = datetime('now') WHERE id = ?`,
args: [
input.planId ?? null,
input.type,
input.name ?? null,
input.startDate,
input.endDate ?? null,
input.durationSeconds ?? null,
input.distanceMeters ?? null,
input.calories ?? null,
input.averageHeartRate ?? null,
input.maxHeartRate ?? null,
input.averagePace ?? null,
input.elevationGain ?? null,
input.status,
input.source,
input.healthKitUUID ?? null,
input.notes ?? null,
input.id
]
});
return { success: true };
} catch (error) {
console.error("Failed to update workout:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update workout"
});
}
}),
deleteWorkout: nessaProcedure
.input(workoutIdSchema)
.mutation(async ({ input, ctx }) => {
try {
const conn = NessaConnectionFactory();
const check = await conn.execute({
sql: "SELECT userId FROM workouts WHERE id = ?",
args: [input.id]
});
if (!check.rows.length || check.rows[0].userId !== ctx.nessaUserId) {
throw new TRPCError({ code: "FORBIDDEN", message: "User mismatch" });
}
await conn.execute({
sql: "DELETE FROM workouts WHERE id = ?",
args: [input.id]
});
return { success: true };
} catch (error) {
console.error("Failed to delete workout:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete workout"
});
}
}),
createHeartRateSample: nessaProcedure
.input(heartRateSchema)
.mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: `INSERT INTO heartRateSamples (id, workoutId, timestamp, bpm, source)
VALUES (?, ?, ?, ?, ?)`,
args: [
input.id,
input.workoutId,
input.timestamp,
input.bpm,
input.source ?? null
]
});
return { success: true };
} catch (error) {
console.error("Failed to create heart rate sample:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create heart rate sample"
});
}
}),
updateHeartRateSample: nessaProcedure
.input(heartRateSchema)
.mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: `UPDATE heartRateSamples SET timestamp = ?, bpm = ?, source = ? WHERE id = ?`,
args: [input.timestamp, input.bpm, input.source ?? null, input.id]
});
return { success: true };
} catch (error) {
console.error("Failed to update heart rate sample:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update heart rate sample"
});
}
}),
deleteHeartRateSample: nessaProcedure
.input(heartRateSchema.pick({ id: true }))
.mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: "DELETE FROM heartRateSamples WHERE id = ?",
args: [input.id]
});
return { success: true };
} catch (error) {
console.error("Failed to delete heart rate sample:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete heart rate sample"
});
}
}),
createLocationSample: nessaProcedure
.input(locationSampleSchema)
.mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: `INSERT INTO locationSamples (id, workoutId, timestamp, latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy, speed, course)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
args: [
input.id,
input.workoutId,
input.timestamp,
input.latitude,
input.longitude,
input.altitude ?? null,
input.horizontalAccuracy ?? null,
input.verticalAccuracy ?? null,
input.speed ?? null,
input.course ?? null
]
});
return { success: true };
} catch (error) {
console.error("Failed to create location sample:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create location sample"
});
}
}),
updateLocationSample: nessaProcedure
.input(locationSampleSchema)
.mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: `UPDATE locationSamples SET timestamp = ?, latitude = ?, longitude = ?, altitude = ?, horizontalAccuracy = ?, verticalAccuracy = ?, speed = ?, course = ? WHERE id = ?`,
args: [
input.timestamp,
input.latitude,
input.longitude,
input.altitude ?? null,
input.horizontalAccuracy ?? null,
input.verticalAccuracy ?? null,
input.speed ?? null,
input.course ?? null,
input.id
]
});
return { success: true };
} catch (error) {
console.error("Failed to update location sample:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update location sample"
});
}
}),
deleteLocationSample: nessaProcedure
.input(locationSampleSchema.pick({ id: true }))
.mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: "DELETE FROM locationSamples WHERE id = ?",
args: [input.id]
});
return { success: true };
} catch (error) {
console.error("Failed to delete location sample:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete location sample"
});
}
}),
createWorkoutSplit: nessaProcedure
.input(workoutSplitSchema)
.mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: `INSERT INTO workoutSplits (id, workoutId, splitNumber, distanceMeters, durationSeconds, startTimestamp, endTimestamp, averageHeartRate, averagePace, elevationGain, elevationLoss)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
args: [
input.id,
input.workoutId,
input.splitNumber,
input.distanceMeters,
input.durationSeconds,
input.startTimestamp,
input.endTimestamp,
input.averageHeartRate ?? null,
input.averagePace ?? null,
input.elevationGain ?? null,
input.elevationLoss ?? null
]
});
return { success: true };
} catch (error) {
console.error("Failed to create workout split:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create workout split"
});
}
}),
updateWorkoutSplit: nessaProcedure
.input(workoutSplitSchema)
.mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: `UPDATE workoutSplits SET splitNumber = ?, distanceMeters = ?, durationSeconds = ?, startTimestamp = ?, endTimestamp = ?, averageHeartRate = ?, averagePace = ?, elevationGain = ?, elevationLoss = ? WHERE id = ?`,
args: [
input.splitNumber,
input.distanceMeters,
input.durationSeconds,
input.startTimestamp,
input.endTimestamp,
input.averageHeartRate ?? null,
input.averagePace ?? null,
input.elevationGain ?? null,
input.elevationLoss ?? null,
input.id
]
});
return { success: true };
} catch (error) {
console.error("Failed to update workout split:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update workout split"
});
}
}),
deleteWorkoutSplit: nessaProcedure
.input(workoutSplitSchema.pick({ id: true }))
.mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: "DELETE FROM workoutSplits WHERE id = ?",
args: [input.id]
});
return { success: true };
} catch (error) {
console.error("Failed to delete workout split:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete workout split"
});
}
}),
createExerciseLibrary: nessaProcedure
.input(exerciseLibrarySchema)
.mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: `INSERT INTO exerciseLibrary (id, name, category, muscleGroups, equipment, instructions, defaultSets, defaultReps, defaultRestSeconds, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
args: [
input.id,
input.name,
input.category,
input.muscleGroups ?? null,
input.equipment ?? null,
input.instructions ?? null,
input.defaultSets ?? null,
input.defaultReps ?? null,
input.defaultRestSeconds ?? null,
input.notes ?? null
]
});
return { success: true };
} catch (error) {
console.error("Failed to create exercise library:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create exercise library"
});
}
}),
updateExerciseLibrary: nessaProcedure
.input(exerciseLibrarySchema)
.mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: `UPDATE exerciseLibrary SET name = ?, category = ?, muscleGroups = ?, equipment = ?, instructions = ?, defaultSets = ?, defaultReps = ?, defaultRestSeconds = ?, notes = ?, updatedAt = datetime('now') WHERE id = ?`,
args: [
input.name,
input.category,
input.muscleGroups ?? null,
input.equipment ?? null,
input.instructions ?? null,
input.defaultSets ?? null,
input.defaultReps ?? null,
input.defaultRestSeconds ?? null,
input.notes ?? null,
input.id
]
});
return { success: true };
} catch (error) {
console.error("Failed to update exercise library:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update exercise library"
});
}
}),
deleteExerciseLibrary: nessaProcedure
.input(exerciseIdSchema)
.mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: "DELETE FROM exerciseLibrary WHERE id = ?",
args: [input.id]
});
return { success: true };
} catch (error) {
console.error("Failed to delete exercise library:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete exercise library"
});
}
}),
createAuthProvider: nessaProcedure
.input(providerSchema)
.mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: `INSERT INTO authProviders (id, userId, provider, providerUserId, email, displayName, avatarUrl)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
args: [
input.id,
input.userId,
input.provider,
input.providerUserId ?? null,
input.email ?? null,
input.displayName ?? null,
input.avatarUrl ?? null
]
});
return { success: true };
} catch (error) {
console.error("Failed to create auth provider:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create auth provider"
});
}
}),
updateAuthProvider: nessaProcedure
.input(providerSchema)
.mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: `UPDATE authProviders SET provider = ?, providerUserId = ?, email = ?, displayName = ?, avatarUrl = ?, lastUsedAt = datetime('now') WHERE id = ?`,
args: [
input.provider,
input.providerUserId ?? null,
input.email ?? null,
input.displayName ?? null,
input.avatarUrl ?? null,
input.id
]
});
return { success: true };
} catch (error) {
console.error("Failed to update auth provider:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update auth provider"
});
}
}),
deleteAuthProvider: nessaProcedure
.input(providerSchema.pick({ id: true }))
.mutation(async ({ input }) => {
try {
const conn = NessaConnectionFactory();
await conn.execute({
sql: "DELETE FROM authProviders WHERE id = ?",
args: [input.id]
});
return { success: true };
} catch (error) {
console.error("Failed to delete auth provider:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete auth provider"
});
}
}),
bulkUpsert: nessaProcedure
.input(bulkSchema)
.mutation(async ({ input, ctx }) => {
try {
const conn = NessaConnectionFactory();
if (input.users?.length) {
for (const user of input.users) {
if (user.id !== ctx.nessaUserId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO users (id, email, emailVerified, firstName, lastName, displayName, avatarUrl, provider, appleUserId, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET email = excluded.email, emailVerified = excluded.emailVerified, firstName = excluded.firstName, lastName = excluded.lastName, displayName = excluded.displayName, avatarUrl = excluded.avatarUrl, provider = excluded.provider, appleUserId = excluded.appleUserId, status = excluded.status, updatedAt = datetime('now')`,
args: [
user.id,
user.email ?? null,
user.emailVerified ?? 0,
user.firstName ?? null,
user.lastName ?? null,
user.displayName ?? null,
user.avatarUrl ?? null,
user.provider ?? null,
user.appleUserId ?? null,
user.status ?? "active"
]
});
}
}
if (input.exerciseLibrary?.length) {
for (const exercise of input.exerciseLibrary) {
await conn.execute({
sql: `INSERT INTO exerciseLibrary (id, name, category, muscleGroups, equipment, instructions, defaultSets, defaultReps, defaultRestSeconds, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET name = excluded.name, category = excluded.category, muscleGroups = excluded.muscleGroups, equipment = excluded.equipment, instructions = excluded.instructions, defaultSets = excluded.defaultSets, defaultReps = excluded.defaultReps, defaultRestSeconds = excluded.defaultRestSeconds, notes = excluded.notes, updatedAt = datetime('now')`,
args: [
exercise.id,
exercise.name,
exercise.category,
exercise.muscleGroups ?? null,
exercise.equipment ?? null,
exercise.instructions ?? null,
exercise.defaultSets ?? null,
exercise.defaultReps ?? null,
exercise.defaultRestSeconds ?? null,
exercise.notes ?? null
]
});
}
}
if (input.workoutPlans?.length) {
for (const plan of input.workoutPlans) {
if (plan.userId !== ctx.nessaUserId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO workoutPlans (id, userId, name, description, category, difficulty, durationMinutes, type, isPublic)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description, category = excluded.category, difficulty = excluded.difficulty, durationMinutes = excluded.durationMinutes, type = excluded.type, isPublic = excluded.isPublic, updatedAt = datetime('now')`,
args: [
plan.id,
plan.userId,
plan.name,
plan.description ?? null,
plan.category,
plan.difficulty ?? "intermediate",
plan.durationMinutes ?? null,
plan.type,
plan.isPublic ?? 0
]
});
}
}
if (input.planExercises?.length) {
for (const planExercise of input.planExercises) {
const planCheck = await conn.execute({
sql: "SELECT userId FROM workoutPlans WHERE id = ?",
args: [planExercise.planId]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO planExercises (id, planId, exerciseId, name, category, orderIndex, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET exerciseId = excluded.exerciseId, name = excluded.name, category = excluded.category, orderIndex = excluded.orderIndex, notes = excluded.notes`,
args: [
planExercise.id,
planExercise.planId,
planExercise.exerciseId ?? null,
planExercise.name,
planExercise.category,
planExercise.orderIndex,
planExercise.notes ?? null
]
});
}
}
if (input.planSets?.length) {
for (const planSet of input.planSets) {
const planExerciseCheck = await conn.execute({
sql: "SELECT planId FROM planExercises WHERE id = ?",
args: [planSet.planExerciseId]
});
if (planExerciseCheck.rows.length) {
const planCheck = await conn.execute({
sql: "SELECT userId FROM workoutPlans WHERE id = ?",
args: [planExerciseCheck.rows[0].planId]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
}
await conn.execute({
sql: `INSERT INTO planSets (id, planExerciseId, setNumber, reps, weight, durationSeconds, rpe, restAfterSeconds, isWarmup, isDropset, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET setNumber = excluded.setNumber, reps = excluded.reps, weight = excluded.weight, durationSeconds = excluded.durationSeconds, rpe = excluded.rpe, restAfterSeconds = excluded.restAfterSeconds, isWarmup = excluded.isWarmup, isDropset = excluded.isDropset, notes = excluded.notes`,
args: [
planSet.id,
planSet.planExerciseId,
planSet.setNumber,
planSet.reps ?? null,
planSet.weight ?? null,
planSet.durationSeconds ?? null,
planSet.rpe ?? null,
planSet.restAfterSeconds ?? null,
planSet.isWarmup ?? 0,
planSet.isDropset ?? 0,
planSet.notes ?? null
]
});
}
}
if (input.routePoints?.length) {
for (const point of input.routePoints) {
const planCheck = await conn.execute({
sql: "SELECT userId FROM workoutPlans WHERE id = ?",
args: [point.planId]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO routePoints (id, planId, latitude, longitude, orderIndex, isWaypoint)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET latitude = excluded.latitude, longitude = excluded.longitude, orderIndex = excluded.orderIndex, isWaypoint = excluded.isWaypoint`,
args: [
point.id,
point.planId,
point.latitude,
point.longitude,
point.orderIndex,
point.isWaypoint ?? 0
]
});
}
}
if (input.workouts?.length) {
for (const workout of input.workouts) {
if (workout.userId !== ctx.nessaUserId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO workouts (id, userId, planId, type, name, startDate, endDate, durationSeconds, distanceMeters, calories, averageHeartRate, maxHeartRate, averagePace, elevationGain, status, source, healthKitUUID, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET planId = excluded.planId, type = excluded.type, name = excluded.name, startDate = excluded.startDate, endDate = excluded.endDate, durationSeconds = excluded.durationSeconds, distanceMeters = excluded.distanceMeters, calories = excluded.calories, averageHeartRate = excluded.averageHeartRate, maxHeartRate = excluded.maxHeartRate, averagePace = excluded.averagePace, elevationGain = excluded.elevationGain, status = excluded.status, source = excluded.source, healthKitUUID = excluded.healthKitUUID, notes = excluded.notes, updatedAt = datetime('now')`,
args: [
workout.id,
workout.userId,
workout.planId ?? null,
workout.type,
workout.name ?? null,
workout.startDate,
workout.endDate ?? null,
workout.durationSeconds ?? null,
workout.distanceMeters ?? null,
workout.calories ?? null,
workout.averageHeartRate ?? null,
workout.maxHeartRate ?? null,
workout.averagePace ?? null,
workout.elevationGain ?? null,
workout.status,
workout.source,
workout.healthKitUUID ?? null,
workout.notes ?? null
]
});
}
}
if (input.heartRateSamples?.length) {
for (const sample of input.heartRateSamples) {
const workoutCheck = await conn.execute({
sql: "SELECT userId FROM workouts WHERE id = ?",
args: [sample.workoutId]
});
if (
!workoutCheck.rows.length ||
workoutCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO heartRateSamples (id, workoutId, timestamp, bpm, source)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET timestamp = excluded.timestamp, bpm = excluded.bpm, source = excluded.source`,
args: [
sample.id,
sample.workoutId,
sample.timestamp,
sample.bpm,
sample.source ?? null
]
});
}
}
if (input.locationSamples?.length) {
for (const sample of input.locationSamples) {
const workoutCheck = await conn.execute({
sql: "SELECT userId FROM workouts WHERE id = ?",
args: [sample.workoutId]
});
if (
!workoutCheck.rows.length ||
workoutCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO locationSamples (id, workoutId, timestamp, latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy, speed, course)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET timestamp = excluded.timestamp, latitude = excluded.latitude, longitude = excluded.longitude, altitude = excluded.altitude, horizontalAccuracy = excluded.horizontalAccuracy, verticalAccuracy = excluded.verticalAccuracy, speed = excluded.speed, course = excluded.course`,
args: [
sample.id,
sample.workoutId,
sample.timestamp,
sample.latitude,
sample.longitude,
sample.altitude ?? null,
sample.horizontalAccuracy ?? null,
sample.verticalAccuracy ?? null,
sample.speed ?? null,
sample.course ?? null
]
});
}
}
if (input.workoutSplits?.length) {
for (const split of input.workoutSplits) {
const workoutCheck = await conn.execute({
sql: "SELECT userId FROM workouts WHERE id = ?",
args: [split.workoutId]
});
if (
!workoutCheck.rows.length ||
workoutCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO workoutSplits (id, workoutId, splitNumber, distanceMeters, durationSeconds, startTimestamp, endTimestamp, averageHeartRate, averagePace, elevationGain, elevationLoss)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET splitNumber = excluded.splitNumber, distanceMeters = excluded.distanceMeters, durationSeconds = excluded.durationSeconds, startTimestamp = excluded.startTimestamp, endTimestamp = excluded.endTimestamp, averageHeartRate = excluded.averageHeartRate, averagePace = excluded.averagePace, elevationGain = excluded.elevationGain, elevationLoss = excluded.elevationLoss`,
args: [
split.id,
split.workoutId,
split.splitNumber,
split.distanceMeters,
split.durationSeconds,
split.startTimestamp,
split.endTimestamp,
split.averageHeartRate ?? null,
split.averagePace ?? null,
split.elevationGain ?? null,
split.elevationLoss ?? null
]
});
}
}
if (input.authProviders?.length) {
for (const provider of input.authProviders) {
if (provider.userId !== ctx.nessaUserId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO authProviders (id, userId, provider, providerUserId, email, displayName, avatarUrl)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET provider = excluded.provider, providerUserId = excluded.providerUserId, email = excluded.email, displayName = excluded.displayName, avatarUrl = excluded.avatarUrl, lastUsedAt = datetime('now')`,
args: [
provider.id,
provider.userId,
provider.provider,
provider.providerUserId ?? null,
provider.email ?? null,
provider.displayName ?? null,
provider.avatarUrl ?? null
]
});
}
}
return { success: true };
} catch (error) {
console.error("Failed bulk upsert:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to bulk upsert"
});
}
})
});