trainingpeaks-sdk
Version:
TypeScript SDK for TrainingPeaks API integration
151 lines (150 loc) • 5.63 kB
JavaScript
import { ValidationError } from '../../domain/errors/domain-errors.js';
import { WorkoutStructureError, WorkoutValidationError, } from '../../domain/errors/workout-errors.js';
import { calculateWorkoutDuration, createStructuredWorkout, createWorkout, updateWorkout, } from './workout.js';
export const createWorkoutAggregate = (id, name, date, duration, description, distance, activityType, structure, fileContent, fileName) => {
let workout;
if (structure) {
validateWorkoutStructure(structure);
const calculatedDuration = calculateWorkoutDuration(structure);
if (Math.abs(duration - calculatedDuration) > 1) {
throw new WorkoutValidationError(`Provided duration (${duration}s) does not match calculated duration (${calculatedDuration}s)`);
}
workout = createStructuredWorkout(id, name, date, structure, description, activityType);
}
else {
workout = createWorkout(id, name, date, duration, description, distance, activityType, structure, fileContent, fileName);
}
return {
workout,
structure,
};
};
export const updateWorkoutAggregate = (aggregate, updates) => {
const updatedWorkout = updateWorkout(aggregate.workout, updates);
return {
...aggregate,
workout: updatedWorkout,
};
};
export const updateWorkoutStructure = (aggregate, newStructure) => {
validateWorkoutStructure(newStructure);
const newDuration = calculateWorkoutDuration(newStructure);
const updatedWorkout = {
...aggregate.workout,
structure: newStructure,
duration: newDuration,
updatedAt: new Date(),
};
return {
workout: updatedWorkout,
structure: newStructure,
};
};
export const removeWorkoutStructure = (aggregate, newDuration) => {
if (newDuration < 0 || !isFinite(newDuration)) {
throw new ValidationError('Duration must be a non-negative finite number', 'duration');
}
const updatedWorkout = {
...aggregate.workout,
structure: undefined,
duration: newDuration,
updatedAt: new Date(),
};
return {
workout: updatedWorkout,
structure: undefined,
};
};
export const addWorkoutFile = (aggregate, fileContent, fileName) => {
if (!fileContent || !fileName) {
throw new ValidationError('File content and name are required');
}
if (fileName.trim().length > 255) {
throw new ValidationError('File name cannot exceed 255 characters', 'fileName');
}
const updatedWorkout = {
...aggregate.workout,
fileContent,
fileName: fileName.trim(),
updatedAt: new Date(),
};
return {
...aggregate,
workout: updatedWorkout,
};
};
export const removeWorkoutFile = (aggregate) => {
const updatedWorkout = {
...aggregate.workout,
fileContent: undefined,
fileName: undefined,
updatedAt: new Date(),
};
return {
...aggregate,
workout: updatedWorkout,
};
};
export const getAggregateDuration = (aggregate) => {
if (aggregate.structure) {
return calculateWorkoutDuration(aggregate.structure);
}
return aggregate.workout.duration;
};
export const isAggregateConsistent = (aggregate) => {
try {
if (aggregate.structure) {
const calculatedDuration = calculateWorkoutDuration(aggregate.structure);
const durationDiff = Math.abs(aggregate.workout.duration - calculatedDuration);
if (durationDiff > 1) {
return false;
}
validateWorkoutStructure(aggregate.structure);
}
if (aggregate.workout.fileContent && !aggregate.workout.fileName) {
return false;
}
if (!aggregate.workout.fileContent && aggregate.workout.fileName) {
return false;
}
return true;
}
catch {
return false;
}
};
export const getAggregateSummary = (aggregate) => {
const { workout, structure } = aggregate;
return {
id: workout.id,
name: workout.name,
date: workout.date,
duration: getAggregateDuration(aggregate),
distance: workout.distance,
activityType: workout.activityType,
isStructured: Boolean(structure),
hasFile: Boolean(workout.fileContent),
structureElementCount: structure?.structure.length || 0,
isConsistent: isAggregateConsistent(aggregate),
};
};
const validateWorkoutStructure = (structure) => {
if (!structure.structure || structure.structure.length === 0) {
throw new WorkoutStructureError('Workout structure must contain at least one element');
}
for (let i = 0; i < structure.structure.length - 1; i++) {
const current = structure.structure[i];
const next = structure.structure[i + 1];
if (current && next && current.end > next.begin) {
throw new WorkoutStructureError(`Structure elements overlap: element ${i} ends after element ${i + 1} begins`, { elementIndex: i, nextElementIndex: i + 1 });
}
}
structure.structure.forEach((element, index) => {
if (!element.steps || element.steps.length === 0) {
throw new WorkoutStructureError(`Structure element ${index} must have at least one step`, { elementIndex: index });
}
if (element.end <= element.begin) {
throw new WorkoutStructureError(`Structure element ${index} end time must be greater than begin time`, { elementIndex: index, begin: element.begin, end: element.end });
}
});
};