@aneuhold/be-ts-db-lib 4.2.11 → 4.2.13
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/CHANGELOG.md +19 -0
- package/lib/repositories/workout/WorkoutExerciseRepository.d.ts +11 -1
- package/lib/repositories/workout/WorkoutExerciseRepository.d.ts.map +1 -1
- package/lib/repositories/workout/WorkoutExerciseRepository.js +181 -1
- package/lib/repositories/workout/WorkoutExerciseRepository.js.map +1 -1
- package/lib/repositories/workout/WorkoutExerciseRepository.ts +231 -2
- package/lib/repositories/workout/WorkoutMuscleGroupRepository.d.ts +10 -1
- package/lib/repositories/workout/WorkoutMuscleGroupRepository.d.ts.map +1 -1
- package/lib/repositories/workout/WorkoutMuscleGroupRepository.js +247 -1
- package/lib/repositories/workout/WorkoutMuscleGroupRepository.js.map +1 -1
- package/lib/repositories/workout/WorkoutMuscleGroupRepository.ts +285 -2
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## 🔖 [4.2.13] (2026-02-28)
|
|
9
|
+
|
|
10
|
+
### ✅ Added
|
|
11
|
+
|
|
12
|
+
- Added `WorkoutExerciseRepository.buildExerciseCTOsForUser()` to build `WorkoutExerciseCTO` objects for all of a user's exercises using parallel MongoDB aggregation pipelines (exercise + equipment + best calibration + best set; last accumulation session + first set).
|
|
13
|
+
- Added `WorkoutMuscleGroupRepository.buildMuscleGroupVolumeCTOsForUser()` to build `WorkoutMuscleGroupVolumeCTO` objects for all of a user's muscle groups using a volume aggregation pipeline across completed mesocycles.
|
|
14
|
+
|
|
15
|
+
### 🏗️ Changed
|
|
16
|
+
|
|
17
|
+
- Updated dependencies: now requires `@aneuhold/be-ts-lib@^3.1.0`, `@aneuhold/core-ts-db-lib@^4.1.12`, and `@aneuhold/core-ts-lib@^2.4.1`.
|
|
18
|
+
|
|
19
|
+
## 🔖 [4.2.12] (2026-02-23)
|
|
20
|
+
|
|
21
|
+
### 🏗️ Changed
|
|
22
|
+
|
|
23
|
+
- Updated dependencies: now requires `@aneuhold/be-ts-lib@^3.0.22` and `@aneuhold/core-ts-db-lib@^4.1.11`.
|
|
24
|
+
|
|
8
25
|
## 🔖 [4.2.11] (2026-02-23)
|
|
9
26
|
|
|
10
27
|
### 🏗️ Changed
|
|
@@ -285,6 +302,8 @@ Updated dependencies: now requires `@aneuhold/core-ts-db-lib@^3.0.0`, `@aneuhold
|
|
|
285
302
|
|
|
286
303
|
<!-- Link References -->
|
|
287
304
|
|
|
305
|
+
[4.2.13]: https://github.com/aneuhold/ts-libs/compare/be-ts-db-lib-v4.2.12...be-ts-db-lib-v4.2.13
|
|
306
|
+
[4.2.12]: https://github.com/aneuhold/ts-libs/compare/be-ts-db-lib-v4.2.11...be-ts-db-lib-v4.2.12
|
|
288
307
|
[4.2.11]: https://github.com/aneuhold/ts-libs/compare/be-ts-db-lib-v4.2.10...be-ts-db-lib-v4.2.11
|
|
289
308
|
[4.2.10]: https://github.com/aneuhold/ts-libs/compare/be-ts-db-lib-v4.2.9...be-ts-db-lib-v4.2.10
|
|
290
309
|
[4.2.9]: https://github.com/aneuhold/ts-libs/compare/be-ts-db-lib-v4.2.8...be-ts-db-lib-v4.2.9
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { User, WorkoutExercise } from '@aneuhold/core-ts-db-lib';
|
|
1
|
+
import type { User, WorkoutExercise, WorkoutExerciseCTO } from '@aneuhold/core-ts-db-lib';
|
|
2
|
+
import type { UUID } from 'crypto';
|
|
2
3
|
import type { RepoListeners } from '../../services/RepoSubscriptionService.js';
|
|
3
4
|
import WorkoutBaseWithUserIdRepository from './WorkoutBaseWithUserIdRepository.js';
|
|
4
5
|
/**
|
|
@@ -10,5 +11,14 @@ export default class WorkoutExerciseRepository extends WorkoutBaseWithUserIdRepo
|
|
|
10
11
|
static getListenersForUserRepo(): RepoListeners<User>;
|
|
11
12
|
protected setupSubscribers(): void;
|
|
12
13
|
static getRepo(): WorkoutExerciseRepository;
|
|
14
|
+
/**
|
|
15
|
+
* Builds {@link WorkoutExerciseCTO} objects for all exercises belonging to a user.
|
|
16
|
+
* Uses two parallel MongoDB aggregation pipelines:
|
|
17
|
+
* - Pipeline A: exercise + equipmentType + bestCalibration + bestSet
|
|
18
|
+
* - Pipeline B: lastSessionExercise + lastFirstSet (from most recent non-deload session)
|
|
19
|
+
*
|
|
20
|
+
* @param userId The user whose exercise CTOs to build.
|
|
21
|
+
*/
|
|
22
|
+
buildExerciseCTOsForUser(userId: UUID): Promise<WorkoutExerciseCTO[]>;
|
|
13
23
|
}
|
|
14
24
|
//# sourceMappingURL=WorkoutExerciseRepository.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkoutExerciseRepository.d.ts","sourceRoot":"","sources":["../../../src/repositories/workout/WorkoutExerciseRepository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"WorkoutExerciseRepository.d.ts","sourceRoot":"","sources":["../../../src/repositories/workout/WorkoutExerciseRepository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EAEJ,eAAe,EAEf,kBAAkB,EAGnB,MAAM,0BAA0B,CAAC;AAWlC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2CAA2C,CAAC;AAE/E,OAAO,+BAA+B,MAAM,sCAAsC,CAAC;AAGnF;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,yBAA0B,SAAQ,+BAA+B,CAAC,eAAe,CAAC;IACrG,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAA4B;IAE7D,OAAO;IAIP,MAAM,CAAC,uBAAuB,IAAI,aAAa,CAAC,IAAI,CAAC;IAYrD,SAAS,CAAC,gBAAgB,IAAI,IAAI;WAIpB,OAAO,IAAI,yBAAyB;IAOlD;;;;;;;OAOG;IACG,wBAAwB,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;CA0M5E"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WorkoutExercise_docType } from '@aneuhold/core-ts-db-lib';
|
|
1
|
+
import { WorkoutEquipmentType_docType, WorkoutExercise_docType, WorkoutExerciseCalibration_docType, WorkoutExerciseCalibrationService, WorkoutExerciseCTOSchema, WorkoutSession_docType, WorkoutSessionExercise_docType, WorkoutSet_docType } from '@aneuhold/core-ts-db-lib';
|
|
2
2
|
import WorkoutExerciseValidator from '../../validators/workout/ExerciseValidator.js';
|
|
3
3
|
import WorkoutBaseWithUserIdRepository from './WorkoutBaseWithUserIdRepository.js';
|
|
4
4
|
import WorkoutExerciseCalibrationRepository from './WorkoutExerciseCalibrationRepository.js';
|
|
@@ -30,5 +30,185 @@ export default class WorkoutExerciseRepository extends WorkoutBaseWithUserIdRepo
|
|
|
30
30
|
}
|
|
31
31
|
return WorkoutExerciseRepository.singletonInstance;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Builds {@link WorkoutExerciseCTO} objects for all exercises belonging to a user.
|
|
35
|
+
* Uses two parallel MongoDB aggregation pipelines:
|
|
36
|
+
* - Pipeline A: exercise + equipmentType + bestCalibration + bestSet
|
|
37
|
+
* - Pipeline B: lastSessionExercise + lastFirstSet (from most recent non-deload session)
|
|
38
|
+
*
|
|
39
|
+
* @param userId The user whose exercise CTOs to build.
|
|
40
|
+
*/
|
|
41
|
+
async buildExerciseCTOsForUser(userId) {
|
|
42
|
+
const collection = await this.getCollection();
|
|
43
|
+
const collName = this.collectionName;
|
|
44
|
+
// Pipeline A: Exercise base + equipmentType + bestCalibration + bestSet
|
|
45
|
+
const pipelineAPromise = collection
|
|
46
|
+
.aggregate([
|
|
47
|
+
// Start with all exercises for this user
|
|
48
|
+
{ $match: { docType: WorkoutExercise_docType, userId } },
|
|
49
|
+
// Join equipment type by workoutEquipmentTypeId
|
|
50
|
+
{
|
|
51
|
+
$lookup: {
|
|
52
|
+
from: collName,
|
|
53
|
+
let: { equipId: '$workoutEquipmentTypeId' },
|
|
54
|
+
pipeline: [
|
|
55
|
+
{
|
|
56
|
+
$match: {
|
|
57
|
+
$expr: { $eq: ['$_id', '$$equipId'] },
|
|
58
|
+
docType: WorkoutEquipmentType_docType
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
as: '_equipArr'
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
// Find the best calibration by highest estimated 1RM
|
|
66
|
+
{
|
|
67
|
+
$lookup: {
|
|
68
|
+
from: collName,
|
|
69
|
+
let: { exId: '$_id' },
|
|
70
|
+
pipeline: [
|
|
71
|
+
{
|
|
72
|
+
$match: {
|
|
73
|
+
$expr: { $eq: ['$workoutExerciseId', '$$exId'] },
|
|
74
|
+
docType: WorkoutExerciseCalibration_docType
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
$addFields: {
|
|
79
|
+
_1rm: WorkoutExerciseCalibrationService.get1RMMongoExpr('$weight', '$reps')
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{ $sort: { _1rm: -1 } },
|
|
83
|
+
{ $limit: 1 },
|
|
84
|
+
{ $project: { _1rm: 0 } }
|
|
85
|
+
],
|
|
86
|
+
as: '_bestCalArr'
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
// Find the best completed set by highest estimated 1RM
|
|
90
|
+
{
|
|
91
|
+
$lookup: {
|
|
92
|
+
from: collName,
|
|
93
|
+
let: { exId: '$_id' },
|
|
94
|
+
pipeline: [
|
|
95
|
+
{
|
|
96
|
+
$match: {
|
|
97
|
+
$expr: { $eq: ['$workoutExerciseId', '$$exId'] },
|
|
98
|
+
docType: WorkoutSet_docType,
|
|
99
|
+
actualWeight: { $ne: null },
|
|
100
|
+
actualReps: { $ne: null }
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
$addFields: {
|
|
105
|
+
_1rm: WorkoutExerciseCalibrationService.get1RMMongoExpr('$actualWeight', '$actualReps')
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{ $sort: { _1rm: -1 } },
|
|
109
|
+
{ $limit: 1 },
|
|
110
|
+
{ $project: { _1rm: 0 } }
|
|
111
|
+
],
|
|
112
|
+
as: '_bestSetArr'
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
])
|
|
116
|
+
.toArray();
|
|
117
|
+
// Pipeline B: lastSessionExercise + lastFirstSet per exercise
|
|
118
|
+
const pipelineBPromise = collection
|
|
119
|
+
.aggregate([
|
|
120
|
+
// Start with all completed sessions for this user
|
|
121
|
+
{ $match: { docType: WorkoutSession_docType, userId, complete: true } },
|
|
122
|
+
// Most recent sessions first
|
|
123
|
+
{ $sort: { startTime: -1 } },
|
|
124
|
+
// Join each session's session exercises
|
|
125
|
+
{
|
|
126
|
+
$lookup: {
|
|
127
|
+
from: collName,
|
|
128
|
+
let: { sessId: '$_id' },
|
|
129
|
+
pipeline: [
|
|
130
|
+
{
|
|
131
|
+
$match: {
|
|
132
|
+
$expr: { $eq: ['$workoutSessionId', '$$sessId'] },
|
|
133
|
+
docType: WorkoutSessionExercise_docType
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
as: '_se'
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
// One row per session exercise (unwind coalescence optimization)
|
|
141
|
+
{ $unwind: '$_se' },
|
|
142
|
+
// Check for non-deload sets (plannedRir is not null)
|
|
143
|
+
{
|
|
144
|
+
$lookup: {
|
|
145
|
+
from: collName,
|
|
146
|
+
let: { seId: '$_se._id' },
|
|
147
|
+
pipeline: [
|
|
148
|
+
{
|
|
149
|
+
$match: {
|
|
150
|
+
$expr: { $eq: ['$workoutSessionExerciseId', '$$seId'] },
|
|
151
|
+
docType: WorkoutSet_docType,
|
|
152
|
+
plannedRir: { $ne: null }
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{ $limit: 1 }
|
|
156
|
+
],
|
|
157
|
+
as: '_nonDeload'
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
// Keep only session exercises with at least one non-deload set
|
|
161
|
+
{ $match: { '_nonDeload.0': { $exists: true } } },
|
|
162
|
+
// Per exercise, keep only the most recent session exercise ($first after sort)
|
|
163
|
+
{
|
|
164
|
+
$group: {
|
|
165
|
+
_id: '$_se.workoutExerciseId',
|
|
166
|
+
lastSessionExercise: { $first: '$_se' }
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
// Join the first set from that session exercise's setOrder
|
|
170
|
+
{
|
|
171
|
+
$lookup: {
|
|
172
|
+
from: collName,
|
|
173
|
+
let: { firstSetId: { $arrayElemAt: ['$lastSessionExercise.setOrder', 0] } },
|
|
174
|
+
pipeline: [
|
|
175
|
+
{
|
|
176
|
+
$match: {
|
|
177
|
+
$expr: { $eq: ['$_id', '$$firstSetId'] },
|
|
178
|
+
docType: WorkoutSet_docType
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
as: '_lastFirstSetArr'
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
])
|
|
186
|
+
.toArray();
|
|
187
|
+
const [rawExercises, rawLastSessions] = await Promise.all([pipelineAPromise, pipelineBPromise]);
|
|
188
|
+
// Build lookup map from Pipeline B
|
|
189
|
+
const lastSessionMap = new Map();
|
|
190
|
+
for (const row of rawLastSessions) {
|
|
191
|
+
lastSessionMap.set(row._id, {
|
|
192
|
+
lastSessionExercise: row.lastSessionExercise,
|
|
193
|
+
lastFirstSet: row._lastFirstSetArr[0] ?? null
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
// Assemble and validate CTOs
|
|
197
|
+
return rawExercises.map((raw) => {
|
|
198
|
+
const { _equipArr, _bestCalArr, _bestSetArr, ...exerciseFields } = raw;
|
|
199
|
+
if (_equipArr.length === 0) {
|
|
200
|
+
throw new Error(`Equipment type not found for exercise ${raw._id}`);
|
|
201
|
+
}
|
|
202
|
+
const lastData = lastSessionMap.get(raw._id);
|
|
203
|
+
return WorkoutExerciseCTOSchema.parse({
|
|
204
|
+
...exerciseFields,
|
|
205
|
+
equipmentType: _equipArr[0],
|
|
206
|
+
bestCalibration: _bestCalArr[0] ?? null,
|
|
207
|
+
bestSet: _bestSetArr[0] ?? null,
|
|
208
|
+
lastSessionExercise: lastData?.lastSessionExercise ?? null,
|
|
209
|
+
lastFirstSet: lastData?.lastFirstSet ?? null
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
33
213
|
}
|
|
34
214
|
//# sourceMappingURL=WorkoutExerciseRepository.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkoutExerciseRepository.js","sourceRoot":"","sources":["../../../src/repositories/workout/WorkoutExerciseRepository.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"WorkoutExerciseRepository.js","sourceRoot":"","sources":["../../../src/repositories/workout/WorkoutExerciseRepository.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,23 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
User,
|
|
3
|
+
WorkoutEquipmentType,
|
|
4
|
+
WorkoutExercise,
|
|
5
|
+
WorkoutExerciseCalibration,
|
|
6
|
+
WorkoutExerciseCTO,
|
|
7
|
+
WorkoutSessionExercise,
|
|
8
|
+
WorkoutSet
|
|
9
|
+
} from '@aneuhold/core-ts-db-lib';
|
|
10
|
+
import {
|
|
11
|
+
WorkoutEquipmentType_docType,
|
|
12
|
+
WorkoutExercise_docType,
|
|
13
|
+
WorkoutExerciseCalibration_docType,
|
|
14
|
+
WorkoutExerciseCalibrationService,
|
|
15
|
+
WorkoutExerciseCTOSchema,
|
|
16
|
+
WorkoutSession_docType,
|
|
17
|
+
WorkoutSessionExercise_docType,
|
|
18
|
+
WorkoutSet_docType
|
|
19
|
+
} from '@aneuhold/core-ts-db-lib';
|
|
20
|
+
import type { UUID } from 'crypto';
|
|
3
21
|
import type { RepoListeners } from '../../services/RepoSubscriptionService.js';
|
|
4
22
|
import WorkoutExerciseValidator from '../../validators/workout/ExerciseValidator.js';
|
|
5
23
|
import WorkoutBaseWithUserIdRepository from './WorkoutBaseWithUserIdRepository.js';
|
|
@@ -37,4 +55,215 @@ export default class WorkoutExerciseRepository extends WorkoutBaseWithUserIdRepo
|
|
|
37
55
|
}
|
|
38
56
|
return WorkoutExerciseRepository.singletonInstance;
|
|
39
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Builds {@link WorkoutExerciseCTO} objects for all exercises belonging to a user.
|
|
61
|
+
* Uses two parallel MongoDB aggregation pipelines:
|
|
62
|
+
* - Pipeline A: exercise + equipmentType + bestCalibration + bestSet
|
|
63
|
+
* - Pipeline B: lastSessionExercise + lastFirstSet (from most recent non-deload session)
|
|
64
|
+
*
|
|
65
|
+
* @param userId The user whose exercise CTOs to build.
|
|
66
|
+
*/
|
|
67
|
+
async buildExerciseCTOsForUser(userId: UUID): Promise<WorkoutExerciseCTO[]> {
|
|
68
|
+
/** Raw shape of Pipeline A results. */
|
|
69
|
+
interface PipelineARow extends WorkoutExercise {
|
|
70
|
+
_equipArr: WorkoutEquipmentType[];
|
|
71
|
+
_bestCalArr: WorkoutExerciseCalibration[];
|
|
72
|
+
_bestSetArr: WorkoutSet[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Raw shape of Pipeline B results. */
|
|
76
|
+
interface PipelineBRow {
|
|
77
|
+
_id: string;
|
|
78
|
+
lastSessionExercise: WorkoutSessionExercise;
|
|
79
|
+
_lastFirstSetArr: WorkoutSet[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const collection = await this.getCollection();
|
|
83
|
+
const collName = this.collectionName;
|
|
84
|
+
|
|
85
|
+
// Pipeline A: Exercise base + equipmentType + bestCalibration + bestSet
|
|
86
|
+
const pipelineAPromise = collection
|
|
87
|
+
.aggregate<PipelineARow>([
|
|
88
|
+
// Start with all exercises for this user
|
|
89
|
+
{ $match: { docType: WorkoutExercise_docType, userId } },
|
|
90
|
+
// Join equipment type by workoutEquipmentTypeId
|
|
91
|
+
{
|
|
92
|
+
$lookup: {
|
|
93
|
+
from: collName,
|
|
94
|
+
let: { equipId: '$workoutEquipmentTypeId' },
|
|
95
|
+
pipeline: [
|
|
96
|
+
{
|
|
97
|
+
$match: {
|
|
98
|
+
$expr: { $eq: ['$_id', '$$equipId'] },
|
|
99
|
+
docType: WorkoutEquipmentType_docType
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
as: '_equipArr'
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
// Find the best calibration by highest estimated 1RM
|
|
107
|
+
{
|
|
108
|
+
$lookup: {
|
|
109
|
+
from: collName,
|
|
110
|
+
let: { exId: '$_id' },
|
|
111
|
+
pipeline: [
|
|
112
|
+
{
|
|
113
|
+
$match: {
|
|
114
|
+
$expr: { $eq: ['$workoutExerciseId', '$$exId'] },
|
|
115
|
+
docType: WorkoutExerciseCalibration_docType
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
$addFields: {
|
|
120
|
+
_1rm: WorkoutExerciseCalibrationService.get1RMMongoExpr('$weight', '$reps')
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{ $sort: { _1rm: -1 } },
|
|
124
|
+
{ $limit: 1 },
|
|
125
|
+
{ $project: { _1rm: 0 } }
|
|
126
|
+
],
|
|
127
|
+
as: '_bestCalArr'
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
// Find the best completed set by highest estimated 1RM
|
|
131
|
+
{
|
|
132
|
+
$lookup: {
|
|
133
|
+
from: collName,
|
|
134
|
+
let: { exId: '$_id' },
|
|
135
|
+
pipeline: [
|
|
136
|
+
{
|
|
137
|
+
$match: {
|
|
138
|
+
$expr: { $eq: ['$workoutExerciseId', '$$exId'] },
|
|
139
|
+
docType: WorkoutSet_docType,
|
|
140
|
+
actualWeight: { $ne: null },
|
|
141
|
+
actualReps: { $ne: null }
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
$addFields: {
|
|
146
|
+
_1rm: WorkoutExerciseCalibrationService.get1RMMongoExpr(
|
|
147
|
+
'$actualWeight',
|
|
148
|
+
'$actualReps'
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{ $sort: { _1rm: -1 } },
|
|
153
|
+
{ $limit: 1 },
|
|
154
|
+
{ $project: { _1rm: 0 } }
|
|
155
|
+
],
|
|
156
|
+
as: '_bestSetArr'
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
])
|
|
160
|
+
.toArray();
|
|
161
|
+
|
|
162
|
+
// Pipeline B: lastSessionExercise + lastFirstSet per exercise
|
|
163
|
+
const pipelineBPromise = collection
|
|
164
|
+
.aggregate<PipelineBRow>([
|
|
165
|
+
// Start with all completed sessions for this user
|
|
166
|
+
{ $match: { docType: WorkoutSession_docType, userId, complete: true } },
|
|
167
|
+
// Most recent sessions first
|
|
168
|
+
{ $sort: { startTime: -1 } },
|
|
169
|
+
// Join each session's session exercises
|
|
170
|
+
{
|
|
171
|
+
$lookup: {
|
|
172
|
+
from: collName,
|
|
173
|
+
let: { sessId: '$_id' },
|
|
174
|
+
pipeline: [
|
|
175
|
+
{
|
|
176
|
+
$match: {
|
|
177
|
+
$expr: { $eq: ['$workoutSessionId', '$$sessId'] },
|
|
178
|
+
docType: WorkoutSessionExercise_docType
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
as: '_se'
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
// One row per session exercise (unwind coalescence optimization)
|
|
186
|
+
{ $unwind: '$_se' },
|
|
187
|
+
// Check for non-deload sets (plannedRir is not null)
|
|
188
|
+
{
|
|
189
|
+
$lookup: {
|
|
190
|
+
from: collName,
|
|
191
|
+
let: { seId: '$_se._id' },
|
|
192
|
+
pipeline: [
|
|
193
|
+
{
|
|
194
|
+
$match: {
|
|
195
|
+
$expr: { $eq: ['$workoutSessionExerciseId', '$$seId'] },
|
|
196
|
+
docType: WorkoutSet_docType,
|
|
197
|
+
plannedRir: { $ne: null }
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
{ $limit: 1 }
|
|
201
|
+
],
|
|
202
|
+
as: '_nonDeload'
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
// Keep only session exercises with at least one non-deload set
|
|
206
|
+
{ $match: { '_nonDeload.0': { $exists: true } } },
|
|
207
|
+
// Per exercise, keep only the most recent session exercise ($first after sort)
|
|
208
|
+
{
|
|
209
|
+
$group: {
|
|
210
|
+
_id: '$_se.workoutExerciseId',
|
|
211
|
+
lastSessionExercise: { $first: '$_se' }
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
// Join the first set from that session exercise's setOrder
|
|
215
|
+
{
|
|
216
|
+
$lookup: {
|
|
217
|
+
from: collName,
|
|
218
|
+
let: { firstSetId: { $arrayElemAt: ['$lastSessionExercise.setOrder', 0] } },
|
|
219
|
+
pipeline: [
|
|
220
|
+
{
|
|
221
|
+
$match: {
|
|
222
|
+
$expr: { $eq: ['$_id', '$$firstSetId'] },
|
|
223
|
+
docType: WorkoutSet_docType
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
],
|
|
227
|
+
as: '_lastFirstSetArr'
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
])
|
|
231
|
+
.toArray();
|
|
232
|
+
|
|
233
|
+
const [rawExercises, rawLastSessions] = await Promise.all([pipelineAPromise, pipelineBPromise]);
|
|
234
|
+
|
|
235
|
+
// Build lookup map from Pipeline B
|
|
236
|
+
const lastSessionMap = new Map<
|
|
237
|
+
string,
|
|
238
|
+
{
|
|
239
|
+
lastSessionExercise: WorkoutSessionExercise;
|
|
240
|
+
lastFirstSet: WorkoutSet | null;
|
|
241
|
+
}
|
|
242
|
+
>();
|
|
243
|
+
for (const row of rawLastSessions) {
|
|
244
|
+
lastSessionMap.set(row._id, {
|
|
245
|
+
lastSessionExercise: row.lastSessionExercise,
|
|
246
|
+
lastFirstSet: row._lastFirstSetArr[0] ?? null
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Assemble and validate CTOs
|
|
251
|
+
return rawExercises.map((raw) => {
|
|
252
|
+
const { _equipArr, _bestCalArr, _bestSetArr, ...exerciseFields } = raw;
|
|
253
|
+
if (_equipArr.length === 0) {
|
|
254
|
+
throw new Error(`Equipment type not found for exercise ${raw._id}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const lastData = lastSessionMap.get(raw._id as string);
|
|
258
|
+
|
|
259
|
+
return WorkoutExerciseCTOSchema.parse({
|
|
260
|
+
...exerciseFields,
|
|
261
|
+
equipmentType: _equipArr[0],
|
|
262
|
+
bestCalibration: _bestCalArr[0] ?? null,
|
|
263
|
+
bestSet: _bestSetArr[0] ?? null,
|
|
264
|
+
lastSessionExercise: lastData?.lastSessionExercise ?? null,
|
|
265
|
+
lastFirstSet: lastData?.lastFirstSet ?? null
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
}
|
|
40
269
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { User, WorkoutMuscleGroup } from '@aneuhold/core-ts-db-lib';
|
|
1
|
+
import type { User, WorkoutMuscleGroup, WorkoutMuscleGroupVolumeCTO } from '@aneuhold/core-ts-db-lib';
|
|
2
|
+
import type { UUID } from 'crypto';
|
|
2
3
|
import type { RepoListeners } from '../../services/RepoSubscriptionService.js';
|
|
3
4
|
import WorkoutBaseWithUserIdRepository from './WorkoutBaseWithUserIdRepository.js';
|
|
4
5
|
/**
|
|
@@ -16,5 +17,13 @@ export default class WorkoutMuscleGroupRepository extends WorkoutBaseWithUserIdR
|
|
|
16
17
|
* Gets the singleton instance of the {@link WorkoutMuscleGroupRepository}.
|
|
17
18
|
*/
|
|
18
19
|
static getRepo(): WorkoutMuscleGroupRepository;
|
|
20
|
+
/**
|
|
21
|
+
* Builds {@link WorkoutMuscleGroupVolumeCTO} objects for all muscle groups
|
|
22
|
+
* belonging to a user. Fetches muscle groups directly, then runs an
|
|
23
|
+
* aggregation pipeline for volume history across completed mesocycles.
|
|
24
|
+
*
|
|
25
|
+
* @param userId The user whose muscle group volume CTOs to build.
|
|
26
|
+
*/
|
|
27
|
+
buildMuscleGroupVolumeCTOsForUser(userId: UUID): Promise<WorkoutMuscleGroupVolumeCTO[]>;
|
|
19
28
|
}
|
|
20
29
|
//# sourceMappingURL=WorkoutMuscleGroupRepository.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkoutMuscleGroupRepository.d.ts","sourceRoot":"","sources":["../../../src/repositories/workout/WorkoutMuscleGroupRepository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"WorkoutMuscleGroupRepository.d.ts","sourceRoot":"","sources":["../../../src/repositories/workout/WorkoutMuscleGroupRepository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,kBAAkB,EAClB,2BAA2B,EAC5B,MAAM,0BAA0B,CAAC;AAUlC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2CAA2C,CAAC;AAE/E,OAAO,+BAA+B,MAAM,sCAAsC,CAAC;AAEnF;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,4BAA6B,SAAQ,+BAA+B,CAAC,kBAAkB,CAAC;IAC3G,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAA+B;IAEhE;;OAEG;IACH,OAAO;IAIP,MAAM,CAAC,uBAAuB,IAAI,aAAa,CAAC,IAAI,CAAC;IAYrD,SAAS,CAAC,gBAAgB,IAAI,IAAI;IAElC;;OAEG;WACW,OAAO,IAAI,4BAA4B;IAOrD;;;;;;OAMG;IACG,iCAAiC,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC,2BAA2B,EAAE,CAAC;CAsQ9F"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WorkoutMuscleGroup_docType } from '@aneuhold/core-ts-db-lib';
|
|
1
|
+
import { WorkoutExercise_docType, WorkoutMesocycle_docType, WorkoutMicrocycle_docType, WorkoutMuscleGroup_docType, WorkoutMuscleGroupVolumeCTOSchema, WorkoutSession_docType, WorkoutSessionExercise_docType } from '@aneuhold/core-ts-db-lib';
|
|
2
2
|
import WorkoutMuscleGroupValidator from '../../validators/workout/MuscleGroupValidator.js';
|
|
3
3
|
import WorkoutBaseWithUserIdRepository from './WorkoutBaseWithUserIdRepository.js';
|
|
4
4
|
/**
|
|
@@ -33,5 +33,251 @@ export default class WorkoutMuscleGroupRepository extends WorkoutBaseWithUserIdR
|
|
|
33
33
|
}
|
|
34
34
|
return WorkoutMuscleGroupRepository.singletonInstance;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Builds {@link WorkoutMuscleGroupVolumeCTO} objects for all muscle groups
|
|
38
|
+
* belonging to a user. Fetches muscle groups directly, then runs an
|
|
39
|
+
* aggregation pipeline for volume history across completed mesocycles.
|
|
40
|
+
*
|
|
41
|
+
* @param userId The user whose muscle group volume CTOs to build.
|
|
42
|
+
*/
|
|
43
|
+
async buildMuscleGroupVolumeCTOsForUser(userId) {
|
|
44
|
+
const collection = await this.getCollection();
|
|
45
|
+
const collName = this.collectionName;
|
|
46
|
+
// Part A: fetch all muscle groups
|
|
47
|
+
const muscleGroupsPromise = this.getAllForUser(userId);
|
|
48
|
+
// Part B: aggregation pipeline for volume data
|
|
49
|
+
const volumePromise = collection
|
|
50
|
+
.aggregate([
|
|
51
|
+
// Start with completed mesocycles for this user
|
|
52
|
+
{
|
|
53
|
+
$match: {
|
|
54
|
+
docType: WorkoutMesocycle_docType,
|
|
55
|
+
userId,
|
|
56
|
+
completedDate: { $type: 'date' }
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
// Most recent first
|
|
60
|
+
{ $sort: { completedDate: -1 } },
|
|
61
|
+
// Limit to 10 most recent
|
|
62
|
+
{ $limit: 10 },
|
|
63
|
+
// Join microcycles, sorted by start date
|
|
64
|
+
{
|
|
65
|
+
$lookup: {
|
|
66
|
+
from: collName,
|
|
67
|
+
let: { mesoId: '$_id' },
|
|
68
|
+
pipeline: [
|
|
69
|
+
{
|
|
70
|
+
$match: {
|
|
71
|
+
$expr: { $eq: ['$workoutMesocycleId', '$$mesoId'] },
|
|
72
|
+
docType: WorkoutMicrocycle_docType
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{ $sort: { startDate: 1 } }
|
|
76
|
+
],
|
|
77
|
+
as: '_microcycles'
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
// One row per microcycle, tracking index for starting/peak logic
|
|
81
|
+
{ $unwind: { path: '$_microcycles', includeArrayIndex: '_microIdx' } },
|
|
82
|
+
// Join sessions for each microcycle
|
|
83
|
+
{
|
|
84
|
+
$lookup: {
|
|
85
|
+
from: collName,
|
|
86
|
+
let: { microId: '$_microcycles._id' },
|
|
87
|
+
pipeline: [
|
|
88
|
+
{
|
|
89
|
+
$match: {
|
|
90
|
+
$expr: { $eq: ['$workoutMicrocycleId', '$$microId'] },
|
|
91
|
+
docType: WorkoutSession_docType
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
as: '_sessions'
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
// One row per session (drop empty microcycles)
|
|
99
|
+
{ $unwind: { path: '$_sessions', preserveNullAndEmptyArrays: false } },
|
|
100
|
+
// Join session exercises
|
|
101
|
+
{
|
|
102
|
+
$lookup: {
|
|
103
|
+
from: collName,
|
|
104
|
+
let: { sessId: '$_sessions._id' },
|
|
105
|
+
pipeline: [
|
|
106
|
+
{
|
|
107
|
+
$match: {
|
|
108
|
+
$expr: { $eq: ['$workoutSessionId', '$$sessId'] },
|
|
109
|
+
docType: WorkoutSessionExercise_docType
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
as: '_se'
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
// One row per session exercise
|
|
117
|
+
{ $unwind: '$_se' },
|
|
118
|
+
// Join exercise doc to get muscle group arrays
|
|
119
|
+
{
|
|
120
|
+
$lookup: {
|
|
121
|
+
from: collName,
|
|
122
|
+
let: { exId: '$_se.workoutExerciseId' },
|
|
123
|
+
pipeline: [
|
|
124
|
+
{
|
|
125
|
+
$match: {
|
|
126
|
+
$expr: { $eq: ['$_id', '$$exId'] },
|
|
127
|
+
docType: WorkoutExercise_docType
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
],
|
|
131
|
+
as: '_exercise'
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
// One row per exercise
|
|
135
|
+
{ $unwind: '$_exercise' },
|
|
136
|
+
// Combine primary + secondary muscle group IDs
|
|
137
|
+
{
|
|
138
|
+
$addFields: {
|
|
139
|
+
_muscleGroups: {
|
|
140
|
+
$concatArrays: [
|
|
141
|
+
{ $ifNull: ['$_exercise.primaryMuscleGroups', []] },
|
|
142
|
+
{ $ifNull: ['$_exercise.secondaryMuscleGroups', []] }
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
// One row per (mesocycle, microcycle, session exercise, muscle group)
|
|
148
|
+
{ $unwind: '$_muscleGroups' },
|
|
149
|
+
// First group: per (mesocycle, muscleGroup, microcycleIndex) — count sets, sum metrics
|
|
150
|
+
{
|
|
151
|
+
$group: {
|
|
152
|
+
_id: {
|
|
153
|
+
mesocycleId: '$_id',
|
|
154
|
+
muscleGroupId: '$_muscleGroups',
|
|
155
|
+
microIdx: '$_microIdx'
|
|
156
|
+
},
|
|
157
|
+
setCount: { $sum: { $size: { $ifNull: ['$_se.setOrder', []] } } },
|
|
158
|
+
rsmSum: {
|
|
159
|
+
$sum: {
|
|
160
|
+
$cond: [
|
|
161
|
+
{
|
|
162
|
+
$and: [
|
|
163
|
+
{ $ne: [{ $ifNull: ['$_se.rsm.mindMuscleConnection', null] }, null] },
|
|
164
|
+
{ $ne: [{ $ifNull: ['$_se.rsm.pump', null] }, null] },
|
|
165
|
+
{ $ne: [{ $ifNull: ['$_se.rsm.disruption', null] }, null] }
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
$add: ['$_se.rsm.mindMuscleConnection', '$_se.rsm.pump', '$_se.rsm.disruption']
|
|
170
|
+
},
|
|
171
|
+
0
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
rsmCount: {
|
|
176
|
+
$sum: {
|
|
177
|
+
$cond: [
|
|
178
|
+
{
|
|
179
|
+
$and: [
|
|
180
|
+
{ $ne: [{ $ifNull: ['$_se.rsm.mindMuscleConnection', null] }, null] },
|
|
181
|
+
{ $ne: [{ $ifNull: ['$_se.rsm.pump', null] }, null] },
|
|
182
|
+
{ $ne: [{ $ifNull: ['$_se.rsm.disruption', null] }, null] }
|
|
183
|
+
]
|
|
184
|
+
},
|
|
185
|
+
1,
|
|
186
|
+
0
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
sorenessSum: {
|
|
191
|
+
$sum: {
|
|
192
|
+
$cond: [
|
|
193
|
+
{ $ne: [{ $ifNull: ['$_se.sorenessScore', null] }, null] },
|
|
194
|
+
'$_se.sorenessScore',
|
|
195
|
+
0
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
sorenessCount: {
|
|
200
|
+
$sum: {
|
|
201
|
+
$cond: [{ $ne: [{ $ifNull: ['$_se.sorenessScore', null] }, null] }, 1, 0]
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
performanceSum: {
|
|
205
|
+
$sum: {
|
|
206
|
+
$cond: [
|
|
207
|
+
{ $ne: [{ $ifNull: ['$_se.performanceScore', null] }, null] },
|
|
208
|
+
'$_se.performanceScore',
|
|
209
|
+
0
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
performanceCount: {
|
|
214
|
+
$sum: {
|
|
215
|
+
$cond: [{ $ne: [{ $ifNull: ['$_se.performanceScore', null] }, null] }, 1, 0]
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
recoveryCount: {
|
|
219
|
+
$sum: { $cond: [{ $eq: ['$_se.isRecoveryExercise', true] }, 1, 0] }
|
|
220
|
+
},
|
|
221
|
+
cycleType: { $first: '$cycleType' },
|
|
222
|
+
completedDate: { $first: '$completedDate' }
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
// Sort by microcycle index so $first gives the starting microcycle
|
|
226
|
+
{ $sort: { '_id.microIdx': 1 } },
|
|
227
|
+
// Second group: per (mesocycle, muscleGroup) — starting/peak set counts, aggregate metrics
|
|
228
|
+
{
|
|
229
|
+
$group: {
|
|
230
|
+
_id: {
|
|
231
|
+
mesocycleId: '$_id.mesocycleId',
|
|
232
|
+
muscleGroupId: '$_id.muscleGroupId'
|
|
233
|
+
},
|
|
234
|
+
startingSetCount: { $first: '$setCount' },
|
|
235
|
+
peakSetCount: { $max: '$setCount' },
|
|
236
|
+
rsmSum: { $sum: '$rsmSum' },
|
|
237
|
+
rsmCount: { $sum: '$rsmCount' },
|
|
238
|
+
sorenessSum: { $sum: '$sorenessSum' },
|
|
239
|
+
sorenessCount: { $sum: '$sorenessCount' },
|
|
240
|
+
performanceSum: { $sum: '$performanceSum' },
|
|
241
|
+
performanceCount: { $sum: '$performanceCount' },
|
|
242
|
+
recoverySessionCount: { $sum: '$recoveryCount' },
|
|
243
|
+
cycleType: { $first: '$cycleType' },
|
|
244
|
+
completedDate: { $first: '$completedDate' }
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
])
|
|
248
|
+
.toArray();
|
|
249
|
+
const [muscleGroups, volumeRows] = await Promise.all([muscleGroupsPromise, volumePromise]);
|
|
250
|
+
// Group volume rows by muscleGroupId
|
|
251
|
+
const historyMap = new Map();
|
|
252
|
+
for (const row of volumeRows) {
|
|
253
|
+
const mgId = row._id.muscleGroupId;
|
|
254
|
+
if (!historyMap.has(mgId)) {
|
|
255
|
+
historyMap.set(mgId, []);
|
|
256
|
+
}
|
|
257
|
+
const arr = historyMap.get(mgId);
|
|
258
|
+
if (arr) {
|
|
259
|
+
arr.push(row);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Assemble CTOs
|
|
263
|
+
return muscleGroups.map((mg) => {
|
|
264
|
+
const rows = historyMap.get(mg._id) ?? [];
|
|
265
|
+
const mesocycleHistory = rows.map((row) => ({
|
|
266
|
+
mesocycleId: row._id.mesocycleId,
|
|
267
|
+
cycleType: row.cycleType,
|
|
268
|
+
startingSetCount: row.startingSetCount,
|
|
269
|
+
peakSetCount: row.peakSetCount,
|
|
270
|
+
avgRsm: row.rsmCount > 0 ? row.rsmSum / row.rsmCount : null,
|
|
271
|
+
avgSorenessScore: row.sorenessCount > 0 ? row.sorenessSum / row.sorenessCount : null,
|
|
272
|
+
avgPerformanceScore: row.performanceCount > 0 ? row.performanceSum / row.performanceCount : null,
|
|
273
|
+
recoverySessionCount: row.recoverySessionCount,
|
|
274
|
+
completedDate: row.completedDate
|
|
275
|
+
}));
|
|
276
|
+
return WorkoutMuscleGroupVolumeCTOSchema.parse({
|
|
277
|
+
...mg,
|
|
278
|
+
mesocycleHistory
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
36
282
|
}
|
|
37
283
|
//# sourceMappingURL=WorkoutMuscleGroupRepository.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkoutMuscleGroupRepository.js","sourceRoot":"","sources":["../../../src/repositories/workout/WorkoutMuscleGroupRepository.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"WorkoutMuscleGroupRepository.js","sourceRoot":"","sources":["../../../src/repositories/workout/WorkoutMuscleGroupRepository.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
User,
|
|
3
|
+
WorkoutMuscleGroup,
|
|
4
|
+
WorkoutMuscleGroupVolumeCTO
|
|
5
|
+
} from '@aneuhold/core-ts-db-lib';
|
|
6
|
+
import {
|
|
7
|
+
WorkoutExercise_docType,
|
|
8
|
+
WorkoutMesocycle_docType,
|
|
9
|
+
WorkoutMicrocycle_docType,
|
|
10
|
+
WorkoutMuscleGroup_docType,
|
|
11
|
+
WorkoutMuscleGroupVolumeCTOSchema,
|
|
12
|
+
WorkoutSession_docType,
|
|
13
|
+
WorkoutSessionExercise_docType
|
|
14
|
+
} from '@aneuhold/core-ts-db-lib';
|
|
15
|
+
import type { UUID } from 'crypto';
|
|
3
16
|
import type { RepoListeners } from '../../services/RepoSubscriptionService.js';
|
|
4
17
|
import WorkoutMuscleGroupValidator from '../../validators/workout/MuscleGroupValidator.js';
|
|
5
18
|
import WorkoutBaseWithUserIdRepository from './WorkoutBaseWithUserIdRepository.js';
|
|
@@ -40,4 +53,274 @@ export default class WorkoutMuscleGroupRepository extends WorkoutBaseWithUserIdR
|
|
|
40
53
|
}
|
|
41
54
|
return WorkoutMuscleGroupRepository.singletonInstance;
|
|
42
55
|
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Builds {@link WorkoutMuscleGroupVolumeCTO} objects for all muscle groups
|
|
59
|
+
* belonging to a user. Fetches muscle groups directly, then runs an
|
|
60
|
+
* aggregation pipeline for volume history across completed mesocycles.
|
|
61
|
+
*
|
|
62
|
+
* @param userId The user whose muscle group volume CTOs to build.
|
|
63
|
+
*/
|
|
64
|
+
async buildMuscleGroupVolumeCTOsForUser(userId: UUID): Promise<WorkoutMuscleGroupVolumeCTO[]> {
|
|
65
|
+
/** Raw shape of volume pipeline results (grouped by mesocycle + muscle group). */
|
|
66
|
+
interface VolumeRow {
|
|
67
|
+
_id: { mesocycleId: string; muscleGroupId: string };
|
|
68
|
+
startingSetCount: number;
|
|
69
|
+
peakSetCount: number;
|
|
70
|
+
rsmSum: number;
|
|
71
|
+
rsmCount: number;
|
|
72
|
+
sorenessSum: number;
|
|
73
|
+
sorenessCount: number;
|
|
74
|
+
performanceSum: number;
|
|
75
|
+
performanceCount: number;
|
|
76
|
+
recoverySessionCount: number;
|
|
77
|
+
cycleType: string;
|
|
78
|
+
completedDate: Date | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const collection = await this.getCollection();
|
|
82
|
+
const collName = this.collectionName;
|
|
83
|
+
|
|
84
|
+
// Part A: fetch all muscle groups
|
|
85
|
+
const muscleGroupsPromise = this.getAllForUser(userId);
|
|
86
|
+
|
|
87
|
+
// Part B: aggregation pipeline for volume data
|
|
88
|
+
const volumePromise = collection
|
|
89
|
+
.aggregate<VolumeRow>([
|
|
90
|
+
// Start with completed mesocycles for this user
|
|
91
|
+
{
|
|
92
|
+
$match: {
|
|
93
|
+
docType: WorkoutMesocycle_docType,
|
|
94
|
+
userId,
|
|
95
|
+
completedDate: { $type: 'date' }
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
// Most recent first
|
|
99
|
+
{ $sort: { completedDate: -1 } },
|
|
100
|
+
// Limit to 10 most recent
|
|
101
|
+
{ $limit: 10 },
|
|
102
|
+
// Join microcycles, sorted by start date
|
|
103
|
+
{
|
|
104
|
+
$lookup: {
|
|
105
|
+
from: collName,
|
|
106
|
+
let: { mesoId: '$_id' },
|
|
107
|
+
pipeline: [
|
|
108
|
+
{
|
|
109
|
+
$match: {
|
|
110
|
+
$expr: { $eq: ['$workoutMesocycleId', '$$mesoId'] },
|
|
111
|
+
docType: WorkoutMicrocycle_docType
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{ $sort: { startDate: 1 } }
|
|
115
|
+
],
|
|
116
|
+
as: '_microcycles'
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
// One row per microcycle, tracking index for starting/peak logic
|
|
120
|
+
{ $unwind: { path: '$_microcycles', includeArrayIndex: '_microIdx' } },
|
|
121
|
+
// Join sessions for each microcycle
|
|
122
|
+
{
|
|
123
|
+
$lookup: {
|
|
124
|
+
from: collName,
|
|
125
|
+
let: { microId: '$_microcycles._id' },
|
|
126
|
+
pipeline: [
|
|
127
|
+
{
|
|
128
|
+
$match: {
|
|
129
|
+
$expr: { $eq: ['$workoutMicrocycleId', '$$microId'] },
|
|
130
|
+
docType: WorkoutSession_docType
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
as: '_sessions'
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
// One row per session (drop empty microcycles)
|
|
138
|
+
{ $unwind: { path: '$_sessions', preserveNullAndEmptyArrays: false } },
|
|
139
|
+
// Join session exercises
|
|
140
|
+
{
|
|
141
|
+
$lookup: {
|
|
142
|
+
from: collName,
|
|
143
|
+
let: { sessId: '$_sessions._id' },
|
|
144
|
+
pipeline: [
|
|
145
|
+
{
|
|
146
|
+
$match: {
|
|
147
|
+
$expr: { $eq: ['$workoutSessionId', '$$sessId'] },
|
|
148
|
+
docType: WorkoutSessionExercise_docType
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
as: '_se'
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
// One row per session exercise
|
|
156
|
+
{ $unwind: '$_se' },
|
|
157
|
+
// Join exercise doc to get muscle group arrays
|
|
158
|
+
{
|
|
159
|
+
$lookup: {
|
|
160
|
+
from: collName,
|
|
161
|
+
let: { exId: '$_se.workoutExerciseId' },
|
|
162
|
+
pipeline: [
|
|
163
|
+
{
|
|
164
|
+
$match: {
|
|
165
|
+
$expr: { $eq: ['$_id', '$$exId'] },
|
|
166
|
+
docType: WorkoutExercise_docType
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
as: '_exercise'
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
// One row per exercise
|
|
174
|
+
{ $unwind: '$_exercise' },
|
|
175
|
+
// Combine primary + secondary muscle group IDs
|
|
176
|
+
{
|
|
177
|
+
$addFields: {
|
|
178
|
+
_muscleGroups: {
|
|
179
|
+
$concatArrays: [
|
|
180
|
+
{ $ifNull: ['$_exercise.primaryMuscleGroups', []] },
|
|
181
|
+
{ $ifNull: ['$_exercise.secondaryMuscleGroups', []] }
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
// One row per (mesocycle, microcycle, session exercise, muscle group)
|
|
187
|
+
{ $unwind: '$_muscleGroups' },
|
|
188
|
+
// First group: per (mesocycle, muscleGroup, microcycleIndex) — count sets, sum metrics
|
|
189
|
+
{
|
|
190
|
+
$group: {
|
|
191
|
+
_id: {
|
|
192
|
+
mesocycleId: '$_id',
|
|
193
|
+
muscleGroupId: '$_muscleGroups',
|
|
194
|
+
microIdx: '$_microIdx'
|
|
195
|
+
},
|
|
196
|
+
setCount: { $sum: { $size: { $ifNull: ['$_se.setOrder', []] } } },
|
|
197
|
+
rsmSum: {
|
|
198
|
+
$sum: {
|
|
199
|
+
$cond: [
|
|
200
|
+
{
|
|
201
|
+
$and: [
|
|
202
|
+
{ $ne: [{ $ifNull: ['$_se.rsm.mindMuscleConnection', null] }, null] },
|
|
203
|
+
{ $ne: [{ $ifNull: ['$_se.rsm.pump', null] }, null] },
|
|
204
|
+
{ $ne: [{ $ifNull: ['$_se.rsm.disruption', null] }, null] }
|
|
205
|
+
]
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
$add: ['$_se.rsm.mindMuscleConnection', '$_se.rsm.pump', '$_se.rsm.disruption']
|
|
209
|
+
},
|
|
210
|
+
0
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
rsmCount: {
|
|
215
|
+
$sum: {
|
|
216
|
+
$cond: [
|
|
217
|
+
{
|
|
218
|
+
$and: [
|
|
219
|
+
{ $ne: [{ $ifNull: ['$_se.rsm.mindMuscleConnection', null] }, null] },
|
|
220
|
+
{ $ne: [{ $ifNull: ['$_se.rsm.pump', null] }, null] },
|
|
221
|
+
{ $ne: [{ $ifNull: ['$_se.rsm.disruption', null] }, null] }
|
|
222
|
+
]
|
|
223
|
+
},
|
|
224
|
+
1,
|
|
225
|
+
0
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
sorenessSum: {
|
|
230
|
+
$sum: {
|
|
231
|
+
$cond: [
|
|
232
|
+
{ $ne: [{ $ifNull: ['$_se.sorenessScore', null] }, null] },
|
|
233
|
+
'$_se.sorenessScore',
|
|
234
|
+
0
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
sorenessCount: {
|
|
239
|
+
$sum: {
|
|
240
|
+
$cond: [{ $ne: [{ $ifNull: ['$_se.sorenessScore', null] }, null] }, 1, 0]
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
performanceSum: {
|
|
244
|
+
$sum: {
|
|
245
|
+
$cond: [
|
|
246
|
+
{ $ne: [{ $ifNull: ['$_se.performanceScore', null] }, null] },
|
|
247
|
+
'$_se.performanceScore',
|
|
248
|
+
0
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
performanceCount: {
|
|
253
|
+
$sum: {
|
|
254
|
+
$cond: [{ $ne: [{ $ifNull: ['$_se.performanceScore', null] }, null] }, 1, 0]
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
recoveryCount: {
|
|
258
|
+
$sum: { $cond: [{ $eq: ['$_se.isRecoveryExercise', true] }, 1, 0] }
|
|
259
|
+
},
|
|
260
|
+
cycleType: { $first: '$cycleType' },
|
|
261
|
+
completedDate: { $first: '$completedDate' }
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
// Sort by microcycle index so $first gives the starting microcycle
|
|
265
|
+
{ $sort: { '_id.microIdx': 1 } },
|
|
266
|
+
// Second group: per (mesocycle, muscleGroup) — starting/peak set counts, aggregate metrics
|
|
267
|
+
{
|
|
268
|
+
$group: {
|
|
269
|
+
_id: {
|
|
270
|
+
mesocycleId: '$_id.mesocycleId',
|
|
271
|
+
muscleGroupId: '$_id.muscleGroupId'
|
|
272
|
+
},
|
|
273
|
+
startingSetCount: { $first: '$setCount' },
|
|
274
|
+
peakSetCount: { $max: '$setCount' },
|
|
275
|
+
rsmSum: { $sum: '$rsmSum' },
|
|
276
|
+
rsmCount: { $sum: '$rsmCount' },
|
|
277
|
+
sorenessSum: { $sum: '$sorenessSum' },
|
|
278
|
+
sorenessCount: { $sum: '$sorenessCount' },
|
|
279
|
+
performanceSum: { $sum: '$performanceSum' },
|
|
280
|
+
performanceCount: { $sum: '$performanceCount' },
|
|
281
|
+
recoverySessionCount: { $sum: '$recoveryCount' },
|
|
282
|
+
cycleType: { $first: '$cycleType' },
|
|
283
|
+
completedDate: { $first: '$completedDate' }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
])
|
|
287
|
+
.toArray();
|
|
288
|
+
|
|
289
|
+
const [muscleGroups, volumeRows] = await Promise.all([muscleGroupsPromise, volumePromise]);
|
|
290
|
+
|
|
291
|
+
// Group volume rows by muscleGroupId
|
|
292
|
+
const historyMap = new Map<string, VolumeRow[]>();
|
|
293
|
+
for (const row of volumeRows) {
|
|
294
|
+
const mgId = row._id.muscleGroupId;
|
|
295
|
+
if (!historyMap.has(mgId)) {
|
|
296
|
+
historyMap.set(mgId, []);
|
|
297
|
+
}
|
|
298
|
+
const arr = historyMap.get(mgId);
|
|
299
|
+
if (arr) {
|
|
300
|
+
arr.push(row);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Assemble CTOs
|
|
305
|
+
return muscleGroups.map((mg) => {
|
|
306
|
+
const rows = historyMap.get(mg._id as string) ?? [];
|
|
307
|
+
const mesocycleHistory = rows.map((row) => ({
|
|
308
|
+
mesocycleId: row._id.mesocycleId,
|
|
309
|
+
cycleType: row.cycleType,
|
|
310
|
+
startingSetCount: row.startingSetCount,
|
|
311
|
+
peakSetCount: row.peakSetCount,
|
|
312
|
+
avgRsm: row.rsmCount > 0 ? row.rsmSum / row.rsmCount : null,
|
|
313
|
+
avgSorenessScore: row.sorenessCount > 0 ? row.sorenessSum / row.sorenessCount : null,
|
|
314
|
+
avgPerformanceScore:
|
|
315
|
+
row.performanceCount > 0 ? row.performanceSum / row.performanceCount : null,
|
|
316
|
+
recoverySessionCount: row.recoverySessionCount,
|
|
317
|
+
completedDate: row.completedDate
|
|
318
|
+
}));
|
|
319
|
+
|
|
320
|
+
return WorkoutMuscleGroupVolumeCTOSchema.parse({
|
|
321
|
+
...mg,
|
|
322
|
+
mesocycleHistory
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
}
|
|
43
326
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@aneuhold/be-ts-db-lib",
|
|
3
3
|
"author": "Anton G. Neuhold Jr.",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "4.2.
|
|
5
|
+
"version": "4.2.13",
|
|
6
6
|
"description": "A backend database library meant to actually interact with various databases in personal projects",
|
|
7
7
|
"packageManager": "pnpm@10.25.0",
|
|
8
8
|
"type": "module",
|
|
@@ -58,16 +58,16 @@
|
|
|
58
58
|
"MongoDB"
|
|
59
59
|
],
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@aneuhold/be-ts-lib": "^3.0
|
|
62
|
-
"@aneuhold/core-ts-db-lib": "^4.1.
|
|
63
|
-
"@aneuhold/core-ts-lib": "^2.4.
|
|
61
|
+
"@aneuhold/be-ts-lib": "^3.1.0",
|
|
62
|
+
"@aneuhold/core-ts-db-lib": "^4.1.12",
|
|
63
|
+
"@aneuhold/core-ts-lib": "^2.4.1",
|
|
64
64
|
"bson": "^7.0.0",
|
|
65
65
|
"mongodb": "^7.0.0",
|
|
66
66
|
"uuid": "^13.0.0",
|
|
67
67
|
"zod": "^4.1.13"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@aneuhold/local-npm-registry": "^0.2.
|
|
70
|
+
"@aneuhold/local-npm-registry": "^0.2.28",
|
|
71
71
|
"@aneuhold/main-scripts": "^2.8.3",
|
|
72
72
|
"@types/node": "^25.0.2",
|
|
73
73
|
"@types/node-fetch": "^2.6.13",
|