@runnerpro/backend 1.20.5 → 1.21.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/lib/cjs/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runWithCostTracking = exports.createBedrockModelFromString = exports.createGoogleModelFromString = exports.createGoogleModel = exports.generateText = exports.generateObject = exports.reprocessRecentMedia = exports.processMediaFile = exports.describeImage = exports.transcribeAudio = exports.updateSenderView = exports.saveResponseTime = 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 = 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");
@@ -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
- declare const saveDoneStructuraWorkout: (idWorkout: any, estructura: any, workoutType: any) => Promise<void>;
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,wCAiCxB,CAAC;AAEF,QAAA,MAAM,wBAAwB,sEAkC7B,CAAC;AAGF,OAAO,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,CAAC"}
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
- declare const saveWorkoutAplication: ({ date, type, title, description, distance, duration, power, desnivel, polyline, idCliente, aplicationType, aplicationId, queryConditionExist }: {
2
- date: any;
3
- type: any;
4
- title: any;
5
- description: any;
6
- distance: any;
7
- duration: any;
8
- power: any;
9
- desnivel: any;
10
- polyline: any;
11
- idCliente: any;
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":"AAUA,QAAA,MAAM,qBAAqB;;;;;;;;;;;;;;6BAwD1B,CAAC;AA8CF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
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,CAmDvB,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
- const saveDoneStructuraWorkout = (idWorkout, estructura, workoutType) => __awaiter(void 0, void 0, void 0, function* () {
71
- const estructuraBBDD = yield (0, db_1.query)('SELECT * FROM [WORKOUT STRUCTURE] WHERE [ID WORKOUT] = ? ORDER BY [INDEX]', [idWorkout]);
72
- let i = 0;
73
- for (const row of estructuraBBDD) {
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
- const pace = estructura ? estructura[i].valueMetric2 : row.valueMetric2;
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)(workoutType).calculateDuration({ distance: row.valueMetric1, pace: pacePlanned });
84
- duration = (0, common_1.getWorkoutParamsByType)(workoutType).calculateDuration({ distance: row.valueMetric1, pace });
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)(workoutType).calculateDistance({ duration: row.valueMetric1, pace: pacePlanned, decimals: 2 });
97
- distance = (0, common_1.getWorkoutParamsByType)(workoutType).calculateDistance({ duration: row.valueMetric1, pace, decimals: 2 });
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
- i++;
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,49 +22,294 @@ 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 saveWorkoutAplication = ({ date, type, title, description, distance, duration, power, desnivel, polyline, idCliente, aplicationType, aplicationId, queryConditionExist }, data) => __awaiter(void 0, void 0, void 0, function* () {
26
- const [exist] = yield (0, db_1.query)(`
27
- SELECT [ID], [TYPE]
28
- FROM [WORKOUT]
29
- WHERE
30
- ([ID CLIENTE] = ? AND [DATE] = ? AND ${queryConditionExist} AND [DONE] = FALSE AND [SHOW CLIENT] = TRUE)
31
- OR
32
- ([ID APLICATION] = ? AND [TIPO APLICATION] = ?)
33
- `, [idCliente, (0, moment_1.default)(date).format('YYYY-MM-DD'), aplicationId, aplicationType]);
34
- let idWorkout;
35
- if (!exist) {
36
- const { title: titleNoTranslate, description: descriptionNoTranslate, titlePreferredLanguage, descriptionPreferredLanguage, } = yield (0, titleDescriptionTranslated_1.getTitleDescriptionTranslated)(idCliente, title, description);
37
- [{ id: idWorkout }] = yield (0, db_1.query)(`
38
- INSERT INTO [WORKOUT] ([ID CLIENTE], [DATE], [TITLE], [DESCRIPTION], [TITLE PREFERRED LANGUAGE], [DESCRIPTION PREFERRED LANGUAGE], [TYPE], [DONE], [SHOW CLIENT])
39
- VALUES (?, ?, ?, ?, ?, ?, ?, TRUE, TRUE) RETURNING [ID]
40
- `, [
41
- idCliente,
42
- (0, moment_1.default)(date).format('YYYY-MM-DD'),
43
- titleNoTranslate,
44
- descriptionNoTranslate,
45
- titlePreferredLanguage,
46
- descriptionPreferredLanguage,
47
- type,
48
- ]);
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;
49
219
  }
50
220
  else {
51
- idWorkout = exist.id;
52
- yield (0, estructuraWorkout_1.saveDoneStructuraWorkout)(exist.id, null, exist.type);
221
+ tipoFinal = row.tipoAplication;
222
+ idFinal = row.idAplication;
53
223
  }
54
- yield (0, db_1.query)(`
55
- UPDATE [WORKOUT] SET [DISTANCE] = ?, [DURATION] = ?, [POTENCIA] = ?, [DESNIVEL] = ?, [POLYLINE] = ?, [TIPO APLICATION] = ?, [ID APLICATION] = ?, [DONE] = TRUE, [RAW] = ?
56
- WHERE [ID] = ?
57
- `, [distance, duration, power, desnivel, getPolyline(polyline), aplicationType, aplicationId, JSON.stringify(data), idWorkout]);
58
- yield saveMap(idWorkout, polyline);
59
- saveShareWorkoutImage(idWorkout, type);
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
+ const [exist] = yield q(`SELECT "ID", "TYPE" FROM "WORKOUT"
256
+ WHERE ("ID CLIENTE" = ? AND "DATE" = ? AND ${pt.sql} AND "DONE" = FALSE AND "SHOW CLIENT" = TRUE)
257
+ OR ("ID APLICATION" = ? AND "TIPO APLICATION" = ?)
258
+ LIMIT 1`, [opts.idCliente, dateStr, ...pt.params, opts.aplicationId, opts.aplicationType]);
259
+ if (exist) {
260
+ yield aplicarMerge(q, exist.id, opts, false);
261
+ yield registrarFuente(q, exist.id, opts.aplicationType, opts.aplicationId);
262
+ return exist.id;
263
+ }
264
+ // 2) dedup CROSS-proveedor.
265
+ const sim = yield buscarWorkoutSimilar(q, opts, dateStr, pt);
266
+ if (sim) {
267
+ yield aplicarMerge(q, sim.id, opts, false);
268
+ yield registrarFuente(q, sim.id, opts.aplicationType, opts.aplicationId);
269
+ return sim.id;
270
+ }
271
+ // 3) INSERT nuevo (comportamiento actual).
272
+ const { title: titleNoTranslate, description: descriptionNoTranslate, titlePreferredLanguage, descriptionPreferredLanguage } = yield (0, titleDescriptionTranslated_1.getTitleDescriptionTranslated)(opts.idCliente, opts.title, opts.description);
273
+ const inserted = yield q(`INSERT INTO "WORKOUT"
274
+ ("ID CLIENTE", "DATE", "TITLE", "DESCRIPTION", "TITLE PREFERRED LANGUAGE", "DESCRIPTION PREFERRED LANGUAGE", "TYPE", "DONE", "SHOW CLIENT")
275
+ VALUES (?, ?, ?, ?, ?, ?, ?, TRUE, TRUE) RETURNING "ID"`, [opts.idCliente, dateStr, titleNoTranslate, descriptionNoTranslate, titlePreferredLanguage, descriptionPreferredLanguage, opts.type]);
276
+ if (!inserted || !inserted[0])
277
+ return null;
278
+ const id = inserted[0].id;
279
+ yield aplicarMerge(q, id, opts, true);
280
+ yield registrarFuente(q, id, opts.aplicationType, opts.aplicationId);
281
+ return id;
282
+ }));
283
+ if (!idWorkout)
284
+ return null;
285
+ yield postHooks(idWorkout, opts);
60
286
  return idWorkout;
61
287
  });
