@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 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,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEtE,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;CAMnD"}
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":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,OAAO,wBAAwB,MAAM,+CAA+C,CAAC;AACrF,OAAO,+BAA+B,MAAM,sCAAsC,CAAC;AACnF,OAAO,oCAAoC,MAAM,2CAA2C,CAAC;AAE7F;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,yBAA0B,SAAQ,+BAAgD;IAC7F,MAAM,CAAC,iBAAiB,CAA6B;IAE7D;QACE,KAAK,CAAC,uBAAuB,EAAE,IAAI,wBAAwB,EAAE,CAAC,CAAC;IACjE,CAAC;IAED,MAAM,CAAC,uBAAuB;QAC5B,MAAM,YAAY,GAAG,yBAAyB,CAAC,OAAO,EAAE,CAAC;QACzD,OAAO;YACL,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE;gBAChC,MAAM,YAAY,CAAC,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YACpD,CAAC;YACD,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;gBAClC,MAAM,YAAY,CAAC,iBAAiB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YACtD,CAAC;SACF,CAAC;IACJ,CAAC;IAES,gBAAgB;QACxB,IAAI,CAAC,kBAAkB,CAAC,oCAAoC,CAAC,2BAA2B,EAAE,CAAC,CAAC;IAC9F,CAAC;IAEM,MAAM,CAAC,OAAO;QACnB,IAAI,CAAC,yBAAyB,CAAC,iBAAiB,EAAE,CAAC;YACjD,yBAAyB,CAAC,iBAAiB,GAAG,IAAI,yBAAyB,EAAE,CAAC;QAChF,CAAC;QACD,OAAO,yBAAyB,CAAC,iBAAiB,CAAC;IACrD,CAAC;CACF"}
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 { User, WorkoutExercise } from '@aneuhold/core-ts-db-lib';
2
- import { WorkoutExercise_docType } from '@aneuhold/core-ts-db-lib';
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,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEzE,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;CAMtD"}
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":"AACA,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AAEtE,OAAO,2BAA2B,MAAM,kDAAkD,CAAC;AAC3F,OAAO,+BAA+B,MAAM,sCAAsC,CAAC;AAEnF;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,4BAA6B,SAAQ,+BAAmD;IACnG,MAAM,CAAC,iBAAiB,CAAgC;IAEhE;;OAEG;IACH;QACE,KAAK,CAAC,0BAA0B,EAAE,IAAI,2BAA2B,EAAE,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,CAAC,uBAAuB;QAC5B,MAAM,eAAe,GAAG,4BAA4B,CAAC,OAAO,EAAE,CAAC;QAC/D,OAAO;YACL,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE;gBAChC,MAAM,eAAe,CAAC,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YACvD,CAAC;YACD,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;gBAClC,MAAM,eAAe,CAAC,iBAAiB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YACzD,CAAC;SACF,CAAC;IACJ,CAAC;IAES,gBAAgB,KAAU,CAAC;IAErC;;OAEG;IACI,MAAM,CAAC,OAAO;QACnB,IAAI,CAAC,4BAA4B,CAAC,iBAAiB,EAAE,CAAC;YACpD,4BAA4B,CAAC,iBAAiB,GAAG,IAAI,4BAA4B,EAAE,CAAC;QACtF,CAAC;QACD,OAAO,4BAA4B,CAAC,iBAAiB,CAAC;IACxD,CAAC;CACF"}
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 { User, WorkoutMuscleGroup } from '@aneuhold/core-ts-db-lib';
2
- import { WorkoutMuscleGroup_docType } from '@aneuhold/core-ts-db-lib';
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.11",
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.21",
62
- "@aneuhold/core-ts-db-lib": "^4.1.10",
63
- "@aneuhold/core-ts-lib": "^2.4.0",
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.27",
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",