@dlwiest/ts-tonal-client 0.1.1 → 0.2.1

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
@@ -67,6 +67,9 @@ Then try the examples:
67
67
  # See all your workouts
68
68
  npm run example:user-workouts
69
69
 
70
+ # Search for workouts by name
71
+ npm run example:workout:name "Upper Body"
72
+
70
73
  # Get your user info
71
74
  npm run example:user
72
75
 
@@ -150,9 +153,16 @@ console.log(`You have ${dailyLifts.length} daily lifts`)
150
153
  // Get daily lifts with specific timezone
151
154
  const dailyLiftsEST = await client.getDailyLifts('America/New_York')
152
155
 
153
- // Get specific workout details
156
+ // Get specific workout details
154
157
  const workout = await client.getWorkoutById('workout-uuid')
155
158
 
159
+ // Search for workouts by name (case-insensitive partial match)
160
+ const workouts = await client.getUserWorkouts(0, 50)
161
+ const matches = workouts.filter(w =>
162
+ w.title.toLowerCase().includes('upper body')
163
+ )
164
+ console.log(`Found ${matches.length} matching workouts`)
165
+
156
166
  // Get shared workout
157
167
  const sharedWorkout = await client.getWorkoutByShareUrl('https://share.tonal.com/workout/...')
158
168
 
@@ -425,6 +435,7 @@ const chestMovements = movements.filter(m =>
425
435
  - `npm run example:goals` - Show available goals
426
436
  - `npm run example:user-workouts` - List your workouts
427
437
  - `npm run example:workout:id <id>` - Get specific workout
438
+ - `npm run example:workout:name <name>` - Search for workouts by name
428
439
  - `npm run example:workout:share <url>` - Get shared workout
429
440
  - `npm run example:estimate` - Estimate workout duration
430
441
  - `npm run example:create-workout` - Create a new workout
@@ -0,0 +1 @@
1
+ import 'dotenv/config';
package/dist/index.d.ts CHANGED
@@ -645,7 +645,8 @@ declare class TonalClient {
645
645
  username: string;
646
646
  password: string;
647
647
  }): Promise<TonalClient>;
648
- getMovements(): Promise<TonalMovement[]>;
648
+ getMovements(useCache?: boolean): Promise<TonalMovement[]>;
649
+ invalidateMovementsCache(): Promise<void>;
649
650
  getUserInfo(): Promise<TonalUserInfo>;
650
651
  getGoals(): Promise<TonalGoal[]>;
651
652
  getTrainingEffectGoals(): Promise<TonalTrainingEffectGoalsResponse>;