62
288
  exports.saveWorkoutAplication = saveWorkoutAplication;
289
+ // postHooks SIEMPRE (fuera de la tx, en los 3 caminos). saveDoneStructuraWorkout
290
+ // es idempotente: cae en ...Copy si aún no hay laps (p.ej. Apple, o Garmin
291
+ // antes de /files) y en ...Watch cuando ya hay laps+streams.
292
+ const postHooks = (idWorkout, opts) => __awaiter(void 0, void 0, void 0, function* () {
293
+ try {
294
+ yield saveMap(idWorkout, opts.polyline);
295
+ }
296
+ catch (_) {
297
+ // Un fallo de Mapbox no debe romper la ingesta (estado ya persistido).
298
+ }
299
+ saveShareWorkoutImage(idWorkout, opts.type);
300
+ yield (0, estructuraWorkout_1.saveDoneStructuraWorkout)(idWorkout);
301
+ });
302
+ // getPolyline ampliado: acepta string ya-encoded (Apple `polylineEncoded`) o el
303
+ // objeto `activity` con map.polyline/summary_polyline (Strava/Garmin).
63
304
  const getPolyline = (activity) => {
64
- var _a, _b;
305
+ var _a;
306
+ if (!activity)
307
+ return null;
308
+ if (typeof activity === 'string')
309
+ return activity; // ya-encoded
65
310
  if (!((_a = activity === null || activity === void 0 ? void 0 : activity.map) === null || _a === void 0 ? void 0 : _a.polyline))
66
311
  return null;
67
- const polyline = ((_b = activity.map) === null || _b === void 0 ? void 0 : _b.polyline.length) > 5000 ? activity.map.summary_polyline : activity.map.polyline;
312
+ const polyline = activity.map.polyline.length > 5000 ? activity.map.summary_polyline : activity.map.polyline;
68
313
  return encodeURIComponent(polyline);
69
314
  };
