@dlwiest/ts-tonal-client 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -441,6 +441,48 @@ if (volumeScores && volumeScores.length >= 2) {
441
441
  }
442
442
  ```
443
443
 
444
+ ### Strength Scores
445
+
446
+ Tonal's headline Strength Score, per body region. This is a different metric from the weekly
447
+ `Functional Strength Score` returned by `getGoalMetrics()` — that one measures goal progress
448
+ for a week, this one is the score the Tonal app shows you.
449
+
450
+ ```typescript
451
+ // Current score for each region
452
+ const scores = await client.getCurrentStrengthScores()
453
+ for (const score of scores) {
454
+ // The Overall row is synthesized: bodyRegionDisplay is empty, familyActivity is absent,
455
+ // workoutActivityId is an all-zero uuid, and updatedAt is a zero date. Fall back to
456
+ // strengthBodyRegion for a label, and do not render its updatedAt.
457
+ const label = score.bodyRegionDisplay || score.strengthBodyRegion
458
+ console.log(`${label}: ${score.score}`)
459
+ }
460
+ // One row per region: Upper Body, Core, Lower Body, Overall
461
+
462
+ // Per-workout history. Defaults to the whole account.
463
+ const history = await client.getStrengthScoreHistory()
464
+ console.log(`${history.length} scored activities`)
465
+ console.log(history[0]) // { upper, lower, core, overall, activityTime, workoutActivityId, ... }
466
+ ```
467
+
468
+ **`days` is a calendar-day lookback, not a row count.** The underlying API parameter is named
469
+ `limit`, but it selects a time window: a value smaller than the gap since your last workout
470
+ returns an **empty array**, not "no results found". Passing `30` on an account last used
471
+ 90 days ago yields nothing.
472
+
473
+ ```typescript
474
+ await client.getStrengthScoreHistory(365) // activities in the last 365 days
475
+ await client.getStrengthScoreHistory('all') // explicit; same as the default
476
+ ```
477
+
478
+ The default `'all'` derives the window from your account creation date, so it stays correct as
479
+ the account ages rather than relying on a large magic number. If `createdAt` is missing,
480
+ unparseable, or in the future it throws `TonalClientError` rather than guessing — pass an
481
+ explicit `days` in that case.
482
+
483
+ Each history entry carries `workoutActivityId`, which makes this the only complete way to
484
+ enumerate an account's activities: the list endpoints are capped at 50 records each.
485
+
444
486
  ### Movements
445
487
 
446
488
  ```typescript
package/dist/index.cjs CHANGED
@@ -301,7 +301,7 @@ class WorkoutService {
301
301
  }
302
302
  return this.httpClient.request('/user-workouts/estimate', {
303
303
  method: 'POST',
304
- body: JSON.stringify({ sets }),
304
+ body: JSON.stringify(sets),
305
305
  });
306
306
  }
