@dlwiest/ts-tonal-client 0.1.0 → 0.2.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
@@ -1,5 +1,8 @@
1
1
  # TypeScript Tonal Client
2
2
 
3
+ [![npm version](https://badge.fury.io/js/@dlwiest%2Fts-tonal-client.svg)](https://badge.fury.io/js/@dlwiest%2Fts-tonal-client)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@dlwiest/ts-tonal-client.svg)](https://www.npmjs.com/package/@dlwiest/ts-tonal-client)
5
+
3
6
  A comprehensive TypeScript client for accessing Tonal's API. This library provides a robust interface to retrieve workout data, user information, movements, and more from your Tonal account.
4
7
 
5
8
  ## Features
@@ -17,12 +20,19 @@ A comprehensive TypeScript client for accessing Tonal's API. This library provid
17
20
 
18
21
  ## Installation
19
22
 
20
- Clone the repository and install dependencies:
23
+ ### Option 1: Install from npm (Recommended)
24
+
25
+ ```bash
26
+ npm install @dlwiest/ts-tonal-client
27
+ ```
28
+
29
+ ### Option 2: Clone and build from source
21
30
 
22
31
  ```bash
23
32
  git clone https://github.com/dlwiest/ts-tonal-client.git
24
33
  cd ts-tonal-client
25
34
  npm install
35
+ npm run build
26
36
  ```
27
37
 
28
38
  ## Quick Start
@@ -30,7 +40,7 @@ npm install
30
40
  Basic usage:
31
41
 
32
42
  ```typescript
33
- import TonalClient from './src/index'
43
+ import TonalClient from '@dlwiest/ts-tonal-client'
34
44
 
35
45
  const client = await TonalClient.create({
36
46
  username: 'your_email@example.com',
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);
@@ -10,7 +13,9 @@ class TonalClientError extends Error {
10
13
  class AuthManager {
11
14
  constructor(username, password) {
12
15
  this.idToken = '';
16
+ this.refreshToken = '';
13
17
  this.tokenExpiresAt = 0;
18
+ this.isRefreshing = false;
14
19
  this.authUrl = 'https://tonal.auth0.com/oauth/token';
15
20
  this.clientId = 'ERCyexW-xoVG_Yy3RDe-eV4xsOnRHP6L';
16
21
  this.username = username;
@@ -46,18 +51,70 @@ class AuthManager {
46
51
  }
47
52
  const tokenData = await response.json();
48
53
  this.idToken = tokenData.id_token;
54
+ this.refreshToken = tokenData.refresh_token;
49
55
  this.tokenExpiresAt = Date.now() + (tokenData.expires_in * 1000);
50
56
  return this.idToken;
51
57
  }
52
- getValidToken() {
53
- if (!this.isTokenValid()) {
54
- throw new TonalClientError('Token expired. Call authenticate() first.');
58
+ async getValidToken() {
59
+ if (this.isTokenValid()) {
60
+ return this.idToken;
55
61
  }
56
- return this.idToken;
62
+ if (this.refreshToken) {
63
+ if (!this.isRefreshing) {
64
+ await this.refreshTokens();
65
+ }
66
+ else {
67
+ // Wait for ongoing refresh to complete
68
+ while (this.isRefreshing) {
69
+ await new Promise(resolve => setTimeout(resolve, 100));
70
+ }
71
+ }
72
+ if (this.isTokenValid()) {
73
+ return this.idToken;
74
+ }
75
+ }
76
+ throw new TonalClientError('Token expired and refresh failed. Call authenticate() first.');
57
77
  }
58
78
  isTokenValid() {
59
79
  return !!this.idToken && Date.now() < this.tokenExpiresAt - 60000; // 1 minute buffer
60
80
  }
81
+ async refreshTokens() {
82
+ if (this.isRefreshing) {
83
+ return; // Prevent concurrent refresh attempts
84
+ }
85
+ this.isRefreshing = true;
86
+ try {
87
+ const response = await fetch(this.authUrl, {
88
+ method: 'POST',
89
+ headers: {
90
+ 'Content-Type': 'application/json',
91
+ },
92
+ body: JSON.stringify({
93
+ client_id: this.clientId,
94
+ grant_type: 'refresh_token',
95
+ refresh_token: this.refreshToken,
96
+ }),
97
+ });
98
+ if (!response.ok) {
99
+ const errorText = await response.text();
100
+ let errorData;
101
+ try {
102
+ errorData = JSON.parse(errorText);
103
+ }
104
+ catch {
105
+ errorData = { error: errorText };
106
+ }
107
+ throw new TonalClientError(errorData.error_description || errorData.error || 'Token refresh failed', response.status, errorData);
108
+ }
109
+ const tokenData = await response.json();
110
+ this.idToken = tokenData.id_token;
111
+ this.refreshToken = tokenData.refresh_token;
112
+ this.tokenExpiresAt = Date.now() + (tokenData.expires_in * 1000);
113
+ }
114
+ finally {
115
+ this.isRefreshing = false;
116
+ }
117
+ }
61
118
  }
62
119
 
63
120
  class HttpClient {
@@ -75,7 +132,7 @@ class HttpClient {
75
132
  const controller = new AbortController();
76
133
  const timeoutId = setTimeout(() => controller.abort(), this.requestTimeout);
77
134
  try {
78
- const token = this.authManager.getValidToken();
135
+ const token = await this.authManager.getValidToken();
79
136
  const response = await fetch(url, {
80
137
  ...options,
81
138
  headers: {
@@ -123,6 +180,16 @@ class HttpClient {
123
180
  }
124
181
  catch (error) {
125
182
  lastError = error instanceof TonalClientError ? error : new TonalClientError('Unknown error', undefined, error);
183
+ // If it's an auth error on first attempt, try to refresh token and retry once
184
+ if (attempt === 1 && lastError.statusCode && (lastError.statusCode === 401 || lastError.statusCode === 403)) {
185
+ try {
186
+ await this.authManager.getValidToken(); // This will refresh if needed
187
+ continue; // Retry the request with the new token
188
+ }
189
+ catch (refreshError) {
190
+ // If refresh fails, continue with normal retry logic
191
+ }
192
+ }
126
193
  if (attempt === this.maxRetries || (lastError.statusCode && lastError.statusCode < 500)) {
127
194
  throw lastError;
128
195
  }
@@ -259,12 +326,85 @@ class WorkoutService {
259
326
  }
260
327
  }
261
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
+
262
390
  class MovementService {
263
391
  constructor(httpClient) {
264
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;
265
405
  }
266
- async getMovements() {
267
- return this.httpClient.request('/movements');
406
+ async invalidateMovementsCache() {
407
+ await this.cacheManager.invalidate('movements');
268
408
  }
269
409
  }
270
410
 
@@ -377,8 +517,11 @@ class TonalClient {
377
517
  return client;
378
518
  }
379
519
  // Movement operations
380
- async getMovements() {
381
- return this.movementService.getMovements();
520
+ async getMovements(useCache = true) {
521
+ return this.movementService.getMovements(useCache);
522
+ }
523
+ async invalidateMovementsCache() {
524
+ return this.movementService.invalidateMovementsCache();
382
525
  }
383
526
  // User operations
384
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);
@@ -14,7 +17,9 @@ class TonalClientError extends Error {
14
17
  class AuthManager {
15
18
  constructor(username, password) {
16
19
  this.idToken = '';
20
+ this.refreshToken = '';
17
21
  this.tokenExpiresAt = 0;
22
+ this.isRefreshing = false;
18
23
  this.authUrl = 'https://tonal.auth0.com/oauth/token';
19
24
  this.clientId = 'ERCyexW-xoVG_Yy3RDe-eV4xsOnRHP6L';
20
25
  this.username = username;
@@ -50,18 +55,70 @@ class AuthManager {
50
55
  }
51
56
  const tokenData = await response.json();
52
57
  this.idToken = tokenData.id_token;
58
+ this.refreshToken = tokenData.refresh_token;
53
59
  this.tokenExpiresAt = Date.now() + (tokenData.expires_in * 1000);
54
60
  return this.idToken;
55
61
  }
56
- getValidToken() {
57
- if (!this.isTokenValid()) {
58
- throw new TonalClientError('Token expired. Call authenticate() first.');
62
+ async getValidToken() {
63
+ if (this.isTokenValid()) {
64
+ return this.idToken;
59
65
  }
60
- return this.idToken;
66
+ if (this.refreshToken) {
67
+ if (!this.isRefreshing) {
68
+ await this.refreshTokens();
69
+ }
70
+ else {
71
+ // Wait for ongoing refresh to complete
72
+ while (this.isRefreshing) {
73
+ await new Promise(resolve => setTimeout(resolve, 100));
74
+ }
75
+ }
76
+ if (this.isTokenValid()) {
77
+ return this.idToken;
78
+ }
79
+ }
80
+ throw new TonalClientError('Token expired and refresh failed. Call authenticate() first.');
61
81
  }
62
82
  isTokenValid() {
63
83
  return !!this.idToken && Date.now() < this.tokenExpiresAt - 60000; // 1 minute buffer
64
84
  }
85
+ async refreshTokens() {
86
+ if (this.isRefreshing) {
87
+ return; // Prevent concurrent refresh attempts
88
+ }
89
+ this.isRefreshing = true;
90
+ try {
91
+ const response = await fetch(this.authUrl, {
92
+ method: 'POST',
93
+ headers: {
94
+ 'Content-Type': 'application/json',
95
+ },
96
+ body: JSON.stringify({
97
+ client_id: this.clientId,
98
+ grant_type: 'refresh_token',
99
+ refresh_token: this.refreshToken,
100
+ }),
101
+ });
102
+ if (!response.ok) {
103
+ const errorText = await response.text();
104
+ let errorData;
105
+ try {
106
+ errorData = JSON.parse(errorText);
107
+ }
108
+ catch {
109
+ errorData = { error: errorText };
110
+ }
111
+ throw new TonalClientError(errorData.error_description || errorData.error || 'Token refresh failed', response.status, errorData);
112
+ }
113
+ const tokenData = await response.json();
114
+ this.idToken = tokenData.id_token;
115
+ this.refreshToken = tokenData.refresh_token;
116
+ this.tokenExpiresAt = Date.now() + (tokenData.expires_in * 1000);
117
+ }
118
+ finally {
119
+ this.isRefreshing = false;
120
+ }
121
+ }
65
122
  }
66
123
 
67
124
  class HttpClient {
@@ -79,7 +136,7 @@ class HttpClient {
79
136
  const controller = new AbortController();
80
137
  const timeoutId = setTimeout(() => controller.abort(), this.requestTimeout);
81
138
  try {
82
- const token = this.authManager.getValidToken();
139
+ const token = await this.authManager.getValidToken();
83
140
  const response = await fetch(url, {
84
141
  ...options,
85
142
  headers: {
@@ -127,6 +184,16 @@ class HttpClient {
127
184
  }
128
185
  catch (error) {
129
186
  lastError = error instanceof TonalClientError ? error : new TonalClientError('Unknown error', undefined, error);
187
+ // If it's an auth error on first attempt, try to refresh token and retry once
188
+ if (attempt === 1 && lastError.statusCode && (lastError.statusCode === 401 || lastError.statusCode === 403)) {
189
+ try {
190
+ await this.authManager.getValidToken(); // This will refresh if needed
191
+ continue; // Retry the request with the new token
192
+ }
193
+ catch (refreshError) {
194
+ // If refresh fails, continue with normal retry logic
195
+ }
196
+ }
130
197
  if (attempt === this.maxRetries || (lastError.statusCode && lastError.statusCode < 500)) {
131
198
  throw lastError;
132
199
  }
@@ -263,12 +330,85 @@ class WorkoutService {
263
330
  }
264
331
  }
265
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
+
266
394
  class MovementService {
267
395
  constructor(httpClient) {
268
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;
269
409
  }
270
- async getMovements() {
271
- return this.httpClient.request('/movements');
410
+ async invalidateMovementsCache() {
411
+ await this.cacheManager.invalidate('movements');
272
412
  }
273
413
  }
274
414
 
@@ -381,8 +521,11 @@ class TonalClient {
381
521
  return client;
382
522
  }
383
523
  // Movement operations
384
- async getMovements() {
385
- return this.movementService.getMovements();
524
+ async getMovements(useCache = true) {
525
+ return this.movementService.getMovements(useCache);
526
+ }
527
+ async invalidateMovementsCache() {
528
+ return this.movementService.invalidateMovementsCache();
386
529
  }
387
530
  // User operations
388
531
  async getUserInfo() {
@@ -2,11 +2,14 @@ export declare class AuthManager {
2
2
  private username;
3
3
  private password;
4
4
  private idToken;
5
+ private refreshToken;
5
6
  private tokenExpiresAt;
7
+ private isRefreshing;
6
8
  private readonly authUrl;
7
9
  private readonly clientId;
8
10
  constructor(username: string, password: string);
9
11
  authenticate(): Promise<string>;
10
- getValidToken(): string;
12
+ getValidToken(): Promise<string>;
11
13
  private isTokenValid;
14
+ private refreshTokens;
12
15
  }
@@ -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.0",
3
+ "version": "0.2.0",
4
4
  "description": "TypeScript client for Tonal API",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -20,7 +20,10 @@
20
20
  "build": "rollup -c --bundleConfigAsCjs",
21
21
  "typecheck": "tsc --noEmit",
22
22
  "dev": "tsc --watch --noEmit",
23
- "prepublishOnly": "npm run typecheck && npm run build",
23
+ "test": "jest",
24
+ "test:watch": "jest --watch",
25
+ "test:coverage": "jest --coverage",
26
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build",
24
27
  "example:movements": "tsx examples/get-movements.ts",
25
28
  "example:movements:save": "tsx examples/get-movements.ts --save",
26
29
  "example:workout:id": "tsx examples/get-workout-by-id.ts",
@@ -53,7 +56,12 @@
53
56
  "type": "git",
54
57
  "url": "git+https://github.com/dlwiest/ts-tonal-client.git"
55
58
  },
56
- "keywords": ["tonal", "tonal-api", "typescript", "client"],
59
+ "keywords": [
60
+ "tonal",
61
+ "tonal-api",
62
+ "typescript",
63
+ "client"
64
+ ],
57
65
  "author": "dlwiest",
58
66
  "license": "ISC",
59
67
  "bugs": {
@@ -62,10 +70,13 @@
62
70
  "homepage": "https://github.com/dlwiest/ts-tonal-client#readme",
63
71
  "devDependencies": {
64
72
  "@rollup/plugin-typescript": "^11.1.0",
73
+ "@types/jest": "^30.0.0",
65
74
  "@types/node": "^20.14.8",
66
75
  "dotenv": "^16.4.5",
76
+ "jest": "^30.2.0",
67
77
  "rollup": "^3.20.2",
68
78
  "rollup-plugin-dts": "^5.3.1",
79
+ "ts-jest": "^29.4.5",
69
80
  "ts-node": "^10.9.2",
70
81
  "tslib": "^2.7.0",
71
82
  "tsx": "^4.20.6",
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 {};