package/dist/index.esm.js CHANGED
@@ -1,3 +1,6 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
1
4
  class TonalClientError extends Error {
2
5
  constructor(message, statusCode, originalError) {
3
6
  super(message);
@@ -323,12 +326,85 @@ class WorkoutService {
323
326
  }
324
327
  }
325
328
 
329
+ class CacheManager {
330
+ constructor(cacheDir = '.cache', defaultTTL = 24 * 60 * 60 * 1000) {
331
+ this.cacheDir = cacheDir;
332
+ this.defaultTTL = defaultTTL;
333
+ this.ensureCacheDir();
334
+ }
335
+ ensureCacheDir() {
336
+ if (!fs.existsSync(this.cacheDir)) {
337
+ fs.mkdirSync(this.cacheDir, { recursive: true });
338
+ }
339
+ }
340
+ getCachePath(key) {
341
+ return path.join(this.cacheDir, `${key}.json`);
342
+ }
343
+ async get(key) {
344
+ const cachePath = this.getCachePath(key);
345
+ if (!fs.existsSync(cachePath)) {
346
+ return null;
347
+ }
348
+ try {
349
+ const content = fs.readFileSync(cachePath, 'utf-8');
350
+ const entry = JSON.parse(content);
351
+ const cachedAt = new Date(entry.cachedAt).getTime();
352
+ const now = Date.now();
353
+ const age = now - cachedAt;
354
+ if (age > entry.ttl) {
355
+ // Cache expired
356
+ return null;
357
+ }
358
+ return entry.data;
359
+ }
360
+ catch (error) {
361
+ // If there's an error reading/parsing cache, treat as cache miss
362
+ return null;
363
+ }
364
+ }
365
+ async set(key, data, ttl) {
366
+ const cachePath = this.getCachePath(key);
367
+ const entry = {
368
+ cachedAt: new Date().toISOString(),
369
+ ttl: ttl || this.defaultTTL,
370
+ data,
371
+ };
372
+ fs.writeFileSync(cachePath, JSON.stringify(entry, null, 2), 'utf-8');
373
+ }
374
+ async invalidate(key) {
375
+ const cachePath = this.getCachePath(key);
376
+ if (fs.existsSync(cachePath)) {
377
+ fs.unlinkSync(cachePath);
378
+ }
379
+ }
380
+ async clear() {
381
+ if (fs.existsSync(this.cacheDir)) {
382
+ const files = fs.readdirSync(this.cacheDir);
383
+ for (const file of files) {
384
+ fs.unlinkSync(path.join(this.cacheDir, file));
385
+ }
386
+ }
387
+ }
388
+ }
389
+
326
390
  class MovementService {
327
391
  constructor(httpClient) {
328
392
  this.httpClient = httpClient;
393
+ this.cacheManager = new CacheManager();
394
+ }
395
+ async getMovements(useCache = true) {
396
+ if (useCache) {
397
+ const cached = await this.cacheManager.get('movements');
398
+ if (cached) {
399
+ return cached;
400
+ }
401
+ }
402
+ const movements = await this.httpClient.request('/movements');
403
+ await this.cacheManager.set('movements', movements);
404
+ return movements;
329
405
  }
330
- async getMovements() {
331
- return this.httpClient.request('/movements');
406
+ async invalidateMovementsCache() {
407
+ await this.cacheManager.invalidate('movements');
332
408
  }
333
409
  }
334
410
 
@@ -441,8 +517,11 @@ class TonalClient {
441
517
  return client;
442
518
  }
443
519
  // Movement operations
444
- async getMovements() {
445
- return this.movementService.getMovements();
520
+ async getMovements(useCache = true) {
521
+ return this.movementService.getMovements(useCache);
522
+ }
523
+ async invalidateMovementsCache() {
524
+ return this.movementService.invalidateMovementsCache();
446
525
  }
447
526
  // User operations
448
527
  async getUserInfo() {
package/dist/index.js CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var fs = require('fs');
6
+ var path = require('path');
7
+
5
8
  class TonalClientError extends Error {
6
9
  constructor(message, statusCode, originalError) {
7
10
  super(message);
@@ -327,12 +330,85 @@ class WorkoutService {
327
330
  }
328
331
  }
329
332
 
333
+ class CacheManager {
334
+ constructor(cacheDir = '.cache', defaultTTL = 24 * 60 * 60 * 1000) {
335
+ this.cacheDir = cacheDir;
336
+ this.defaultTTL = defaultTTL;
337
+ this.ensureCacheDir();
338
+ }
339
+ ensureCacheDir() {
340
+ if (!fs.existsSync(this.cacheDir)) {
341
+ fs.mkdirSync(this.cacheDir, { recursive: true });
342
+ }
343
+ }
344
+ getCachePath(key) {
345
+ return path.join(this.cacheDir, `${key}.json`);
346
+ }
347
+ async get(key) {
348
+ const cachePath = this.getCachePath(key);
349
+ if (!fs.existsSync(cachePath)) {
350
+ return null;
351
+ }
352
+ try {
353
+ const content = fs.readFileSync(cachePath, 'utf-8');
354
+ const entry = JSON.parse(content);
355
+ const cachedAt = new Date(entry.cachedAt).getTime();
356
+ const now = Date.now();
357
+ const age = now - cachedAt;
358
+ if (age > entry.ttl) {
359
+ // Cache expired
360
+ return null;
361
+ }
362
+ return entry.data;
363
+ }
364
+ catch (error) {
365
+ // If there's an error reading/parsing cache, treat as cache miss
366
+ return null;
367
+ }
368
+ }
369
+ async set(key, data, ttl) {
370
+ const cachePath = this.getCachePath(key);
371
+ const entry = {
372
+ cachedAt: new Date().toISOString(),
373
+ ttl: ttl || this.defaultTTL,
374
+ data,
375
+ };
376
+ fs.writeFileSync(cachePath, JSON.stringify(entry, null, 2), 'utf-8');
377
+ }
378
+ async invalidate(key) {
379
+ const cachePath = this.getCachePath(key);
380
+ if (fs.existsSync(cachePath)) {
381
+ fs.unlinkSync(cachePath);
382
+ }
383
+ }
384
+ async clear() {
385
+ if (fs.existsSync(this.cacheDir)) {
386
+ const files = fs.readdirSync(this.cacheDir);
387
+ for (const file of files) {
388
+ fs.unlinkSync(path.join(this.cacheDir, file));
389
+ }
390
+ }
391
+ }
392
+ }
393
+
330
394
  class MovementService {
331
395
  constructor(httpClient) {
332
396
  this.httpClient = httpClient;
397
+ this.cacheManager = new CacheManager();
398
+ }
399
+ async getMovements(useCache = true) {
400
+ if (useCache) {
401
+ const cached = await this.cacheManager.get('movements');
402
+ if (cached) {
403
+ return cached;
404
+ }
405
+ }
406
+ const movements = await this.httpClient.request('/movements');
407
+ await this.cacheManager.set('movements', movements);
408
+ return movements;
333
409
  }
334
- async getMovements() {
335
- return this.httpClient.request('/movements');
410
+ async invalidateMovementsCache() {
411
+ await this.cacheManager.invalidate('movements');
336
412
  }
337
413
  }
338
414
 
@@ -445,8 +521,11 @@ class TonalClient {
445
521
  return client;
446
522
  }
447
523
  // Movement operations
448
- async getMovements() {
449
- return this.movementService.getMovements();
524
+ async getMovements(useCache = true) {
525
+ return this.movementService.getMovements(useCache);
526
+ }
527
+ async invalidateMovementsCache() {
528
+ return this.movementService.invalidateMovementsCache();
450
529
  }
451
530
  // User operations
452
531
  async getUserInfo() {
@@ -10,7 +10,8 @@ export declare class TonalClient {
10
10
  username: string;
11
11
  password: string;
12
12
  }): Promise<TonalClient>;
13
- getMovements(): Promise<TonalMovement[]>;
13
+ getMovements(useCache?: boolean): Promise<TonalMovement[]>;
14
+ invalidateMovementsCache(): Promise<void>;
14
15
  getUserInfo(): Promise<TonalUserInfo>;
15
16
  getGoals(): Promise<TonalGoal[]>;
16
17
  getTrainingEffectGoals(): Promise<TonalTrainingEffectGoalsResponse>;
@@ -2,6 +2,8 @@ import { HttpClient } from '../http/http-client';
2
2
  import { TonalMovement } from '../types';
3
3
  export declare class MovementService {
4
4
  private httpClient;
5
+ private cacheManager;
5
6
  constructor(httpClient: HttpClient);
6
- getMovements(): Promise<TonalMovement[]>;
7
+ getMovements(useCache?: boolean): Promise<TonalMovement[]>;
8
+ invalidateMovementsCache(): Promise<void>;
7
9
  }
@@ -0,0 +1,11 @@
1
+ export declare class CacheManager {
2
+ private cacheDir;
3
+ private defaultTTL;
4
+ constructor(cacheDir?: string, defaultTTL?: number);
5
+ private ensureCacheDir;
6
+ private getCachePath;
7
+ get<T>(key: string): Promise<T | null>;
8
+ set<T>(key: string, data: T, ttl?: number): Promise<void>;
9
+ invalidate(key: string): Promise<void>;
10
+ clear(): Promise<void>;
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dlwiest/ts-tonal-client",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "TypeScript client for Tonal API",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -27,6 +27,7 @@
27
27
  "example:movements": "tsx examples/get-movements.ts",
28
28
  "example:movements:save": "tsx examples/get-movements.ts --save",
29
29
  "example:workout:id": "tsx examples/get-workout-by-id.ts",
30
+ "example:workout:name": "tsx examples/get-workout-by-name.ts",
30
31
  "example:workout:share": "tsx examples/get-workout-by-share-url.ts",
31
32
  "example:user": "tsx examples/get-user-info.ts",
32
33
  "example:goals": "tsx examples/get-goals.ts",
package/dist/client.d.ts DELETED
@@ -1,16 +0,0 @@
1
- import { TonalMovement, TonalSharedWorkout, TonalWorkout } from './types';
2
- export declare class TonalClient {
3
- private username;
4
- private password;
5
- private idToken;
6
- private tokenExpiresAt;
7
- private constructor();
8
- static create({ username, password }: {
9
- username: string;
10
- password: string;
11
- }): Promise<TonalClient>;
12
- private refreshToken;
13
- getMovements(): Promise<TonalMovement[]>;
14
- getWorkoutById(id: string): Promise<TonalWorkout>;
15
- getWorkoutByShareUrl(shareUrl: string): Promise<TonalSharedWorkout>;
16
- }
package/dist/types.d.ts DELETED
@@ -1,115 +0,0 @@
1
- export interface OAuthTokenResponse {
2
- access_token: string;
3
- id_token: string;
4
- refresh_token: string;
5
- scope: string;
6
- token_type: string;
7
- expires_in: number;
8
- }
9
- export type MuscleGroup = 'Obliques' | 'Abs' | 'Shoulders' | 'Glutes' | 'Back' | 'Biceps' | 'Quads' | 'Triceps' | 'Chest' | 'Hamstrings' | 'Calves' | 'Forearms';
10
- export interface TonalMovement {
11
- id: string;
12
- createdAt: string;
13
- updatedAt: string;
14
- name: string;
15
- shortName: string;
16
- muscleGroups: MuscleGroup[];
17
- bodyRegion: string;
18
- bodyRegionDisplay: string;
19
- baseOfSupport: string;
20
- pushPull: string;
21
- family: string;
22
- familyDisplay: string;
23
- inFreeLift: boolean;
24
- onMachine: boolean;
25
- countReps: boolean;
26
- isTwoSided: boolean;
27
- isBilateral: boolean;
28
- isAlternating: boolean;
29
- offMachineAccessory: string;
30
- descriptionHow: string;
31
- descriptionWhy: string;
32
- sortOrder: number;
33
- imageAssetId: string;
34
- skillLevel: number;
35
- active: boolean;
36
- featureGroupIds: null | string[];
37
- isGeneric: boolean;
38
- }
39
- export interface TonalWorkout {
40
- id: string;
41
- createdAt: string;
42
- title: string;
43
- shortDescription: string;
44
- description: string;
45
- productionCode: string;
46
- assetId: string;
47
- coachId: string;
48
- sets: WorkoutSet[];
49
- duration: number;
50
- publishState: string;
51
- programId: string | null;
52
- level: string;
53
- groupIds: string[];
54
- targetArea: string;
55
- tags: string[] | null;
56
- bodyRegions: MuscleGroup[];
57
- goalIds: string[] | null;
58
- trainingEffectGoals: string[];
59
- disableModification: boolean;
60
- publishedAt: string;
61
- localPublishedAt: string;
62
- type: string;
63
- userId: string;
64
- style: string;
65
- trainingType: string;
66
- trainingTypeIds: string[] | null;
67
- mobileFriendly: boolean;
68
- live: boolean;
69
- recoveryWeight: boolean;
70
- supportedDevices: string[] | null;
71
- featureGroupIds: string[] | null;
72
- movementIds: string[];
73
- muscleGroupsForExclusion: MuscleGroup[] | null;
74
- playbackType: string;
75
- isImported: boolean;
76
- }
77
- interface WorkoutSet {
78
- id: string;
79
- workoutId: string;
80
- blockStart: boolean;
81
- movementId: string;
82
- prescribedReps: number;
83
- repetition: number;
84
- repetitionTotal: number;
85
- blockNumber: number;
86
- burnout: boolean;
87
- spotter: boolean;
88
- eccentric: boolean;
89
- chains: boolean;
90
- skipSetup: boolean;
91
- skipDemo: boolean;
92
- finalSet: boolean;
93
- calibration: boolean;
94
- practice: boolean;
95
- flex: boolean;
96
- progressive: boolean;
97
- weightPercentage: number;
98
- warmUp: boolean;
99
- durationBasedRepGoal: number;
100
- setGroup: number;
101
- round: number;
102
- description: string;
103
- dropSet: boolean;
104
- omitempty: null | any;
105
- }
106
- export interface TonalSharedWorkout {
107
- id: string;
108
- sharerUserId: string;
109
- parentWorkoutId: string;
110
- workoutSnapshotId: string;
111
- workoutSnapshotHash: string;
112
- deepLinkUrl: string;
113
- workoutSnapshot: TonalWorkout;
114
- }
115
- export {};