@kaiord/tcx 4.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Pablo Albaladejo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the “Software”), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # @kaiord/tcx
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@kaiord/tcx.svg)](https://www.npmjs.com/package/@kaiord/tcx)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ TCX format adapter for Kaiord workout data conversion. Provides reading, writing, and XSD validation of Garmin Training Center XML files.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pnpm add @kaiord/core @kaiord/tcx
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ### With Core Providers (Recommended)
17
+
18
+ ```typescript
19
+ import { createDefaultProviders } from "@kaiord/core";
20
+ import { createTcxProviders } from "@kaiord/tcx";
21
+
22
+ const providers = createDefaultProviders({
23
+ tcx: createTcxProviders(),
24
+ });
25
+
26
+ // TCX to KRD
27
+ const krd = await providers.convertTcxToKrd!({ tcxString });
28
+
29
+ // KRD to TCX
30
+ const tcxString = await providers.convertKrdToTcx!({ krd });
31
+ ```
32
+
33
+ ### Standalone Adapter Access
34
+
35
+ ```typescript
36
+ import {
37
+ createFastXmlTcxReader,
38
+ createFastXmlTcxWriter,
39
+ createXsdTcxValidator,
40
+ } from "@kaiord/tcx";
41
+ import { createConsoleLogger } from "@kaiord/core";
42
+
43
+ const logger = createConsoleLogger();
44
+ const reader = createFastXmlTcxReader(logger);
45
+ const writer = createFastXmlTcxWriter(logger, createXsdTcxValidator(logger));
46
+ const validator = createXsdTcxValidator(logger);
47
+ ```
48
+
49
+ ## API
50
+
51
+ ### `createTcxProviders(logger?: Logger): TcxProviders`
52
+
53
+ Creates TCX adapter instances for use with `createDefaultProviders()`.
54
+
55
+ ### `createFastXmlTcxReader(logger: Logger): TcxReader`
56
+
57
+ Creates a TCX file reader using fast-xml-parser.
58
+
59
+ ### `createFastXmlTcxWriter(logger: Logger, validator: TcxValidator): TcxWriter`
60
+
61
+ Creates a TCX file writer using fast-xml-parser.
62
+
63
+ ### `createXsdTcxValidator(logger: Logger): TcxValidator`
64
+
65
+ Creates an XSD schema validator for TCX files.
66
+
67
+ ## Supported TCX Features
68
+
69
+ - Workout definitions with structured steps
70
+ - Heart rate, speed, and cadence targets
71
+ - Time-based and distance-based durations
72
+ - Repeat blocks (intervals)
73
+ - Multiple sport types
74
+
75
+ ## License
76
+
77
+ MIT
@@ -0,0 +1,15 @@
1
+ import { Logger, TcxReader, TcxWriter, TcxValidator } from '@kaiord/core';
2
+
3
+ type TcxProviders = {
4
+ tcxReader: TcxReader;
5
+ tcxWriter: TcxWriter;
6
+ tcxValidator: TcxValidator;
7
+ };
8
+ declare const createTcxProviders: (logger?: Logger) => TcxProviders;
9
+
10
+ declare const createFastXmlTcxReader: (logger: Logger) => TcxReader;
11
+ declare const createFastXmlTcxWriter: (logger: Logger, validator: TcxValidator) => TcxWriter;
12
+
13
+ declare const createXsdTcxValidator: (logger: Logger) => TcxValidator;
14
+
15
+ export { type TcxProviders, createFastXmlTcxReader, createFastXmlTcxWriter, createTcxProviders, createXsdTcxValidator };
package/dist/index.js ADDED
@@ -0,0 +1,723 @@
1
+ import { createTcxParsingError, createTcxValidationError, createConsoleLogger } from '@kaiord/core';
2
+ import { XMLParser, XMLBuilder, XMLValidator } from 'fast-xml-parser';
3
+ import { z } from 'zod';
4
+
5
+ // src/providers.ts
6
+
7
+ // src/adapters/workout/metadata-extractor.ts
8
+ var extractKaiordMetadata = (trainingCenterDatabase, workout) => {
9
+ const metadata = {
10
+ created: trainingCenterDatabase["@_kaiord:timeCreated"] || (/* @__PURE__ */ new Date()).toISOString(),
11
+ sport: workout.sport,
12
+ subSport: workout.subSport
13
+ };
14
+ if (trainingCenterDatabase["@_kaiord:manufacturer"]) {
15
+ metadata.manufacturer = trainingCenterDatabase["@_kaiord:manufacturer"];
16
+ }
17
+ if (trainingCenterDatabase["@_kaiord:product"]) {
18
+ metadata.product = trainingCenterDatabase["@_kaiord:product"];
19
+ }
20
+ if (trainingCenterDatabase["@_kaiord:serialNumber"]) {
21
+ const serialNumber = trainingCenterDatabase["@_kaiord:serialNumber"];
22
+ metadata.serialNumber = typeof serialNumber === "number" ? String(serialNumber) : serialNumber;
23
+ }
24
+ return metadata;
25
+ };
26
+ var tcxSportSchema = z.enum(["Running", "Biking", "Other"]);
27
+ var TCX_TO_KRD_SPORT = {
28
+ Running: "running",
29
+ Biking: "cycling",
30
+ Other: "generic"
31
+ };
32
+ var KRD_TO_TCX_SPORT = {
33
+ running: "Running",
34
+ cycling: "Biking",
35
+ swimming: "Other",
36
+ generic: "Other"
37
+ };
38
+
39
+ // src/adapters/duration/duration-kaiord-restorer.ts
40
+ var restoreHeartRateLessThan = (tcxDuration, logger) => {
41
+ const bpm = tcxDuration["@_kaiord:originalDurationBpm"];
42
+ if (typeof bpm === "number") {
43
+ logger.debug("Restoring heart_rate_less_than from kaiord attributes", {
44
+ bpm
45
+ });
46
+ return { type: "heart_rate_less_than", bpm };
47
+ }
48
+ return null;
49
+ };
50
+ var restorePowerLessThan = (tcxDuration, logger) => {
51
+ const watts = tcxDuration["@_kaiord:originalDurationWatts"];
52
+ if (typeof watts === "number") {
53
+ logger.debug("Restoring power_less_than from kaiord attributes", {
54
+ watts
55
+ });
56
+ return { type: "power_less_than", watts };
57
+ }
58
+ return null;
59
+ };
60
+ var restorePowerGreaterThan = (tcxDuration, logger) => {
61
+ const watts = tcxDuration["@_kaiord:originalDurationWatts"];
62
+ if (typeof watts === "number") {
63
+ logger.debug("Restoring power_greater_than from kaiord attributes", {
64
+ watts
65
+ });
66
+ return { type: "power_greater_than", watts };
67
+ }
68
+ return null;
69
+ };
70
+ var restoreCalories = (tcxDuration, logger) => {
71
+ const calories = tcxDuration["@_kaiord:originalDurationCalories"];
72
+ if (typeof calories === "number") {
73
+ logger.debug("Restoring calories from kaiord attributes", {
74
+ calories
75
+ });
76
+ return { type: "calories", calories };
77
+ }
78
+ return null;
79
+ };
80
+ var restoreKaiordDuration = (tcxDuration, logger) => {
81
+ const originalDurationType = tcxDuration["@_kaiord:originalDurationType"];
82
+ if (originalDurationType === "heart_rate_less_than") {
83
+ return restoreHeartRateLessThan(tcxDuration, logger);
84
+ }
85
+ if (originalDurationType === "power_less_than") {
86
+ return restorePowerLessThan(tcxDuration, logger);
87
+ }
88
+ if (originalDurationType === "power_greater_than") {
89
+ return restorePowerGreaterThan(tcxDuration, logger);
90
+ }
91
+ if (originalDurationType === "calories") {
92
+ return restoreCalories(tcxDuration, logger);
93
+ }
94
+ return null;
95
+ };
96
+
97
+ // src/adapters/duration/duration-standard-converter.ts
98
+ var convertStandardTcxDuration = (tcxDuration) => {
99
+ const durationType = tcxDuration["@_xsi:type"];
100
+ if (durationType === "Time_t") {
101
+ const seconds = tcxDuration.Seconds;
102
+ if (typeof seconds === "number" && seconds > 0) {
103
+ return { type: "time", seconds };
104
+ }
105
+ }
106
+ if (durationType === "Distance_t") {
107
+ const meters = tcxDuration.Meters;
108
+ if (typeof meters === "number" && meters > 0) {
109
+ return { type: "distance", meters };
110
+ }
111
+ }
112
+ if (durationType === "LapButton_t") {
113
+ return { type: "open" };
114
+ }
115
+ return null;
116
+ };
117
+
118
+ // src/adapters/duration/duration.mapper.ts
119
+ var convertTcxDuration = (tcxDuration, logger) => {
120
+ if (!tcxDuration) {
121
+ return null;
122
+ }
123
+ const kaiordDuration = restoreKaiordDuration(tcxDuration, logger);
124
+ if (kaiordDuration) {
125
+ return kaiordDuration;
126
+ }
127
+ const standardDuration = convertStandardTcxDuration(tcxDuration);
128
+ if (standardDuration) {
129
+ return standardDuration;
130
+ }
131
+ const durationType = tcxDuration["@_xsi:type"];
132
+ logger.warn("Unsupported duration type", { durationType });
133
+ return null;
134
+ };
135
+ var convertHeartRateTarget = (heartRateZone) => {
136
+ if (!heartRateZone) return null;
137
+ const zoneType = heartRateZone["@_xsi:type"];
138
+ if (zoneType === "PredefinedHeartRateZone_t") {
139
+ const zoneNumber = heartRateZone.Number;
140
+ if (typeof zoneNumber === "number") {
141
+ return {
142
+ type: "heart_rate",
143
+ value: { unit: "zone", value: zoneNumber }
144
+ };
145
+ }
146
+ }
147
+ if (zoneType === "CustomHeartRateZone_t") {
148
+ const low = heartRateZone.Low;
149
+ const high = heartRateZone.High;
150
+ if (typeof low === "number" && typeof high === "number") {
151
+ return {
152
+ type: "heart_rate",
153
+ value: { unit: "range", min: low, max: high }
154
+ };
155
+ }
156
+ }
157
+ return null;
158
+ };
159
+ var convertTcxTarget = (tcxTarget, logger) => {
160
+ if (!tcxTarget) {
161
+ return { type: "open" };
162
+ }
163
+ const targetType = tcxTarget["@_xsi:type"];
164
+ if (targetType === "None_t") {
165
+ return { type: "open" };
166
+ }
167
+ if (targetType === "HeartRate_t") {
168
+ const heartRateZone = tcxTarget.HeartRateZone;
169
+ const result = convertHeartRateTarget(heartRateZone);
170
+ if (result) return result;
171
+ }
172
+ logger.warn("Unsupported target type", { targetType });
173
+ return { type: "open" };
174
+ };
175
+
176
+ // src/adapters/workout/step-helpers.ts
177
+ var extractIntensity = (tcxStep) => {
178
+ const raw = tcxStep.Intensity;
179
+ const value = raw?.toLowerCase();
180
+ switch (value) {
181
+ case "warmup":
182
+ case "active":
183
+ case "cooldown":
184
+ case "rest":
185
+ return value;
186
+ case "resting":
187
+ return "rest";
188
+ default:
189
+ return void 0;
190
+ }
191
+ };
192
+ var extractPowerFromExtensions = (extensions, logger) => {
193
+ if (extensions.TPX) {
194
+ const tpx = extensions.TPX;
195
+ if (typeof tpx.Watts === "number") {
196
+ logger.debug("Found power data in TCX extensions", {
197
+ watts: tpx.Watts
198
+ });
199
+ return tpx.Watts;
200
+ }
201
+ }
202
+ if (extensions.Power && typeof extensions.Power === "number") {
203
+ logger.debug("Found power data in TCX extensions", {
204
+ watts: extensions.Power
205
+ });
206
+ return extensions.Power;
207
+ }
208
+ return void 0;
209
+ };
210
+ var extractExtensions = (tcxStep, logger) => {
211
+ const extensions = tcxStep.Extensions;
212
+ if (!extensions) {
213
+ return void 0;
214
+ }
215
+ logger.debug("Extracting TCX extensions from step");
216
+ return { ...extensions };
217
+ };
218
+
219
+ // src/adapters/workout/step.converter.ts
220
+ var convertTargetWithExtensions = (tcxStep, extensions, logger) => {
221
+ const target = convertTcxTarget(
222
+ tcxStep.Target,
223
+ logger
224
+ );
225
+ if (!target) return null;
226
+ if (target.type === "open" && extensions) {
227
+ const powerWatts = extractPowerFromExtensions(extensions, logger);
228
+ if (powerWatts !== void 0) {
229
+ logger.debug("Converting open target to power target from extensions", {
230
+ watts: powerWatts
231
+ });
232
+ return {
233
+ type: "power",
234
+ value: {
235
+ unit: "watts",
236
+ value: powerWatts
237
+ }
238
+ };
239
+ }
240
+ }
241
+ return target;
242
+ };
243
+ var buildWorkoutStep = (stepIndex, name, duration, target, tcxStep, extensions) => {
244
+ const step = {
245
+ stepIndex,
246
+ name,
247
+ durationType: duration.type,
248
+ duration,
249
+ targetType: target.type,
250
+ target,
251
+ intensity: extractIntensity(tcxStep)
252
+ };
253
+ return extensions ? { ...step, extensions: { tcx: extensions } } : step;
254
+ };
255
+ var convertTcxStep = (tcxStep, stepIndex, logger) => {
256
+ logger.debug("Converting TCX step", { stepIndex });
257
+ const stepType = tcxStep["@_xsi:type"];
258
+ if (stepType === "Repeat_t") {
259
+ logger.warn("Repetition blocks not yet supported", { stepIndex });
260
+ return null;
261
+ }
262
+ const duration = convertTcxDuration(
263
+ tcxStep.Duration,
264
+ logger
265
+ );
266
+ if (!duration) {
267
+ logger.warn("Step has no valid duration, skipping", { stepIndex });
268
+ return null;
269
+ }
270
+ const extensions = extractExtensions(tcxStep, logger);
271
+ const target = convertTargetWithExtensions(tcxStep, extensions, logger);
272
+ if (!target) {
273
+ logger.warn("Step has no valid target, skipping", { stepIndex });
274
+ return null;
275
+ }
276
+ return buildWorkoutStep(
277
+ stepIndex,
278
+ tcxStep.Name,
279
+ duration,
280
+ target,
281
+ tcxStep,
282
+ extensions
283
+ );
284
+ };
285
+
286
+ // src/adapters/workout/workout.converter.ts
287
+ var extractWorkoutExtensions = (tcxWorkout, logger) => {
288
+ const extensions = tcxWorkout.Extensions;
289
+ if (!extensions) {
290
+ return void 0;
291
+ }
292
+ logger.debug("Extracting TCX extensions from workout");
293
+ return { ...extensions };
294
+ };
295
+ var convertSteps = (tcxSteps, logger) => {
296
+ const steps = [];
297
+ if (!tcxSteps) return steps;
298
+ const stepArray = Array.isArray(tcxSteps) ? tcxSteps : [tcxSteps];
299
+ let stepIndex = 0;
300
+ for (const tcxStep of stepArray) {
301
+ const step = convertTcxStep(
302
+ tcxStep,
303
+ stepIndex,
304
+ logger
305
+ );
306
+ if (step) {
307
+ steps.push(step);
308
+ stepIndex++;
309
+ }
310
+ }
311
+ return steps;
312
+ };
313
+ var convertTcxWorkout = (tcxWorkout, logger) => {
314
+ logger.debug("Converting TCX workout");
315
+ const sportAttr = tcxWorkout["@_Sport"];
316
+ const sportResult = tcxSportSchema.safeParse(sportAttr);
317
+ const sport = sportResult.success ? TCX_TO_KRD_SPORT[sportResult.data] : "generic";
318
+ const name = tcxWorkout.Name;
319
+ const steps = convertSteps(tcxWorkout.Step, logger);
320
+ const extensions = extractWorkoutExtensions(tcxWorkout, logger);
321
+ const workout = {
322
+ name,
323
+ sport,
324
+ steps
325
+ };
326
+ if (extensions) {
327
+ return {
328
+ ...workout,
329
+ extensions: {
330
+ tcx: extensions
331
+ }
332
+ };
333
+ }
334
+ return workout;
335
+ };
336
+
337
+ // src/adapters/workout/krd.converter.ts
338
+ var extractTcxExtensions = (trainingCenterDatabase, logger) => {
339
+ const extensions = trainingCenterDatabase.Extensions;
340
+ if (!extensions) {
341
+ return void 0;
342
+ }
343
+ logger.debug("Extracting TCX extensions from TrainingCenterDatabase");
344
+ return { ...extensions };
345
+ };
346
+ var extractWorkoutData = (trainingCenterDatabase) => {
347
+ const workouts = trainingCenterDatabase.Workouts;
348
+ if (!workouts) {
349
+ throw createTcxParsingError("No workouts found in TCX file");
350
+ }
351
+ const workoutArray = Array.isArray(workouts.Workout) ? workouts.Workout : [workouts.Workout];
352
+ if (workoutArray.length === 0) {
353
+ throw createTcxParsingError("No workout data found in TCX file");
354
+ }
355
+ return workoutArray[0];
356
+ };
357
+ var convertTcxToKRD = (tcxData, logger) => {
358
+ logger.debug("Converting TCX to KRD");
359
+ const trainingCenterDatabase = tcxData.TrainingCenterDatabase;
360
+ const tcxWorkout = extractWorkoutData(trainingCenterDatabase);
361
+ const workout = convertTcxWorkout(tcxWorkout, logger);
362
+ const tcxExtensions = extractTcxExtensions(trainingCenterDatabase, logger);
363
+ const metadata = extractKaiordMetadata(trainingCenterDatabase, workout);
364
+ const krd = {
365
+ version: "1.0",
366
+ type: "structured_workout",
367
+ metadata,
368
+ extensions: {
369
+ structured_workout: workout,
370
+ ...tcxExtensions && { tcx: tcxExtensions }
371
+ }
372
+ };
373
+ logger.debug("TCX to KRD conversion complete");
374
+ return krd;
375
+ };
376
+
377
+ // src/adapters/workout/metadata-builder.ts
378
+ var addKaiordMetadata = (trainingCenterDatabase, krd) => {
379
+ if (krd.metadata.created) {
380
+ trainingCenterDatabase["@_kaiord:timeCreated"] = krd.metadata.created;
381
+ }
382
+ if (krd.metadata.manufacturer) {
383
+ trainingCenterDatabase["@_kaiord:manufacturer"] = krd.metadata.manufacturer;
384
+ }
385
+ if (krd.metadata.product) {
386
+ trainingCenterDatabase["@_kaiord:product"] = krd.metadata.product;
387
+ }
388
+ if (krd.metadata.serialNumber) {
389
+ trainingCenterDatabase["@_kaiord:serialNumber"] = krd.metadata.serialNumber;
390
+ }
391
+ };
392
+
393
+ // src/adapters/workout/duration-to-tcx-encoder.ts
394
+ var addKaiordAttributes = (tcxDuration, duration) => {
395
+ if (duration.type === "heart_rate_less_than" && duration.bpm !== void 0) {
396
+ tcxDuration["@_kaiord:originalDurationType"] = "heart_rate_less_than";
397
+ tcxDuration["@_kaiord:originalDurationBpm"] = duration.bpm;
398
+ } else if (duration.type === "power_less_than" && duration.watts !== void 0) {
399
+ tcxDuration["@_kaiord:originalDurationType"] = "power_less_than";
400
+ tcxDuration["@_kaiord:originalDurationWatts"] = duration.watts;
401
+ } else if (duration.type === "power_greater_than" && duration.watts !== void 0) {
402
+ tcxDuration["@_kaiord:originalDurationType"] = "power_greater_than";
403
+ tcxDuration["@_kaiord:originalDurationWatts"] = duration.watts;
404
+ } else if (duration.type === "calories" && duration.calories !== void 0) {
405
+ tcxDuration["@_kaiord:originalDurationType"] = "calories";
406
+ tcxDuration["@_kaiord:originalDurationCalories"] = duration.calories;
407
+ }
408
+ };
409
+ var convertDurationToTcx = (step) => {
410
+ const duration = step.duration;
411
+ if (duration.type === "time") {
412
+ return {
413
+ "@_xsi:type": "Time_t",
414
+ Seconds: duration.seconds
415
+ };
416
+ }
417
+ if (duration.type === "distance") {
418
+ return {
419
+ "@_xsi:type": "Distance_t",
420
+ Meters: duration.meters
421
+ };
422
+ }
423
+ const tcxDuration = {
424
+ "@_xsi:type": "LapButton_t"
425
+ };
426
+ addKaiordAttributes(tcxDuration, duration);
427
+ return tcxDuration;
428
+ };
429
+
430
+ // src/adapters/workout/target-to-tcx.converter.ts
431
+ var convertHeartRateToTcx = (value) => {
432
+ if (value.unit === "zone") {
433
+ return {
434
+ "@_xsi:type": "HeartRate_t",
435
+ HeartRateZone: {
436
+ "@_xsi:type": "PredefinedHeartRateZone_t",
437
+ Number: value.value
438
+ }
439
+ };
440
+ }
441
+ if (value.unit === "bpm" || value.unit === "range") {
442
+ const min = "min" in value ? value.min : value.value;
443
+ const max = "max" in value ? value.max : value.value;
444
+ return {
445
+ "@_xsi:type": "HeartRate_t",
446
+ HeartRateZone: {
447
+ "@_xsi:type": "CustomHeartRateZone_t",
448
+ Low: min,
449
+ High: max
450
+ }
451
+ };
452
+ }
453
+ return { "@_xsi:type": "None_t" };
454
+ };
455
+ var convertPaceToTcx = (value) => {
456
+ if (value.unit === "meters_per_second" || value.unit === "range") {
457
+ const min = "min" in value ? value.min : value.value;
458
+ const max = "max" in value ? value.max : value.value;
459
+ return {
460
+ "@_xsi:type": "Speed_t",
461
+ SpeedZone: {
462
+ "@_xsi:type": "CustomSpeedZone_t",
463
+ LowInMetersPerSecond: min,
464
+ HighInMetersPerSecond: max
465
+ }
466
+ };
467
+ }
468
+ return { "@_xsi:type": "None_t" };
469
+ };
470
+ var convertCadenceToTcx = (value) => {
471
+ if (value.unit === "rpm" || value.unit === "range") {
472
+ const min = "min" in value ? value.min : value.value;
473
+ const max = "max" in value ? value.max : value.value;
474
+ return {
475
+ "@_xsi:type": "Cadence_t",
476
+ CadenceZone: {
477
+ "@_xsi:type": "CustomCadenceZone_t",
478
+ Low: min,
479
+ High: max
480
+ }
481
+ };
482
+ }
483
+ return { "@_xsi:type": "None_t" };
484
+ };
485
+ var convertTargetToTcx = (step) => {
486
+ if (step.target.type === "open") {
487
+ return { "@_xsi:type": "None_t" };
488
+ }
489
+ if (step.target.type === "heart_rate") {
490
+ const value = step.target.value;
491
+ if (value && "unit" in value) {
492
+ return convertHeartRateToTcx(value);
493
+ }
494
+ }
495
+ if (step.target.type === "pace") {
496
+ const value = step.target.value;
497
+ if (value && "unit" in value) {
498
+ return convertPaceToTcx(value);
499
+ }
500
+ }
501
+ if (step.target.type === "cadence") {
502
+ const value = step.target.value;
503
+ if (value && "unit" in value) {
504
+ return convertCadenceToTcx(value);
505
+ }
506
+ }
507
+ return { "@_xsi:type": "None_t" };
508
+ };
509
+
510
+ // src/adapters/workout/step-to-tcx.converter.ts
511
+ var capitalizeFirst = (str) => {
512
+ return str.charAt(0).toUpperCase() + str.slice(1);
513
+ };
514
+ var addPowerExtensions = (step, tcxStep, logger) => {
515
+ if (step.target.type === "power") {
516
+ logger.debug("Encoding power target to TCX extensions", {
517
+ stepIndex: step.stepIndex
518
+ });
519
+ const powerValue = step.target.value;
520
+ if (powerValue && "unit" in powerValue && powerValue.unit === "watts") {
521
+ tcxStep.Extensions = {
522
+ TPX: {
523
+ "@_xmlns": "http://www.garmin.com/xmlschemas/ActivityExtension/v2",
524
+ Watts: powerValue.value
525
+ }
526
+ };
527
+ }
528
+ }
529
+ };
530
+ var convertStepToTcx = (step, index, logger) => {
531
+ logger.debug("Converting step to TCX", { stepIndex: step.stepIndex });
532
+ const tcxStep = {
533
+ "@_xsi:type": "Step_t",
534
+ StepId: index + 1
535
+ };
536
+ if (step.name) {
537
+ tcxStep.Name = step.name;
538
+ }
539
+ tcxStep.Duration = convertDurationToTcx(step);
540
+ if (step.intensity) {
541
+ tcxStep.Intensity = capitalizeFirst(step.intensity);
542
+ }
543
+ tcxStep.Target = convertTargetToTcx(step);
544
+ if (step.extensions?.tcx) {
545
+ logger.debug("Restoring step-level TCX extensions", {
546
+ stepIndex: step.stepIndex
547
+ });
548
+ tcxStep.Extensions = step.extensions.tcx;
549
+ } else {
550
+ addPowerExtensions(step, tcxStep, logger);
551
+ }
552
+ return tcxStep;
553
+ };
554
+
555
+ // src/adapters/workout/tcx.converter.ts
556
+ var buildTcxWorkout = (workout, logger) => {
557
+ const tcxSport = KRD_TO_TCX_SPORT[workout.sport] || "Other";
558
+ const tcxSteps = workout.steps.map(
559
+ (step, index) => convertStepToTcx(step, index, logger)
560
+ );
561
+ const tcxWorkout = {
562
+ "@_Sport": tcxSport,
563
+ Name: workout.name,
564
+ Step: tcxSteps
565
+ };
566
+ if (workout.extensions?.tcx) {
567
+ logger.debug("Restoring workout-level TCX extensions");
568
+ tcxWorkout.Extensions = workout.extensions.tcx;
569
+ }
570
+ return tcxWorkout;
571
+ };
572
+ var buildTrainingCenterDatabase = (tcxWorkout, krd, logger) => {
573
+ const trainingCenterDatabase = {
574
+ "@_xmlns": "http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2",
575
+ "@_xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
576
+ "@_xmlns:kaiord": "http://kaiord.dev/tcx-extensions/1.0",
577
+ Workouts: {
578
+ Workout: tcxWorkout
579
+ }
580
+ };
581
+ addKaiordMetadata(trainingCenterDatabase, krd);
582
+ if (krd.extensions?.tcx) {
583
+ logger.debug("Restoring TrainingCenterDatabase-level TCX extensions");
584
+ trainingCenterDatabase.Extensions = krd.extensions.tcx;
585
+ }
586
+ return trainingCenterDatabase;
587
+ };
588
+ var convertKRDToTcx = (krd, logger) => {
589
+ logger.debug("Converting KRD to TCX structure");
590
+ if (!krd.extensions?.structured_workout) {
591
+ throw createTcxParsingError(
592
+ "KRD does not contain workout data in extensions"
593
+ );
594
+ }
595
+ const workout = krd.extensions.structured_workout;
596
+ const tcxWorkout = buildTcxWorkout(workout, logger);
597
+ const trainingCenterDatabase = buildTrainingCenterDatabase(
598
+ tcxWorkout,
599
+ krd,
600
+ logger
601
+ );
602
+ return {
603
+ TrainingCenterDatabase: trainingCenterDatabase
604
+ };
605
+ };
606
+
607
+ // src/adapters/fast-xml-parser.ts
608
+ var createFastXmlTcxReader = (logger) => async (xmlString) => {
609
+ logger.debug("Parsing TCX file", { xmlLength: xmlString.length });
610
+ let tcxData;
611
+ try {
612
+ const parser = new XMLParser({
613
+ ignoreAttributes: false,
614
+ attributeNamePrefix: "@_",
615
+ parseAttributeValue: true
616
+ });
617
+ tcxData = parser.parse(xmlString);
618
+ } catch (error) {
619
+ logger.error("Failed to parse TCX XML", { error });
620
+ throw createTcxParsingError("Failed to parse TCX file", error);
621
+ }
622
+ if (!tcxData || typeof tcxData !== "object" || !("TrainingCenterDatabase" in tcxData)) {
623
+ const error = createTcxParsingError(
624
+ "Invalid TCX format: missing TrainingCenterDatabase element"
625
+ );
626
+ logger.error("Invalid TCX structure", { error });
627
+ throw error;
628
+ }
629
+ logger.info("TCX file parsed successfully");
630
+ return convertTcxToKRD(tcxData, logger);
631
+ };
632
+ var createFastXmlTcxWriter = (logger, validator) => async (krd) => {
633
+ logger.debug("Encoding KRD to TCX");
634
+ let tcxData;
635
+ try {
636
+ tcxData = convertKRDToTcx2(krd, logger);
637
+ } catch (error) {
638
+ logger.error("Failed to convert KRD to TCX structure", { error });
639
+ throw createTcxParsingError("Failed to convert KRD to TCX", error);
640
+ }
641
+ let xmlString;
642
+ try {
643
+ const builder = new XMLBuilder({
644
+ ignoreAttributes: false,
645
+ attributeNamePrefix: "@_",
646
+ format: true,
647
+ indentBy: " "
648
+ });
649
+ xmlString = builder.build(tcxData);
650
+ } catch (error) {
651
+ logger.error("Failed to build TCX XML", { error });
652
+ throw createTcxParsingError("Failed to build TCX XML", error);
653
+ }
654
+ logger.debug("Validating generated TCX against XSD");
655
+ const validationResult = await validator(xmlString);
656
+ if (!validationResult.valid) {
657
+ logger.error("Generated TCX does not conform to XSD schema", {
658
+ errors: validationResult.errors
659
+ });
660
+ throw createTcxValidationError(
661
+ "Generated TCX file does not conform to XSD schema",
662
+ validationResult.errors.map((err) => ({
663
+ field: err.path,
664
+ message: err.message
665
+ }))
666
+ );
667
+ }
668
+ logger.info("KRD encoded to TCX successfully");
669
+ return xmlString;
670
+ };
671
+ var convertKRDToTcx2 = (krd, logger) => {
672
+ return convertKRDToTcx(krd, logger);
673
+ };
674
+ var createXsdTcxValidator = (logger) => async (xmlString) => {
675
+ try {
676
+ logger.debug("Validating TCX XML structure");
677
+ const xmlValidation = XMLValidator.validate(xmlString, {
678
+ allowBooleanAttributes: true
679
+ });
680
+ if (xmlValidation !== true) {
681
+ logger.warn("TCX XML validation failed", {
682
+ error: xmlValidation.err
683
+ });
684
+ return {
685
+ valid: false,
686
+ errors: [
687
+ {
688
+ path: `line ${xmlValidation.err.line}`,
689
+ message: `XML validation failed: ${xmlValidation.err.msg}`
690
+ }
691
+ ]
692
+ };
693
+ }
694
+ logger.info("TCX XML structure is valid");
695
+ return { valid: true, errors: [] };
696
+ } catch (error) {
697
+ logger.error("TCX validation failed", { error });
698
+ return {
699
+ valid: false,
700
+ errors: [
701
+ {
702
+ path: "root",
703
+ message: error instanceof Error ? error.message : "Unknown error"
704
+ }
705
+ ]
706
+ };
707
+ }
708
+ };
709
+
710
+ // src/providers.ts
711
+ var createTcxProviders = (logger) => {
712
+ const log = logger || createConsoleLogger();
713
+ const tcxValidator = createXsdTcxValidator(log);
714
+ return {
715
+ tcxReader: createFastXmlTcxReader(log),
716
+ tcxWriter: createFastXmlTcxWriter(log, tcxValidator),
717
+ tcxValidator
718
+ };
719
+ };
720
+
721
+ export { createFastXmlTcxReader, createFastXmlTcxWriter, createTcxProviders, createXsdTcxValidator };
722
+ //# sourceMappingURL=index.js.map
723
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/workout/metadata-extractor.ts","../src/adapters/schemas/tcx-sport.ts","../src/adapters/duration/duration-kaiord-restorer.ts","../src/adapters/duration/duration-standard-converter.ts","../src/adapters/duration/duration.mapper.ts","../src/adapters/target/tcx-to-krd.mapper.ts","../src/adapters/workout/step-helpers.ts","../src/adapters/workout/step.converter.ts","../src/adapters/workout/workout.converter.ts","../src/adapters/workout/krd.converter.ts","../src/adapters/workout/metadata-builder.ts","../src/adapters/workout/duration-to-tcx-encoder.ts","../src/adapters/workout/target-to-tcx.converter.ts","../src/adapters/workout/step-to-tcx.converter.ts","../src/adapters/workout/tcx.converter.ts","../src/adapters/fast-xml-parser.ts","../src/adapters/xsd-validator.ts","../src/providers.ts"],"names":["createTcxParsingError","convertKRDToTcx"],"mappings":"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","file":"index.js","sourcesContent":["import type { KRD } from \"@kaiord/core\";\nimport type { Workout } from \"@kaiord/core\";\n\nexport const extractKaiordMetadata = (\n trainingCenterDatabase: Record<string, unknown>,\n workout: Workout\n): KRD[\"metadata\"] => {\n const metadata: KRD[\"metadata\"] = {\n created:\n (trainingCenterDatabase[\"@_kaiord:timeCreated\"] as string | undefined) ||\n new Date().toISOString(),\n sport: workout.sport,\n subSport: workout.subSport,\n };\n\n if (trainingCenterDatabase[\"@_kaiord:manufacturer\"]) {\n metadata.manufacturer = trainingCenterDatabase[\n \"@_kaiord:manufacturer\"\n ] as string;\n }\n if (trainingCenterDatabase[\"@_kaiord:product\"]) {\n metadata.product = trainingCenterDatabase[\"@_kaiord:product\"] as string;\n }\n if (trainingCenterDatabase[\"@_kaiord:serialNumber\"]) {\n const serialNumber = trainingCenterDatabase[\"@_kaiord:serialNumber\"];\n // Convert to string if it's a number (XML parser may parse as number)\n metadata.serialNumber =\n typeof serialNumber === \"number\"\n ? String(serialNumber)\n : (serialNumber as string);\n }\n\n return metadata;\n};\n","import { z } from \"zod\";\nimport type { Sport } from \"@kaiord/core\";\n\nexport const tcxSportSchema = z.enum([\"Running\", \"Biking\", \"Other\"]);\n\nexport type TcxSport = z.infer<typeof tcxSportSchema>;\n\nexport const TCX_TO_KRD_SPORT: Record<TcxSport, Sport> = {\n Running: \"running\",\n Biking: \"cycling\",\n Other: \"generic\",\n};\n\nexport const KRD_TO_TCX_SPORT: Record<Sport, TcxSport> = {\n running: \"Running\",\n cycling: \"Biking\",\n swimming: \"Other\",\n generic: \"Other\",\n};\n","import type { Duration } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\n\nconst restoreHeartRateLessThan = (\n tcxDuration: Record<string, unknown>,\n logger: Logger\n): Duration | null => {\n const bpm = tcxDuration[\"@_kaiord:originalDurationBpm\"] as number | undefined;\n if (typeof bpm === \"number\") {\n logger.debug(\"Restoring heart_rate_less_than from kaiord attributes\", {\n bpm,\n });\n return { type: \"heart_rate_less_than\", bpm };\n }\n return null;\n};\n\nconst restorePowerLessThan = (\n tcxDuration: Record<string, unknown>,\n logger: Logger\n): Duration | null => {\n const watts = tcxDuration[\"@_kaiord:originalDurationWatts\"] as\n | number\n | undefined;\n if (typeof watts === \"number\") {\n logger.debug(\"Restoring power_less_than from kaiord attributes\", {\n watts,\n });\n return { type: \"power_less_than\", watts };\n }\n return null;\n};\n\nconst restorePowerGreaterThan = (\n tcxDuration: Record<string, unknown>,\n logger: Logger\n): Duration | null => {\n const watts = tcxDuration[\"@_kaiord:originalDurationWatts\"] as\n | number\n | undefined;\n if (typeof watts === \"number\") {\n logger.debug(\"Restoring power_greater_than from kaiord attributes\", {\n watts,\n });\n return { type: \"power_greater_than\", watts };\n }\n return null;\n};\n\nconst restoreCalories = (\n tcxDuration: Record<string, unknown>,\n logger: Logger\n): Duration | null => {\n const calories = tcxDuration[\"@_kaiord:originalDurationCalories\"] as\n | number\n | undefined;\n if (typeof calories === \"number\") {\n logger.debug(\"Restoring calories from kaiord attributes\", {\n calories,\n });\n return { type: \"calories\", calories };\n }\n return null;\n};\n\nexport const restoreKaiordDuration = (\n tcxDuration: Record<string, unknown>,\n logger: Logger\n): Duration | null => {\n const originalDurationType = tcxDuration[\"@_kaiord:originalDurationType\"] as\n | string\n | undefined;\n\n if (originalDurationType === \"heart_rate_less_than\") {\n return restoreHeartRateLessThan(tcxDuration, logger);\n }\n\n if (originalDurationType === \"power_less_than\") {\n return restorePowerLessThan(tcxDuration, logger);\n }\n\n if (originalDurationType === \"power_greater_than\") {\n return restorePowerGreaterThan(tcxDuration, logger);\n }\n\n if (originalDurationType === \"calories\") {\n return restoreCalories(tcxDuration, logger);\n }\n\n return null;\n};\n","import type { Duration } from \"@kaiord/core\";\n\nexport const convertStandardTcxDuration = (\n tcxDuration: Record<string, unknown>\n): Duration | null => {\n const durationType = tcxDuration[\"@_xsi:type\"] as string | undefined;\n\n if (durationType === \"Time_t\") {\n const seconds = tcxDuration.Seconds as number | undefined;\n if (typeof seconds === \"number\" && seconds > 0) {\n return { type: \"time\", seconds };\n }\n }\n\n if (durationType === \"Distance_t\") {\n const meters = tcxDuration.Meters as number | undefined;\n if (typeof meters === \"number\" && meters > 0) {\n return { type: \"distance\", meters };\n }\n }\n\n if (durationType === \"LapButton_t\") {\n return { type: \"open\" };\n }\n\n return null;\n};\n","import { durationTypeSchema, type Duration } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\nimport { tcxDurationTypeSchema } from \"../schemas/tcx-duration\";\nimport { restoreKaiordDuration } from \"./duration-kaiord-restorer\";\nimport { convertStandardTcxDuration } from \"./duration-standard-converter\";\n\nexport type TcxDurationData = {\n durationType?: string;\n seconds?: number;\n meters?: number;\n};\n\nexport const mapTcxDuration = (data: TcxDurationData): Duration => {\n const result = tcxDurationTypeSchema.safeParse(data.durationType);\n\n if (!result.success) {\n return { type: durationTypeSchema.enum.open };\n }\n\n // Time duration\n if (\n result.data === tcxDurationTypeSchema.enum.Time &&\n data.seconds !== undefined\n ) {\n return {\n type: durationTypeSchema.enum.time,\n seconds: data.seconds,\n };\n }\n\n // Distance duration\n if (\n result.data === tcxDurationTypeSchema.enum.Distance &&\n data.meters !== undefined\n ) {\n return {\n type: durationTypeSchema.enum.distance,\n meters: data.meters,\n };\n }\n\n // LapButton duration\n if (result.data === tcxDurationTypeSchema.enum.LapButton) {\n return {\n type: durationTypeSchema.enum.open,\n };\n }\n\n // Default to open for unsupported types (HeartRateAbove, HeartRateBelow, CaloriesBurned)\n // These will be handled by the converter and stored in extensions\n return { type: durationTypeSchema.enum.open };\n};\n\nexport const convertTcxDuration = (\n tcxDuration: Record<string, unknown> | undefined,\n logger: Logger\n): Duration | null => {\n if (!tcxDuration) {\n return null;\n }\n\n // First check for kaiord attributes to restore advanced duration types\n const kaiordDuration = restoreKaiordDuration(tcxDuration, logger);\n if (kaiordDuration) {\n return kaiordDuration;\n }\n\n // Then check for standard TCX duration types\n const standardDuration = convertStandardTcxDuration(tcxDuration);\n if (standardDuration) {\n return standardDuration;\n }\n\n const durationType = tcxDuration[\"@_xsi:type\"] as string | undefined;\n logger.warn(\"Unsupported duration type\", { durationType });\n return null;\n};\n\n// KRD → TCX duration mappers\n\nexport type TcxDurationElement = Record<string, unknown>;\n\nexport const mapTimeDurationToTcx = (seconds: number): TcxDurationElement => {\n return {\n \"@_xsi:type\": \"Time_t\",\n Seconds: seconds,\n };\n};\n\nexport const mapDistanceDurationToTcx = (\n meters: number\n): TcxDurationElement => {\n return {\n \"@_xsi:type\": \"Distance_t\",\n Meters: meters,\n };\n};\n\nexport const mapOpenDurationToTcx = (): TcxDurationElement => {\n return {\n \"@_xsi:type\": \"LapButton_t\",\n };\n};\n","import type { Target } from \"@kaiord/core\";\nimport { targetTypeSchema, type TargetType } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\nimport { tcxTargetTypeSchema } from \"../schemas/tcx-target\";\n\nexport const mapTargetType = (\n tcxTargetType: string | undefined\n): TargetType => {\n if (tcxTargetType === tcxTargetTypeSchema.enum.HeartRate)\n return targetTypeSchema.enum.heart_rate;\n if (tcxTargetType === tcxTargetTypeSchema.enum.Speed)\n return targetTypeSchema.enum.pace;\n if (tcxTargetType === tcxTargetTypeSchema.enum.Cadence)\n return targetTypeSchema.enum.cadence;\n if (tcxTargetType === tcxTargetTypeSchema.enum.None)\n return targetTypeSchema.enum.open;\n return targetTypeSchema.enum.open;\n};\n\nconst convertHeartRateTarget = (\n heartRateZone: Record<string, unknown> | undefined\n): Target | null => {\n if (!heartRateZone) return null;\n\n const zoneType = heartRateZone[\"@_xsi:type\"] as string | undefined;\n\n if (zoneType === \"PredefinedHeartRateZone_t\") {\n const zoneNumber = heartRateZone.Number as number | undefined;\n if (typeof zoneNumber === \"number\") {\n return {\n type: \"heart_rate\",\n value: { unit: \"zone\", value: zoneNumber },\n };\n }\n }\n\n if (zoneType === \"CustomHeartRateZone_t\") {\n const low = heartRateZone.Low as number | undefined;\n const high = heartRateZone.High as number | undefined;\n if (typeof low === \"number\" && typeof high === \"number\") {\n return {\n type: \"heart_rate\",\n value: { unit: \"range\", min: low, max: high },\n };\n }\n }\n\n return null;\n};\n\nexport const convertTcxTarget = (\n tcxTarget: Record<string, unknown> | undefined,\n logger: Logger\n): Target | null => {\n if (!tcxTarget) {\n return { type: \"open\" };\n }\n\n const targetType = tcxTarget[\"@_xsi:type\"] as string | undefined;\n\n if (targetType === \"None_t\") {\n return { type: \"open\" };\n }\n\n if (targetType === \"HeartRate_t\") {\n const heartRateZone = tcxTarget.HeartRateZone as\n | Record<string, unknown>\n | undefined;\n const result = convertHeartRateTarget(heartRateZone);\n if (result) return result;\n }\n\n logger.warn(\"Unsupported target type\", { targetType });\n return { type: \"open\" };\n};\n","import type { Logger } from \"@kaiord/core\";\n\nexport const extractIntensity = (\n tcxStep: Record<string, unknown>\n): \"warmup\" | \"active\" | \"cooldown\" | \"rest\" | undefined => {\n const raw = tcxStep.Intensity as string | undefined;\n const value = raw?.toLowerCase();\n\n switch (value) {\n case \"warmup\":\n case \"active\":\n case \"cooldown\":\n case \"rest\":\n return value;\n case \"resting\":\n return \"rest\";\n default:\n return undefined;\n }\n};\n\nexport const extractPowerFromExtensions = (\n extensions: Record<string, unknown>,\n logger: Logger\n): number | undefined => {\n if (extensions.TPX) {\n const tpx = extensions.TPX as Record<string, unknown>;\n if (typeof tpx.Watts === \"number\") {\n logger.debug(\"Found power data in TCX extensions\", {\n watts: tpx.Watts,\n });\n return tpx.Watts;\n }\n }\n\n if (extensions.Power && typeof extensions.Power === \"number\") {\n logger.debug(\"Found power data in TCX extensions\", {\n watts: extensions.Power,\n });\n return extensions.Power;\n }\n\n return undefined;\n};\n\nexport const extractExtensions = (\n tcxStep: Record<string, unknown>,\n logger: Logger\n): Record<string, unknown> | undefined => {\n const extensions = tcxStep.Extensions as Record<string, unknown> | undefined;\n if (!extensions) {\n return undefined;\n }\n\n logger.debug(\"Extracting TCX extensions from step\");\n return { ...extensions };\n};\n","import type { Duration } from \"@kaiord/core\";\nimport type { Target } from \"@kaiord/core\";\nimport type { WorkoutStep } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\nimport { convertTcxDuration } from \"../duration/duration.mapper\";\nimport { convertTcxTarget } from \"../target/target.mapper\";\nimport {\n extractExtensions,\n extractIntensity,\n extractPowerFromExtensions,\n} from \"./step-helpers\";\n\nconst convertTargetWithExtensions = (\n tcxStep: Record<string, unknown>,\n extensions: Record<string, unknown> | undefined,\n logger: Logger\n): Target | null => {\n const target = convertTcxTarget(\n tcxStep.Target as Record<string, unknown> | undefined,\n logger\n );\n if (!target) return null;\n\n if (target.type === \"open\" && extensions) {\n const powerWatts = extractPowerFromExtensions(extensions, logger);\n if (powerWatts !== undefined) {\n logger.debug(\"Converting open target to power target from extensions\", {\n watts: powerWatts,\n });\n return {\n type: \"power\",\n value: {\n unit: \"watts\",\n value: powerWatts,\n },\n };\n }\n }\n\n return target;\n};\n\nconst buildWorkoutStep = (\n stepIndex: number,\n name: string | undefined,\n duration: Duration,\n target: Target,\n tcxStep: Record<string, unknown>,\n extensions: Record<string, unknown> | undefined\n): WorkoutStep => {\n const step: WorkoutStep = {\n stepIndex,\n name,\n durationType: duration.type,\n duration,\n targetType: target.type,\n target,\n intensity: extractIntensity(tcxStep),\n };\n\n return extensions ? { ...step, extensions: { tcx: extensions } } : step;\n};\n\nexport const convertTcxStep = (\n tcxStep: Record<string, unknown>,\n stepIndex: number,\n logger: Logger\n): WorkoutStep | null => {\n logger.debug(\"Converting TCX step\", { stepIndex });\n\n const stepType = tcxStep[\"@_xsi:type\"] as string | undefined;\n if (stepType === \"Repeat_t\") {\n logger.warn(\"Repetition blocks not yet supported\", { stepIndex });\n return null;\n }\n\n const duration = convertTcxDuration(\n tcxStep.Duration as Record<string, unknown> | undefined,\n logger\n );\n if (!duration) {\n logger.warn(\"Step has no valid duration, skipping\", { stepIndex });\n return null;\n }\n\n const extensions = extractExtensions(tcxStep, logger);\n const target = convertTargetWithExtensions(tcxStep, extensions, logger);\n if (!target) {\n logger.warn(\"Step has no valid target, skipping\", { stepIndex });\n return null;\n }\n\n return buildWorkoutStep(\n stepIndex,\n tcxStep.Name as string | undefined,\n duration,\n target,\n tcxStep,\n extensions\n );\n};\n","import type { Workout, WorkoutStep } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\nimport { TCX_TO_KRD_SPORT, tcxSportSchema } from \"../schemas/tcx-sport\";\nimport { convertTcxStep } from \"./step.converter\";\n\nconst extractWorkoutExtensions = (\n tcxWorkout: Record<string, unknown>,\n logger: Logger\n): Record<string, unknown> | undefined => {\n const extensions = tcxWorkout.Extensions as\n | Record<string, unknown>\n | undefined;\n if (!extensions) {\n return undefined;\n }\n\n logger.debug(\"Extracting TCX extensions from workout\");\n\n // Store the raw TCX extensions for round-trip preservation\n return { ...extensions };\n};\n\nconst convertSteps = (\n tcxSteps: unknown,\n logger: Logger\n): Array<WorkoutStep> => {\n const steps: Array<WorkoutStep> = [];\n\n if (!tcxSteps) return steps;\n\n const stepArray = Array.isArray(tcxSteps) ? tcxSteps : [tcxSteps];\n let stepIndex = 0;\n\n for (const tcxStep of stepArray) {\n const step = convertTcxStep(\n tcxStep as Record<string, unknown>,\n stepIndex,\n logger\n );\n if (step) {\n steps.push(step);\n stepIndex++;\n }\n }\n\n return steps;\n};\n\nexport const convertTcxWorkout = (\n tcxWorkout: Record<string, unknown>,\n logger: Logger\n): Workout => {\n logger.debug(\"Converting TCX workout\");\n\n const sportAttr = tcxWorkout[\"@_Sport\"] as string | undefined;\n const sportResult = tcxSportSchema.safeParse(sportAttr);\n const sport = sportResult.success\n ? TCX_TO_KRD_SPORT[sportResult.data]\n : \"generic\";\n\n const name = tcxWorkout.Name as string | undefined;\n const steps = convertSteps(tcxWorkout.Step, logger);\n const extensions = extractWorkoutExtensions(tcxWorkout, logger);\n\n const workout: Workout = {\n name,\n sport,\n steps,\n };\n\n if (extensions) {\n return {\n ...workout,\n extensions: {\n tcx: extensions,\n },\n };\n }\n\n return workout;\n};\n","import type { KRD } from \"@kaiord/core\";\nimport { createTcxParsingError } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\nimport { extractKaiordMetadata } from \"./metadata-extractor\";\nimport { convertTcxWorkout } from \"./workout.converter\";\n\nconst extractTcxExtensions = (\n trainingCenterDatabase: Record<string, unknown>,\n logger: Logger\n): Record<string, unknown> | undefined => {\n const extensions = trainingCenterDatabase.Extensions as\n | Record<string, unknown>\n | undefined;\n if (!extensions) {\n return undefined;\n }\n\n logger.debug(\"Extracting TCX extensions from TrainingCenterDatabase\");\n\n // Store the raw TCX extensions for round-trip preservation\n return { ...extensions };\n};\n\nconst extractWorkoutData = (\n trainingCenterDatabase: Record<string, unknown>\n): Record<string, unknown> => {\n const workouts = trainingCenterDatabase.Workouts as\n | Record<string, unknown>\n | undefined;\n\n if (!workouts) {\n throw createTcxParsingError(\"No workouts found in TCX file\");\n }\n\n const workoutArray = Array.isArray(workouts.Workout)\n ? workouts.Workout\n : [workouts.Workout];\n\n if (workoutArray.length === 0) {\n throw createTcxParsingError(\"No workout data found in TCX file\");\n }\n\n return workoutArray[0] as Record<string, unknown>;\n};\n\nexport const convertTcxToKRD = (\n tcxData: Record<string, unknown>,\n logger: Logger\n): KRD => {\n logger.debug(\"Converting TCX to KRD\");\n\n const trainingCenterDatabase = tcxData.TrainingCenterDatabase as Record<\n string,\n unknown\n >;\n\n const tcxWorkout = extractWorkoutData(trainingCenterDatabase);\n const workout = convertTcxWorkout(tcxWorkout, logger);\n const tcxExtensions = extractTcxExtensions(trainingCenterDatabase, logger);\n const metadata = extractKaiordMetadata(trainingCenterDatabase, workout);\n\n const krd: KRD = {\n version: \"1.0\",\n type: \"structured_workout\",\n metadata,\n extensions: {\n structured_workout: workout,\n ...(tcxExtensions && { tcx: tcxExtensions }),\n },\n };\n\n logger.debug(\"TCX to KRD conversion complete\");\n return krd;\n};\n","import type { KRD } from \"@kaiord/core\";\n\nexport const addKaiordMetadata = (\n trainingCenterDatabase: Record<string, unknown>,\n krd: KRD\n): void => {\n if (krd.metadata.created) {\n trainingCenterDatabase[\"@_kaiord:timeCreated\"] = krd.metadata.created;\n }\n if (krd.metadata.manufacturer) {\n trainingCenterDatabase[\"@_kaiord:manufacturer\"] = krd.metadata.manufacturer;\n }\n if (krd.metadata.product) {\n trainingCenterDatabase[\"@_kaiord:product\"] = krd.metadata.product;\n }\n if (krd.metadata.serialNumber) {\n trainingCenterDatabase[\"@_kaiord:serialNumber\"] = krd.metadata.serialNumber;\n }\n};\n","import type { Duration } from \"@kaiord/core\";\n\nconst addKaiordAttributes = (\n tcxDuration: Record<string, unknown>,\n duration: Duration\n): void => {\n if (duration.type === \"heart_rate_less_than\" && duration.bpm !== undefined) {\n tcxDuration[\"@_kaiord:originalDurationType\"] = \"heart_rate_less_than\";\n tcxDuration[\"@_kaiord:originalDurationBpm\"] = duration.bpm;\n } else if (\n duration.type === \"power_less_than\" &&\n duration.watts !== undefined\n ) {\n tcxDuration[\"@_kaiord:originalDurationType\"] = \"power_less_than\";\n tcxDuration[\"@_kaiord:originalDurationWatts\"] = duration.watts;\n } else if (\n duration.type === \"power_greater_than\" &&\n duration.watts !== undefined\n ) {\n tcxDuration[\"@_kaiord:originalDurationType\"] = \"power_greater_than\";\n tcxDuration[\"@_kaiord:originalDurationWatts\"] = duration.watts;\n } else if (duration.type === \"calories\" && duration.calories !== undefined) {\n tcxDuration[\"@_kaiord:originalDurationType\"] = \"calories\";\n tcxDuration[\"@_kaiord:originalDurationCalories\"] = duration.calories;\n }\n};\n\nexport const convertDurationToTcx = (step: {\n duration: Duration;\n}): Record<string, unknown> => {\n const duration = step.duration;\n\n // Standard TCX duration types\n if (duration.type === \"time\") {\n return {\n \"@_xsi:type\": \"Time_t\",\n Seconds: duration.seconds,\n };\n }\n\n if (duration.type === \"distance\") {\n return {\n \"@_xsi:type\": \"Distance_t\",\n Meters: duration.meters,\n };\n }\n\n // For advanced duration types not natively supported by TCX,\n // use LapButton_t and preserve original type in kaiord attributes\n const tcxDuration: Record<string, unknown> = {\n \"@_xsi:type\": \"LapButton_t\",\n };\n\n addKaiordAttributes(tcxDuration, duration);\n\n return tcxDuration;\n};\n","import type { WorkoutStep } from \"@kaiord/core\";\n\nexport const convertHeartRateToTcx = (value: {\n unit: string;\n value?: number;\n min?: number;\n max?: number;\n}): Record<string, unknown> => {\n if (value.unit === \"zone\") {\n return {\n \"@_xsi:type\": \"HeartRate_t\",\n HeartRateZone: {\n \"@_xsi:type\": \"PredefinedHeartRateZone_t\",\n Number: value.value,\n },\n };\n }\n\n if (value.unit === \"bpm\" || value.unit === \"range\") {\n const min = \"min\" in value ? value.min : value.value;\n const max = \"max\" in value ? value.max : value.value;\n return {\n \"@_xsi:type\": \"HeartRate_t\",\n HeartRateZone: {\n \"@_xsi:type\": \"CustomHeartRateZone_t\",\n Low: min,\n High: max,\n },\n };\n }\n\n return { \"@_xsi:type\": \"None_t\" };\n};\n\nexport const convertPaceToTcx = (value: {\n unit: string;\n value?: number;\n min?: number;\n max?: number;\n}): Record<string, unknown> => {\n if (value.unit === \"meters_per_second\" || value.unit === \"range\") {\n const min = \"min\" in value ? value.min : value.value;\n const max = \"max\" in value ? value.max : value.value;\n return {\n \"@_xsi:type\": \"Speed_t\",\n SpeedZone: {\n \"@_xsi:type\": \"CustomSpeedZone_t\",\n LowInMetersPerSecond: min,\n HighInMetersPerSecond: max,\n },\n };\n }\n\n return { \"@_xsi:type\": \"None_t\" };\n};\n\nexport const convertCadenceToTcx = (value: {\n unit: string;\n value?: number;\n min?: number;\n max?: number;\n}): Record<string, unknown> => {\n if (value.unit === \"rpm\" || value.unit === \"range\") {\n const min = \"min\" in value ? value.min : value.value;\n const max = \"max\" in value ? value.max : value.value;\n return {\n \"@_xsi:type\": \"Cadence_t\",\n CadenceZone: {\n \"@_xsi:type\": \"CustomCadenceZone_t\",\n Low: min,\n High: max,\n },\n };\n }\n\n return { \"@_xsi:type\": \"None_t\" };\n};\n\nexport const convertTargetToTcx = (\n step: WorkoutStep\n): Record<string, unknown> => {\n if (step.target.type === \"open\") {\n return { \"@_xsi:type\": \"None_t\" };\n }\n\n if (step.target.type === \"heart_rate\") {\n const value = step.target.value;\n if (value && \"unit\" in value) {\n return convertHeartRateToTcx(value);\n }\n }\n\n if (step.target.type === \"pace\") {\n const value = step.target.value;\n if (value && \"unit\" in value) {\n return convertPaceToTcx(value);\n }\n }\n\n if (step.target.type === \"cadence\") {\n const value = step.target.value;\n if (value && \"unit\" in value) {\n return convertCadenceToTcx(value);\n }\n }\n\n return { \"@_xsi:type\": \"None_t\" };\n};\n","import type { WorkoutStep } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\nimport { convertDurationToTcx } from \"./duration-to-tcx-encoder\";\nimport { convertTargetToTcx } from \"./target-to-tcx.converter\";\n\nconst capitalizeFirst = (str: string): string => {\n return str.charAt(0).toUpperCase() + str.slice(1);\n};\n\nconst addPowerExtensions = (\n step: WorkoutStep,\n tcxStep: Record<string, unknown>,\n logger: Logger\n): void => {\n if (step.target.type === \"power\") {\n logger.debug(\"Encoding power target to TCX extensions\", {\n stepIndex: step.stepIndex,\n });\n const powerValue = step.target.value;\n if (powerValue && \"unit\" in powerValue && powerValue.unit === \"watts\") {\n tcxStep.Extensions = {\n TPX: {\n \"@_xmlns\": \"http://www.garmin.com/xmlschemas/ActivityExtension/v2\",\n Watts: powerValue.value,\n },\n };\n }\n }\n};\n\nexport const convertStepToTcx = (\n step: WorkoutStep,\n index: number,\n logger: Logger\n): Record<string, unknown> => {\n logger.debug(\"Converting step to TCX\", { stepIndex: step.stepIndex });\n\n const tcxStep: Record<string, unknown> = {\n \"@_xsi:type\": \"Step_t\",\n StepId: index + 1,\n };\n\n if (step.name) {\n tcxStep.Name = step.name;\n }\n\n tcxStep.Duration = convertDurationToTcx(step);\n\n if (step.intensity) {\n tcxStep.Intensity = capitalizeFirst(step.intensity);\n }\n\n tcxStep.Target = convertTargetToTcx(step);\n\n if (step.extensions?.tcx) {\n logger.debug(\"Restoring step-level TCX extensions\", {\n stepIndex: step.stepIndex,\n });\n tcxStep.Extensions = step.extensions.tcx;\n } else {\n addPowerExtensions(step, tcxStep, logger);\n }\n\n return tcxStep;\n};\n","import type { KRD } from \"@kaiord/core\";\nimport type { Workout, WorkoutStep } from \"@kaiord/core\";\nimport { createTcxParsingError } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\nimport { KRD_TO_TCX_SPORT } from \"../schemas/tcx-sport\";\nimport { addKaiordMetadata } from \"./metadata-builder\";\nimport { convertStepToTcx } from \"./step-to-tcx.converter\";\n\nconst buildTcxWorkout = (\n workout: Workout,\n logger: Logger\n): Record<string, unknown> => {\n const tcxSport =\n KRD_TO_TCX_SPORT[workout.sport as keyof typeof KRD_TO_TCX_SPORT] || \"Other\";\n\n const tcxSteps = workout.steps.map((step, index) =>\n convertStepToTcx(step as WorkoutStep, index, logger)\n );\n\n const tcxWorkout: Record<string, unknown> = {\n \"@_Sport\": tcxSport,\n Name: workout.name,\n Step: tcxSteps,\n };\n\n if (workout.extensions?.tcx) {\n logger.debug(\"Restoring workout-level TCX extensions\");\n tcxWorkout.Extensions = workout.extensions.tcx;\n }\n\n return tcxWorkout;\n};\n\nconst buildTrainingCenterDatabase = (\n tcxWorkout: Record<string, unknown>,\n krd: KRD,\n logger: Logger\n): Record<string, unknown> => {\n const trainingCenterDatabase: Record<string, unknown> = {\n \"@_xmlns\": \"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\",\n \"@_xmlns:xsi\": \"http://www.w3.org/2001/XMLSchema-instance\",\n \"@_xmlns:kaiord\": \"http://kaiord.dev/tcx-extensions/1.0\",\n Workouts: {\n Workout: tcxWorkout,\n },\n };\n\n addKaiordMetadata(trainingCenterDatabase, krd);\n\n if (krd.extensions?.tcx) {\n logger.debug(\"Restoring TrainingCenterDatabase-level TCX extensions\");\n trainingCenterDatabase.Extensions = krd.extensions.tcx;\n }\n\n return trainingCenterDatabase;\n};\n\nexport const convertKRDToTcx = (krd: KRD, logger: Logger): unknown => {\n logger.debug(\"Converting KRD to TCX structure\");\n\n if (!krd.extensions?.structured_workout) {\n throw createTcxParsingError(\n \"KRD does not contain workout data in extensions\"\n );\n }\n\n const workout = krd.extensions.structured_workout as Workout;\n const tcxWorkout = buildTcxWorkout(workout, logger);\n const trainingCenterDatabase = buildTrainingCenterDatabase(\n tcxWorkout,\n krd,\n logger\n );\n\n return {\n TrainingCenterDatabase: trainingCenterDatabase,\n };\n};\n","import { XMLBuilder, XMLParser } from \"fast-xml-parser\";\nimport type { KRD } from \"@kaiord/core\";\nimport { createTcxParsingError, createTcxValidationError } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\nimport type { TcxReader } from \"@kaiord/core\";\nimport type { TcxValidator } from \"@kaiord/core\";\nimport type { TcxWriter } from \"@kaiord/core\";\nimport { convertTcxToKRD } from \"./workout/krd.converter\";\nimport { convertKRDToTcx as convertKRDToTcxStructure } from \"./workout/tcx.converter\";\n\nexport const createFastXmlTcxReader =\n (logger: Logger): TcxReader =>\n async (xmlString: string): Promise<KRD> => {\n logger.debug(\"Parsing TCX file\", { xmlLength: xmlString.length });\n\n let tcxData: unknown;\n try {\n const parser = new XMLParser({\n ignoreAttributes: false,\n attributeNamePrefix: \"@_\",\n parseAttributeValue: true,\n });\n\n tcxData = parser.parse(xmlString);\n } catch (error) {\n logger.error(\"Failed to parse TCX XML\", { error });\n throw createTcxParsingError(\"Failed to parse TCX file\", error);\n }\n\n if (\n !tcxData ||\n typeof tcxData !== \"object\" ||\n !(\"TrainingCenterDatabase\" in tcxData)\n ) {\n const error = createTcxParsingError(\n \"Invalid TCX format: missing TrainingCenterDatabase element\"\n );\n logger.error(\"Invalid TCX structure\", { error });\n throw error;\n }\n\n logger.info(\"TCX file parsed successfully\");\n\n return convertTcxToKRD(tcxData, logger);\n };\n\nexport const createFastXmlTcxWriter =\n (logger: Logger, validator: TcxValidator): TcxWriter =>\n async (krd: KRD): Promise<string> => {\n logger.debug(\"Encoding KRD to TCX\");\n\n let tcxData: unknown;\n try {\n tcxData = convertKRDToTcx(krd, logger);\n } catch (error) {\n logger.error(\"Failed to convert KRD to TCX structure\", { error });\n throw createTcxParsingError(\"Failed to convert KRD to TCX\", error);\n }\n\n let xmlString: string;\n try {\n const builder = new XMLBuilder({\n ignoreAttributes: false,\n attributeNamePrefix: \"@_\",\n format: true,\n indentBy: \" \",\n });\n\n xmlString = builder.build(tcxData);\n } catch (error) {\n logger.error(\"Failed to build TCX XML\", { error });\n throw createTcxParsingError(\"Failed to build TCX XML\", error);\n }\n\n logger.debug(\"Validating generated TCX against XSD\");\n\n const validationResult = await validator(xmlString);\n if (!validationResult.valid) {\n logger.error(\"Generated TCX does not conform to XSD schema\", {\n errors: validationResult.errors,\n });\n throw createTcxValidationError(\n \"Generated TCX file does not conform to XSD schema\",\n validationResult.errors.map((err) => ({\n field: err.path,\n message: err.message,\n }))\n );\n }\n\n logger.info(\"KRD encoded to TCX successfully\");\n return xmlString;\n };\n\nconst convertKRDToTcx = (krd: KRD, logger: Logger): unknown => {\n return convertKRDToTcxStructure(krd, logger);\n};\n","import { XMLValidator } from \"fast-xml-parser\";\nimport type { Logger } from \"@kaiord/core\";\nimport type { TcxValidationResult, TcxValidator } from \"@kaiord/core\";\n\nexport const createXsdTcxValidator =\n (logger: Logger): TcxValidator =>\n async (xmlString: string): Promise<TcxValidationResult> => {\n try {\n logger.debug(\"Validating TCX XML structure\");\n\n // Validate XML structure using fast-xml-parser\n const xmlValidation = XMLValidator.validate(xmlString, {\n allowBooleanAttributes: true,\n });\n\n if (xmlValidation !== true) {\n logger.warn(\"TCX XML validation failed\", {\n error: xmlValidation.err,\n });\n\n return {\n valid: false,\n errors: [\n {\n path: `line ${xmlValidation.err.line}`,\n message: `XML validation failed: ${xmlValidation.err.msg}`,\n },\n ],\n };\n }\n\n logger.info(\"TCX XML structure is valid\");\n return { valid: true, errors: [] };\n } catch (error) {\n logger.error(\"TCX validation failed\", { error });\n\n return {\n valid: false,\n errors: [\n {\n path: \"root\",\n message: error instanceof Error ? error.message : \"Unknown error\",\n },\n ],\n };\n }\n };\n","import type { Logger, TcxReader, TcxValidator, TcxWriter } from \"@kaiord/core\";\nimport { createConsoleLogger } from \"@kaiord/core\";\nimport {\n createFastXmlTcxReader,\n createFastXmlTcxWriter,\n} from \"./adapters/fast-xml-parser\";\nimport { createXsdTcxValidator } from \"./adapters/xsd-validator\";\n\nexport type TcxProviders = {\n tcxReader: TcxReader;\n tcxWriter: TcxWriter;\n tcxValidator: TcxValidator;\n};\n\nexport const createTcxProviders = (logger?: Logger): TcxProviders => {\n const log = logger || createConsoleLogger();\n const tcxValidator = createXsdTcxValidator(log);\n\n return {\n tcxReader: createFastXmlTcxReader(log),\n tcxWriter: createFastXmlTcxWriter(log, tcxValidator),\n tcxValidator,\n };\n};\n"]}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@kaiord/tcx",
3
+ "version": "4.0.0",
4
+ "description": "TCX format adapter for Kaiord workout data conversion",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "fast-xml-parser": "^5.3.4",
20
+ "zod": "^3.22.4",
21
+ "@kaiord/core": "^4.1.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^24.0.0",
25
+ "@vitest/coverage-v8": "^4.0.18",
26
+ "tsup": "^8.5.1",
27
+ "tsx": "^4.7.0",
28
+ "typescript": "^5.3.3",
29
+ "vitest": "^4.0.18"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/pablo-albaladejo/kaiord.git",
34
+ "directory": "packages/tcx"
35
+ },
36
+ "homepage": "https://www.npmjs.com/package/@kaiord/tcx",
37
+ "bugs": {
38
+ "url": "https://github.com/pablo-albaladejo/kaiord/issues"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "keywords": [
44
+ "kaiord",
45
+ "tcx",
46
+ "workout",
47
+ "converter",
48
+ "fitness"
49
+ ],
50
+ "author": "Kaiord Contributors",
51
+ "license": "MIT",
52
+ "scripts": {
53
+ "build": "tsup",
54
+ "test": "vitest --run",
55
+ "test:watch": "vitest",
56
+ "lint": "tsc --noEmit && eslint . && prettier --check src",
57
+ "lint:fix": "eslint . --fix && prettier --write src",
58
+ "clean": "rm -rf dist"
59
+ }
60
+ }