307
307
  async createWorkout(workoutData) {
@@ -518,6 +518,16 @@ class UserService {
518
518
  async getCurrentStreak(userId) {
519
519
  return this.httpClient.request(`/users/${userId}/streaks/current`);
520
520
  }
521
+ async getCurrentStrengthScores(userId) {
522
+ return this.httpClient.request(`/users/${userId}/strength-scores/current`);
523
+ }
524
+ async getStrengthScoreHistory(userId, days) {
525
+ if (!Number.isSafeInteger(days) || days <= 0) {
526
+ throw new TonalClientError('Strength score history days must be a positive safe integer');
527
+ }
528
+ // Tonal's "limit" query parameter is a calendar-day window; small values return an empty array.
529
+ return this.httpClient.request(`/users/${userId}/strength-scores/history?limit=${days}`);
530
+ }
521
531
  async getActivitySummaries(userId) {
522
532
  return this.httpClient.request(`/users/${userId}/activity-summaries`);
523
533
  }
@@ -623,6 +633,34 @@ class TonalClient {
623
633
  const userInfo = await this.getUserInfo();
624
634
  return this.userService.getCurrentStreak(userInfo.id);
625
635
  }
636
+ async getCurrentStrengthScores() {
637
+ const userInfo = await this.getUserInfo();
638
+ return this.userService.getCurrentStrengthScores(userInfo.id);
639
+ }
640
+ async getStrengthScoreHistory(days = 'all') {
641
+ if (days !== 'all' && (!Number.isSafeInteger(days) || days <= 0)) {
642
+ throw new TonalClientError('Strength score history days must be a positive safe integer');
643
+ }
644
+ const userInfo = await this.getUserInfo();
645
+ let lookbackDays;
646
+ if (days === 'all') {
647
+ const createdAt = userInfo.createdAt;
648
+ const createdAtMs = typeof createdAt === 'string' ? Date.parse(createdAt) : Number.NaN;
649
+ const now = Date.now();
650
+ if (!Number.isFinite(createdAtMs) || createdAtMs > now) {
651
+ throw new TonalClientError('Cannot derive all strength score history from user createdAt; pass explicit days');
652
+ }
653
+ // +2 covers the account-creation calendar day and the server's midnight-boundary
654
+ // ambiguity; over-fetching before account creation is harmless. The Math.max floor is
655
+ // belt-and-braces only -- the future-createdAt rejection above already guarantees a
656
+ // non-negative difference, so this expression is always >= 2.
657
+ lookbackDays = Math.max(1, Math.ceil((now - createdAtMs) / 86400000) + 2);
658
+ }
659
+ else {
660
+ lookbackDays = days;
661
+ }
662
+ return this.userService.getStrengthScoreHistory(userInfo.id, lookbackDays);
663
+ }
626
664
  async getActivitySummaries() {
627
665
  const userInfo = await this.getUserInfo();
628
666
  return this.userService.getActivitySummaries(userInfo.id);
package/dist/index.d.ts CHANGED
@@ -373,6 +373,30 @@ interface TonalCurrentStreak {
373
373
  maxStreakStartDate: string;
374
374
  updatedByActivityId: string;
375
375
  }
376
+ type TonalStrengthBodyRegion = 'Upper Body' | 'Core' | 'Lower Body' | 'Overall' | string;
377
+ interface TonalStrengthScore {
378
+ id: string;
379
+ createdAt: string;
380
+ updatedAt: string;
381
+ userId: string;
382
+ workoutActivityId: string;
383
+ strengthBodyRegion: TonalStrengthBodyRegion;
384
+ bodyRegionDisplay: string;
385
+ score: number;
386
+ current: boolean;
387
+ familyActivity?: unknown[];
388
+ }
389
+ interface TonalStrengthScoreHistoryEntry {
390
+ id: string;
391
+ userId: string;
392
+ workoutActivityId: string;
393
+ upper: number;
394
+ lower: number;
395
+ core: number;
396
+ overall: number;
397
+ activityTime: string;
398
+ }
399
+ type TonalStrengthScoreHistoryLookback = number | 'all';
376
400
  interface TonalActivitySummary {
377
401
  id: string;
378
402
  deletedAt: string | null;
@@ -661,6 +685,8 @@ declare class TonalClient {
661
685
  getUserSettings(): Promise<TonalUserSettings>;
662
686
  getDailyMetrics(days?: number): Promise<TonalDailyMetrics[]>;
663
687
  getCurrentStreak(): Promise<TonalCurrentStreak>;
688
+ getCurrentStrengthScores(): Promise<TonalStrengthScore[]>;
689
+ getStrengthScoreHistory(days?: TonalStrengthScoreHistoryLookback): Promise<TonalStrengthScoreHistoryEntry[]>;
664
690
  getActivitySummaries(): Promise<TonalActivitySummary[]>;
665
691
  getUserStatistics(): Promise<TonalUserStatistics>;
666
692
  getAchievementStats(): Promise<TonalAchievementStats>;
@@ -680,4 +706,4 @@ declare class TonalClient {
680
706
  deleteWorkout(workoutId: string): Promise<void>;
681
707
  }
682
708
 
683
- export { MuscleGroup, OAuthTokenResponse, TonalAchievementCategory, TonalAchievementDefinition, TonalAchievementMilestone, TonalAchievementStats, TonalActivitySummary, TonalApiError, TonalCalendarTile, TonalClient, TonalClientError, TonalCompatibilityStatus, TonalCurrentStreak, TonalDailyMetrics, TonalDailySchedule, TonalDeviceRegistration, TonalEarnedAchievement, TonalGoal, TonalGoalMetric, TonalHomeCalendar, TonalMetricScore, TonalMetricScoresResponse, TonalMovement, TonalMuscleReadiness, TonalMuscleUtilization, TonalProgram, TonalProgramTrainingEffectGoal, TonalProgramWorkout, TonalSharedWorkout, TonalTargetScore, TonalTargetScoresResponse, TonalTrainingEffectGoal, TonalTrainingEffectGoalRelation, TonalTrainingEffectGoalsResponse, TonalTrainingType, TonalUserDevice, TonalUserGoal, TonalUserInfo, TonalUserPermissions, TonalUserSettings, TonalUserStatistics, TonalWorkout, TonalWorkoutCreateRequest, TonalWorkoutEstimateResponse, TonalWorkoutEstimateSet, TonalWorkoutSummaryData, TonalWorkoutUpdateRequest, WorkoutPublishState, WorkoutSet, TonalClient as default };
709
+ export { MuscleGroup, OAuthTokenResponse, TonalAchievementCategory, TonalAchievementDefinition, TonalAchievementMilestone, TonalAchievementStats, TonalActivitySummary, TonalApiError, TonalCalendarTile, TonalClient, TonalClientError, TonalCompatibilityStatus, TonalCurrentStreak, TonalDailyMetrics, TonalDailySchedule, TonalDeviceRegistration, TonalEarnedAchievement, TonalGoal, TonalGoalMetric, TonalHomeCalendar, TonalMetricScore, TonalMetricScoresResponse, TonalMovement, TonalMuscleReadiness, TonalMuscleUtilization, TonalProgram, TonalProgramTrainingEffectGoal, TonalProgramWorkout, TonalSharedWorkout, TonalStrengthBodyRegion, TonalStrengthScore, TonalStrengthScoreHistoryEntry, TonalStrengthScoreHistoryLookback, TonalTargetScore, TonalTargetScoresResponse, TonalTrainingEffectGoal, TonalTrainingEffectGoalRelation, TonalTrainingEffectGoalsResponse, TonalTrainingType, TonalUserDevice, TonalUserGoal, TonalUserInfo, TonalUserPermissions, TonalUserSettings, TonalUserStatistics, TonalWorkout, TonalWorkoutCreateRequest, TonalWorkoutEstimateResponse, TonalWorkoutEstimateSet, TonalWorkoutSummaryData, TonalWorkoutUpdateRequest, WorkoutPublishState, WorkoutSet, TonalClient as default };
package/dist/index.esm.js CHANGED
@@ -297,7 +297,7 @@ class WorkoutService {
297
297
  }
298
298
  return this.httpClient.request('/user-workouts/estimate', {
299
299
  method: 'POST',
300
- body: JSON.stringify({ sets }),
300
+ body: JSON.stringify(sets),
301
301
  });
302
302
  }
303
303
  async createWorkout(workoutData) {
@@ -514,6 +514,16 @@ class UserService {
514
514
  async getCurrentStreak(userId) {
515
515
  return this.httpClient.request(`/users/${userId}/streaks/current`);
516
516
  }
517
+ async getCurrentStrengthScores(userId) {
518
+ return this.httpClient.request(`/users/${userId}/strength-scores/current`);
519
+ }
520
+ async getStrengthScoreHistory(userId, days) {
521
+ if (!Number.isSafeInteger(days) || days <= 0) {
522
+ throw new TonalClientError('Strength score history days must be a positive safe integer');
523
+ }
524
+ // Tonal's "limit" query parameter is a calendar-day window; small values return an empty array.
525
+ return this.httpClient.request(`/users/${userId}/strength-scores/history?limit=${days}`);
526
+ }
517
527
  async getActivitySummaries(userId) {
518
528
  return this.httpClient.request(`/users/${userId}/activity-summaries`);
519
529
  }
@@ -619,6 +629,34 @@ class TonalClient {
619
629
  const userInfo = await this.getUserInfo();
620
630
  return this.userService.getCurrentStreak(userInfo.id);
621
631
  }
632
+ async getCurrentStrengthScores() {
633
+ const userInfo = await this.getUserInfo();
634
+ return this.userService.getCurrentStrengthScores(userInfo.id);
635
+ }
636
+ async getStrengthScoreHistory(days = 'all') {
637
+ if (days !== 'all' && (!Number.isSafeInteger(days) || days <= 0)) {
638
+ throw new TonalClientError('Strength score history days must be a positive safe integer');
639
+ }
640
+ const userInfo = await this.getUserInfo();
641
+ let lookbackDays;
642
+ if (days === 'all') {
643
+ const createdAt = userInfo.createdAt;
644
+ const createdAtMs = typeof createdAt === 'string' ? Date.parse(createdAt) : Number.NaN;
645
+ const now = Date.now();
646
+ if (!Number.isFinite(createdAtMs) || createdAtMs > now) {
647
+ throw new TonalClientError('Cannot derive all strength score history from user createdAt; pass explicit days');
648
+ }
649
+ // +2 covers the account-creation calendar day and the server's midnight-boundary
650
+ // ambiguity; over-fetching before account creation is harmless. The Math.max floor is
651
+ // belt-and-braces only -- the future-createdAt rejection above already guarantees a
652
+ // non-negative difference, so this expression is always >= 2.
653
+ lookbackDays = Math.max(1, Math.ceil((now - createdAtMs) / 86400000) + 2);
654
+ }
655
+ else {
656
+ lookbackDays = days;
657
+ }
658
+ return this.userService.getStrengthScoreHistory(userInfo.id, lookbackDays);
659
+ }
622
660
  async getActivitySummaries() {
623
661
  const userInfo = await this.getUserInfo();
624
662
  return this.userService.getActivitySummaries(userInfo.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dlwiest/ts-tonal-client",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "TypeScript client for Tonal API",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.esm.js",