@runnerpro/backend 1.21.4 → 1.21.6
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.
|
@@ -73,7 +73,7 @@ const getConversation = (req, res, { isClient }) => __awaiter(void 0, void 0, vo
|
|
|
73
73
|
"ENTRENADOR"."NAME" AS "ENTRENADOR NAME", "ENTRENADOR"."PHOTO" AS "ENTRENADOR PHOTO",
|
|
74
74
|
(SELECT "NAME" FROM "ENTRENADOR" WHERE "ID" = "CHAT MESSAGE"."ID SENDER") AS "ENTRENADOR REAL NAME"
|
|
75
75
|
FROM "CHAT MESSAGE"
|
|
76
|
-
LEFT JOIN "WORKOUT" ON "WORKOUT"."ID" = "CHAT MESSAGE"."ID WORKOUT"
|
|
76
|
+
LEFT JOIN "WORKOUT" ON "WORKOUT"."ID" = "CHAT MESSAGE"."ID WORKOUT"::integer
|
|
77
77
|
LEFT JOIN "CLIENTE" ON "CLIENTE"."ID" = "CHAT MESSAGE"."ID CLIENTE"
|
|
78
78
|
LEFT JOIN "ENTRENADOR" ON "ENTRENADOR"."ID" = COALESCE("CHAT MESSAGE"."ID SENDER VIEW", "CLIENTE"."ID ENTRENADOR PRINCIPAL")
|
|
79
79
|
AND "CHAT MESSAGE"."ID CLIENTE" != "CHAT MESSAGE"."ID SENDER"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @param opts {{
|
|
3
3
|
* date, type, title, description, distance, duration, power, desnivel, polyline,
|
|
4
|
+
* startAt, // ISO/Date de inicio (UTC) — clave del dedup cross-proveedor
|
|
4
5
|
* idCliente, aplicationType, aplicationId,
|
|
5
6
|
* queryConditionExist?, // LEGACY string SQL — deprecated, fallback
|
|
6
7
|
* tiposEquivalentes?, // string[] de TYPEs equivalentes
|
|
@@ -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":"AAuRA;;;;;;;;;;GAUG;AACH,QAAA,MAAM,qBAAqB,SACnB,GAAG,UAED,GAAG,KACV,QAAQ,MAAM,GAAG,IAAI,CAsEvB,CAAC;AA+DF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
|
|
@@ -147,12 +147,42 @@ const similar = (a, b, tol) => {
|
|
|
147
147
|
return na === nb;
|
|
148
148
|
return Math.abs(na - nb) / Math.max(na, nb) <= tol;
|
|
149
149
|
};
|
|
150
|
+
// Ventana de solape de hora de inicio para el dedup cross-proveedor. Las
|
|
151
|
+
// grabaciones de la MISMA actividad (típicamente un mismo reloj que sincroniza a
|
|
152
|
+
// Strava/Apple/Garmin) empiezan en el mismo instante; 5 min absorben cualquier
|
|
153
|
+
// desfase de redondeo/zona horaria sin abrir falsos positivos (no se inician dos
|
|
154
|
+
// actividades del mismo grupo de tipo en 5 min).
|
|
155
|
+
const VENTANA_INICIO_MS = 5 * 60 * 1000;
|
|
156
|
+
const inicioSolapa = (a, b) => {
|
|
157
|
+
if (!a || !b)
|
|
158
|
+
return false;
|
|
159
|
+
const ta = new Date(a).getTime();
|
|
160
|
+
const tb = new Date(b).getTime();
|
|
161
|
+
if (Number.isNaN(ta) || Number.isNaN(tb))
|
|
162
|
+
return false;
|
|
163
|
+
return Math.abs(ta - tb) <= VENTANA_INICIO_MS;
|
|
164
|
+
};
|
|
150
165
|
const buscarWorkoutSimilar = (q, opts, dateStr, pt) => __awaiter(void 0, void 0, void 0, function* () {
|
|
151
|
-
|
|
166
|
+
// Pre-filtro por ventana de día ±1: el match real es por hora de inicio (no por
|
|
167
|
+
// DATE exacto). Strava deriva DATE de la hora LOCAL y Apple/Garmin de UTC, así
|
|
168
|
+
// que la misma actividad puede caer en días distintos cerca de medianoche; ±1
|
|
169
|
+
// día cubre ese borde sin ensanchar el rango de más.
|
|
170
|
+
const dayPrev = (0, moment_1.default)(dateStr, 'YYYY-MM-DD').subtract(1, 'day').format('YYYY-MM-DD');
|
|
171
|
+
const dayNext = (0, moment_1.default)(dateStr, 'YYYY-MM-DD').add(1, 'day').format('YYYY-MM-DD');
|
|
172
|
+
const rows = yield q(`SELECT "ID", "DURATION", "DISTANCE", "TIPO APLICATION", "START AT"
|
|
152
173
|
FROM "WORKOUT"
|
|
153
|
-
WHERE "ID CLIENTE" = ? AND "DATE"
|
|
154
|
-
AND "DONE" = TRUE AND "TIPO APLICATION" IS NOT NULL AND "TIPO APLICATION" <> ?`, [opts.idCliente,
|
|
155
|
-
const cand = rows.filter((r) =>
|
|
174
|
+
WHERE "ID CLIENTE" = ? AND "DATE" BETWEEN ? AND ? AND ${pt.sql}
|
|
175
|
+
AND "DONE" = TRUE AND "TIPO APLICATION" IS NOT NULL AND "TIPO APLICATION" <> ?`, [opts.idCliente, dayPrev, dayNext, ...pt.params, opts.aplicationType]);
|
|
176
|
+
const cand = rows.filter((r) => {
|
|
177
|
+
// Señal robusta: solape de hora de inicio. Inmune a las unidades de distancia
|
|
178
|
+
// (Apple en millas) y a elapsed-vs-moving en la duración (webhook Strava).
|
|
179
|
+
// Sólo aplica si AMBAS filas tienen "START AT".
|
|
180
|
+
if (opts.startAt && r.startAt)
|
|
181
|
+
return inicioSolapa(r.startAt, opts.startAt);
|
|
182
|
+
// Fallback legacy: filas sin "START AT" (anteriores a esta feature, sin
|
|
183
|
+
// backfill) → se mantiene el heurístico distancia+duración para no regresar.
|
|
184
|
+
return similar(r.duration, opts.duration, 0.1) && similar(r.distance, opts.distance, 0.1);
|
|
185
|
+
});
|
|
156
186
|
cand.sort((a, b) => prioridadFuente(b.tipoAplication) - prioridadFuente(a.tipoAplication));
|
|
157
187
|
return cand[0] || null;
|
|
158
188
|
});
|
|
@@ -193,9 +223,9 @@ const registrarFuente = (q, idWorkout, tipo, idApp) => __awaiter(void 0, void 0,
|
|
|
193
223
|
DO UPDATE SET "ID APLICATION" = EXCLUDED."ID APLICATION", "INGESTED AT" = NOW()`, [idWorkout, String(tipo), idApp !== null && idApp !== undefined ? String(idApp) : null]);
|
|
194
224
|
});
|
|
195
225
|
const aplicarMerge = (q, idWorkout, opts, esFilaNueva) => __awaiter(void 0, void 0, void 0, function* () {
|
|
196
|
-
var _a, _b;
|
|
226
|
+
var _a, _b, _c;
|
|
197
227
|
const [row] = yield q(`SELECT "DISTANCE", "DURATION", "POTENCIA", "DESNIVEL", "CALORIES", "FC MEDIA",
|
|
198
|
-
"ZONE", "POLYLINE", "TIPO APLICATION", "ID APLICATION"
|
|
228
|
+
"ZONE", "POLYLINE", "TIPO APLICATION", "ID APLICATION", "START AT"
|
|
199
229
|
FROM "WORKOUT" WHERE "ID" = ? FOR UPDATE`, [idWorkout]);
|
|
200
230
|
const prioNueva = prioridadFuente(opts.aplicationType);
|
|
201
231
|
const prioActual = esFilaNueva ? -1 : prioridadFuente(row === null || row === void 0 ? void 0 : row.tipoAplication);
|
|
@@ -210,6 +240,9 @@ const aplicarMerge = (q, idWorkout, opts, esFilaNueva) => __awaiter(void 0, void
|
|
|
210
240
|
const zone = (0, paceZone_1.getZone)({ duration, distance, zones: cli });
|
|
211
241
|
// POLYLINE: la primera fuente que la puso manda; nunca se pisa.
|
|
212
242
|
const polyline = row && row.polyline ? row.polyline : getPolyline(opts.polyline);
|
|
243
|
+
// START AT: la primera fuente que lo puso manda; nunca se pisa (igual que
|
|
244
|
+
// POLYLINE). Es la clave del dedup cross-proveedor (solape de hora de inicio).
|
|
245
|
+
const startAt = row && row.startAt ? row.startAt : (_c = opts.startAt) !== null && _c !== void 0 ? _c : null;
|
|
213
246
|
// TIPO/ID APLICATION: sólo se setean en INSERT o si la fila no tenía fuente;
|
|
214
247
|
// nunca se reemplazan (la primera fuente se queda).
|
|
215
248
|
let tipoFinal;
|
|
@@ -225,9 +258,9 @@ const aplicarMerge = (q, idWorkout, opts, esFilaNueva) => __awaiter(void 0, void
|
|
|
225
258
|
const feelingsAsked = (0, moment_1.default)(opts.date).format('YYYY-MM-DD') !== (0, moment_1.default)().format('YYYY-MM-DD');
|
|
226
259
|
yield q(`UPDATE "WORKOUT" SET
|
|
227
260
|
"DISTANCE" = ?, "DURATION" = ?, "POTENCIA" = ?, "DESNIVEL" = ?, "CALORIES" = ?,
|
|
228
|
-
"FC MEDIA" = ?, "ZONE" = ?, "POLYLINE" = ?, "TIPO APLICATION" = ?, "ID APLICATION" = ?,
|
|
261
|
+
"FC MEDIA" = ?, "ZONE" = ?, "POLYLINE" = ?, "START AT" = ?, "TIPO APLICATION" = ?, "ID APLICATION" = ?,
|
|
229
262
|
"FEELINGS ASKED" = ?, "DONE" = TRUE
|
|
230
|
-
WHERE "ID" = ?`, [distance, duration, potencia, desnivel, calories, fcMedia, zone, polyline, tipoFinal, idFinal, feelingsAsked, idWorkout]);
|
|
263
|
+
WHERE "ID" = ?`, [distance, duration, potencia, desnivel, calories, fcMedia, zone, polyline, startAt, tipoFinal, idFinal, feelingsAsked, idWorkout]);
|
|
231
264
|
yield mergeStreams(q, idWorkout, opts.streams);
|
|
232
265
|
});
|
|
233
266
|
// =============================================================================
|
|
@@ -236,6 +269,7 @@ const aplicarMerge = (q, idWorkout, opts, esFilaNueva) => __awaiter(void 0, void
|
|
|
236
269
|
/**
|
|
237
270
|
* @param opts {{
|
|
238
271
|
* date, type, title, description, distance, duration, power, desnivel, polyline,
|
|
272
|
+
* startAt, // ISO/Date de inicio (UTC) — clave del dedup cross-proveedor
|
|
239
273
|
* idCliente, aplicationType, aplicationId,
|
|
240
274
|
* queryConditionExist?, // LEGACY string SQL — deprecated, fallback
|
|
241
275
|
* tiposEquivalentes?, // string[] de TYPEs equivalentes
|
|
@@ -259,6 +293,7 @@ _data) => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
259
293
|
// extra para clientes no-ES con doble proveedor, a cambio de no bloquear el lock.
|
|
260
294
|
const translated = yield (0, titleDescriptionTranslated_1.getTitleDescriptionTranslated)(opts.idCliente, opts.title, opts.description);
|
|
261
295
|
const idWorkout = yield withTx((q) => __awaiter(void 0, void 0, void 0, function* () {
|
|
296
|
+
var _d;
|
|
262
297
|
// Lock anti-carrera: serializa ingestas del mismo cliente/día/bucket-tipo
|
|
263
298
|
// (Strava-webhook y Garmin-push pueden entrar casi a la vez).
|
|
264
299
|
yield q('SELECT pg_advisory_xact_lock(?)', [hashInt(`${opts.idCliente}|${dateStr}|${bucketTipo(opts.type)}`)]);
|
|
@@ -290,8 +325,8 @@ _data) => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
290
325
|
// de abrir la tx (ver `translated` arriba) para no retener el advisory lock.
|
|
291
326
|
const { title: titleNoTranslate, description: descriptionNoTranslate, titlePreferredLanguage, descriptionPreferredLanguage } = translated;
|
|
292
327
|
const inserted = yield q(`INSERT INTO "WORKOUT"
|
|
293
|
-
("ID CLIENTE", "DATE", "TITLE", "DESCRIPTION", "TITLE PREFERRED LANGUAGE", "DESCRIPTION PREFERRED LANGUAGE", "TYPE", "DONE", "SHOW CLIENT")
|
|
294
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, TRUE, TRUE) RETURNING "ID"`, [opts.idCliente, dateStr, titleNoTranslate, descriptionNoTranslate, titlePreferredLanguage, descriptionPreferredLanguage, opts.type]);
|
|
328
|
+
("ID CLIENTE", "DATE", "START AT", "TITLE", "DESCRIPTION", "TITLE PREFERRED LANGUAGE", "DESCRIPTION PREFERRED LANGUAGE", "TYPE", "DONE", "SHOW CLIENT")
|
|
329
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, TRUE, TRUE) RETURNING "ID"`, [opts.idCliente, dateStr, (_d = opts.startAt) !== null && _d !== void 0 ? _d : null, titleNoTranslate, descriptionNoTranslate, titlePreferredLanguage, descriptionPreferredLanguage, opts.type]);
|
|
295
330
|
if (!inserted || !inserted[0])
|
|
296
331
|
return null;
|
|
297
332
|
const id = inserted[0].id;
|