@runnerpro/backend 1.20.6 → 1.21.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.
- package/lib/cjs/index.js +8 -2
- package/lib/cjs/types/index.d.ts +4 -1
- package/lib/cjs/types/index.d.ts.map +1 -1
- package/lib/cjs/types/workout/estructuraWorkout.d.ts +18 -1
- package/lib/cjs/types/workout/estructuraWorkout.d.ts.map +1 -1
- package/lib/cjs/types/workout/paceZone.d.ts +19 -0
- package/lib/cjs/types/workout/paceZone.d.ts.map +1 -0
- package/lib/cjs/types/workout/saveWorkoutAplication.d.ts +11 -15
- package/lib/cjs/types/workout/saveWorkoutAplication.d.ts.map +1 -1
- package/lib/cjs/types/workout/saveWorkoutLaps.d.ts +28 -0
- package/lib/cjs/types/workout/saveWorkoutLaps.d.ts.map +1 -0
- package/lib/cjs/workout/estructuraWorkout.js +222 -14
- package/lib/cjs/workout/paceZone.js +34 -0
- package/lib/cjs/workout/saveWorkoutAplication.js +291 -41
- package/lib/cjs/workout/saveWorkoutLaps.js +58 -0
- package/package.json +1 -1
package/lib/cjs/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.calculateCost = exports.MODEL_PRICING = exports.AISTUDIO_PREFIX = exports.BEDROCK_CLAUDE_HAIKU = exports.BEDROCK_CLAUDE_SONNET_4 = exports.BEDROCK_CLAUDE_SONNET_4_5 = exports.BEDROCK_CLAUDE_OPUS_4_1 = exports.BEDROCK_CLAUDE_OPUS_4_5 = exports.BEDROCK_CLAUDE_SONNET = exports.BEDROCK_CLAUDE_OPUS = exports.FALLBACK_MODELS = exports.PRODUCTION_MODELS = exports.GOOGLE_MODELS = exports.AI_MODELS = exports.MODEL_TIER = exports.runWithModels = void 0;
|
|
3
|
+
exports.createGoogleModel = exports.generateText = exports.generateObject = exports.reprocessRecentMedia = exports.processMediaFile = exports.describeImage = exports.transcribeAudio = exports.updateSenderView = exports.saveResponseTime = exports.getZone = exports.saveDoneStructuraWorkout = exports.saveWorkoutLaps = exports.saveWorkoutAplication = exports.getPlanificacionPrueba7dias = exports.sendWorkoutToWatch = exports.getDefaultWorkoutImage = exports.generateShareMap = exports.reduceSizeImage = exports.getLetter = exports.getNumberByLetter = exports.appendSheet = exports.writeSheet = exports.findCellByValue = exports.readSheet = exports.NOTION_DATABASES_ID = exports.notionEditPage = exports.notionAddPage = exports.notionGetDatabase = exports.notionGetUsers = exports.getCountNotificaciones = exports.chatExposed = exports.chatApi = exports.chat = exports.getExerciseTranslatedDescription = exports.useTranslation = exports.LANGUAGES = exports.translate = exports.CHANNEL_SLACK = exports.notifySlack = exports.fetchIA = exports.err = exports.sendMail = exports.pool = exports.toPgArray = exports.batchQuery = exports.longRunningQuery = exports.queryWithClient = exports.query = exports.sleep = exports.sendNotification = void 0;
|
|
4
|
+
exports.calculateCost = exports.MODEL_PRICING = exports.AISTUDIO_PREFIX = exports.BEDROCK_CLAUDE_HAIKU = exports.BEDROCK_CLAUDE_SONNET_4 = exports.BEDROCK_CLAUDE_SONNET_4_5 = exports.BEDROCK_CLAUDE_OPUS_4_1 = exports.BEDROCK_CLAUDE_OPUS_4_5 = exports.BEDROCK_CLAUDE_SONNET = exports.BEDROCK_CLAUDE_OPUS = exports.FALLBACK_MODELS = exports.PRODUCTION_MODELS = exports.GOOGLE_MODELS = exports.AI_MODELS = exports.MODEL_TIER = exports.runWithModels = exports.runWithCostTracking = exports.createBedrockModelFromString = exports.createGoogleModelFromString = void 0;
|
|
5
5
|
const sendNotification_1 = require("./sendNotification");
|
|
6
6
|
Object.defineProperty(exports, "sendNotification", { enumerable: true, get: function () { return sendNotification_1.sendNotification; } });
|
|
7
7
|
const sleep_1 = require("./sleep");
|
|
@@ -57,6 +57,12 @@ const planificacionPrueba7dias_1 = require("./workout/planificacionPrueba7dias")
|
|
|
57
57
|
Object.defineProperty(exports, "getPlanificacionPrueba7dias", { enumerable: true, get: function () { return planificacionPrueba7dias_1.getPlanificacionPrueba7dias; } });
|
|
58
58
|
const saveWorkoutAplication_1 = require("./workout/saveWorkoutAplication");
|
|
59
59
|
Object.defineProperty(exports, "saveWorkoutAplication", { enumerable: true, get: function () { return saveWorkoutAplication_1.saveWorkoutAplication; } });
|
|
60
|
+
const saveWorkoutLaps_1 = require("./workout/saveWorkoutLaps");
|
|
61
|
+
Object.defineProperty(exports, "saveWorkoutLaps", { enumerable: true, get: function () { return saveWorkoutLaps_1.saveWorkoutLaps; } });
|
|
62
|
+
const estructuraWorkout_1 = require("./workout/estructuraWorkout");
|
|
63
|
+
Object.defineProperty(exports, "saveDoneStructuraWorkout", { enumerable: true, get: function () { return estructuraWorkout_1.saveDoneStructuraWorkout; } });
|
|
64
|
+
const paceZone_1 = require("./workout/paceZone");
|
|
65
|
+
Object.defineProperty(exports, "getZone", { enumerable: true, get: function () { return paceZone_1.getZone; } });
|
|
60
66
|
const saveResponseTime_1 = require("./chat/saveResponseTime");
|
|
61
67
|
Object.defineProperty(exports, "saveResponseTime", { enumerable: true, get: function () { return saveResponseTime_1.saveResponseTime; } });
|
|
62
68
|
const conversation_1 = require("./chat/api/conversation");
|
package/lib/cjs/types/index.d.ts
CHANGED
|
@@ -15,9 +15,12 @@ import { getDefaultWorkoutImage } from './image/getDefaultWorkoutImage';
|
|
|
15
15
|
import { sendWorkoutToWatch } from './workout/sendToWatch';
|
|
16
16
|
import { getPlanificacionPrueba7dias } from './workout/planificacionPrueba7dias';
|
|
17
17
|
import { saveWorkoutAplication } from './workout/saveWorkoutAplication';
|
|
18
|
+
import { saveWorkoutLaps } from './workout/saveWorkoutLaps';
|
|
19
|
+
import { saveDoneStructuraWorkout } from './workout/estructuraWorkout';
|
|
20
|
+
import { getZone } from './workout/paceZone';
|
|
18
21
|
import { saveResponseTime } from './chat/saveResponseTime';
|
|
19
22
|
import { updateSenderView } from './chat/api/conversation';
|
|
20
23
|
import { transcribeAudio, describeImage, processMediaFile, reprocessRecentMedia } from './mediaProcessing';
|
|
21
24
|
import { generateObject, generateText, createGoogleModel, createGoogleModelFromString, createBedrockModelFromString, runWithCostTracking, runWithModels, MODEL_TIER, AI_MODELS, GOOGLE_MODELS, PRODUCTION_MODELS, FALLBACK_MODELS, BEDROCK_CLAUDE_OPUS, BEDROCK_CLAUDE_SONNET, BEDROCK_CLAUDE_OPUS_4_5, BEDROCK_CLAUDE_OPUS_4_1, BEDROCK_CLAUDE_SONNET_4_5, BEDROCK_CLAUDE_SONNET_4, BEDROCK_CLAUDE_HAIKU, AISTUDIO_PREFIX, MODEL_PRICING, calculateCost } from './prompt';
|
|
22
|
-
export { sendNotification, sleep, query, queryWithClient, longRunningQuery, batchQuery, toPgArray, pool, sendMail, err, fetchIA, notifySlack, CHANNEL_SLACK, translate, LANGUAGES, useTranslation, getExerciseTranslatedDescription, chat, chatApi, chatExposed, getCountNotificaciones, notionGetUsers, notionGetDatabase, notionAddPage, notionEditPage, NOTION_DATABASES_ID, readSheet, findCellByValue, writeSheet, appendSheet, getNumberByLetter, getLetter, reduceSizeImage, generateShareMap, getDefaultWorkoutImage, sendWorkoutToWatch, getPlanificacionPrueba7dias, saveWorkoutAplication, saveResponseTime, updateSenderView, transcribeAudio, describeImage, processMediaFile, reprocessRecentMedia, generateObject, generateText, createGoogleModel, createGoogleModelFromString, createBedrockModelFromString, runWithCostTracking, runWithModels, MODEL_TIER, AI_MODELS, GOOGLE_MODELS, PRODUCTION_MODELS, FALLBACK_MODELS, BEDROCK_CLAUDE_OPUS, BEDROCK_CLAUDE_SONNET, BEDROCK_CLAUDE_OPUS_4_5, BEDROCK_CLAUDE_OPUS_4_1, BEDROCK_CLAUDE_SONNET_4_5, BEDROCK_CLAUDE_SONNET_4, BEDROCK_CLAUDE_HAIKU, AISTUDIO_PREFIX, MODEL_PRICING, calculateCost, };
|
|
25
|
+
export { sendNotification, sleep, query, queryWithClient, longRunningQuery, batchQuery, toPgArray, pool, sendMail, err, fetchIA, notifySlack, CHANNEL_SLACK, translate, LANGUAGES, useTranslation, getExerciseTranslatedDescription, chat, chatApi, chatExposed, getCountNotificaciones, notionGetUsers, notionGetDatabase, notionAddPage, notionEditPage, NOTION_DATABASES_ID, readSheet, findCellByValue, writeSheet, appendSheet, getNumberByLetter, getLetter, reduceSizeImage, generateShareMap, getDefaultWorkoutImage, sendWorkoutToWatch, getPlanificacionPrueba7dias, saveWorkoutAplication, saveWorkoutLaps, saveDoneStructuraWorkout, getZone, saveResponseTime, updateSenderView, transcribeAudio, describeImage, processMediaFile, reprocessRecentMedia, generateObject, generateText, createGoogleModel, createGoogleModelFromString, createBedrockModelFromString, runWithCostTracking, runWithModels, MODEL_TIER, AI_MODELS, GOOGLE_MODELS, PRODUCTION_MODELS, FALLBACK_MODELS, BEDROCK_CLAUDE_OPUS, BEDROCK_CLAUDE_SONNET, BEDROCK_CLAUDE_OPUS_4_5, BEDROCK_CLAUDE_OPUS_4_1, BEDROCK_CLAUDE_SONNET_4_5, BEDROCK_CLAUDE_SONNET_4, BEDROCK_CLAUDE_HAIKU, AISTUDIO_PREFIX, MODEL_PRICING, calculateCost, };
|
|
23
26
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
declare const getStructuraWorkout: (workout: any, estructura: any) => any;
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* CANÓNICA — guarda los valores reales por serie en WORKOUT STRUCTURE.
|
|
4
|
+
*
|
|
5
|
+
* Promovida desde `third-party-backend/src/utils/functions/estructuraWorkout.js`
|
|
6
|
+
* (implementación de referencia) para reemplazar las 4 copias divergentes
|
|
7
|
+
* (Strava/Garmin/Apple/lib). Firma unificada `(idWorkout)`, autosuficiente:
|
|
8
|
+
* lee de BD WORKOUT LAP, WORKOUT STREAM, WORKOUT y WORKOUT STRUCTURE.
|
|
9
|
+
*
|
|
10
|
+
* Invariante de no-degradación: `hasWatchData` se evalúa leyendo WORKOUT LAP /
|
|
11
|
+
* WORKOUT STREAM de la BD (no de quién invoca). Por tanto, una vez que un
|
|
12
|
+
* workout tiene laps, cualquier recálculo posterior — incluido el de una fuente
|
|
13
|
+
* sin laps que mergee después (p.ej. Apple) — vuelve a entrar en `...Watch` y
|
|
14
|
+
* NUNCA degrada la estructura real a planificada. Comportamiento monótono:
|
|
15
|
+
* "copia primero (Copy), verdad cuando lleguen los laps (Watch)".
|
|
16
|
+
*
|
|
17
|
+
* @param {number|string} idWorkout
|
|
18
|
+
*/
|
|
19
|
+
declare const saveDoneStructuraWorkout: (idWorkout: any) => Promise<void>;
|
|
3
20
|
export { getStructuraWorkout, saveDoneStructuraWorkout };
|
|
4
21
|
//# sourceMappingURL=estructuraWorkout.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"estructuraWorkout.d.ts","sourceRoot":"","sources":["../../../../src/workout/estructuraWorkout.ts"],"names":[],"mappings":"AAuCA,QAAA,MAAM,mBAAmB,
|
|
1
|
+
{"version":3,"file":"estructuraWorkout.d.ts","sourceRoot":"","sources":["../../../../src/workout/estructuraWorkout.ts"],"names":[],"mappings":"AAuCA,QAAA,MAAM,mBAAmB,YAAa,GAAG,cAAc,GAAG,QAiCzD,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,QAAA,MAAM,wBAAwB,cAAqB,GAAG,kBAkBrD,CAAC;AAuOF,OAAO,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cálculo de la zona de ritmo (1-6) de un workout a partir de su pace y de
|
|
3
|
+
* las pace zones del cliente. Portado de
|
|
4
|
+
* `Cliente-backend/src/utils/functions/aplicaciones/appleHealth.js` para
|
|
5
|
+
* centralizar la lógica en la lib (Fase 1 del plan de dedup cross-proveedor).
|
|
6
|
+
*
|
|
7
|
+
* `zones` viene de un SELECT de las columnas "PACE ZONE 1".."PACE ZONE 6"
|
|
8
|
+
* de la tabla CLIENTE; el helper `query` las cameliza a paceZone1..paceZone6.
|
|
9
|
+
*
|
|
10
|
+
* @param {{ duration: number|null, distance: number|null, zones: any }} input
|
|
11
|
+
* @returns {number} zona 1-6 (1 si no hay datos suficientes)
|
|
12
|
+
*/
|
|
13
|
+
declare const getZone: ({ duration, distance, zones }: {
|
|
14
|
+
duration: any;
|
|
15
|
+
distance: any;
|
|
16
|
+
zones: any;
|
|
17
|
+
}) => number;
|
|
18
|
+
export { getZone };
|
|
19
|
+
//# sourceMappingURL=paceZone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paceZone.d.ts","sourceRoot":"","sources":["../../../../src/workout/paceZone.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,QAAA,MAAM,OAAO;cAA+C,GAAG;cAAY,GAAG;WAAS,GAAG;MAAK,MAkB9F,CAAC;AAEF,OAAO,EAAE,OAAO,EAAE,CAAC"}
|
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
aplicationType: any;
|
|
13
|
-
aplicationId: any;
|
|
14
|
-
queryConditionExist: any;
|
|
15
|
-
}, _data: any) => Promise<any>;
|
|
1
|
+
/**
|
|
2
|
+
* @param opts {{
|
|
3
|
+
* date, type, title, description, distance, duration, power, desnivel, polyline,
|
|
4
|
+
* idCliente, aplicationType, aplicationId,
|
|
5
|
+
* queryConditionExist?, // LEGACY string SQL — deprecated, fallback
|
|
6
|
+
* tiposEquivalentes?, // string[] de TYPEs equivalentes
|
|
7
|
+
* calories?, heartrate?, streams?
|
|
8
|
+
* }}
|
|
9
|
+
* @returns {Promise<number|null>} idWorkout (o null si no se pudo crear) — firma intacta
|
|
10
|
+
*/
|
|
11
|
+
declare const saveWorkoutAplication: (opts: any, _data?: any) => Promise<number | null>;
|
|
16
12
|
export { saveWorkoutAplication };
|
|
17
13
|
//# sourceMappingURL=saveWorkoutAplication.d.ts.map
|
|
@@ -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":"AAsPA;;;;;;;;;GASG;AACH,QAAA,MAAM,qBAAqB,SACnB,GAAG,UAED,GAAG,KACV,QAAQ,MAAM,GAAG,IAAI,CA0DvB,CAAC;AA+DF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistencia centralizada de WORKOUT LAP (Fase 1 plan dedup cross-proveedor).
|
|
3
|
+
*
|
|
4
|
+
* `laps` viene ya normalizado por el adaptador del proveedor:
|
|
5
|
+
* [{ index: number, durationStartLap: number }]
|
|
6
|
+
*
|
|
7
|
+
* Política de merge cross-proveedor (coherente con `mergeStreams`):
|
|
8
|
+
* gana el conjunto con MÁS laps. Tras persistir, re-dispara
|
|
9
|
+
* `saveDoneStructuraWorkout(idWorkout)` para recalcular la estructura real
|
|
10
|
+
* ahora que ya hay laps en BD (rama `...Watch`).
|
|
11
|
+
*
|
|
12
|
+
* Es función pública aparte (no entra en la firma de `saveWorkoutAplication`)
|
|
13
|
+
* porque Garmin entrega los laps en el webhook `/files`, desacoplado del
|
|
14
|
+
* `/details` que crea el workout.
|
|
15
|
+
*
|
|
16
|
+
* NOTA (// TODO): no se persiste la fuente que escribió los laps, así que el
|
|
17
|
+
* desempate por prioridad de fuente con igual nº de laps no es posible sin
|
|
18
|
+
* estado extra; se usa estrictamente "más laps gana" (empate → se conserva lo
|
|
19
|
+
* existente, sin churn). El parámetro `aplicationType` se acepta para futura
|
|
20
|
+
* trazabilidad y coherencia de firma con el resto de la lib.
|
|
21
|
+
*
|
|
22
|
+
* @param {number|string} idWorkout
|
|
23
|
+
* @param {{index:number,durationStartLap:number}[]} laps
|
|
24
|
+
* @param {number} aplicationType TIPO_APLICACION.* (reservado para trazabilidad)
|
|
25
|
+
*/
|
|
26
|
+
declare const saveWorkoutLaps: (idWorkout: any, laps: any[], aplicationType?: any) => Promise<void>;
|
|
27
|
+
export { saveWorkoutLaps };
|
|
28
|
+
//# sourceMappingURL=saveWorkoutLaps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saveWorkoutLaps.d.ts","sourceRoot":"","sources":["../../../../src/workout/saveWorkoutLaps.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,QAAA,MAAM,eAAe,cAAqB,GAAG,QAAQ,GAAG,EAAE,mBAAmB,GAAG,KAAG,QAAQ,IAAI,CAuB9F,CAAC;AAEF,OAAO,EAAE,eAAe,EAAE,CAAC"}
|
|
@@ -11,6 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.saveDoneStructuraWorkout = exports.getStructuraWorkout = void 0;
|
|
13
13
|
const db_1 = require("../db");
|
|
14
|
+
const sendMail_1 = require("../sendMail");
|
|
14
15
|
const common_1 = require("@runnerpro/common");
|
|
15
16
|
const calculateZone = (values) => {
|
|
16
17
|
const percentage = Number(values.pacePercentage);
|
|
@@ -67,38 +68,245 @@ const getStructuraWorkout = (workout, estructura) => {
|
|
|
67
68
|
}
|
|
68
69
|
};
|
|
69
70
|
exports.getStructuraWorkout = getStructuraWorkout;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
/**
|
|
72
|
+
* CANÓNICA — guarda los valores reales por serie en WORKOUT STRUCTURE.
|
|
73
|
+
*
|
|
74
|
+
* Promovida desde `third-party-backend/src/utils/functions/estructuraWorkout.js`
|
|
75
|
+
* (implementación de referencia) para reemplazar las 4 copias divergentes
|
|
76
|
+
* (Strava/Garmin/Apple/lib). Firma unificada `(idWorkout)`, autosuficiente:
|
|
77
|
+
* lee de BD WORKOUT LAP, WORKOUT STREAM, WORKOUT y WORKOUT STRUCTURE.
|
|
78
|
+
*
|
|
79
|
+
* Invariante de no-degradación: `hasWatchData` se evalúa leyendo WORKOUT LAP /
|
|
80
|
+
* WORKOUT STREAM de la BD (no de quién invoca). Por tanto, una vez que un
|
|
81
|
+
* workout tiene laps, cualquier recálculo posterior — incluido el de una fuente
|
|
82
|
+
* sin laps que mergee después (p.ej. Apple) — vuelve a entrar en `...Watch` y
|
|
83
|
+
* NUNCA degrada la estructura real a planificada. Comportamiento monótono:
|
|
84
|
+
* "copia primero (Copy), verdad cuando lleguen los laps (Watch)".
|
|
85
|
+
*
|
|
86
|
+
* @param {number|string} idWorkout
|
|
87
|
+
*/
|
|
88
|
+
const saveDoneStructuraWorkout = (idWorkout) => __awaiter(void 0, void 0, void 0, function* () {
|
|
89
|
+
const [laps, streamData, [workout], estructura] = yield Promise.all([
|
|
90
|
+
(0, db_1.query)('SELECT * FROM "WORKOUT LAP" WHERE "ID WORKOUT" = ? ORDER BY "INDEX"', [idWorkout]),
|
|
91
|
+
(0, db_1.query)('SELECT * FROM "WORKOUT STREAM" WHERE "ID WORKOUT" = ?', [idWorkout]),
|
|
92
|
+
(0, db_1.query)('SELECT "DURATION", "ID CLIENTE", "TYPE" FROM "WORKOUT" WHERE "ID" = ?', [idWorkout]),
|
|
93
|
+
(0, db_1.query)('SELECT * FROM "WORKOUT STRUCTURE" WHERE "ID WORKOUT" = ? ORDER BY "INDEX"', [idWorkout]),
|
|
94
|
+
]);
|
|
95
|
+
// Workout sin estructura (no es entrenamiento por series) → nada que hacer.
|
|
96
|
+
if (!estructura || estructura.length === 0 || !workout)
|
|
97
|
+
return;
|
|
98
|
+
const hasWatchData = laps.length > 0 && streamData.length > 0;
|
|
99
|
+
if (hasWatchData) {
|
|
100
|
+
yield saveDoneStructuraWorkoutWatch(idWorkout, streamData, laps, workout, estructura);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
yield saveDoneStructuraWorkoutCopy(idWorkout, workout, estructura);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
exports.saveDoneStructuraWorkout = saveDoneStructuraWorkout;
|
|
107
|
+
const saveDoneStructuraWorkoutWatch = (idWorkout, streamData, laps, workout, estructura) => __awaiter(void 0, void 0, void 0, function* () {
|
|
108
|
+
const streams = {};
|
|
109
|
+
streamData.forEach((item) => {
|
|
110
|
+
try {
|
|
111
|
+
streams[item.metricType] = typeof item.data === 'string' ? JSON.parse(item.data) : item.data;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
streams[item.metricType] = item.data;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
const numPasos = estructura.length;
|
|
118
|
+
const numLaps = laps.length;
|
|
119
|
+
if (numPasos !== numLaps && numPasos !== numLaps + 1) {
|
|
120
|
+
return (0, sendMail_1.sendMail)({
|
|
121
|
+
title: 'Estructura workout: laps no coinciden con pasos',
|
|
122
|
+
subject: 'Dedup cross-proveedor - Error saving steps structure workout',
|
|
123
|
+
body: JSON.stringify({ idWorkout, laps, estructura }),
|
|
124
|
+
to: ['david.jimenez@runnerpro.app'],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
for (let i = 0; i < estructura.length; i++) {
|
|
128
|
+
const row = estructura[i];
|
|
74
129
|
let distance = 0;
|
|
75
130
|
let duration = 0;
|
|
76
131
|
let distancePlanned = 0;
|
|
77
132
|
let durationPlanned = 0;
|
|
78
|
-
|
|
133
|
+
let fcMedia = null;
|
|
134
|
+
const pacePlanned = row.valueMetric2;
|
|
135
|
+
if (Number(row.typeMetric1) === 1) {
|
|
136
|
+
distancePlanned = row.valueMetric1;
|
|
137
|
+
durationPlanned = (0, common_1.getWorkoutParamsByType)(workout.type).calculateDuration({ distance: row.valueMetric1, pace: pacePlanned });
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
durationPlanned = row.valueMetric1;
|
|
141
|
+
if (Number(row.type) === 3) {
|
|
142
|
+
distancePlanned = null;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
distancePlanned = (0, common_1.getWorkoutParamsByType)(workout.type).calculateDistance({ duration: row.valueMetric1, pace: pacePlanned, decimals: 2 });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
let lapIndex = null;
|
|
149
|
+
if (numPasos === numLaps + 1) {
|
|
150
|
+
if (i < numLaps)
|
|
151
|
+
lapIndex = i;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
lapIndex = i;
|
|
155
|
+
}
|
|
156
|
+
if (lapIndex !== null && lapIndex >= 0 && lapIndex < laps.length) {
|
|
157
|
+
const currentLap = laps[lapIndex];
|
|
158
|
+
const nextLap = laps[lapIndex + 1];
|
|
159
|
+
if (nextLap) {
|
|
160
|
+
duration = nextLap.durationStartLap - currentLap.durationStartLap;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
duration = workout.duration - currentLap.durationStartLap;
|
|
164
|
+
}
|
|
165
|
+
if (Number(row.typeMetric1) === 2 && Math.abs(duration - Number(durationPlanned)) < 10) {
|
|
166
|
+
duration = Number(durationPlanned);
|
|
167
|
+
}
|
|
168
|
+
const timeStream = streams.time || [];
|
|
169
|
+
const distanceStream = streams.distance || [];
|
|
170
|
+
if (timeStream.length > 0 && distanceStream.length > 0) {
|
|
171
|
+
const startTime = currentLap.durationStartLap;
|
|
172
|
+
const endTime = nextLap ? nextLap.durationStartLap : workout.duration;
|
|
173
|
+
let startIndex = 0;
|
|
174
|
+
let endIndex = timeStream.length - 1;
|
|
175
|
+
for (let j = 0; j < timeStream.length; j++) {
|
|
176
|
+
if (timeStream[j] >= startTime && startIndex === 0)
|
|
177
|
+
startIndex = j;
|
|
178
|
+
if (timeStream[j] >= endTime) {
|
|
179
|
+
endIndex = j;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const startDistance = distanceStream[startIndex] || 0;
|
|
184
|
+
const endDistance = distanceStream[endIndex] || 0;
|
|
185
|
+
distance = Math.round(Math.max(0, endDistance - startDistance));
|
|
186
|
+
if (Number(row.typeMetric1) === 1 && Math.abs(distance - Number(distancePlanned)) < 20) {
|
|
187
|
+
distance = Number(distancePlanned);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const heartrateStream = streams.heartrate || [];
|
|
191
|
+
if (heartrateStream.length > 0) {
|
|
192
|
+
const startTime = currentLap.durationStartLap;
|
|
193
|
+
const endTime = nextLap ? nextLap.durationStartLap : workout.duration;
|
|
194
|
+
let startIndex = 0;
|
|
195
|
+
let endIndex = timeStream.length - 1;
|
|
196
|
+
for (let j = 0; j < timeStream.length; j++) {
|
|
197
|
+
if (timeStream[j] >= startTime && startIndex === 0)
|
|
198
|
+
startIndex = j;
|
|
199
|
+
if (timeStream[j] >= endTime) {
|
|
200
|
+
endIndex = j;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const heartrateValues = heartrateStream
|
|
205
|
+
.slice(startIndex, endIndex + 1)
|
|
206
|
+
.filter((hr) => hr !== null && hr !== undefined && !isNaN(hr));
|
|
207
|
+
if (heartrateValues.length > 0) {
|
|
208
|
+
const sum = heartrateValues.reduce((acc, hr) => acc + Number(hr), 0);
|
|
209
|
+
fcMedia = Math.round(sum / heartrateValues.length);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else if (numPasos === numLaps + 1 && i === numLaps) {
|
|
214
|
+
const lastLap = laps[laps.length - 1];
|
|
215
|
+
duration = workout.duration - lastLap.durationStartLap;
|
|
216
|
+
if (Number(row.typeMetric1) === 2 && Math.abs(duration - Number(durationPlanned)) < 10) {
|
|
217
|
+
duration = Number(durationPlanned);
|
|
218
|
+
}
|
|
219
|
+
const timeStream = streams.time || [];
|
|
220
|
+
const distanceStream = streams.distance || [];
|
|
221
|
+
if (timeStream.length > 0 && distanceStream.length > 0) {
|
|
222
|
+
const startTime = lastLap.durationStartLap;
|
|
223
|
+
const endTime = workout.duration;
|
|
224
|
+
let startIndex = 0;
|
|
225
|
+
let endIndex = timeStream.length - 1;
|
|
226
|
+
for (let j = 0; j < timeStream.length; j++) {
|
|
227
|
+
if (timeStream[j] >= startTime && startIndex === 0)
|
|
228
|
+
startIndex = j;
|
|
229
|
+
if (timeStream[j] >= endTime) {
|
|
230
|
+
endIndex = j;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const startDistance = distanceStream[startIndex] || 0;
|
|
235
|
+
const endDistance = distanceStream[endIndex] || 0;
|
|
236
|
+
distance = Math.round(Math.max(0, endDistance - startDistance));
|
|
237
|
+
if (Number(row.typeMetric1) === 1 && Math.abs(distance - Number(distancePlanned)) < 20) {
|
|
238
|
+
distance = Number(distancePlanned);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const heartrateStream = streams.heartrate || [];
|
|
242
|
+
if (heartrateStream.length > 0) {
|
|
243
|
+
const startTime = lastLap.durationStartLap;
|
|
244
|
+
const endTime = workout.duration;
|
|
245
|
+
const timeStreamForHR = streams.time || [];
|
|
246
|
+
let startIndex = 0;
|
|
247
|
+
let endIndex = timeStreamForHR.length - 1;
|
|
248
|
+
for (let j = 0; j < timeStreamForHR.length; j++) {
|
|
249
|
+
if (timeStreamForHR[j] >= startTime && startIndex === 0)
|
|
250
|
+
startIndex = j;
|
|
251
|
+
if (timeStreamForHR[j] >= endTime) {
|
|
252
|
+
endIndex = j;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const heartrateValues = heartrateStream
|
|
257
|
+
.slice(startIndex, endIndex + 1)
|
|
258
|
+
.filter((hr) => hr !== null && hr !== undefined && !isNaN(hr));
|
|
259
|
+
if (heartrateValues.length > 0) {
|
|
260
|
+
const sum = heartrateValues.reduce((acc, hr) => acc + Number(hr), 0);
|
|
261
|
+
fcMedia = Math.round(sum / heartrateValues.length);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
const pace = row.valueMetric2;
|
|
267
|
+
if (Number(row.typeMetric1) === 1) {
|
|
268
|
+
distance = row.valueMetric1;
|
|
269
|
+
duration = (0, common_1.getWorkoutParamsByType)(workout.type).calculateDuration({ distance: row.valueMetric1, pace });
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
duration = row.valueMetric1;
|
|
273
|
+
if (Number(row.type) === 3) {
|
|
274
|
+
distance = null;
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
distance = (0, common_1.getWorkoutParamsByType)(workout.type).calculateDistance({ duration: row.valueMetric1, pace, decimals: 2 });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
yield (0, db_1.query)('UPDATE "WORKOUT STRUCTURE" SET "DISTANCE PLANNED" = ?, "DISTANCE" = ?, "DURATION PLANNED" = ?, "DURATION" = ?, "FC MEDIA" = ?, "DESNIVEL" = ? WHERE "INDEX" = ? AND "ID WORKOUT" = ?', [distancePlanned, distance, durationPlanned, duration, fcMedia, row.desnivelPlanned, row.index, idWorkout]);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
const saveDoneStructuraWorkoutCopy = (idWorkout, workout, estructura) => __awaiter(void 0, void 0, void 0, function* () {
|
|
285
|
+
for (const row of estructura) {
|
|
286
|
+
let distance = 0;
|
|
287
|
+
let duration = 0;
|
|
288
|
+
let distancePlanned = 0;
|
|
289
|
+
let durationPlanned = 0;
|
|
290
|
+
const pace = row.valueMetric2;
|
|
79
291
|
const pacePlanned = row.valueMetric2;
|
|
80
292
|
if (Number(row.typeMetric1) === 1) {
|
|
81
293
|
distancePlanned = row.valueMetric1;
|
|
82
294
|
distance = row.valueMetric1;
|
|
83
|
-
durationPlanned = (0, common_1.getWorkoutParamsByType)(
|
|
84
|
-
duration = (0, common_1.getWorkoutParamsByType)(
|
|
295
|
+
durationPlanned = (0, common_1.getWorkoutParamsByType)(workout.type).calculateDuration({ distance: row.valueMetric1, pace: pacePlanned });
|
|
296
|
+
duration = (0, common_1.getWorkoutParamsByType)(workout.type).calculateDuration({ distance: row.valueMetric1, pace });
|
|
85
297
|
}
|
|
86
298
|
else {
|
|
87
299
|
durationPlanned = row.valueMetric1;
|
|
88
300
|
duration = row.valueMetric1;
|
|
89
301
|
if (Number(row.type) === 3) {
|
|
90
|
-
// @ts-ignore
|
|
91
302
|
distancePlanned = null;
|
|
92
|
-
// @ts-ignore
|
|
93
303
|
distance = null;
|
|
94
304
|
}
|
|
95
305
|
else {
|
|
96
|
-
distancePlanned = (0, common_1.getWorkoutParamsByType)(
|
|
97
|
-
distance = (0, common_1.getWorkoutParamsByType)(
|
|
306
|
+
distancePlanned = (0, common_1.getWorkoutParamsByType)(workout.type).calculateDistance({ duration: row.valueMetric1, pace: pacePlanned, decimals: 2 });
|
|
307
|
+
distance = (0, common_1.getWorkoutParamsByType)(workout.type).calculateDistance({ duration: row.valueMetric1, pace, decimals: 2 });
|
|
98
308
|
}
|
|
99
309
|
}
|
|
100
|
-
|
|
101
|
-
yield (0, db_1.query)('UPDATE [WORKOUT STRUCTURE] SET [DISTANCE PLANNED] = ?, [DISTANCE] = ?, [DURATION PLANNED] = ?, [DURATION] = ?, [DESNIVEL] = ? WHERE [INDEX] = ? AND [ID WORKOUT] = ?', [distancePlanned, distance, durationPlanned, duration, row.desnivelPlanned, row.index, idWorkout]);
|
|
310
|
+
yield (0, db_1.query)('UPDATE "WORKOUT STRUCTURE" SET "DISTANCE PLANNED" = ?, "DISTANCE" = ?, "DURATION PLANNED" = ?, "DURATION" = ?, "FC MEDIA" = ?, "DESNIVEL" = ? WHERE "INDEX" = ? AND "ID WORKOUT" = ?', [distancePlanned, distance, durationPlanned, duration, null, row.desnivelPlanned, row.index, idWorkout]);
|
|
102
311
|
}
|
|
103
312
|
});
|
|
104
|
-
exports.saveDoneStructuraWorkout = saveDoneStructuraWorkout;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getZone = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Cálculo de la zona de ritmo (1-6) de un workout a partir de su pace y de
|
|
6
|
+
* las pace zones del cliente. Portado de
|
|
7
|
+
* `Cliente-backend/src/utils/functions/aplicaciones/appleHealth.js` para
|
|
8
|
+
* centralizar la lógica en la lib (Fase 1 del plan de dedup cross-proveedor).
|
|
9
|
+
*
|
|
10
|
+
* `zones` viene de un SELECT de las columnas "PACE ZONE 1".."PACE ZONE 6"
|
|
11
|
+
* de la tabla CLIENTE; el helper `query` las cameliza a paceZone1..paceZone6.
|
|
12
|
+
*
|
|
13
|
+
* @param {{ duration: number|null, distance: number|null, zones: any }} input
|
|
14
|
+
* @returns {number} zona 1-6 (1 si no hay datos suficientes)
|
|
15
|
+
*/
|
|
16
|
+
const getZone = ({ duration, distance, zones }) => {
|
|
17
|
+
if (!distance || !duration || !zones)
|
|
18
|
+
return 1;
|
|
19
|
+
const pace = Number(duration) / (Number(distance) / 1000); // s/km
|
|
20
|
+
let closestZone = 1;
|
|
21
|
+
let minDifference = Infinity;
|
|
22
|
+
for (let i = 1; i <= 6; i += 1) {
|
|
23
|
+
const zonePace = zones[`paceZone${i}`];
|
|
24
|
+
if (zonePace !== null && zonePace !== undefined && !Number.isNaN(Number(zonePace))) {
|
|
25
|
+
const diff = Math.abs(pace - Number(zonePace));
|
|
26
|
+
if (diff < minDifference) {
|
|
27
|
+
minDifference = diff;
|
|
28
|
+
closestZone = i;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return closestZone;
|
|
33
|
+
};
|
|
34
|
+
exports.getZone = getZone;
|
|
@@ -22,53 +22,301 @@ const generateShareMap_1 = require("../image/generateShareMap");
|
|
|
22
22
|
const getDefaultWorkoutImage_1 = require("../image/getDefaultWorkoutImage");
|
|
23
23
|
const moment_1 = __importDefault(require("moment"));
|
|
24
24
|
const estructuraWorkout_1 = require("./estructuraWorkout");
|
|
25
|
-
const
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
25
|
+
const paceZone_1 = require("./paceZone");
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// Prioridad de fuentes (Fase 1 plan dedup cross-proveedor)
|
|
28
|
+
// Mayor = más fiable. No se mete en @runnerpro/common para no propagar una
|
|
29
|
+
// constante de esta feature a todos los consumidores de la lib.
|
|
30
|
+
// =============================================================================
|
|
31
|
+
const APLICACION_PRIORITY = { 2: 3 /*GARMIN*/, 1: 2 /*STRAVA*/, 5: 1 /*APPLE_HEALTH*/ };
|
|
32
|
+
const prioridadFuente = (t) => { var _a; return (_a = APLICACION_PRIORITY[Number(t)]) !== null && _a !== void 0 ? _a : 0; };
|
|
33
|
+
// Grupo de tipos equivalentes para el advisory lock: serializa sólo las
|
|
34
|
+
// ingestas que PODRÍAN matchear entre sí (CORRER/TRAIL/COMPETICION colisionan).
|
|
35
|
+
const bucketTipo = (type) => {
|
|
36
|
+
if (['CORRER', 'TRAIL', 'COMPETICION'].includes(String(type)))
|
|
37
|
+
return 'CORRER_GROUP';
|
|
38
|
+
return String(type || 'UNKNOWN');
|
|
39
|
+
};
|
|
40
|
+
// Hash determinista string → int32 (clave de pg_advisory_xact_lock).
|
|
41
|
+
const hashInt = (s) => {
|
|
42
|
+
let h = 0;
|
|
43
|
+
for (let i = 0; i < s.length; i++) {
|
|
44
|
+
h = (Math.imul(h, 31) + s.charCodeAt(i)) | 0;
|
|
45
|
+
}
|
|
46
|
+
return h;
|
|
47
|
+
};
|
|
48
|
+
// Predicado de tipo parametrizado (resuelve el Unknown queryConditionExist).
|
|
49
|
+
// Sin interpolación de SQL crudo salvo el fallback legacy deprecated.
|
|
50
|
+
const predicadoTipo = (opts) => {
|
|
51
|
+
if (Array.isArray(opts.tiposEquivalentes) && opts.tiposEquivalentes.length > 0) {
|
|
52
|
+
return { sql: '"TYPE" IN (?)', params: [opts.tiposEquivalentes] };
|
|
53
|
+
}
|
|
54
|
+
if (opts.queryConditionExist) {
|
|
55
|
+
// LEGACY deprecated: string SQL ya interpolado por el adaptador. Se elimina
|
|
56
|
+
// cuando los 4 repos migren a `tiposEquivalentes` (cierre Fase 4).
|
|
57
|
+
return { sql: opts.queryConditionExist, params: [] };
|
|
58
|
+
}
|
|
59
|
+
return { sql: '"TYPE" = ?', params: [opts.type] };
|
|
60
|
+
};
|
|
61
|
+
// =============================================================================
|
|
62
|
+
// Helper de transacción (la lib no exporta uno; replica el patrón de
|
|
63
|
+
// Cliente-backend/src/utils/functions/workoutAlternatives.js adaptado al
|
|
64
|
+
// cliente pg de la lib, reusando la misma conversión de placeholders que
|
|
65
|
+
// `db.getParseQuery` para mantener la convención `[ ]`/`?`/arrays).
|
|
66
|
+
// =============================================================================
|
|
67
|
+
function parseTx(queryText, values) {
|
|
68
|
+
queryText = queryText.split('[').join('"').split(']').join('"');
|
|
69
|
+
const splitted = queryText.split('?');
|
|
70
|
+
let count = 1;
|
|
71
|
+
return splitted.reduce((acc, chunk, i) => {
|
|
72
|
+
if (!(values[i] instanceof Array)) {
|
|
73
|
+
const text = `${acc}${chunk}${i + 1 === splitted.length ? '' : `$${count}`}`;
|
|
74
|
+
count++;
|
|
75
|
+
return text;
|
|
76
|
+
}
|
|
77
|
+
const aux = [];
|
|
78
|
+
values[i].forEach(() => {
|
|
79
|
+
aux.push(`$${count}`);
|
|
80
|
+
count++;
|
|
81
|
+
});
|
|
82
|
+
return `${acc}${chunk}${i + 1 === splitted.length ? '' : aux.join(',')}`;
|
|
83
|
+
}, '');
|
|
84
|
+
}
|
|
85
|
+
function camelize(str) {
|
|
86
|
+
return str
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => (index === 0 ? word.toLowerCase() : word.toUpperCase()))
|
|
89
|
+
.replace(/\s+/g, '');
|
|
90
|
+
}
|
|
91
|
+
function camelizeRow(row) {
|
|
92
|
+
const aux = {};
|
|
93
|
+
Object.keys(row).forEach((key) => {
|
|
94
|
+
aux[camelize(key)] = row[key];
|
|
95
|
+
});
|
|
96
|
+
return aux;
|
|
97
|
+
}
|
|
98
|
+
const withTx = (fn) => __awaiter(void 0, void 0, void 0, function* () {
|
|
99
|
+
const client = yield db_1.pool.connect();
|
|
100
|
+
try {
|
|
101
|
+
yield client.query('BEGIN');
|
|
102
|
+
const q = (text, values = []) => __awaiter(void 0, void 0, void 0, function* () {
|
|
103
|
+
const parsed = parseTx(text, values);
|
|
104
|
+
const result = yield client.query({ text: parsed, values: values.flat() });
|
|
105
|
+
return (result.rows || []).map(camelizeRow);
|
|
106
|
+
});
|
|
107
|
+
const res = yield fn(q);
|
|
108
|
+
yield client.query('COMMIT');
|
|
109
|
+
return res;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
try {
|
|
113
|
+
yield client.query('ROLLBACK');
|
|
114
|
+
}
|
|
115
|
+
catch (_) {
|
|
116
|
+
// Ignoramos el error de rollback: relanzamos el original.
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
client.release();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
// =============================================================================
|
|
125
|
+
// Merge campo a campo
|
|
126
|
+
// =============================================================================
|
|
127
|
+
const resolverEscalar = (actual, nuevo, prioActual, prioNueva) => {
|
|
128
|
+
if (nuevo === null || nuevo === undefined)
|
|
129
|
+
return actual !== null && actual !== void 0 ? actual : null;
|
|
130
|
+
if (actual === null || actual === undefined)
|
|
131
|
+
return nuevo;
|
|
132
|
+
if (prioNueva >= prioActual)
|
|
133
|
+
return nuevo;
|
|
134
|
+
return actual;
|
|
135
|
+
};
|
|
136
|
+
const similar = (a, b, tol) => {
|
|
137
|
+
const an = a === null || a === undefined;
|
|
138
|
+
const bn = b === null || b === undefined;
|
|
139
|
+
if (an && bn)
|
|
140
|
+
return true;
|
|
141
|
+
if (an !== bn)
|
|
142
|
+
return false;
|
|
143
|
+
const na = Number(a);
|
|
144
|
+
const nb = Number(b);
|
|
145
|
+
if (na <= 0 || nb <= 0)
|
|
146
|
+
return na === nb;
|
|
147
|
+
return Math.abs(na - nb) / Math.max(na, nb) <= tol;
|
|
148
|
+
};
|
|
149
|
+
const buscarWorkoutSimilar = (q, opts, dateStr, pt) => __awaiter(void 0, void 0, void 0, function* () {
|
|
150
|
+
const rows = yield q(`SELECT "ID", "DURATION", "DISTANCE", "TIPO APLICATION"
|
|
151
|
+
FROM "WORKOUT"
|
|
152
|
+
WHERE "ID CLIENTE" = ? AND "DATE" = ? AND ${pt.sql}
|
|
153
|
+
AND "DONE" = TRUE AND "TIPO APLICATION" IS NOT NULL AND "TIPO APLICATION" <> ?`, [opts.idCliente, dateStr, ...pt.params, opts.aplicationType]);
|
|
154
|
+
const cand = rows.filter((r) => similar(r.duration, opts.duration, 0.1) && similar(r.distance, opts.distance, 0.1));
|
|
155
|
+
cand.sort((a, b) => prioridadFuente(b.tipoAplication) - prioridadFuente(a.tipoAplication));
|
|
156
|
+
return cand[0] || null;
|
|
157
|
+
});
|
|
158
|
+
const mergeStreams = (q, idWorkout, nuevos) => __awaiter(void 0, void 0, void 0, function* () {
|
|
159
|
+
if (!nuevos || typeof nuevos !== 'object')
|
|
160
|
+
return;
|
|
161
|
+
const ex = yield q('SELECT "METRIC TYPE", "DATA" FROM "WORKOUT STREAM" WHERE "ID WORKOUT" = ?', [idWorkout]);
|
|
162
|
+
const lenByType = {};
|
|
163
|
+
ex.forEach((r) => {
|
|
164
|
+
try {
|
|
165
|
+
const d = typeof r.data === 'string' ? JSON.parse(r.data) : r.data;
|
|
166
|
+
lenByType[r.metricType] = Array.isArray(d) ? d.length : 0;
|
|
167
|
+
}
|
|
168
|
+
catch (_) {
|
|
169
|
+
lenByType[r.metricType] = 0;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
for (const key of Object.keys(nuevos)) {
|
|
173
|
+
const arr = nuevos[key];
|
|
174
|
+
if (!Array.isArray(arr) || arr.length === 0)
|
|
175
|
+
continue;
|
|
176
|
+
if (arr.length > (lenByType[key] || 0)) {
|
|
177
|
+
// Borrado SELECTIVO por METRIC TYPE — nunca DELETE global (destruiría
|
|
178
|
+
// streams aportados por otra fuente).
|
|
179
|
+
yield q('DELETE FROM "WORKOUT STREAM" WHERE "ID WORKOUT" = ? AND "METRIC TYPE" = ?', [idWorkout, key]);
|
|
180
|
+
yield q('INSERT INTO "WORKOUT STREAM" ("ID WORKOUT", "METRIC TYPE", "DATA") VALUES (?, ?, ?)', [
|
|
181
|
+
idWorkout,
|
|
182
|
+
key,
|
|
183
|
+
JSON.stringify(arr),
|
|
184
|
+
]);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
const registrarFuente = (q, idWorkout, tipo, idApp) => __awaiter(void 0, void 0, void 0, function* () {
|
|
189
|
+
yield q(`INSERT INTO "WORKOUT SOURCES" ("ID WORKOUT", "TIPO APLICATION", "ID APLICATION")
|
|
190
|
+
VALUES (?, ?, ?)
|
|
191
|
+
ON CONFLICT ("ID WORKOUT", "TIPO APLICATION")
|
|
192
|
+
DO UPDATE SET "ID APLICATION" = EXCLUDED."ID APLICATION", "INGESTED AT" = NOW()`, [idWorkout, String(tipo), idApp !== null && idApp !== undefined ? String(idApp) : null]);
|
|
193
|
+
});
|
|
194
|
+
const aplicarMerge = (q, idWorkout, opts, esFilaNueva) => __awaiter(void 0, void 0, void 0, function* () {
|
|
195
|
+
var _a, _b;
|
|
196
|
+
const [row] = yield q(`SELECT "DISTANCE", "DURATION", "POTENCIA", "DESNIVEL", "CALORIES", "FC MEDIA",
|
|
197
|
+
"ZONE", "POLYLINE", "TIPO APLICATION", "ID APLICATION"
|
|
198
|
+
FROM "WORKOUT" WHERE "ID" = ? FOR UPDATE`, [idWorkout]);
|
|
199
|
+
const prioNueva = prioridadFuente(opts.aplicationType);
|
|
200
|
+
const prioActual = esFilaNueva ? -1 : prioridadFuente(row === null || row === void 0 ? void 0 : row.tipoAplication);
|
|
201
|
+
const distance = resolverEscalar(row === null || row === void 0 ? void 0 : row.distance, opts.distance, prioActual, prioNueva);
|
|
202
|
+
const duration = resolverEscalar(row === null || row === void 0 ? void 0 : row.duration, opts.duration, prioActual, prioNueva);
|
|
203
|
+
const potencia = resolverEscalar(row === null || row === void 0 ? void 0 : row.potencia, opts.power, prioActual, prioNueva);
|
|
204
|
+
const desnivel = resolverEscalar(row === null || row === void 0 ? void 0 : row.desnivel, opts.desnivel, prioActual, prioNueva);
|
|
205
|
+
const calories = resolverEscalar(row === null || row === void 0 ? void 0 : row.calories, (_a = opts.calories) !== null && _a !== void 0 ? _a : null, prioActual, prioNueva);
|
|
206
|
+
const fcMedia = resolverEscalar(row === null || row === void 0 ? void 0 : row.fcMedia, (_b = opts.heartrate) !== null && _b !== void 0 ? _b : null, prioActual, prioNueva);
|
|
207
|
+
const [cli] = yield q(`SELECT "PACE ZONE 1", "PACE ZONE 2", "PACE ZONE 3", "PACE ZONE 4", "PACE ZONE 5", "PACE ZONE 6"
|
|
208
|
+
FROM "CLIENTE" WHERE "ID" = ?`, [opts.idCliente]);
|
|
209
|
+
const zone = (0, paceZone_1.getZone)({ duration, distance, zones: cli });
|
|
210
|
+
// POLYLINE: la primera fuente que la puso manda; nunca se pisa.
|
|
211
|
+
const polyline = row && row.polyline ? row.polyline : getPolyline(opts.polyline);
|
|
212
|
+
// TIPO/ID APLICATION: sólo se setean en INSERT o si la fila no tenía fuente;
|
|
213
|
+
// nunca se reemplazan (la primera fuente se queda).
|
|
214
|
+
let tipoFinal;
|
|
215
|
+
let idFinal;
|
|
216
|
+
if (esFilaNueva || !(row === null || row === void 0 ? void 0 : row.tipoAplication)) {
|
|
217
|
+
tipoFinal = opts.aplicationType;
|
|
218
|
+
idFinal = opts.aplicationId;
|
|
51
219
|
}
|
|
52
220
|
else {
|
|
53
|
-
|
|
54
|
-
|
|
221
|
+
tipoFinal = row.tipoAplication;
|
|
222
|
+
idFinal = row.idAplication;
|
|
55
223
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
yield
|
|
63
|
-
|
|
224
|
+
const feelingsAsked = (0, moment_1.default)(opts.date).format('YYYY-MM-DD') !== (0, moment_1.default)().format('YYYY-MM-DD');
|
|
225
|
+
yield q(`UPDATE "WORKOUT" SET
|
|
226
|
+
"DISTANCE" = ?, "DURATION" = ?, "POTENCIA" = ?, "DESNIVEL" = ?, "CALORIES" = ?,
|
|
227
|
+
"FC MEDIA" = ?, "ZONE" = ?, "POLYLINE" = ?, "TIPO APLICATION" = ?, "ID APLICATION" = ?,
|
|
228
|
+
"FEELINGS ASKED" = ?, "DONE" = TRUE
|
|
229
|
+
WHERE "ID" = ?`, [distance, duration, potencia, desnivel, calories, fcMedia, zone, polyline, tipoFinal, idFinal, feelingsAsked, idWorkout]);
|
|
230
|
+
yield mergeStreams(q, idWorkout, opts.streams);
|
|
231
|
+
});
|
|
232
|
+
// =============================================================================
|
|
233
|
+
// Orquestación principal
|
|
234
|
+
// =============================================================================
|
|
235
|
+
/**
|
|
236
|
+
* @param opts {{
|
|
237
|
+
* date, type, title, description, distance, duration, power, desnivel, polyline,
|
|
238
|
+
* idCliente, aplicationType, aplicationId,
|
|
239
|
+
* queryConditionExist?, // LEGACY string SQL — deprecated, fallback
|
|
240
|
+
* tiposEquivalentes?, // string[] de TYPEs equivalentes
|
|
241
|
+
* calories?, heartrate?, streams?
|
|
242
|
+
* }}
|
|
243
|
+
* @returns {Promise<number|null>} idWorkout (o null si no se pudo crear) — firma intacta
|
|
244
|
+
*/
|
|
245
|
+
const saveWorkoutAplication = (opts,
|
|
246
|
+
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
|
247
|
+
_data) => __awaiter(void 0, void 0, void 0, function* () {
|
|
248
|
+
const dateStr = (0, moment_1.default)(opts.date).format('YYYY-MM-DD');
|
|
249
|
+
const pt = predicadoTipo(opts);
|
|
250
|
+
const idWorkout = yield withTx((q) => __awaiter(void 0, void 0, void 0, function* () {
|
|
251
|
+
// Lock anti-carrera: serializa ingestas del mismo cliente/día/bucket-tipo
|
|
252
|
+
// (Strava-webhook y Garmin-push pueden entrar casi a la vez).
|
|
253
|
+
yield q('SELECT pg_advisory_xact_lock(?)', [hashInt(`${opts.idCliente}|${dateStr}|${bucketTipo(opts.type)}`)]);
|
|
254
|
+
// 1) dedup intra-proveedor / completar planificado (comportamiento actual común).
|
|
255
|
+
// Ambas ramas filtran por "ID CLIENTE": el activityId de un proveedor es
|
|
256
|
+
// global pero pertenece a un único usuario en su plataforma; si por
|
|
257
|
+
// cualquier motivo (test, re-conexión OAuth con cuenta distinta, fallo
|
|
258
|
+
// de resolución upstream) llegara con un idCliente distinto al que ya
|
|
259
|
+
// tenía esa fila, NO debemos hacer merge cross-cliente.
|
|
260
|
+
const [exist] = yield q(`SELECT "ID", "TYPE" FROM "WORKOUT"
|
|
261
|
+
WHERE "ID CLIENTE" = ? AND (
|
|
262
|
+
("DATE" = ? AND ${pt.sql} AND "DONE" = FALSE AND "SHOW CLIENT" = TRUE)
|
|
263
|
+
OR ("ID APLICATION" = ? AND "TIPO APLICATION" = ?)
|
|
264
|
+
)
|
|
265
|
+
LIMIT 1`, [opts.idCliente, dateStr, ...pt.params, opts.aplicationId, opts.aplicationType]);
|
|
266
|
+
if (exist) {
|
|
267
|
+
yield aplicarMerge(q, exist.id, opts, false);
|
|
268
|
+
yield registrarFuente(q, exist.id, opts.aplicationType, opts.aplicationId);
|
|
269
|
+
return exist.id;
|
|
270
|
+
}
|
|
271
|
+
// 2) dedup CROSS-proveedor.
|
|
272
|
+
const sim = yield buscarWorkoutSimilar(q, opts, dateStr, pt);
|
|
273
|
+
if (sim) {
|
|
274
|
+
yield aplicarMerge(q, sim.id, opts, false);
|
|
275
|
+
yield registrarFuente(q, sim.id, opts.aplicationType, opts.aplicationId);
|
|
276
|
+
return sim.id;
|
|
277
|
+
}
|
|
278
|
+
// 3) INSERT nuevo (comportamiento actual).
|
|
279
|
+
const { title: titleNoTranslate, description: descriptionNoTranslate, titlePreferredLanguage, descriptionPreferredLanguage } = yield (0, titleDescriptionTranslated_1.getTitleDescriptionTranslated)(opts.idCliente, opts.title, opts.description);
|
|
280
|
+
const inserted = yield q(`INSERT INTO "WORKOUT"
|
|
281
|
+
("ID CLIENTE", "DATE", "TITLE", "DESCRIPTION", "TITLE PREFERRED LANGUAGE", "DESCRIPTION PREFERRED LANGUAGE", "TYPE", "DONE", "SHOW CLIENT")
|
|
282
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, TRUE, TRUE) RETURNING "ID"`, [opts.idCliente, dateStr, titleNoTranslate, descriptionNoTranslate, titlePreferredLanguage, descriptionPreferredLanguage, opts.type]);
|
|
283
|
+
if (!inserted || !inserted[0])
|
|
284
|
+
return null;
|
|
285
|
+
const id = inserted[0].id;
|
|
286
|
+
yield aplicarMerge(q, id, opts, true);
|
|
287
|
+
yield registrarFuente(q, id, opts.aplicationType, opts.aplicationId);
|
|
288
|
+
return id;
|
|
289
|
+
}));
|
|
290
|
+
if (!idWorkout)
|
|
291
|
+
return null;
|
|
292
|
+
yield postHooks(idWorkout, opts);
|
|
64
293
|
return idWorkout;
|
|
65
294
|
});
|
|
66
295
|
exports.saveWorkoutAplication = saveWorkoutAplication;
|
|
296
|
+
// postHooks SIEMPRE (fuera de la tx, en los 3 caminos). saveDoneStructuraWorkout
|
|
297
|
+
// es idempotente: cae en ...Copy si aún no hay laps (p.ej. Apple, o Garmin
|
|
298
|
+
// antes de /files) y en ...Watch cuando ya hay laps+streams.
|
|
299
|
+
const postHooks = (idWorkout, opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
300
|
+
try {
|
|
301
|
+
yield saveMap(idWorkout, opts.polyline);
|
|
302
|
+
}
|
|
303
|
+
catch (_) {
|
|
304
|
+
// Un fallo de Mapbox no debe romper la ingesta (estado ya persistido).
|
|
305
|
+
}
|
|
306
|
+
saveShareWorkoutImage(idWorkout, opts.type);
|
|
307
|
+
yield (0, estructuraWorkout_1.saveDoneStructuraWorkout)(idWorkout);
|
|
308
|
+
});
|
|
309
|
+
// getPolyline ampliado: acepta string ya-encoded (Apple `polylineEncoded`) o el
|
|
310
|
+
// objeto `activity` con map.polyline/summary_polyline (Strava/Garmin).
|
|
67
311
|
const getPolyline = (activity) => {
|
|
68
|
-
var _a
|
|
312
|
+
var _a;
|
|
313
|
+
if (!activity)
|
|
314
|
+
return null;
|
|
315
|
+
if (typeof activity === 'string')
|
|
316
|
+
return activity; // ya-encoded
|
|
69
317
|
if (!((_a = activity === null || activity === void 0 ? void 0 : activity.map) === null || _a === void 0 ? void 0 : _a.polyline))
|
|
70
318
|
return null;
|
|
71
|
-
const polyline =
|
|
319
|
+
const polyline = activity.map.polyline.length > 5000 ? activity.map.summary_polyline : activity.map.polyline;
|
|
72
320
|
return encodeURIComponent(polyline);
|
|
73
321
|
};
|
|
74
322
|
const saveMap = (idWorkout, polyline) => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -77,13 +325,15 @@ const saveMap = (idWorkout, polyline) => __awaiter(void 0, void 0, void 0, funct
|
|
|
77
325
|
// @ts-ignore
|
|
78
326
|
const serviceKey = path_1.default.join(process.cwd(), process.env.FIREBASE_STORAGE_CREDENTIALS);
|
|
79
327
|
const storage = new storage_1.Storage({ keyFilename: serviceKey });
|
|
80
|
-
const encodedPolyline = encodeURIComponent(polyline);
|
|
328
|
+
const encodedPolyline = encodeURIComponent(typeof polyline === 'string' ? polyline : getPolyline(polyline) || '');
|
|
329
|
+
if (!encodedPolyline)
|
|
330
|
+
return;
|
|
81
331
|
const result = yield axios_1.default.get(`https://api.mapbox.com/styles/v1/david-runnerpro/clqwrghcj013401qr5cch6z45/static/path-3+ea5b1b-100(${encodedPolyline})/auto/1080x1080?access_token=pk.eyJ1IjoiZGF2aWQtcnVubmVycHJvIiwiYSI6ImNscXdrZW56YTA0MTQybWxmNWx2b3Z6NjYifQ.g3vCR3HDRobLyr_4WFAeJA&logo=false`, { responseType: 'arraybuffer' });
|
|
82
332
|
const imageBuffer = Buffer.from(result.data, 'binary');
|
|
83
333
|
// @ts-ignore
|
|
84
334
|
yield storage.bucket(process.env.CLOUD_STORAGE_BUCKET_PUBLIC).file(`Workout/${idWorkout}`).save(imageBuffer);
|
|
85
335
|
const urlMap = `https://storage.googleapis.com/${process.env.CLOUD_STORAGE_BUCKET_PUBLIC}/Workout/${idWorkout}`;
|
|
86
|
-
yield (0, db_1.query)('UPDATE
|
|
336
|
+
yield (0, db_1.query)('UPDATE "WORKOUT" SET "HAVE MAP IMAGE" = TRUE, "PHOTO URL" = ? WHERE "ID" = ?', [urlMap, idWorkout]);
|
|
87
337
|
});
|
|
88
338
|
const saveShareWorkoutImage = (id, type) => __awaiter(void 0, void 0, void 0, function* () {
|
|
89
339
|
// @ts-ignore
|
|
@@ -101,5 +351,5 @@ const saveShareWorkoutImage = (id, type) => __awaiter(void 0, void 0, void 0, fu
|
|
|
101
351
|
// @ts-ignore
|
|
102
352
|
yield storage.bucket(process.env.CLOUD_STORAGE_BUCKET_PUBLIC).file(`Workout/${id}-share`).save(shareMap);
|
|
103
353
|
const urlShare = `https://storage.googleapis.com/${process.env.CLOUD_STORAGE_BUCKET_PUBLIC}/Workout/${id}-share`;
|
|
104
|
-
yield (0, db_1.query)('UPDATE
|
|
354
|
+
yield (0, db_1.query)('UPDATE "WORKOUT" SET "PHOTO URL SHARE" = ? WHERE "ID" = ?', [urlShare, id]);
|
|
105
355
|
});
|
|
@@ -0,0 +1,58 @@
|
|
|
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.saveWorkoutLaps = void 0;
|
|
13
|
+
const db_1 = require("../db");
|
|
14
|
+
const estructuraWorkout_1 = require("./estructuraWorkout");
|
|
15
|
+
/**
|
|
16
|
+
* Persistencia centralizada de WORKOUT LAP (Fase 1 plan dedup cross-proveedor).
|
|
17
|
+
*
|
|
18
|
+
* `laps` viene ya normalizado por el adaptador del proveedor:
|
|
19
|
+
* [{ index: number, durationStartLap: number }]
|
|
20
|
+
*
|
|
21
|
+
* Política de merge cross-proveedor (coherente con `mergeStreams`):
|
|
22
|
+
* gana el conjunto con MÁS laps. Tras persistir, re-dispara
|
|
23
|
+
* `saveDoneStructuraWorkout(idWorkout)` para recalcular la estructura real
|
|
24
|
+
* ahora que ya hay laps en BD (rama `...Watch`).
|
|
25
|
+
*
|
|
26
|
+
* Es función pública aparte (no entra en la firma de `saveWorkoutAplication`)
|
|
27
|
+
* porque Garmin entrega los laps en el webhook `/files`, desacoplado del
|
|
28
|
+
* `/details` que crea el workout.
|
|
29
|
+
*
|
|
30
|
+
* NOTA (// TODO): no se persiste la fuente que escribió los laps, así que el
|
|
31
|
+
* desempate por prioridad de fuente con igual nº de laps no es posible sin
|
|
32
|
+
* estado extra; se usa estrictamente "más laps gana" (empate → se conserva lo
|
|
33
|
+
* existente, sin churn). El parámetro `aplicationType` se acepta para futura
|
|
34
|
+
* trazabilidad y coherencia de firma con el resto de la lib.
|
|
35
|
+
*
|
|
36
|
+
* @param {number|string} idWorkout
|
|
37
|
+
* @param {{index:number,durationStartLap:number}[]} laps
|
|
38
|
+
* @param {number} aplicationType TIPO_APLICACION.* (reservado para trazabilidad)
|
|
39
|
+
*/
|
|
40
|
+
const saveWorkoutLaps = (idWorkout, laps, aplicationType) => __awaiter(void 0, void 0, void 0, function* () {
|
|
41
|
+
if (!idWorkout)
|
|
42
|
+
return;
|
|
43
|
+
const nuevos = Array.isArray(laps)
|
|
44
|
+
? laps.filter((l) => l && l.index !== null && l.index !== undefined && l.durationStartLap !== null && l.durationStartLap !== undefined)
|
|
45
|
+
: [];
|
|
46
|
+
const [countRow] = yield (0, db_1.query)('SELECT COUNT(*)::int AS "N" FROM "WORKOUT LAP" WHERE "ID WORKOUT" = ?', [idWorkout]);
|
|
47
|
+
const existentes = Number((countRow === null || countRow === void 0 ? void 0 : countRow.n) || 0);
|
|
48
|
+
if (nuevos.length > existentes) {
|
|
49
|
+
yield (0, db_1.query)('DELETE FROM "WORKOUT LAP" WHERE "ID WORKOUT" = ?', [idWorkout]);
|
|
50
|
+
for (const lap of nuevos) {
|
|
51
|
+
yield (0, db_1.query)('INSERT INTO "WORKOUT LAP" ("ID WORKOUT", "INDEX", "DURATION START LAP") VALUES (?, ?, ?)', [idWorkout, lap.index, lap.durationStartLap]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// si nuevos.length <= existentes: se conserva el conjunto existente (más detalle gana)
|
|
55
|
+
// Re-dispara el recálculo de la estructura real ahora que hay laps en BD.
|
|
56
|
+
yield (0, estructuraWorkout_1.saveDoneStructuraWorkout)(idWorkout);
|
|
57
|
+
});
|
|
58
|
+
exports.saveWorkoutLaps = saveWorkoutLaps;
|