70
315
  const saveMap = (idWorkout, polyline) => __awaiter(void 0, void 0, void 0, function* () {
@@ -73,13 +318,15 @@ const saveMap = (idWorkout, polyline) => __awaiter(void 0, void 0, void 0, funct
73
318
  // @ts-ignore
74
319
  const serviceKey = path_1.default.join(process.cwd(), process.env.FIREBASE_STORAGE_CREDENTIALS);
75
320
  const storage = new storage_1.Storage({ keyFilename: serviceKey });
76
- const encodedPolyline = encodeURIComponent(polyline);
321
+ const encodedPolyline = encodeURIComponent(typeof polyline === 'string' ? polyline : getPolyline(polyline) || '');
322
+ if (!encodedPolyline)
323
+ return;
77
324
  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' });
78
325
  const imageBuffer = Buffer.from(result.data, 'binary');
79
326
  // @ts-ignore
80
327
  yield storage.bucket(process.env.CLOUD_STORAGE_BUCKET_PUBLIC).file(`Workout/${idWorkout}`).save(imageBuffer);
81
328
  const urlMap = `https://storage.googleapis.com/${process.env.CLOUD_STORAGE_BUCKET_PUBLIC}/Workout/${idWorkout}`;
82
- yield (0, db_1.query)('UPDATE [WORKOUT] SET [HAVE MAP IMAGE] = TRUE, [PHOTO URL] = ? WHERE [ID] = ?', [urlMap, idWorkout]);
329
+ yield (0, db_1.query)('UPDATE "WORKOUT" SET "HAVE MAP IMAGE" = TRUE, "PHOTO URL" = ? WHERE "ID" = ?', [urlMap, idWorkout]);
83
330
  });
84
331
  const saveShareWorkoutImage = (id, type) => __awaiter(void 0, void 0, void 0, function* () {
85
332
  // @ts-ignore
@@ -97,5 +344,5 @@ const saveShareWorkoutImage = (id, type) => __awaiter(void 0, void 0, void 0, fu
97
344
  // @ts-ignore
98
345
  yield storage.bucket(process.env.CLOUD_STORAGE_BUCKET_PUBLIC).file(`Workout/${id}-share`).save(shareMap);
99
346
  const urlShare = `https://storage.googleapis.com/${process.env.CLOUD_STORAGE_BUCKET_PUBLIC}/Workout/${id}-share`;
100
- yield (0, db_1.query)('UPDATE [WORKOUT] SET [PHOTO URL SHARE] = ? WHERE [ID] = ?', [urlShare, id]);
347
+ yield (0, db_1.query)('UPDATE "WORKOUT" SET "PHOTO URL SHARE" = ? WHERE "ID" = ?', [urlShare, id]);
101
348
  });
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runnerpro/backend",
3
- "version": "1.20.5",
3
+ "version": "1.21.0",
4
4
  "description": "A collection of common backend functions",
5
5
  "exports": {
6
6
  ".": "./lib/cjs/index.js"