@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 +12 -1
- package/dist/examples/get-workout-by-name.d.ts +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.esm.js +83 -4
- package/dist/index.js +83 -4
- package/dist/src/client.d.ts +2 -1
- package/dist/src/services/movement-service.d.ts +3 -1
- package/dist/src/utils/cache-manager.d.ts +11 -0
- package/package.json +2 -1
- package/dist/client.d.ts +0 -16
- package/dist/types.d.ts +0 -115
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
|
|
331
|
-
|
|
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
|
|
335
|
-
|
|
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() {
|
package/dist/src/client.d.ts
CHANGED
|
@@ -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.
|
|
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 {};
|