@runnerpro/backend 1.32.0 → 1.33.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.
@@ -1 +1 @@
1
- {"version":3,"file":"garmin.d.ts","sourceRoot":"","sources":["../../../../src/workout/garmin.ts"],"names":[],"mappings":"AAoBA,UAAU,iBAAiB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAC7B;AAED,UAAU,aAAa;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B;AA4FD,QAAA,MAAM,sBAAsB,iCAyD3B,CAAC;AA+EF,QAAA,MAAM,mBAAmB;;;EAWxB,CAAC;AAEF,QAAA,MAAM,mBAAmB,6CASxB,CAAC;AAEF,QAAA,MAAM,mBAAmB,6CAMxB,CAAC;AAEF,QAAA,MAAM,qBAAqB,6CAU1B,CAAC;AAEF,QAAA,MAAM,2BAA2B,4DAWhC,CAAC;AAIF,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,2BAA2B,GAC5B,CAAC"}
1
+ {"version":3,"file":"garmin.d.ts","sourceRoot":"","sources":["../../../../src/workout/garmin.ts"],"names":[],"mappings":"AAoBA,UAAU,iBAAiB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAC7B;AAED,UAAU,aAAa;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B;AAgHD,QAAA,MAAM,sBAAsB,iCA4D3B,CAAC;AAmFF,QAAA,MAAM,mBAAmB;;;EAcxB,CAAC;AAEF,QAAA,MAAM,mBAAmB,6CASxB,CAAC;AAEF,QAAA,MAAM,mBAAmB,6CAMxB,CAAC;AAEF,QAAA,MAAM,qBAAqB,6CAU1B,CAAC;AAEF,QAAA,MAAM,2BAA2B,4DAWhC,CAAC;AAIF,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,2BAA2B,GAC5B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"saveWorkoutAplication.d.ts","sourceRoot":"","sources":["../../../../src/workout/saveWorkoutAplication.ts"],"names":[],"mappings":"AAuTA;;;;;;;;;;GAUG;AACH,QAAA,MAAM,qBAAqB,SACnB,GAAG,UAED,GAAG,KACV,QAAQ,MAAM,GAAG,IAAI,CA6EvB,CAAC;AA0GF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
1
+ {"version":3,"file":"saveWorkoutAplication.d.ts","sourceRoot":"","sources":["../../../../src/workout/saveWorkoutAplication.ts"],"names":[],"mappings":"AAsTA;;;;;;;;;;GAUG;AACH,QAAA,MAAM,qBAAqB,SACnB,GAAG,UAED,GAAG,KACV,QAAQ,MAAM,GAAG,IAAI,CAqFvB,CAAC;AA0GF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"sendToWatch.d.ts","sourceRoot":"","sources":["../../../../src/workout/sendToWatch.ts"],"names":[],"mappings":"AAKA,QAAA,MAAM,kBAAkB;;;;;;EAmBvB,CAAC;AAEF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
1
+ {"version":3,"file":"sendToWatch.d.ts","sourceRoot":"","sources":["../../../../src/workout/sendToWatch.ts"],"names":[],"mappings":"AAKA,QAAA,MAAM,kBAAkB;;;;;;EAuBvB,CAAC;AAEF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
@@ -65,22 +65,40 @@ const garminOauthActivity = ({ url, httpMethod, userid, params = {} }) => __awai
65
65
  const secondsPerKmToMetersPerSecond = (secondsPerKm) => {
66
66
  return Math.round((1000 / secondsPerKm) * 100) / 100;
67
67
  };
68
+ /**
69
+ * Devuelve la metrica planificada como numero, o null si no la hay.
70
+ *
71
+ * OJO: Postgres devuelve las columnas `numeric` como TEXTO, asi que una distancia
72
+ * de 0 llega como la cadena "0", que en JS es truthy. Los entrenos de bici y
73
+ * natacion se planifican por tiempo y guardan `DISTANCE PLANNED` = 0: comparar
74
+ * en crudo hacia colar entrenos de 0 metros a Garmin.
75
+ */
76
+ const metricaPlanificada = (valor) => {
77
+ const n = Number(valor);
78
+ return Number.isFinite(n) && n > 0 ? n : null;
79
+ };
68
80
  const getWorkoutGarminDuration = (workout) => {
81
+ const distancia = metricaPlanificada(workout.distancePlanned);
82
+ const duracion = metricaPlanificada(workout.durationPlanned);
69
83
  const rangePlanning = (0, common_1.getWorkoutRangePlanning)({
70
84
  workoutType: workout.type,
71
85
  distancePlanned: workout.distancePlanned,
72
86
  durationPlanned: workout.durationPlanned,
73
87
  zonePlanned: workout.zonePlanned,
74
88
  });
75
- if (rangePlanning.fixedMetric === common_1.WORKOUT_FIXED_METRIC.TIME) {
89
+ // `fixedMetric` solo se calcula cuando hay rango de ritmo, y eso solo pasa en
90
+ // CORRER/COMPETICION. Para bici y natacion siempre es null, asi que sin este
91
+ // fallback caian SIEMPRE por distancia (y se enviaba un paso de 0 metros).
92
+ const prefiereTiempo = rangePlanning.fixedMetric === common_1.WORKOUT_FIXED_METRIC.TIME;
93
+ if (duracion && (prefiereTiempo || !distancia)) {
76
94
  return {
77
95
  type: 'TIME',
78
- value: Number(workout.durationPlanned),
96
+ value: duracion,
79
97
  };
80
98
  }
81
99
  return {
82
100
  type: 'DISTANCE',
83
- value: Number(workout.distancePlanned),
101
+ value: distancia,
84
102
  valueType: 'METER',
85
103
  };
86
104
  };
@@ -126,7 +144,10 @@ const translateWorkoutGarmin = (workout) => {
126
144
  });
127
145
  }
128
146
  else if (!workout.estructura) {
129
- if (!workout.distancePlanned || !workout.durationPlanned)
147
+ // Basta con una de las dos: bici/natacion se planifican por tiempo y correr
148
+ // por distancia. Exigir las dos (comparando ademas en crudo) era lo que
149
+ // dejaba fuera a los entrenos por tiempo.
150
+ if (!metricaPlanificada(workout.distancePlanned) && !metricaPlanificada(workout.durationPlanned))
130
151
  return null;
131
152
  const targetType = getWorkoutGarminTargetType(workout, null);
132
153
  const targetValue = getWorkoutGarminTargetValue(workout, null);
@@ -203,7 +224,11 @@ const getWorkoutGarminTargetValue = (workout, row) => {
203
224
  // } else {
204
225
  // El resto calcular el ritmo
205
226
  // No se puede añadir potencia ya que el valor que hay que dar es una zona, no absoluto
206
- value = Number(workout.distancePlanned) / Number(workout.durationPlanned);
227
+ const distancia = metricaPlanificada(workout.distancePlanned);
228
+ const duracion = metricaPlanificada(workout.durationPlanned);
229
+ // Sin las dos metricas no hay ritmo objetivo: el paso va sin target (que es
230
+ // lo que ya se hace hoy en natacion y Garmin acepta).
231
+ value = distancia && duracion ? distancia / duracion : null;
207
232
  paceRange = (0, common_1.getWorkoutRangePlanning)({
208
233
  workoutType: workout.type,
209
234
  distancePlanned: workout.distancePlanned,
@@ -247,6 +272,10 @@ const getWorkoutGarminTargetValue = (workout, row) => {
247
272
  };
248
273
  const createWorkoutGarmin = (userid, workout) => __awaiter(void 0, void 0, void 0, function* () {
249
274
  const workoutGarmin = translateWorkoutGarmin(workout);
275
+ // Sin payload no se manda nada: antes se hacia el POST con body null y Garmin
276
+ // respondia un error opaco.
277
+ if (!workoutGarmin)
278
+ throw new Error(`Workout ${workout.id} no es traducible a Garmin (sin estructura y sin distancia ni duracion)`);
250
279
  const data = yield garminOauthActivity({
251
280
  url: 'https://apis.garmin.com/training-api/workout',
252
281
  userid,
@@ -14,7 +14,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.saveWorkoutAplication = void 0;
16
16
  const db_1 = require("../db");
17
- const titleDescriptionTranslated_1 = require("../translation/titleDescriptionTranslated");
18
17
  const storage_1 = require("@google-cloud/storage");
19
18
  const path_1 = __importDefault(require("path"));
20
19
  const axios_1 = __importDefault(require("axios"));
@@ -316,16 +315,24 @@ _data) => __awaiter(void 0, void 0, void 0, function* () {
316
315
  }
317
316
  const dateStr = (0, moment_1.default)(opts.date).format('YYYY-MM-DD');
318
317
  const pt = predicadoTipo(opts);
319
- // Traducción FUERA de la sección crítica. getTitleDescriptionTranslated hace
320
- // una llamada de red a Google Cloud Translation; si se ejecutara dentro de
321
- // `withTx` retendría el advisory lock durante todo el round-trip (causa de los
322
- // picos de varios minutos en pg_advisory_xact_lock cuando la API va lenta).
323
- // Sólo depende de idCliente/title/description —nada de la transacción— así que
324
- // se calcula antes de abrir la tx. Semántica intacta: si la traducción falla,
325
- // se propaga el error y el workout no se guarda, igual que antes. En las rutas
326
- // de merge (1 y 2) el valor se descarta; el único sobrecoste es una traducción
327
- // extra para clientes no-ES con doble proveedor, a cambio de no bloquear el lock.
328
- const translated = yield (0, titleDescriptionTranslated_1.getTitleDescriptionTranslated)(opts.idCliente, opts.title, opts.description);
318
+ // SIN traducción en la ingesta: título y descripción se guardan tal cual llegan
319
+ // del proveedor (Garmin/Strava/Apple). Antes se traducían al español con Google
320
+ // Cloud Translation, lo que metía una llamada de red —y una dependencia de las
321
+ // credenciales de GCP— en el camino crítico del guardado: si Translate fallaba,
322
+ // el entreno NO se guardaba y el webhook devolvía 5xx. Con la migración a Azure
323
+ // eso se rompió (sin metadata server, las ADC resuelven a la service account de
324
+ // Storage, que no tiene permiso de Translate PERMISSION_DENIED) y dos
325
+ // actividades envenenadas bastaron para atascar la cola de reintentos de Garmin
326
+ // y frenar la ingesta del resto.
327
+ // Consecuencia asumida: para clientes no-ES el "TITLE" queda en el idioma del
328
+ // dispositivo en vez de normalizado a español —lo notan entrenadores y la IA, no
329
+ // el cliente: la app cae a "TITLE" cuando "TITLE PREFERRED LANGUAGE" es null.
330
+ const translated = {
331
+ title: opts.title,
332
+ description: opts.description,
333
+ titlePreferredLanguage: null,
334
+ descriptionPreferredLanguage: null,
335
+ };
329
336
  const idWorkout = yield withTx((q) => __awaiter(void 0, void 0, void 0, function* () {
330
337
  var _e;
331
338
  // Lock anti-carrera: serializa ingestas del mismo cliente/día/bucket-tipo
@@ -28,6 +28,10 @@ const sendWorkoutToWatch = (clientId, workoutId) => __awaiter(void 0, void 0, vo
28
28
  const estructura = yield (0, db_1.query)('SELECT * FROM [WORKOUT STRUCTURE] WHERE [ID WORKOUT] = ? ORDER BY [INDEX]', [workoutId]);
29
29
  if (estructura.length > 0)
30
30
  workout.estructura = estructura;
31
+ // Si el entreno no se puede traducir, se avisa con un 400 legible en vez de
32
+ // dejar que el POST a Garmin reviente con un 500 opaco.
33
+ if (!(0, garmin_1.translateWorkoutGarmin)(workout))
34
+ return { error: 'Este entrenamiento no se puede enviar al reloj' };
31
35
  // Llamar a la función que envía el workout al Garmin
32
36
  const { workoutGarminId } = yield (0, garmin_1.createWorkoutGarmin)(clientId, workout);
33
37
  yield (0, garmin_1.scheduleWorkoutGarmin)(clientId, { date: (0, moment_1.default)(workout.date).format('YYYY-MM-DD'), workoutGarminId });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runnerpro/backend",
3
- "version": "1.32.0",
3
+ "version": "1.33.1",
4
4
  "description": "A collection of common backend functions",
5
5
  "exports": {
6
6
  ".": "./lib/cjs/index.js"
@@ -1,38 +0,0 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.getTitleDescriptionTranslated = void 0;
13
- const db_1 = require("../db");
14
- const index_1 = require("./index");
15
- const common_1 = require("@runnerpro/common");
16
- const getTitleDescriptionTranslated = (idCliente, titleOriginal, descriptionOriginal) => __awaiter(void 0, void 0, void 0, function* () {
17
- let title;
18
- let description;
19
- let titlePreferredLanguage;
20
- let descriptionPreferredLanguage;
21
- const [cliente] = yield (0, db_1.query)('SELECT [PREFERRED LANGUAGE] FROM [CLIENTE] WHERE [ID] = ?', [idCliente]);
22
- if ((cliente === null || cliente === void 0 ? void 0 : cliente.preferredLanguage) && cliente.preferredLanguage !== common_1.LANGUAGES.ES) {
23
- // @ts-ignore
24
- title = titleOriginal ? yield (0, index_1.translate)(titleOriginal, { fromLanguage: cliente.preferredLanguage, toLanguage: common_1.LANGUAGES.ES }) : null;
25
- description = descriptionOriginal
26
- ? // @ts-ignore
27
- yield (0, index_1.translate)(descriptionOriginal, { fromLanguage: cliente.preferredLanguage, toLanguage: common_1.LANGUAGES.ES })
28
- : null;
29
- titlePreferredLanguage = titleOriginal;
30
- descriptionPreferredLanguage = descriptionOriginal;
31
- }
32
- else {
33
- title = titleOriginal;
34
- description = descriptionOriginal;
35
- }
36
- return { title, description, titlePreferredLanguage, descriptionPreferredLanguage };
37
- });
38
- exports.getTitleDescriptionTranslated = getTitleDescriptionTranslated;
@@ -1,8 +0,0 @@
1
- declare const getTitleDescriptionTranslated: (idCliente: any, titleOriginal: any, descriptionOriginal: any) => Promise<{
2
- title: any;
3
- description: any;
4
- titlePreferredLanguage: any;
5
- descriptionPreferredLanguage: any;
6
- }>;
7
- export { getTitleDescriptionTranslated };
8
- //# sourceMappingURL=titleDescriptionTranslated.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"titleDescriptionTranslated.d.ts","sourceRoot":"","sources":["../../../../src/translation/titleDescriptionTranslated.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,6BAA6B;;;;;EAsBlC,CAAC;AAEF,OAAO,EAAE,6BAA6B,EAAE,CAAC"}