@runnerpro/backend 1.31.2 → 1.33.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 +13 -0
- package/lib/cjs/achievements/index.js +61 -20
- package/lib/cjs/db/index.js +150 -28
- package/lib/cjs/image/generateShareMap.js +24 -8
- package/lib/cjs/sendNotification/index.js +24 -10
- package/lib/cjs/types/achievements/index.d.ts.map +1 -1
- package/lib/cjs/types/db/index.d.ts +22 -0
- package/lib/cjs/types/db/index.d.ts.map +1 -1
- package/lib/cjs/types/image/generateShareMap.d.ts.map +1 -1
- package/lib/cjs/types/sendNotification/index.d.ts.map +1 -1
- package/lib/cjs/types/workout/estructuraWorkout.d.ts.map +1 -1
- package/lib/cjs/types/workout/saveWorkoutAplication.d.ts.map +1 -1
- package/lib/cjs/types/workout/saveWorkoutLaps.d.ts.map +1 -1
- package/lib/cjs/workout/estructuraWorkout.js +19 -6
- package/lib/cjs/workout/saveWorkoutAplication.js +51 -14
- package/lib/cjs/workout/saveWorkoutLaps.js +25 -7
- package/package.json +3 -2
- package/lib/cjs/translation/titleDescriptionTranslated.js +0 -38
- package/lib/cjs/types/translation/titleDescriptionTranslated.d.ts +0 -8
- package/lib/cjs/types/translation/titleDescriptionTranslated.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -16,6 +16,19 @@ Connection with the PostgresSQL database
|
|
|
16
16
|
- <b>Param</b>: array of values
|
|
17
17
|
- <b>Return</b>: promise of array of values (each item is a row of the table)
|
|
18
18
|
|
|
19
|
+
> ⚠️ **T-219 — un fallo de CONEXIÓN puede rechazar, y se enciende por servicio.** `query()` no
|
|
20
|
+
> lanzaba nunca: tanto «la BD no respondió» como «no hay filas» devolvían `[]`. Desde la **1.32.0**,
|
|
21
|
+
> con `RP_DB_RECHAZA_FALLO_CONEXION=1` en el entorno, un fallo de conexión —agotado el reintento (1,
|
|
22
|
+
> a los 500 ms)— **rechaza la promesa**. Un error de SQL (sintaxis, constraint, columna inexistente)
|
|
23
|
+
> se sigue logueando y devolviendo `[]` en los dos modos, igual que siempre.
|
|
24
|
+
>
|
|
25
|
+
> **Sin esa variable no cambia nada**, y por eso puede salir en una menor: a un `^1.x` le entra en
|
|
26
|
+
> cuanto alguien refresque su lock (`npm install`/`npm update`, sin tocar el rango) y ahí se comporta
|
|
27
|
+
> como la 1.31.x. Con `npm ci` el lock manda, así que hasta ese refresco no llega. **Antes de encenderla en un
|
|
28
|
+
> servicio, lee [docs/T-219-auditoria-consumidores.md](./docs/T-219-auditoria-consumidores.md)**: ahí
|
|
29
|
+
> está, repo por repo y con fichero y línea, dónde aterriza ese rechazo hoy y qué hay que cerrar
|
|
30
|
+
> antes. Encenderla sin cerrar esos puntos deja peticiones sin respuesta.
|
|
31
|
+
|
|
19
32
|
### batchQuery
|
|
20
33
|
|
|
21
34
|
Connection with PostgreSQL database in batches. Use for faster query execution when we need to execute many queries.
|
|
@@ -27,6 +27,29 @@ for (const meters of Object.keys(catalog_1.PR_DISTANCE_BADGE)) {
|
|
|
27
27
|
BADGE_PR_DISTANCE[catalog_1.PR_DISTANCE_BADGE[Number(meters)]] = Number(meters);
|
|
28
28
|
}
|
|
29
29
|
const typesList = (types) => types.map((t) => `'${t}'`).join(',');
|
|
30
|
+
/**
|
|
31
|
+
* Traza el fallo de un grupo de logros sin tumbar el resto.
|
|
32
|
+
*
|
|
33
|
+
* Los grupos van aislados a propósito (un fallo no debe impedir evaluar los demás), pero hasta
|
|
34
|
+
* T-219 lo hacían con un `catch (_) {}` mudo: cuando el pool no conectaba, los logros de ese
|
|
35
|
+
* cliente simplemente no se evaluaban y no quedaba ni rastro. Ahora sigue sin tumbar el resto,
|
|
36
|
+
* pero se sabe a quién le pasó y en qué grupo.
|
|
37
|
+
*/
|
|
38
|
+
const avisoLogros = (etiqueta, userId, error) => {
|
|
39
|
+
// eslint-disable-next-line no-console
|
|
40
|
+
console.error(`achievements: fallo evaluando ${etiqueta} de idCliente=${userId} (${(error === null || error === void 0 ? void 0 : error.message) || error}). Ese grupo se queda sin evaluar.`);
|
|
41
|
+
};
|
|
42
|
+
/** Igual que `avisoLogros` pero para lo que NO va aislado: deja la traza y vuelve a lanzar. */
|
|
43
|
+
const conTraza = (etiqueta, userId, fn) => __awaiter(void 0, void 0, void 0, function* () {
|
|
44
|
+
try {
|
|
45
|
+
return yield fn();
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
// eslint-disable-next-line no-console
|
|
49
|
+
console.error(`achievements: fallo leyendo/escribiendo ${etiqueta} de idCliente=${userId} (${(error === null || error === void 0 ? void 0 : error.message) || error}).`);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
30
53
|
const countScalar = (sql, params) => __awaiter(void 0, void 0, void 0, function* () {
|
|
31
54
|
const [row] = yield (0, db_1.query)(sql, params);
|
|
32
55
|
return Number(row === null || row === void 0 ? void 0 : row.c) || 0;
|
|
@@ -49,7 +72,9 @@ const getAllAggregates = (userId, sinceDate) => __awaiter(void 0, void 0, void 0
|
|
|
49
72
|
params.push(sinceDate);
|
|
50
73
|
}
|
|
51
74
|
const runList = typesList(catalog_1.RUNNING_TYPES);
|
|
52
|
-
|
|
75
|
+
// ⚠️ T-219: 9 de los 176 client_login_timeout de la semana medida caen aquí. No va aislado a
|
|
76
|
+
// propósito: sin agregados no hay nada que evaluar, así que se traza y se propaga.
|
|
77
|
+
const [row] = yield conTraza('los agregados de workouts', userId, () => (0, db_1.query)(`SELECT
|
|
53
78
|
COALESCE(MAX("DISTANCE") FILTER (WHERE "TYPE" IN (${runList})), 0) AS "RUN MAX",
|
|
54
79
|
COALESCE(SUM("DISTANCE") FILTER (WHERE "TYPE" IN (${runList})), 0) AS "RUN TOTAL",
|
|
55
80
|
COUNT(*) FILTER (WHERE "TYPE" = 'FUERZA') AS "STR COUNT",
|
|
@@ -62,7 +87,7 @@ const getAllAggregates = (userId, sinceDate) => __awaiter(void 0, void 0, void 0
|
|
|
62
87
|
COUNT(*) FILTER (WHERE "TYPE" = 'COMPETICION') AS "RACE COUNT",
|
|
63
88
|
COUNT(*) FILTER (WHERE "FC MEDIA" IS NOT NULL) AS "HR COUNT"
|
|
64
89
|
FROM "WORKOUT"
|
|
65
|
-
WHERE "ID CLIENTE" = ? AND "DONE" = true${sinceFilter}`, params);
|
|
90
|
+
WHERE "ID CLIENTE" = ? AND "DONE" = true${sinceFilter}`, params));
|
|
66
91
|
return {
|
|
67
92
|
runMax: Number(row === null || row === void 0 ? void 0 : row.runMax) || 0,
|
|
68
93
|
runTotal: Number(row === null || row === void 0 ? void 0 : row.runTotal) || 0,
|
|
@@ -311,10 +336,12 @@ const persistLogros = (userId, ids, markSeen = false) => __awaiter(void 0, void
|
|
|
311
336
|
const params = [];
|
|
312
337
|
for (const id of uniq)
|
|
313
338
|
params.push(userId, id);
|
|
314
|
-
|
|
339
|
+
// ⚠️ T-219: es el alta de los logros. Si esto se pierde en silencio, el cliente no desbloquea
|
|
340
|
+
// nada y no queda rastro; se traza con el idCliente y se propaga.
|
|
341
|
+
const rows = yield conTraza(`el alta de ${uniq.length} logro(s)`, userId, () => (0, db_1.query)(`INSERT INTO "CLIENTE LOGRO" ("ID CLIENTE", "LOGRO ID", "FECHA VISTO")
|
|
315
342
|
VALUES ${valuesSql}
|
|
316
343
|
ON CONFLICT ("ID CLIENTE", "LOGRO ID") DO NOTHING
|
|
317
|
-
RETURNING "LOGRO ID" AS "ID", "FECHA DESBLOQUEO"`, params);
|
|
344
|
+
RETURNING "LOGRO ID" AS "ID", "FECHA DESBLOQUEO"`, params));
|
|
318
345
|
return rows.map((r) => ({ id: r.id, fechaDesbloqueo: r.fechaDesbloqueo }));
|
|
319
346
|
});
|
|
320
347
|
// ── Reglas por categoría ────────────────────────────────────────────────────
|
|
@@ -366,7 +393,9 @@ const evalCount = (rules, count) => rules.filter((r) => count >= r.count).map((r
|
|
|
366
393
|
* los 4 bloques (base/desarrollo/pico/taper). Devuelve null si no hay plan.
|
|
367
394
|
*/
|
|
368
395
|
const getPlanContext = (userId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
369
|
-
|
|
396
|
+
// ⚠️ T-219: va dentro de un grupo aislado (`evaluatePlan`), así que el error lo recoge
|
|
397
|
+
// `avisoLogros` arriba; aquí se deja la traza con el idCliente para saber a quién le pasó.
|
|
398
|
+
const [row] = yield conTraza('el contexto del plan', userId, () => (0, db_1.query)(`SELECT
|
|
370
399
|
"ID PLAN CARRERA" AS "PLAN ID",
|
|
371
400
|
"START DATE PLAN"::date AS "PLAN START",
|
|
372
401
|
"GOAL DATE"::date AS "PLAN GOAL",
|
|
@@ -380,7 +409,7 @@ const getPlanContext = (userId) => __awaiter(void 0, void 0, void 0, function* (
|
|
|
380
409
|
"ALGORITMO BLOQUE 3 FIN"::date AS "PICO FIN",
|
|
381
410
|
"ALGORITMO BLOQUE 4 INICIO"::date AS "TAPER INI",
|
|
382
411
|
"ALGORITMO BLOQUE 4 FIN"::date AS "TAPER FIN"
|
|
383
|
-
FROM "CLIENTE" WHERE "ID" = ?`, [userId]);
|
|
412
|
+
FROM "CLIENTE" WHERE "ID" = ?`, [userId]));
|
|
384
413
|
return row || null;
|
|
385
414
|
});
|
|
386
415
|
/**
|
|
@@ -682,8 +711,8 @@ const evaluateAchievements = (userId, event = 'all', opts = {}) => __awaiter(voi
|
|
|
682
711
|
else
|
|
683
712
|
nuevos.push(...(yield recomputeAllPRs(userId, anchor, markSeen)));
|
|
684
713
|
}
|
|
685
|
-
catch (
|
|
686
|
-
|
|
714
|
+
catch (error) {
|
|
715
|
+
avisoLogros('los récords personales (PR)', userId, error);
|
|
687
716
|
}
|
|
688
717
|
const [streak, agg] = yield Promise.all([
|
|
689
718
|
(0, streak_1.computeStreakStats)(userId, anchor),
|
|
@@ -697,8 +726,8 @@ const evaluateAchievements = (userId, event = 'all', opts = {}) => __awaiter(voi
|
|
|
697
726
|
try {
|
|
698
727
|
yield (0, db_1.query)('UPDATE "CLIENTE" SET "RACHA MAXIMA" = GREATEST(COALESCE("RACHA MAXIMA", 0), ?) WHERE "ID" = ?', [streak.longest, userId]);
|
|
699
728
|
}
|
|
700
|
-
catch (
|
|
701
|
-
|
|
729
|
+
catch (error) {
|
|
730
|
+
avisoLogros('la racha máxima', userId, error);
|
|
702
731
|
}
|
|
703
732
|
eligible.push(...evaluateConsistency(streak.longest), ...evaluateDistance(agg.runMax), ...evaluateVolume(agg.runTotal), ...evaluateStrength(agg.strCount), ...evaluateCycling(agg), ...evaluateSwim(agg), ...evaluateTriSwim(agg), ...evaluateTrail(agg));
|
|
704
733
|
const raceDates = agg.raceCount >= 3 ? yield getRaceDates(userId, anchor) : [];
|
|
@@ -711,31 +740,41 @@ const evaluateAchievements = (userId, event = 'all', opts = {}) => __awaiter(voi
|
|
|
711
740
|
try {
|
|
712
741
|
eligible.push(...(yield evaluatePlan(userId, anchor)));
|
|
713
742
|
}
|
|
714
|
-
catch (
|
|
743
|
+
catch (error) {
|
|
744
|
+
avisoLogros('el plan y sus hitos', userId, error);
|
|
745
|
+
}
|
|
715
746
|
try {
|
|
716
747
|
const strengthDates = yield getStrengthDates(userId, anchor);
|
|
717
748
|
eligible.push(...evaluateStrengthFrequency(strengthDates));
|
|
718
749
|
}
|
|
719
|
-
catch (
|
|
750
|
+
catch (error) {
|
|
751
|
+
avisoLogros('la frecuencia de fuerza', userId, error);
|
|
752
|
+
}
|
|
720
753
|
try {
|
|
721
754
|
eligible.push(...(yield evaluateBrick(userId, anchor)));
|
|
722
755
|
}
|
|
723
|
-
catch (
|
|
756
|
+
catch (error) {
|
|
757
|
+
avisoLogros('los brick', userId, error);
|
|
758
|
+
}
|
|
724
759
|
try {
|
|
725
760
|
eligible.push(...(yield evaluateRecovery(userId, anchor)));
|
|
726
761
|
}
|
|
727
|
-
catch (
|
|
762
|
+
catch (error) {
|
|
763
|
+
avisoLogros('la recuperación', userId, error);
|
|
764
|
+
}
|
|
728
765
|
try {
|
|
729
766
|
eligible.push(...(yield evaluateCoachCall(userId)));
|
|
730
767
|
}
|
|
731
|
-
catch (
|
|
768
|
+
catch (error) {
|
|
769
|
+
avisoLogros('la llamada con el entrenador', userId, error);
|
|
770
|
+
}
|
|
732
771
|
}
|
|
733
772
|
if (want('chat') || want('survey') || want('availability') || want('nutrition') || want('food_photo') || want('wearable')) {
|
|
734
773
|
try {
|
|
735
774
|
eligible.push(...(yield evaluateNonWorkout(userId, event)));
|
|
736
775
|
}
|
|
737
|
-
catch (
|
|
738
|
-
|
|
776
|
+
catch (error) {
|
|
777
|
+
avisoLogros(`los logros de '${event}'`, userId, error);
|
|
739
778
|
}
|
|
740
779
|
}
|
|
741
780
|
// 'goal' (añadir objetivo) re-evalúa los logros de Plan (incl. pc_nuevo_capitulo).
|
|
@@ -744,7 +783,9 @@ const evaluateAchievements = (userId, event = 'all', opts = {}) => __awaiter(voi
|
|
|
744
783
|
try {
|
|
745
784
|
eligible.push(...(yield evaluatePlan(userId, anchor)));
|
|
746
785
|
}
|
|
747
|
-
catch (
|
|
786
|
+
catch (error) {
|
|
787
|
+
avisoLogros('el plan y sus hitos', userId, error);
|
|
788
|
+
}
|
|
748
789
|
}
|
|
749
790
|
nuevos.push(...(yield persistLogros(userId, eligible, markSeen)));
|
|
750
791
|
// pace_*/pr_* se derivan del mapa de PRs ya persistido en los PARAMS de dist_*.
|
|
@@ -753,8 +794,8 @@ const evaluateAchievements = (userId, event = 'all', opts = {}) => __awaiter(voi
|
|
|
753
794
|
const prMap = yield getPrMap(userId);
|
|
754
795
|
nuevos.push(...(yield persistLogros(userId, [...evaluatePace(prMap), ...evaluateTimePR(prMap)])));
|
|
755
796
|
}
|
|
756
|
-
catch (
|
|
757
|
-
|
|
797
|
+
catch (error) {
|
|
798
|
+
avisoLogros('los logros de ritmo y marca', userId, error);
|
|
758
799
|
}
|
|
759
800
|
}
|
|
760
801
|
// Dedup por id, preservando el PRIMERO de cada id. applyPRs se acumula antes que
|
package/lib/cjs/db/index.js
CHANGED
|
@@ -34,31 +34,167 @@ pool.on('error', (err) => {
|
|
|
34
34
|
console.error('PG pool error', err);
|
|
35
35
|
// El pool se encarga automáticamente del manejo
|
|
36
36
|
});
|
|
37
|
-
//
|
|
37
|
+
// Reintento de conexión: mismos números que `queryWithClient` (1 reintento, 500 ms).
|
|
38
|
+
const REINTENTOS_CONEXION = 1;
|
|
39
|
+
const ESPERA_REINTENTO_MS = 500;
|
|
40
|
+
// Códigos y mensajes que NO son un error de SQL sino del pool o del socket. La clase 08 de
|
|
41
|
+
// PostgreSQL es "Connection Exception"; 57P01 es "admin_shutdown"; el resto son errno de Node.
|
|
42
|
+
const CODIGOS_CONEXION = [
|
|
43
|
+
'EPIPE',
|
|
44
|
+
'ECONNRESET',
|
|
45
|
+
'ECONNREFUSED',
|
|
46
|
+
'ETIMEDOUT',
|
|
47
|
+
'ENOTFOUND',
|
|
48
|
+
'EHOSTUNREACH',
|
|
49
|
+
'57P01',
|
|
50
|
+
'08000',
|
|
51
|
+
'08001',
|
|
52
|
+
'08003',
|
|
53
|
+
'08004',
|
|
54
|
+
'08006',
|
|
55
|
+
'08P01',
|
|
56
|
+
];
|
|
57
|
+
// pg y pg-pool no ponen `code` en sus propios timeouts, así que hay que mirar el mensaje.
|
|
58
|
+
// Los cuatro primeros son, literalmente, los que salen en prod-third-party-backend (T-219).
|
|
59
|
+
const MENSAJES_CONEXION = [
|
|
60
|
+
'client_login_timeout',
|
|
61
|
+
'timeout exceeded when trying to connect',
|
|
62
|
+
'connection terminated due to connection timeout',
|
|
63
|
+
'query read timeout',
|
|
64
|
+
'connection terminated unexpectedly',
|
|
65
|
+
'connection ended unexpectedly',
|
|
66
|
+
'client has encountered a connection error',
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
69
|
+
* ¿Este error es "no he podido hablar con la BD" y no "tu SQL está mal"?
|
|
70
|
+
*
|
|
71
|
+
* Sirve para decidir dos cosas: si merece la pena reintentar y si hay que devolver la conexión
|
|
72
|
+
* al pool como rota. Un error de sintaxis o una constraint violada NO entran aquí: reintentarlos
|
|
73
|
+
* es tirar el tiempo y el resultado sería el mismo.
|
|
74
|
+
*/
|
|
75
|
+
const esErrorDeConexion = (error) => {
|
|
76
|
+
if (!error)
|
|
77
|
+
return false;
|
|
78
|
+
if (error.code && CODIGOS_CONEXION.includes(String(error.code)))
|
|
79
|
+
return true;
|
|
80
|
+
const mensaje = String(error.message || '').toLowerCase();
|
|
81
|
+
return MENSAJES_CONEXION.some((m) => mensaje.includes(m));
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Interruptor del contrato de T-219, APAGADO por defecto y a propósito.
|
|
85
|
+
*
|
|
86
|
+
* El rechazo salió primero como 2.0.0 justamente para que `^1.x` no se lo llevara solo. Sale ahora
|
|
87
|
+
* en una menor —que cualquier `^1.x` se lleva en cuanto refresque su lock, y esos repos suben
|
|
88
|
+
* dependencias a menudo— así que la barrera tiene que seguir en pie de otra forma: apagado, la
|
|
89
|
+
* librería se comporta exactamente como la 1.31.x.
|
|
90
|
+
*
|
|
91
|
+
* Cada servicio lo enciende con `RP_DB_RECHAZA_FALLO_CONEXION=1` CUANDO haya cerrado sus puntos
|
|
92
|
+
* de `docs/T-219-auditoria-consumidores.md` (hoy: Cliente-backend 10, Dashboard-backend 6,
|
|
93
|
+
* Cron-backend 3, IA-backend 0, back-office sin auditar).
|
|
94
|
+
*
|
|
95
|
+
* Se lee en cada llamada, no al cargar el módulo: así se puede encender por entorno sin depender
|
|
96
|
+
* del orden de los imports, y las pruebas pueden ir y volver.
|
|
97
|
+
*/
|
|
98
|
+
const rechazaFalloDeConexion = () => process.env.RP_DB_RECHAZA_FALLO_CONEXION === '1';
|
|
99
|
+
const mapearFilas = (result) => ((result === null || result === void 0 ? void 0 : result.rows) || []).map((row) => {
|
|
100
|
+
const json = Object.keys(row);
|
|
101
|
+
const aux = {};
|
|
102
|
+
json.forEach((key) => {
|
|
103
|
+
aux[camelize(key)] = row[key];
|
|
104
|
+
});
|
|
105
|
+
return aux;
|
|
106
|
+
});
|
|
107
|
+
/**
|
|
108
|
+
* Coge una conexión del pool reintentando si el fallo es de conexión/login.
|
|
109
|
+
*
|
|
110
|
+
* Se separa del `client.query` a propósito (T-219): aquí todavía no se ha mandado nada a
|
|
111
|
+
* PostgreSQL, así que reintentar es seguro incluso con un INSERT o un UPDATE. Reintentar la
|
|
112
|
+
* query ya enviada no lo sería —podría duplicar una escritura—, por eso `query()` no lo hace.
|
|
113
|
+
*/
|
|
114
|
+
const conectarConReintento = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
+
for (let intento = 0;; intento++) {
|
|
116
|
+
try {
|
|
117
|
+
return yield pool.connect();
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (intento >= REINTENTOS_CONEXION || !esErrorDeConexion(error))
|
|
121
|
+
throw error;
|
|
122
|
+
// eslint-disable-next-line no-console
|
|
123
|
+
console.error(`PG connect error (intento ${intento + 1}/${REINTENTOS_CONEXION + 1}): ${error === null || error === void 0 ? void 0 : error.message}. Reintentando en ${ESPERA_REINTENTO_MS} ms.`);
|
|
124
|
+
yield sleep(ESPERA_REINTENTO_MS);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
/**
|
|
129
|
+
* ✅ FUNCIÓN PRINCIPAL - queryDirect (recomendada para 90% de casos)
|
|
130
|
+
*
|
|
131
|
+
* ⚠️ CONTRATO (T-219): con `RP_DB_RECHAZA_FALLO_CONEXION=1`, un fallo de CONEXIÓN, agotados los
|
|
132
|
+
* reintentos, **rechaza**. Sin esa variable —el caso por defecto— devuelve `[]` como toda la 1.x.
|
|
133
|
+
* Un error de SQL (sintaxis, constraint, columna que no existe) se sigue logueando y
|
|
134
|
+
* devolviendo `[]` en los dos modos, como siempre.
|
|
135
|
+
*
|
|
136
|
+
* Antes todo devolvía `[]`, así que "la BD no ha conectado" y "no hay filas" eran la misma
|
|
137
|
+
* cosa para quien llamaba. En 7 días eso escondió 176 `client_login_timeout` en
|
|
138
|
+
* prod-third-party-backend: unos pocos reventaban con un TypeError y la mayoría se saltaban
|
|
139
|
+
* trabajo en silencio (entrenos que se quedaron sin estructura, notificaciones que no
|
|
140
|
+
* salieron). Ver docs de T-219.
|
|
141
|
+
*
|
|
142
|
+
* Para el que llama: si la fila puede no existir de verdad, sigue comprobándolo. Lo que ya no
|
|
143
|
+
* hace falta es adivinar si el `[]` era un fallo de BD.
|
|
144
|
+
*
|
|
145
|
+
* El cambio sale publicado en **1.32.0** con el interruptor apagado: la versión puede entrar en
|
|
146
|
+
* cualquier servicio sin avisar y aun así no cambia nada hasta que ese servicio lo enciende. Antes de
|
|
147
|
+
* encenderlo, `docs/T-219-auditoria-consumidores.md` dice, con fichero y línea, dónde cae ese
|
|
148
|
+
* rechazo en cada repo y qué queda por cerrar.
|
|
149
|
+
*/
|
|
38
150
|
const query = (queryText, values = []) => __awaiter(void 0, void 0, void 0, function* () {
|
|
151
|
+
let client;
|
|
152
|
+
try {
|
|
153
|
+
client = yield conectarConReintento();
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
// Agotados los reintentos: se propaga para que el llamante decida, en vez de fingir "0 filas".
|
|
157
|
+
const enhancedError = enhancePostgreSQLError(error, queryText, values);
|
|
158
|
+
// eslint-disable-next-line no-console
|
|
159
|
+
console.error('PG query error', enhancedError);
|
|
160
|
+
if (rechazaFalloDeConexion())
|
|
161
|
+
throw error;
|
|
162
|
+
// Interruptor apagado = contrato de la 1.x: se devuelve []. La traza de arriba es lo único
|
|
163
|
+
// que queda del fallo, y es justo lo que T-219 vino a arreglar: enciéndelo cuando puedas.
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
let clienteRoto = false;
|
|
39
167
|
try {
|
|
40
168
|
const text = getParseQuery(queryText, values);
|
|
41
|
-
const result = (yield
|
|
169
|
+
const result = (yield client.query({
|
|
42
170
|
text,
|
|
43
|
-
// @ts-ignore
|
|
44
171
|
values: values.flat(),
|
|
45
172
|
}));
|
|
46
|
-
return (result
|
|
47
|
-
const json = Object.keys(row);
|
|
48
|
-
const aux = {};
|
|
49
|
-
json.forEach((key) => {
|
|
50
|
-
aux[camelize(key)] = row[key];
|
|
51
|
-
});
|
|
52
|
-
return aux;
|
|
53
|
-
});
|
|
173
|
+
return mapearFilas(result);
|
|
54
174
|
}
|
|
55
175
|
catch (error) {
|
|
56
176
|
// Logging mejorado con información detallada del error
|
|
57
177
|
const enhancedError = enhancePostgreSQLError(error, queryText, values);
|
|
58
178
|
// eslint-disable-next-line no-console
|
|
59
179
|
console.error('PG query error', enhancedError);
|
|
180
|
+
// Se cayó el socket a mitad de la query: la conexión ya no vale y el llamante tiene que
|
|
181
|
+
// enterarse. Un error de SQL, en cambio, mantiene el contrato de siempre y devuelve [].
|
|
182
|
+
// Marcar el cliente como roto NO depende del interruptor: devolver al pool una conexión
|
|
183
|
+
// muerta es un fallo por sí solo, y arreglarlo no cambia lo que ve quien llama.
|
|
184
|
+
clienteRoto = esErrorDeConexion(error);
|
|
185
|
+
if (clienteRoto && rechazaFalloDeConexion())
|
|
186
|
+
throw error;
|
|
60
187
|
return [];
|
|
61
188
|
}
|
|
189
|
+
finally {
|
|
190
|
+
try {
|
|
191
|
+
client.release(clienteRoto);
|
|
192
|
+
}
|
|
193
|
+
catch (releaseError) {
|
|
194
|
+
// eslint-disable-next-line no-console
|
|
195
|
+
console.error('Error releasing client:', releaseError);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
62
198
|
});
|
|
63
199
|
exports.query = query;
|
|
64
200
|
// ✅ Para casos especiales que requieren control manual de conexiones
|
|
@@ -72,14 +208,7 @@ const queryWithClient = (queryText, values = [], _retry = false) => __awaiter(vo
|
|
|
72
208
|
text,
|
|
73
209
|
values: values.flat(),
|
|
74
210
|
}));
|
|
75
|
-
return (result
|
|
76
|
-
const json = Object.keys(row);
|
|
77
|
-
const aux = {};
|
|
78
|
-
json.forEach((key) => {
|
|
79
|
-
aux[camelize(key)] = row[key];
|
|
80
|
-
});
|
|
81
|
-
return aux;
|
|
82
|
-
});
|
|
211
|
+
return mapearFilas(result);
|
|
83
212
|
}
|
|
84
213
|
catch (error) {
|
|
85
214
|
// Logging mejorado con información detallada del error
|
|
@@ -93,7 +222,7 @@ const queryWithClient = (queryText, values = [], _retry = false) => __awaiter(vo
|
|
|
93
222
|
released = true;
|
|
94
223
|
}
|
|
95
224
|
if (!_retry) {
|
|
96
|
-
yield sleep(
|
|
225
|
+
yield sleep(ESPERA_REINTENTO_MS);
|
|
97
226
|
return queryWithClient(queryText, values, true);
|
|
98
227
|
}
|
|
99
228
|
}
|
|
@@ -144,14 +273,7 @@ const longRunningQuery = (queryText, values = [], timeoutMs = 300000 // 5 minuto
|
|
|
144
273
|
// @ts-ignore
|
|
145
274
|
values: values.flat(),
|
|
146
275
|
}));
|
|
147
|
-
return (result
|
|
148
|
-
const json = Object.keys(row);
|
|
149
|
-
const aux = {};
|
|
150
|
-
json.forEach((key) => {
|
|
151
|
-
aux[camelize(key)] = row[key];
|
|
152
|
-
});
|
|
153
|
-
return aux;
|
|
154
|
-
});
|
|
276
|
+
return mapearFilas(result);
|
|
155
277
|
}
|
|
156
278
|
finally {
|
|
157
279
|
client.release();
|
|
@@ -41,20 +41,36 @@ canvas_1.GlobalFonts.registerFromPath(path_1.default.join(__dirname, '../../..',
|
|
|
41
41
|
*/
|
|
42
42
|
const generateShareMap = (image, idWorkout, options = {}) => __awaiter(void 0, void 0, void 0, function* () {
|
|
43
43
|
const useDefaultPhoto = (options === null || options === void 0 ? void 0 : options.useDefaultPhoto) || false;
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
44
|
+
// ⚠️ El guard va ANTES de usar `workout`: `query` devuelve [] cuando el entreno no existe.
|
|
45
|
+
// Leer `workout.idCliente` sin comprobarlo rompía el webhook con TypeError (T-066).
|
|
46
|
+
// Desde T-219 un fallo de CONEXIÓN ya no llega como [] sino como excepción, así que se
|
|
47
|
+
// captura aquí para mantener el contrato de la función (devolver `null`, no lanzar) y se
|
|
48
|
+
// distingue en el log de "el entreno no existe".
|
|
49
|
+
let workout, cliente;
|
|
50
|
+
try {
|
|
51
|
+
[workout] = yield (0, index_1.query)('SELECT [ID], [ID CLIENTE], [TYPE], [DISTANCE], [DURATION], [DESNIVEL] FROM [WORKOUT] WHERE [ID] = ?', [idWorkout]);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.error(`generateShareMap: la BD no respondió al leer el idWorkout=${idWorkout} (${error === null || error === void 0 ? void 0 : error.message}). No se genera la imagen de compartir.`);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
49
58
|
if (!workout) {
|
|
50
59
|
// eslint-disable-next-line no-console
|
|
51
|
-
console.error(`generateShareMap: sin fila de WORKOUT para idWorkout=${idWorkout}; el entreno no existe
|
|
60
|
+
console.error(`generateShareMap: sin fila de WORKOUT para idWorkout=${idWorkout}; el entreno no existe. No se genera la imagen de compartir.`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
[cliente] = yield (0, index_1.query)('SELECT [PREFERRED LANGUAGE] FROM [CLIENTE] WHERE [ID] = ?', [workout.idCliente]);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.error(`generateShareMap: la BD no respondió al leer el idCliente=${workout.idCliente} del idWorkout=${idWorkout} (${error === null || error === void 0 ? void 0 : error.message}). No se genera la imagen de compartir.`);
|
|
52
69
|
return null;
|
|
53
70
|
}
|
|
54
|
-
const [cliente] = yield (0, index_1.query)('SELECT [PREFERRED LANGUAGE] FROM [CLIENTE] WHERE [ID] = ?', [workout.idCliente]);
|
|
55
71
|
if (!cliente) {
|
|
56
72
|
// eslint-disable-next-line no-console
|
|
57
|
-
console.error(`generateShareMap: sin fila de CLIENTE idCliente=${workout.idCliente} del idWorkout=${idWorkout}; el cliente no existe
|
|
73
|
+
console.error(`generateShareMap: sin fila de CLIENTE idCliente=${workout.idCliente} del idWorkout=${idWorkout}; el cliente no existe. No se genera la imagen de compartir.`);
|
|
58
74
|
return null;
|
|
59
75
|
}
|
|
60
76
|
const width = 1080;
|
|
@@ -13,16 +13,30 @@ exports.sendNotification = void 0;
|
|
|
13
13
|
const index_1 = require("../db/index");
|
|
14
14
|
const common_1 = require("@runnerpro/common");
|
|
15
15
|
const sendNotification = ({ firebaseMessaging, idCliente, title, body, screen = common_1.NOTIFICATION_SCREEN_TYPES.HOME, screenParams = {}, }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
// ⚠️ T-219: el INSERT lleva RETURNING y se destructura directo, así que un [] revienta con
|
|
17
|
+
// "TypeError: Cannot read properties of undefined (reading 'id')" —2 veces en la semana
|
|
18
|
+
// medida—. Se traza con el idCliente y se propaga: sin fila no hay notificación que mandar.
|
|
19
|
+
let devices, idNotification;
|
|
20
|
+
try {
|
|
21
|
+
devices = yield (0, index_1.query)('SELECT [SUBSCRIPTION], [TYPE] FROM [PUSH MANAGER] WHERE [ID CLIENTE] = ?', [idCliente]);
|
|
22
|
+
const [fila] = yield (0, index_1.query)('INSERT INTO "CLIENTE NOTIFICACION" ("ID CLIENTE", "TIMESTAMP", "TITLE", "BODY", "PARAMS") VALUES (?, NOW(), ?, ?, ?) RETURNING "ID"', [
|
|
23
|
+
idCliente,
|
|
24
|
+
title !== null && title !== void 0 ? title : '',
|
|
25
|
+
body,
|
|
26
|
+
JSON.stringify({
|
|
27
|
+
screen,
|
|
28
|
+
screenParams,
|
|
29
|
+
}),
|
|
30
|
+
]);
|
|
31
|
+
if (!fila)
|
|
32
|
+
throw new Error('el INSERT de CLIENTE NOTIFICACION no ha devuelto fila');
|
|
33
|
+
idNotification = fila.id;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
// eslint-disable-next-line no-console
|
|
37
|
+
console.error(`sendNotification: no se ha podido registrar la notificación de idCliente=${idCliente} (${error === null || error === void 0 ? void 0 : error.message}). No se envía.`);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
26
40
|
const screenParamsString = JSON.stringify(screenParams);
|
|
27
41
|
for (const device of devices) {
|
|
28
42
|
yield notificationWEB(firebaseMessaging, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/achievements/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/achievements/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AA6L9C,qGAAqG;AAErG,eAAO,MAAM,qBAAqB,YAAa,GAAG,UAAU,GAAG,KAAG,GAuBjE,CAAC;AA0jBF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,oBAAoB,WACvB,GAAG,UACJ,MAAM,SACP,GAAG,KACR,QAAQ,MAAM;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,GAAG,CAAC;IAAC,MAAM,CAAC,EAAE,GAAG,CAAA;CAAE,CAAC,CAyGnE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,WAC9B,GAAG,UACJ,MAAM,SACP,GAAG,yBAER,QAAQ,MAAM;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,GAAG,CAAC;IAAC,MAAM,CAAC,EAAE,GAAG,CAAA;CAAE,CAAC,CAWnE,CAAC;AAEF,uFAAuF;AACvF,eAAO,MAAM,uBAAuB,WAAkB,GAAG,KAAG,QAAQ,MAAM,EAAE,CAO3E,CAAC"}
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import postgresql from 'pg';
|
|
2
2
|
declare const pool: postgresql.Pool;
|
|
3
|
+
/**
|
|
4
|
+
* ✅ FUNCIÓN PRINCIPAL - queryDirect (recomendada para 90% de casos)
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ CONTRATO (T-219): con `RP_DB_RECHAZA_FALLO_CONEXION=1`, un fallo de CONEXIÓN, agotados los
|
|
7
|
+
* reintentos, **rechaza**. Sin esa variable —el caso por defecto— devuelve `[]` como toda la 1.x.
|
|
8
|
+
* Un error de SQL (sintaxis, constraint, columna que no existe) se sigue logueando y
|
|
9
|
+
* devolviendo `[]` en los dos modos, como siempre.
|
|
10
|
+
*
|
|
11
|
+
* Antes todo devolvía `[]`, así que "la BD no ha conectado" y "no hay filas" eran la misma
|
|
12
|
+
* cosa para quien llamaba. En 7 días eso escondió 176 `client_login_timeout` en
|
|
13
|
+
* prod-third-party-backend: unos pocos reventaban con un TypeError y la mayoría se saltaban
|
|
14
|
+
* trabajo en silencio (entrenos que se quedaron sin estructura, notificaciones que no
|
|
15
|
+
* salieron). Ver docs de T-219.
|
|
16
|
+
*
|
|
17
|
+
* Para el que llama: si la fila puede no existir de verdad, sigue comprobándolo. Lo que ya no
|
|
18
|
+
* hace falta es adivinar si el `[]` era un fallo de BD.
|
|
19
|
+
*
|
|
20
|
+
* El cambio sale publicado en **1.32.0** con el interruptor apagado: la versión puede entrar en
|
|
21
|
+
* cualquier servicio sin avisar y aun así no cambia nada hasta que ese servicio lo enciende. Antes de
|
|
22
|
+
* encenderlo, `docs/T-219-auditoria-consumidores.md` dice, con fichero y línea, dónde cae ese
|
|
23
|
+
* rechazo en cada repo y qué queda por cerrar.
|
|
24
|
+
*/
|
|
3
25
|
declare const query: (queryText: string, values?: (string | number | boolean)[]) => Promise<any>;
|
|
4
26
|
declare const queryWithClient: (queryText: string, values?: (string | number | boolean)[], _retry?: boolean) => any;
|
|
5
27
|
declare const batchQuery: (queries: any[], batchSize?: number) => Promise<any[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/db/index.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,IAAI,CAAC;AAI5B,QAAA,MAAM,IAAI,iBAYR,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/db/index.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,IAAI,CAAC;AAI5B,QAAA,MAAM,IAAI,iBAYR,CAAC;AAyGH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,QAAA,MAAM,KAAK,cAAqB,MAAM,WAAU,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,EAAE,iBA+C5E,CAAC;AAGF,QAAA,MAAM,eAAe,cAAqB,MAAM,WAAU,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,EAAE,0BA4CtF,CAAC;AAGF,QAAA,MAAM,UAAU,YAAmB,GAAG,EAAE,uCAmBvC,CAAC;AAGF,QAAA,MAAM,gBAAgB,cACT,MAAM,WACT,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,EAAE,qCAkBtC,CAAC;AAGF,QAAA,MAAM,SAAS,QAAS,MAAM,EAAE,WAM/B,CAAC;AAoJF,OAAO,EACL,KAAK,EAAE,oCAAoC;AAC3C,eAAe,EAAE,wBAAwB;AACzC,gBAAgB,EAAE,uBAAuB;AACzC,UAAU,EAAE,aAAa;AACzB,SAAS,EAAE,WAAW;AACtB,IAAI,GACL,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generateShareMap.d.ts","sourceRoot":"","sources":["../../../../src/image/generateShareMap.ts"],"names":[],"mappings":";AAYA;;;;;;;;;;;;;;;GAeG;AACH,QAAA,MAAM,gBAAgB,+
|
|
1
|
+
{"version":3,"file":"generateShareMap.d.ts","sourceRoot":"","sources":["../../../../src/image/generateShareMap.ts"],"names":[],"mappings":";AAYA;;;;;;;;;;;;;;;GAeG;AACH,QAAA,MAAM,gBAAgB,+DA6CrB,CAAC;AA0HF,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/sendNotification/index.ts"],"names":[],"mappings":"AAEA,UAAU,YAAY;IACpB,iBAAiB,EAAE,GAAG,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,GAAG,CAAC;CACpB;AAED,QAAA,MAAM,gBAAgB,yEAOnB,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/sendNotification/index.ts"],"names":[],"mappings":"AAEA,UAAU,YAAY;IACpB,iBAAiB,EAAE,GAAG,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,GAAG,CAAC;CACpB;AAED,QAAA,MAAM,gBAAgB,yEAOnB,YAAY,kBA0Cd,CAAC;AAuBF,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"estructuraWorkout.d.ts","sourceRoot":"","sources":["../../../../src/workout/estructuraWorkout.ts"],"names":[],"mappings":"AAsCA,QAAA,MAAM,mBAAmB,YAAa,GAAG,cAAc,GAAG,QAiCzD,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,QAAA,MAAM,wBAAwB,cAAqB,GAAG,
|
|
1
|
+
{"version":3,"file":"estructuraWorkout.d.ts","sourceRoot":"","sources":["../../../../src/workout/estructuraWorkout.ts"],"names":[],"mappings":"AAsCA,QAAA,MAAM,mBAAmB,YAAa,GAAG,cAAc,GAAG,QAiCzD,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,QAAA,MAAM,wBAAwB,cAAqB,GAAG,kBAgCrD,CAAC;AAkPF,OAAO,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"saveWorkoutAplication.d.ts","sourceRoot":"","sources":["../../../../src/workout/saveWorkoutAplication.ts"],"names":[],"mappings":"
|
|
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":"saveWorkoutLaps.d.ts","sourceRoot":"","sources":["../../../../src/workout/saveWorkoutLaps.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,QAAA,MAAM,eAAe,cAAqB,GAAG,QAAQ,GAAG,EAAE,mBAAmB,GAAG,KAAG,QAAQ,IAAI,
|
|
1
|
+
{"version":3,"file":"saveWorkoutLaps.d.ts","sourceRoot":"","sources":["../../../../src/workout/saveWorkoutLaps.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,QAAA,MAAM,eAAe,cAAqB,GAAG,QAAQ,GAAG,EAAE,mBAAmB,GAAG,KAAG,QAAQ,IAAI,CAyC9F,CAAC;AAEF,OAAO,EAAE,eAAe,EAAE,CAAC"}
|
|
@@ -85,12 +85,25 @@ exports.getStructuraWorkout = getStructuraWorkout;
|
|
|
85
85
|
* @param {number|string} idWorkout
|
|
86
86
|
*/
|
|
87
87
|
const saveDoneStructuraWorkout = (idWorkout) => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
// ⚠️ T-219: estas 4 lecturas son el punto donde más veces se cae el pool (135 de los 176
|
|
89
|
+
// client_login_timeout de una semana en prod-third-party-backend). Antes el fallo llegaba
|
|
90
|
+
// como `[]` y el `return` de abajo lo confundía con "este entreno no es por series": el
|
|
91
|
+
// entreno se quedaba sin estructura real y nadie se enteraba. Ahora se traza con el
|
|
92
|
+
// idWorkout y se propaga, para que el webhook que lo llamó lo registre.
|
|
93
|
+
let laps, streamData, workout, estructura;
|
|
94
|
+
try {
|
|
95
|
+
[laps, streamData, [workout], estructura] = yield Promise.all([
|
|
96
|
+
(0, db_1.query)('SELECT * FROM "WORKOUT LAP" WHERE "ID WORKOUT" = ? ORDER BY "INDEX"', [idWorkout]),
|
|
97
|
+
(0, db_1.query)('SELECT * FROM "WORKOUT STREAM" WHERE "ID WORKOUT" = ?', [idWorkout]),
|
|
98
|
+
(0, db_1.query)('SELECT "DURATION", "ID CLIENTE", "TYPE" FROM "WORKOUT" WHERE "ID" = ?', [idWorkout]),
|
|
99
|
+
(0, db_1.query)('SELECT * FROM "WORKOUT STRUCTURE" WHERE "ID WORKOUT" = ? ORDER BY "INDEX"', [idWorkout]),
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.error(`saveDoneStructuraWorkout: no se han podido leer los datos del idWorkout=${idWorkout} (${error === null || error === void 0 ? void 0 : error.message}). El entreno se queda SIN estructura real.`);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
94
107
|
// Workout sin estructura (no es entrenamiento por series) → nada que hacer.
|
|
95
108
|
if (!estructura || estructura.length === 0 || !workout)
|
|
96
109
|
return;
|
|
@@ -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
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
// se
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
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
|
|
@@ -391,7 +398,21 @@ const postHooks = (idWorkout, opts) => __awaiter(void 0, void 0, void 0, functio
|
|
|
391
398
|
// La imagen de compartir es accesoria: si falla (Storage, canvas, BD) no debe
|
|
392
399
|
// romper la ingesta ni escapar como unhandled rejection (así llegaba a Sentry).
|
|
393
400
|
}
|
|
394
|
-
|
|
401
|
+
// ⚠️ T-219: aislado como el resto de hooks, y por un motivo concreto. El WORKOUT ya está
|
|
402
|
+
// commiteado y la estructura real es un DERIVADO idempotente que se vuelve a calcular solo
|
|
403
|
+
// (saveWorkoutLaps la re-dispara al llegar los laps, y cualquier merge posterior también).
|
|
404
|
+
// Si el rechazo escapara de aquí subiría por saveWorkoutAplication hasta el llamante y
|
|
405
|
+
// abortaría lo que este hace DESPUÉS —los laps de Strava y la notificación de sentimientos—,
|
|
406
|
+
// que sí son irrecuperables: esos webhooks ya han respondido 200 y nadie los reintenta.
|
|
407
|
+
// Como aquí es donde más se cae el pool (135 de los 176 client_login_timeout), dejarlo
|
|
408
|
+
// propagar convertía el fallo más frecuente en la pérdida más cara. saveDoneStructuraWorkout
|
|
409
|
+
// ya traza el idWorkout antes de lanzar, así que el fallo no se pierde de vista.
|
|
410
|
+
try {
|
|
411
|
+
yield (0, estructuraWorkout_1.saveDoneStructuraWorkout)(idWorkout);
|
|
412
|
+
}
|
|
413
|
+
catch (_) {
|
|
414
|
+
// ignore — trazado dentro; la estructura se recalcula en cuanto lleguen laps o un merge.
|
|
415
|
+
}
|
|
395
416
|
// Logros: evaluar al completar el entreno (este es el embudo de los entrenos
|
|
396
417
|
// sincronizados). Se pasa el idWorkout para calcular el PR solo de ESTE entreno
|
|
397
418
|
// (incremental). Aislado: un fallo de logros NUNCA debe romper la ingesta.
|
|
@@ -429,7 +450,15 @@ const saveMap = (idWorkout, polyline) => __awaiter(void 0, void 0, void 0, funct
|
|
|
429
450
|
// @ts-ignore
|
|
430
451
|
yield storage.bucket(process.env.CLOUD_STORAGE_BUCKET_PUBLIC).file(`Workout/${idWorkout}`).save(imageBuffer);
|
|
431
452
|
const urlMap = `https://storage.googleapis.com/${process.env.CLOUD_STORAGE_BUCKET_PUBLIC}/Workout/${idWorkout}`;
|
|
432
|
-
|
|
453
|
+
try {
|
|
454
|
+
yield (0, db_1.query)('UPDATE "WORKOUT" SET "HAVE MAP IMAGE" = TRUE, "PHOTO URL" = ? WHERE "ID" = ?', [urlMap, idWorkout]);
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
// T-219: la imagen ya está en Storage; si esto falla, el entreno se queda sin su mapa en BD.
|
|
458
|
+
// eslint-disable-next-line no-console
|
|
459
|
+
console.error(`saveMap: mapa subido pero NO guardado en BD para idWorkout=${idWorkout} (${error === null || error === void 0 ? void 0 : error.message}).`);
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
433
462
|
});
|
|
434
463
|
const saveShareWorkoutImage = (id, type) => __awaiter(void 0, void 0, void 0, function* () {
|
|
435
464
|
// @ts-ignore
|
|
@@ -451,5 +480,13 @@ const saveShareWorkoutImage = (id, type) => __awaiter(void 0, void 0, void 0, fu
|
|
|
451
480
|
// @ts-ignore
|
|
452
481
|
yield storage.bucket(process.env.CLOUD_STORAGE_BUCKET_PUBLIC).file(`Workout/${id}-share`).save(shareMap);
|
|
453
482
|
const urlShare = `https://storage.googleapis.com/${process.env.CLOUD_STORAGE_BUCKET_PUBLIC}/Workout/${id}-share`;
|
|
454
|
-
|
|
483
|
+
try {
|
|
484
|
+
yield (0, db_1.query)('UPDATE "WORKOUT" SET "PHOTO URL SHARE" = ? WHERE "ID" = ?', [urlShare, id]);
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
// T-219: la imagen de compartir ya está subida; sin esta URL el usuario no la ve en la app.
|
|
488
|
+
// eslint-disable-next-line no-console
|
|
489
|
+
console.error(`saveShareWorkoutImage: imagen subida pero NO guardada en BD para idWorkout=${id} (${error === null || error === void 0 ? void 0 : error.message}).`);
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
455
492
|
});
|
|
@@ -44,16 +44,34 @@ const saveWorkoutLaps = (idWorkout, laps, aplicationType) => __awaiter(void 0, v
|
|
|
44
44
|
const nuevos = Array.isArray(laps)
|
|
45
45
|
? laps.filter((l) => l && l.index !== null && l.index !== undefined && l.durationStartLap !== null && l.durationStartLap !== undefined)
|
|
46
46
|
: [];
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
yield (0, db_1.query)('
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
// ⚠️ T-219: si el pool se cae en mitad del DELETE + INSERT, los laps quedan a medias. Se traza
|
|
48
|
+
// con el idWorkout y cuántos laps había que escribir, y se propaga para que quede registrado.
|
|
49
|
+
try {
|
|
50
|
+
const [countRow] = yield (0, db_1.query)('SELECT COUNT(*)::int AS "N" FROM "WORKOUT LAP" WHERE "ID WORKOUT" = ?', [idWorkout]);
|
|
51
|
+
const existentes = Number((countRow === null || countRow === void 0 ? void 0 : countRow.n) || 0);
|
|
52
|
+
if (nuevos.length > existentes) {
|
|
53
|
+
yield (0, db_1.query)('DELETE FROM "WORKOUT LAP" WHERE "ID WORKOUT" = ?', [idWorkout]);
|
|
54
|
+
for (const lap of nuevos) {
|
|
55
|
+
yield (0, db_1.query)('INSERT INTO "WORKOUT LAP" ("ID WORKOUT", "INDEX", "DURATION START LAP") VALUES (?, ?, ?)', [idWorkout, lap.index, lap.durationStartLap]);
|
|
56
|
+
}
|
|
53
57
|
}
|
|
54
58
|
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// eslint-disable-next-line no-console
|
|
61
|
+
console.error(`saveWorkoutLaps: fallo guardando los ${nuevos.length} laps del idWorkout=${idWorkout} (${error === null || error === void 0 ? void 0 : error.message}). Pueden haber quedado a medias.`);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
55
64
|
// si nuevos.length <= existentes: se conserva el conjunto existente (más detalle gana)
|
|
56
65
|
// Re-dispara el recálculo de la estructura real ahora que hay laps en BD.
|
|
57
|
-
|
|
66
|
+
// ⚠️ T-219: misma política que en los postHooks de saveWorkoutAplication — la ESCRITURA de
|
|
67
|
+
// laps de arriba propaga (es dato que se pierde), pero este recálculo es un derivado de lo
|
|
68
|
+
// que ya está en BD: si se deja propagar tumba lo que el llamante haga después (en Strava,
|
|
69
|
+
// la notificación de sentimientos) por un fallo que se puede rehacer. Se traza dentro.
|
|
70
|
+
try {
|
|
71
|
+
yield (0, estructuraWorkout_1.saveDoneStructuraWorkout)(idWorkout);
|
|
72
|
+
}
|
|
73
|
+
catch (_) {
|
|
74
|
+
// ignore — trazado dentro; los laps ya están persistidos y el recálculo es rehacible.
|
|
75
|
+
}
|
|
58
76
|
});
|
|
59
77
|
exports.saveWorkoutLaps = saveWorkoutLaps;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runnerpro/backend",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.0",
|
|
4
4
|
"description": "A collection of common backend functions",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./lib/cjs/index.js"
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"lint": "eslint --ext .ts --ignore-path .gitignore .",
|
|
19
19
|
"test:translate-workout-garmin": "ts-node --transpile-only scripts/translateWorkoutGarmin.ts",
|
|
20
20
|
"test:generate-share-map-guard": "ts-node --transpile-only -P configs/tsconfig.cjs.json scripts/testGenerateShareMapGuard.ts",
|
|
21
|
+
"test:pg-query-conexion": "ts-node --transpile-only -P configs/tsconfig.cjs.json scripts/testPgQueryConexion.ts",
|
|
21
22
|
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
|
|
22
23
|
"prepare": "husky"
|
|
23
24
|
},
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
},
|
|
32
33
|
"repository": {
|
|
33
34
|
"type": "git",
|
|
34
|
-
"url": "https://
|
|
35
|
+
"url": "https://github.com/david-jimenez-lamas/runnerpro-backend.git"
|
|
35
36
|
},
|
|
36
37
|
"author": "Runner Pro",
|
|
37
38
|
"license": "MIT",
|
|
@@ -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"}
|