@aneuhold/be-ts-db-lib 4.2.21 → 4.2.23
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 +20 -1
- package/lib/repositories/workout/WorkoutExerciseRepository.d.ts +5 -2
- package/lib/repositories/workout/WorkoutExerciseRepository.d.ts.map +1 -1
- package/lib/repositories/workout/WorkoutExerciseRepository.js +107 -88
- package/lib/repositories/workout/WorkoutExerciseRepository.js.map +1 -1
- package/lib/repositories/workout/WorkoutExerciseRepository.ts +63 -46
- package/lib/services/MigrationService.d.ts +5 -0
- package/lib/services/MigrationService.d.ts.map +1 -1
- package/lib/services/MigrationService.js +61 -33
- package/lib/services/MigrationService.js.map +1 -1
- package/lib/services/MigrationService.ts +77 -38
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ 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.23] (2026-04-15)
|
|
9
|
+
|
|
10
|
+
### ✅ Added
|
|
11
|
+
|
|
12
|
+
- `WorkoutExerciseRepository.buildExerciseCTOs` now runs a third parallel aggregation pipeline to populate `lastSessionExercise`/`lastSessionSets` from the true latest completed session (any cycle type, including deloads and free-form).
|
|
13
|
+
|
|
14
|
+
### 🏗️ Changed
|
|
15
|
+
|
|
16
|
+
- Repository now populates `lastAccumulationSessionExercise`/`lastAccumulationSessionSets` alongside `lastSessionExercise`/`lastSessionSets` in each `WorkoutExerciseCTO`.
|
|
17
|
+
- Updated dependencies on `@aneuhold/be-ts-lib` to `^3.1.10` and `@aneuhold/core-ts-db-lib` to `^5.0.5`.
|
|
18
|
+
|
|
19
|
+
## 🔖 [4.2.22] (2026-03-26)
|
|
20
|
+
|
|
21
|
+
### 🏗️ Changed
|
|
22
|
+
|
|
23
|
+
- Refactored `MigrationService` to migrate `DashboardUserConfig` records instead of `User.projectAccess` fields; now backfills `enableAdminPage` and `enabledFeatures.adminPage` with safe defaults.
|
|
24
|
+
- Updated dependencies on `@aneuhold/be-ts-lib` to `^3.1.9` and `@aneuhold/core-ts-db-lib` to `^5.0.4`.
|
|
25
|
+
|
|
8
26
|
## 🔖 [4.2.21] (2026-03-20)
|
|
9
27
|
|
|
10
28
|
### 🏗️ Changed
|
|
@@ -371,7 +389,8 @@ Updated dependencies: now requires `@aneuhold/core-ts-db-lib@^3.0.0`, `@aneuhold
|
|
|
371
389
|
- Updated workflow permissions to allow repository write access
|
|
372
390
|
|
|
373
391
|
<!-- Link References -->
|
|
374
|
-
|
|
392
|
+
[4.2.23]: https://github.com/aneuhold/ts-libs/compare/be-ts-db-lib-v4.2.22...be-ts-db-lib-v4.2.23
|
|
393
|
+
[4.2.22]: https://github.com/aneuhold/ts-libs/compare/be-ts-db-lib-v4.2.21...be-ts-db-lib-v4.2.22
|
|
375
394
|
[4.2.21]: https://github.com/aneuhold/ts-libs/compare/be-ts-db-lib-v4.2.20...be-ts-db-lib-v4.2.21
|
|
376
395
|
[4.2.20]: https://github.com/aneuhold/ts-libs/compare/be-ts-db-lib-v4.2.19...be-ts-db-lib-v4.2.20
|
|
377
396
|
[4.2.19]: https://github.com/aneuhold/ts-libs/compare/be-ts-db-lib-v4.2.18...be-ts-db-lib-v4.2.19
|
|
@@ -13,9 +13,12 @@ export default class WorkoutExerciseRepository extends WorkoutBaseWithUserIdRepo
|
|
|
13
13
|
static getRepo(): WorkoutExerciseRepository;
|
|
14
14
|
/**
|
|
15
15
|
* Builds {@link WorkoutExerciseCTO} objects for all exercises belonging to a user.
|
|
16
|
-
* Uses
|
|
16
|
+
* Uses three parallel MongoDB aggregation pipelines:
|
|
17
17
|
* - Pipeline A: exercise + equipmentType + bestCalibration + bestSet
|
|
18
|
-
* - Pipeline B: lastSessionExercise + lastSessionSets
|
|
18
|
+
* - Pipeline B (true latest): lastSessionExercise + lastSessionSets from the most
|
|
19
|
+
* recent completed session, regardless of cycle type / deload status.
|
|
20
|
+
* - Pipeline C (accumulation): lastAccumulationSessionExercise +
|
|
21
|
+
* lastAccumulationSessionSets from the most recent completed non-deload session.
|
|
19
22
|
*
|
|
20
23
|
* @param userId The user whose exercise CTOs to build.
|
|
21
24
|
*/
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;AAEnC,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;;;;;;;;;;OAUG;IACG,wBAAwB,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;CA+N5E"}
|
|
@@ -32,9 +32,12 @@ export default class WorkoutExerciseRepository extends WorkoutBaseWithUserIdRepo
|
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
34
|
* Builds {@link WorkoutExerciseCTO} objects for all exercises belonging to a user.
|
|
35
|
-
* Uses
|
|
35
|
+
* Uses three parallel MongoDB aggregation pipelines:
|
|
36
36
|
* - Pipeline A: exercise + equipmentType + bestCalibration + bestSet
|
|
37
|
-
* - Pipeline B: lastSessionExercise + lastSessionSets
|
|
37
|
+
* - Pipeline B (true latest): lastSessionExercise + lastSessionSets from the most
|
|
38
|
+
* recent completed session, regardless of cycle type / deload status.
|
|
39
|
+
* - Pipeline C (accumulation): lastAccumulationSessionExercise +
|
|
40
|
+
* lastAccumulationSessionSets from the most recent completed non-deload session.
|
|
38
41
|
*
|
|
39
42
|
* @param userId The user whose exercise CTOs to build.
|
|
40
43
|
*/
|
|
@@ -114,107 +117,123 @@ export default class WorkoutExerciseRepository extends WorkoutBaseWithUserIdRepo
|
|
|
114
117
|
}
|
|
115
118
|
])
|
|
116
119
|
.toArray();
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
120
|
+
// Builds the pipeline that finds, per exercise, the most recent session
|
|
121
|
+
// exercise from a completed session — optionally restricted to non-deload
|
|
122
|
+
// sessions (where plannedRir is not null on at least one set).
|
|
123
|
+
const buildLastSessionPipeline = (accumulationOnly) => {
|
|
124
|
+
const accumulationFilter = accumulationOnly
|
|
125
|
+
? [
|
|
126
|
+
// Check for non-deload sets (plannedRir is not null)
|
|
127
|
+
{
|
|
128
|
+
$lookup: {
|
|
129
|
+
from: collName,
|
|
130
|
+
let: { seId: '$_se._id' },
|
|
131
|
+
pipeline: [
|
|
132
|
+
{
|
|
133
|
+
$match: {
|
|
134
|
+
$expr: { $eq: ['$workoutSessionExerciseId', '$$seId'] },
|
|
135
|
+
docType: WorkoutSet_docType,
|
|
136
|
+
plannedRir: { $ne: null }
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{ $limit: 1 }
|
|
140
|
+
],
|
|
141
|
+
as: '_nonDeload'
|
|
135
142
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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 all sets from the session exercise's setOrder
|
|
170
|
-
{
|
|
171
|
-
$lookup: {
|
|
172
|
-
from: collName,
|
|
173
|
-
let: { setIds: '$lastSessionExercise.setOrder' },
|
|
174
|
-
pipeline: [
|
|
175
|
-
{
|
|
176
|
-
$match: {
|
|
177
|
-
$expr: { $in: ['$_id', '$$setIds'] },
|
|
178
|
-
docType: WorkoutSet_docType
|
|
143
|
+
},
|
|
144
|
+
// Keep only session exercises with at least one non-deload set
|
|
145
|
+
{ $match: { '_nonDeload.0': { $exists: true } } }
|
|
146
|
+
]
|
|
147
|
+
: [];
|
|
148
|
+
return [
|
|
149
|
+
// Start with all completed sessions for this user
|
|
150
|
+
{ $match: { docType: WorkoutSession_docType, userId, complete: true } },
|
|
151
|
+
// Most recent sessions first
|
|
152
|
+
{ $sort: { startTime: -1 } },
|
|
153
|
+
// Join each session's session exercises
|
|
154
|
+
{
|
|
155
|
+
$lookup: {
|
|
156
|
+
from: collName,
|
|
157
|
+
let: { sessId: '$_id' },
|
|
158
|
+
pipeline: [
|
|
159
|
+
{
|
|
160
|
+
$match: {
|
|
161
|
+
$expr: { $eq: ['$workoutSessionId', '$$sessId'] },
|
|
162
|
+
docType: WorkoutSessionExercise_docType
|
|
163
|
+
}
|
|
179
164
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
165
|
+
],
|
|
166
|
+
as: '_se'
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
// One row per session exercise (unwind coalescence optimization)
|
|
170
|
+
{ $unwind: '$_se' },
|
|
171
|
+
...accumulationFilter,
|
|
172
|
+
// Per exercise, keep only the most recent session exercise ($first after sort)
|
|
173
|
+
{
|
|
174
|
+
$group: {
|
|
175
|
+
_id: '$_se.workoutExerciseId',
|
|
176
|
+
lastSessionExercise: { $first: '$_se' }
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
// Join all sets from the session exercise's setOrder
|
|
180
|
+
{
|
|
181
|
+
$lookup: {
|
|
182
|
+
from: collName,
|
|
183
|
+
let: { setIds: '$lastSessionExercise.setOrder' },
|
|
184
|
+
pipeline: [
|
|
185
|
+
{
|
|
186
|
+
$match: {
|
|
187
|
+
$expr: { $in: ['$_id', '$$setIds'] },
|
|
188
|
+
docType: WorkoutSet_docType
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
// Preserve the original setOrder by sorting on index position
|
|
192
|
+
{
|
|
193
|
+
$addFields: {
|
|
194
|
+
_sortIdx: { $indexOfArray: ['$$setIds', '$_id'] }
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
{ $sort: { _sortIdx: 1 } },
|
|
198
|
+
{ $project: { _sortIdx: 0 } }
|
|
199
|
+
],
|
|
200
|
+
as: '_lastSessionSetsArr'
|
|
201
|
+
}
|
|
191
202
|
}
|
|
192
|
-
|
|
193
|
-
|
|
203
|
+
];
|
|
204
|
+
};
|
|
205
|
+
// Pipeline B: most recent session exercise per exercise (any cycle type)
|
|
206
|
+
const pipelineBPromise = collection
|
|
207
|
+
.aggregate(buildLastSessionPipeline(false))
|
|
194
208
|
.toArray();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
209
|
+
// Pipeline C: most recent accumulation (non-deload) session exercise per exercise
|
|
210
|
+
const pipelineCPromise = collection
|
|
211
|
+
.aggregate(buildLastSessionPipeline(true))
|
|
212
|
+
.toArray();
|
|
213
|
+
const [rawExercises, rawLastSessions, rawLastAccumulationSessions] = await Promise.all([
|
|
214
|
+
pipelineAPromise,
|
|
215
|
+
pipelineBPromise,
|
|
216
|
+
pipelineCPromise
|
|
217
|
+
]);
|
|
218
|
+
const lastSessionMap = new Map(rawLastSessions.map((row) => [row._id, row]));
|
|
219
|
+
const lastAccumulationMap = new Map(rawLastAccumulationSessions.map((row) => [row._id, row]));
|
|
204
220
|
// Assemble and validate CTOs
|
|
205
221
|
return rawExercises.map((raw) => {
|
|
206
222
|
const { _equipArr, _bestCalArr, _bestSetArr, ...exerciseFields } = raw;
|
|
207
223
|
if (_equipArr.length === 0) {
|
|
208
224
|
throw new Error(`Equipment type not found for exercise ${raw._id}`);
|
|
209
225
|
}
|
|
210
|
-
const
|
|
226
|
+
const lastRow = lastSessionMap.get(raw._id);
|
|
227
|
+
const lastAccumulationRow = lastAccumulationMap.get(raw._id);
|
|
211
228
|
return WorkoutExerciseCTOSchema.parse({
|
|
212
229
|
...exerciseFields,
|
|
213
230
|
equipmentType: _equipArr[0],
|
|
214
231
|
bestCalibration: _bestCalArr[0] ?? null,
|
|
215
232
|
bestSet: _bestSetArr[0] ?? null,
|
|
216
|
-
lastSessionExercise:
|
|
217
|
-
lastSessionSets:
|
|
233
|
+
lastSessionExercise: lastRow?.lastSessionExercise ?? null,
|
|
234
|
+
lastSessionSets: lastRow?._lastSessionSetsArr,
|
|
235
|
+
lastAccumulationSessionExercise: lastAccumulationRow?.lastSessionExercise ?? null,
|
|
236
|
+
lastAccumulationSessionSets: lastAccumulationRow?._lastSessionSetsArr
|
|
218
237
|
});
|
|
219
238
|
});
|
|
220
239
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkoutExerciseRepository.js","sourceRoot":"","sources":["../../../src/repositories/workout/WorkoutExerciseRepository.ts"],"names":[],"mappings":"AASA,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,kCAAkC,EAClC,iCAAiC,EACjC,wBAAwB,EACxB,sBAAsB,EACtB,8BAA8B,EAC9B,kBAAkB,EACnB,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"WorkoutExerciseRepository.js","sourceRoot":"","sources":["../../../src/repositories/workout/WorkoutExerciseRepository.ts"],"names":[],"mappings":"AASA,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,kCAAkC,EAClC,iCAAiC,EACjC,wBAAwB,EACxB,sBAAsB,EACtB,8BAA8B,EAC9B,kBAAkB,EACnB,MAAM,0BAA0B,CAAC;AAIlC,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;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,wBAAwB,CAAC,MAAY;QAezC,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;QAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC;QAErC,wEAAwE;QACxE,MAAM,gBAAgB,GAAG,UAAU;aAChC,SAAS,CAAe;YACvB,yCAAyC;YACzC,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,uBAAuB,EAAE,MAAM,EAAE,EAAE;YACxD,gDAAgD;YAChD;gBACE,OAAO,EAAE;oBACP,IAAI,EAAE,QAAQ;oBACd,GAAG,EAAE,EAAE,OAAO,EAAE,yBAAyB,EAAE;oBAC3C,QAAQ,EAAE;wBACR;4BACE,MAAM,EAAE;gCACN,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE;gCACrC,OAAO,EAAE,4BAA4B;6BACtC;yBACF;qBACF;oBACD,EAAE,EAAE,WAAW;iBAChB;aACF;YACD,qDAAqD;YACrD;gBACE,OAAO,EAAE;oBACP,IAAI,EAAE,QAAQ;oBACd,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;oBACrB,QAAQ,EAAE;wBACR;4BACE,MAAM,EAAE;gCACN,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC,oBAAoB,EAAE,QAAQ,CAAC,EAAE;gCAChD,OAAO,EAAE,kCAAkC;6BAC5C;yBACF;wBACD;4BACE,UAAU,EAAE;gCACV,IAAI,EAAE,iCAAiC,CAAC,eAAe,CAAC,SAAS,EAAE,OAAO,CAAC;6BAC5E;yBACF;wBACD,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE;wBACvB,EAAE,MAAM,EAAE,CAAC,EAAE;wBACb,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE;qBAC1B;oBACD,EAAE,EAAE,aAAa;iBAClB;aACF;YACD,uDAAuD;YACvD;gBACE,OAAO,EAAE;oBACP,IAAI,EAAE,QAAQ;oBACd,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;oBACrB,QAAQ,EAAE;wBACR;4BACE,MAAM,EAAE;gCACN,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC,oBAAoB,EAAE,QAAQ,CAAC,EAAE;gCAChD,OAAO,EAAE,kBAAkB;gCAC3B,YAAY,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE;gCAC3B,UAAU,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE;6BAC1B;yBACF;wBACD;4BACE,UAAU,EAAE;gCACV,IAAI,EAAE,iCAAiC,CAAC,eAAe,CACrD,eAAe,EACf,aAAa,CACd;6BACF;yBACF;wBACD,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE;wBACvB,EAAE,MAAM,EAAE,CAAC,EAAE;wBACb,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE;qBAC1B;oBACD,EAAE,EAAE,aAAa;iBAClB;aACF;SACF,CAAC;aACD,OAAO,EAAE,CAAC;QAEb,wEAAwE;QACxE,0EAA0E;QAC1E,+DAA+D;QAC/D,MAAM,wBAAwB,GAAG,CAAC,gBAAyB,EAAc,EAAE;YACzE,MAAM,kBAAkB,GAAe,gBAAgB;gBACrD,CAAC,CAAC;oBACE,qDAAqD;oBACrD;wBACE,OAAO,EAAE;4BACP,IAAI,EAAE,QAAQ;4BACd,GAAG,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;4BACzB,QAAQ,EAAE;gCACR;oCACE,MAAM,EAAE;wCACN,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC,2BAA2B,EAAE,QAAQ,CAAC,EAAE;wCACvD,OAAO,EAAE,kBAAkB;wCAC3B,UAAU,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE;qCAC1B;iCACF;gCACD,EAAE,MAAM,EAAE,CAAC,EAAE;6BACd;4BACD,EAAE,EAAE,YAAY;yBACjB;qBACF;oBACD,+DAA+D;oBAC/D,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE;iBAClD;gBACH,CAAC,CAAC,EAAE,CAAC;YAEP,OAAO;gBACL,kDAAkD;gBAClD,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;gBACvE,6BAA6B;gBAC7B,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;gBAC5B,wCAAwC;gBACxC;oBACE,OAAO,EAAE;wBACP,IAAI,EAAE,QAAQ;wBACd,GAAG,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE;wBACvB,QAAQ,EAAE;4BACR;gCACE,MAAM,EAAE;oCACN,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC,mBAAmB,EAAE,UAAU,CAAC,EAAE;oCACjD,OAAO,EAAE,8BAA8B;iCACxC;6BACF;yBACF;wBACD,EAAE,EAAE,KAAK;qBACV;iBACF;gBACD,iEAAiE;gBACjE,EAAE,OAAO,EAAE,MAAM,EAAE;gBACnB,GAAG,kBAAkB;gBACrB,+EAA+E;gBAC/E;oBACE,MAAM,EAAE;wBACN,GAAG,EAAE,wBAAwB;wBAC7B,mBAAmB,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE;qBACxC;iBACF;gBACD,qDAAqD;gBACrD;oBACE,OAAO,EAAE;wBACP,IAAI,EAAE,QAAQ;wBACd,GAAG,EAAE,EAAE,MAAM,EAAE,+BAA+B,EAAE;wBAChD,QAAQ,EAAE;4BACR;gCACE,MAAM,EAAE;oCACN,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE;oCACpC,OAAO,EAAE,kBAAkB;iCAC5B;6BACF;4BACD,8DAA8D;4BAC9D;gCACE,UAAU,EAAE;oCACV,QAAQ,EAAE,EAAE,aAAa,EAAE,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE;iCAClD;6BACF;4BACD,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE;4BAC1B,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE;yBAC9B;wBACD,EAAE,EAAE,qBAAqB;qBAC1B;iBACF;aACF,CAAC;QACJ,CAAC,CAAC;QAEF,yEAAyE;QACzE,MAAM,gBAAgB,GAAG,UAAU;aAChC,SAAS,CAAiB,wBAAwB,CAAC,KAAK,CAAC,CAAC;aAC1D,OAAO,EAAE,CAAC;QAEb,kFAAkF;QAClF,MAAM,gBAAgB,GAAG,UAAU;aAChC,SAAS,CAAiB,wBAAwB,CAAC,IAAI,CAAC,CAAC;aACzD,OAAO,EAAE,CAAC;QAEb,MAAM,CAAC,YAAY,EAAE,eAAe,EAAE,2BAA2B,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YACrF,gBAAgB;YAChB,gBAAgB;YAChB,gBAAgB;SACjB,CAAC,CAAC;QAEH,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QAC7E,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC,2BAA2B,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QAE9F,6BAA6B;QAC7B,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;YAC9B,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,GAAG,cAAc,EAAE,GAAG,GAAG,CAAC;YACvE,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC3B,MAAM,IAAI,KAAK,CAAC,yCAAyC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;YACtE,CAAC;YAED,MAAM,OAAO,GAAG,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,GAAa,CAAC,CAAC;YACtD,MAAM,mBAAmB,GAAG,mBAAmB,CAAC,GAAG,CAAC,GAAG,CAAC,GAAa,CAAC,CAAC;YAEvE,OAAO,wBAAwB,CAAC,KAAK,CAAC;gBACpC,GAAG,cAAc;gBACjB,aAAa,EAAE,SAAS,CAAC,CAAC,CAAC;gBAC3B,eAAe,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,IAAI;gBACvC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,IAAI;gBAC/B,mBAAmB,EAAE,OAAO,EAAE,mBAAmB,IAAI,IAAI;gBACzD,eAAe,EAAE,OAAO,EAAE,mBAAmB;gBAC7C,+BAA+B,EAAE,mBAAmB,EAAE,mBAAmB,IAAI,IAAI;gBACjF,2BAA2B,EAAE,mBAAmB,EAAE,mBAAmB;aACtE,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
WorkoutSet_docType
|
|
19
19
|
} from '@aneuhold/core-ts-db-lib';
|
|
20
20
|
import type { UUID } from 'crypto';
|
|
21
|
+
import type { Document } from 'mongodb';
|
|
21
22
|
import type { RepoListeners } from '../../services/RepoSubscriptionService.js';
|
|
22
23
|
import WorkoutExerciseValidator from '../../validators/workout/ExerciseValidator.js';
|
|
23
24
|
import WorkoutBaseWithUserIdRepository from './WorkoutBaseWithUserIdRepository.js';
|
|
@@ -58,9 +59,12 @@ export default class WorkoutExerciseRepository extends WorkoutBaseWithUserIdRepo
|
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
61
|
* Builds {@link WorkoutExerciseCTO} objects for all exercises belonging to a user.
|
|
61
|
-
* Uses
|
|
62
|
+
* Uses three parallel MongoDB aggregation pipelines:
|
|
62
63
|
* - Pipeline A: exercise + equipmentType + bestCalibration + bestSet
|
|
63
|
-
* - Pipeline B: lastSessionExercise + lastSessionSets
|
|
64
|
+
* - Pipeline B (true latest): lastSessionExercise + lastSessionSets from the most
|
|
65
|
+
* recent completed session, regardless of cycle type / deload status.
|
|
66
|
+
* - Pipeline C (accumulation): lastAccumulationSessionExercise +
|
|
67
|
+
* lastAccumulationSessionSets from the most recent completed non-deload session.
|
|
64
68
|
*
|
|
65
69
|
* @param userId The user whose exercise CTOs to build.
|
|
66
70
|
*/
|
|
@@ -72,8 +76,8 @@ export default class WorkoutExerciseRepository extends WorkoutBaseWithUserIdRepo
|
|
|
72
76
|
_bestSetArr: WorkoutSet[];
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
/** Raw shape of Pipeline B results. */
|
|
76
|
-
interface
|
|
79
|
+
/** Raw shape of Pipeline B/C results. */
|
|
80
|
+
interface LastSessionRow {
|
|
77
81
|
_id: string;
|
|
78
82
|
lastSessionExercise: WorkoutSessionExercise;
|
|
79
83
|
_lastSessionSetsArr: WorkoutSet[];
|
|
@@ -159,9 +163,36 @@ export default class WorkoutExerciseRepository extends WorkoutBaseWithUserIdRepo
|
|
|
159
163
|
])
|
|
160
164
|
.toArray();
|
|
161
165
|
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
// Builds the pipeline that finds, per exercise, the most recent session
|
|
167
|
+
// exercise from a completed session — optionally restricted to non-deload
|
|
168
|
+
// sessions (where plannedRir is not null on at least one set).
|
|
169
|
+
const buildLastSessionPipeline = (accumulationOnly: boolean): Document[] => {
|
|
170
|
+
const accumulationFilter: Document[] = accumulationOnly
|
|
171
|
+
? [
|
|
172
|
+
// Check for non-deload sets (plannedRir is not null)
|
|
173
|
+
{
|
|
174
|
+
$lookup: {
|
|
175
|
+
from: collName,
|
|
176
|
+
let: { seId: '$_se._id' },
|
|
177
|
+
pipeline: [
|
|
178
|
+
{
|
|
179
|
+
$match: {
|
|
180
|
+
$expr: { $eq: ['$workoutSessionExerciseId', '$$seId'] },
|
|
181
|
+
docType: WorkoutSet_docType,
|
|
182
|
+
plannedRir: { $ne: null }
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
{ $limit: 1 }
|
|
186
|
+
],
|
|
187
|
+
as: '_nonDeload'
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
// Keep only session exercises with at least one non-deload set
|
|
191
|
+
{ $match: { '_nonDeload.0': { $exists: true } } }
|
|
192
|
+
]
|
|
193
|
+
: [];
|
|
194
|
+
|
|
195
|
+
return [
|
|
165
196
|
// Start with all completed sessions for this user
|
|
166
197
|
{ $match: { docType: WorkoutSession_docType, userId, complete: true } },
|
|
167
198
|
// Most recent sessions first
|
|
@@ -184,26 +215,7 @@ export default class WorkoutExerciseRepository extends WorkoutBaseWithUserIdRepo
|
|
|
184
215
|
},
|
|
185
216
|
// One row per session exercise (unwind coalescence optimization)
|
|
186
217
|
{ $unwind: '$_se' },
|
|
187
|
-
|
|
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 } } },
|
|
218
|
+
...accumulationFilter,
|
|
207
219
|
// Per exercise, keep only the most recent session exercise ($first after sort)
|
|
208
220
|
{
|
|
209
221
|
$group: {
|
|
@@ -235,25 +247,27 @@ export default class WorkoutExerciseRepository extends WorkoutBaseWithUserIdRepo
|
|
|
235
247
|
as: '_lastSessionSetsArr'
|
|
236
248
|
}
|
|
237
249
|
}
|
|
238
|
-
]
|
|
250
|
+
];
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Pipeline B: most recent session exercise per exercise (any cycle type)
|
|
254
|
+
const pipelineBPromise = collection
|
|
255
|
+
.aggregate<LastSessionRow>(buildLastSessionPipeline(false))
|
|
239
256
|
.toArray();
|
|
240
257
|
|
|
241
|
-
|
|
258
|
+
// Pipeline C: most recent accumulation (non-deload) session exercise per exercise
|
|
259
|
+
const pipelineCPromise = collection
|
|
260
|
+
.aggregate<LastSessionRow>(buildLastSessionPipeline(true))
|
|
261
|
+
.toArray();
|
|
242
262
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
for (const row of rawLastSessions) {
|
|
252
|
-
lastSessionMap.set(row._id, {
|
|
253
|
-
lastSessionExercise: row.lastSessionExercise,
|
|
254
|
-
lastSessionSets: row._lastSessionSetsArr
|
|
255
|
-
});
|
|
256
|
-
}
|
|
263
|
+
const [rawExercises, rawLastSessions, rawLastAccumulationSessions] = await Promise.all([
|
|
264
|
+
pipelineAPromise,
|
|
265
|
+
pipelineBPromise,
|
|
266
|
+
pipelineCPromise
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
const lastSessionMap = new Map(rawLastSessions.map((row) => [row._id, row]));
|
|
270
|
+
const lastAccumulationMap = new Map(rawLastAccumulationSessions.map((row) => [row._id, row]));
|
|
257
271
|
|
|
258
272
|
// Assemble and validate CTOs
|
|
259
273
|
return rawExercises.map((raw) => {
|
|
@@ -262,15 +276,18 @@ export default class WorkoutExerciseRepository extends WorkoutBaseWithUserIdRepo
|
|
|
262
276
|
throw new Error(`Equipment type not found for exercise ${raw._id}`);
|
|
263
277
|
}
|
|
264
278
|
|
|
265
|
-
const
|
|
279
|
+
const lastRow = lastSessionMap.get(raw._id as string);
|
|
280
|
+
const lastAccumulationRow = lastAccumulationMap.get(raw._id as string);
|
|
266
281
|
|
|
267
282
|
return WorkoutExerciseCTOSchema.parse({
|
|
268
283
|
...exerciseFields,
|
|
269
284
|
equipmentType: _equipArr[0],
|
|
270
285
|
bestCalibration: _bestCalArr[0] ?? null,
|
|
271
286
|
bestSet: _bestSetArr[0] ?? null,
|
|
272
|
-
lastSessionExercise:
|
|
273
|
-
lastSessionSets:
|
|
287
|
+
lastSessionExercise: lastRow?.lastSessionExercise ?? null,
|
|
288
|
+
lastSessionSets: lastRow?._lastSessionSetsArr,
|
|
289
|
+
lastAccumulationSessionExercise: lastAccumulationRow?.lastSessionExercise ?? null,
|
|
290
|
+
lastAccumulationSessionSets: lastAccumulationRow?._lastSessionSetsArr
|
|
274
291
|
});
|
|
275
292
|
});
|
|
276
293
|
}
|
|
@@ -16,5 +16,10 @@ export default class MigrationService {
|
|
|
16
16
|
* @param dryRun Whether or not to actually make the changes or just log them.
|
|
17
17
|
*/
|
|
18
18
|
static migrateDb(dryRun?: boolean): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Migrates dashboard user configs that are missing enableAdminPage
|
|
21
|
+
* or the adminPage feature flag.
|
|
22
|
+
*/
|
|
23
|
+
private static migrateUserConfigs;
|
|
19
24
|
}
|
|
20
25
|
//# sourceMappingURL=MigrationService.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MigrationService.d.ts","sourceRoot":"","sources":["../../src/services/MigrationService.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MigrationService.d.ts","sourceRoot":"","sources":["../../src/services/MigrationService.ts"],"names":[],"mappings":"AAoBA;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,OAAO,gBAAgB;IACnC;;;;;;;;OAQG;WACU,SAAS,CAAC,MAAM,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAQrD;;;OAGG;mBACkB,kBAAkB;CAwExC"}
|
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
// @ts-nocheck
|
|
3
3
|
import { DR } from '@aneuhold/core-ts-lib';
|
|
4
4
|
import UserRepository from '../repositories/common/UserRepository.js';
|
|
5
|
+
import DashboardUserConfigRepository from '../repositories/dashboard/DashboardUserConfigRepository.js';
|
|
5
6
|
/**
|
|
6
|
-
* The default values for
|
|
7
|
-
* added here and defaulted to `false
|
|
8
|
-
* automatically.
|
|
7
|
+
* The default values for enabledFeatures. New feature flags should be
|
|
8
|
+
* added here and defaulted to `false`.
|
|
9
9
|
*/
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const ENABLED_FEATURES_DEFAULTS = {
|
|
11
|
+
financePage: false,
|
|
12
|
+
automationPage: false,
|
|
13
|
+
entertainmentPage: false,
|
|
14
|
+
homePageLinks: false,
|
|
15
|
+
useConfettiForTasks: false,
|
|
16
|
+
catImageOnHomePage: false,
|
|
17
|
+
adminPage: false
|
|
13
18
|
};
|
|
14
19
|
/**
|
|
15
20
|
* A service for migrating the DB to a new state after an existing document
|
|
@@ -29,40 +34,63 @@ export default class MigrationService {
|
|
|
29
34
|
* @param dryRun Whether or not to actually make the changes or just log them.
|
|
30
35
|
*/
|
|
31
36
|
static async migrateDb(dryRun = false) {
|
|
32
|
-
DR.logger.info(
|
|
37
|
+
DR.logger.info(`Starting migration... (dryRun=${dryRun})`);
|
|
38
|
+
await this.migrateUserConfigs(dryRun);
|
|
39
|
+
DR.logger.success('Migration complete.');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Migrates dashboard user configs that are missing enableAdminPage
|
|
43
|
+
* or the adminPage feature flag.
|
|
44
|
+
*/
|
|
45
|
+
static async migrateUserConfigs(dryRun) {
|
|
46
|
+
const configRepo = DashboardUserConfigRepository.getRepo();
|
|
47
|
+
const allConfigs = await configRepo.getAll();
|
|
48
|
+
// Build a userId -> userName lookup
|
|
33
49
|
const userRepo = UserRepository.getRepo();
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
const allUsers = await userRepo.getAll();
|
|
51
|
+
const userNameMap = new Map(allUsers.map((u) => [u._id, u.userName]));
|
|
52
|
+
// Build all updates in memory first, tracking only changed fields
|
|
53
|
+
const updates = [];
|
|
54
|
+
for (const config of allConfigs) {
|
|
55
|
+
const changes = [];
|
|
56
|
+
const updateDoc = { _id: config._id };
|
|
57
|
+
if (config.enableAdminPage === undefined) {
|
|
58
|
+
changes.push('enableAdminPage: undefined → false');
|
|
59
|
+
updateDoc.enableAdminPage = false;
|
|
60
|
+
}
|
|
61
|
+
const existingFeatures = config.enabledFeatures ?? {};
|
|
62
|
+
if (!existingFeatures || existingFeatures.adminPage === undefined) {
|
|
63
|
+
changes.push('enabledFeatures.adminPage: undefined → false');
|
|
64
|
+
updateDoc.enabledFeatures = { ...ENABLED_FEATURES_DEFAULTS, ...existingFeatures };
|
|
65
|
+
}
|
|
66
|
+
if (changes.length === 0) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const userName = userNameMap.get(config.userId) ?? 'unknown';
|
|
70
|
+
updates.push({ configId: config._id, userName, changes, updateDoc });
|
|
71
|
+
}
|
|
72
|
+
DR.logger.info(`Found ${updates.length} of ${allConfigs.length} user configs needing admin page fields.`);
|
|
73
|
+
if (updates.length === 0) {
|
|
74
|
+
DR.logger.success('No user config migration needed.');
|
|
45
75
|
return;
|
|
46
76
|
}
|
|
77
|
+
// Log exactly what will change
|
|
78
|
+
for (const update of updates) {
|
|
79
|
+
DR.logger.info(` ${update.userName} (${update.configId}):`);
|
|
80
|
+
for (const change of update.changes) {
|
|
81
|
+
DR.logger.info(` ${change}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
47
84
|
if (dryRun) {
|
|
48
|
-
DR.logger.info(
|
|
49
|
-
usersToUpdate.forEach((user) => {
|
|
50
|
-
const existing = user.projectAccess ?? {};
|
|
51
|
-
const merged = { ...PROJECT_ACCESS_DEFAULTS, ...existing };
|
|
52
|
-
DR.logger.info(` - ${user.userName} (${user._id}): ${JSON.stringify(existing)} → ${JSON.stringify(merged)}`);
|
|
53
|
-
});
|
|
85
|
+
DR.logger.info(`Dry run complete. ${updates.length} configs would be updated.`);
|
|
54
86
|
return;
|
|
55
87
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
_id: user._id,
|
|
61
|
-
projectAccess: merged
|
|
62
|
-
});
|
|
63
|
-
DR.logger.info(`Updated user ${user.userName} (${user._id}): ${JSON.stringify(existing)} → ${JSON.stringify(merged)}`);
|
|
88
|
+
// Execute updates
|
|
89
|
+
for (const update of updates) {
|
|
90
|
+
await configRepo.update(update.updateDoc);
|
|
91
|
+
DR.logger.info(`Updated config for ${update.userName} (${update.configId})`);
|
|
64
92
|
}
|
|
65
|
-
DR.logger.success(`
|
|
93
|
+
DR.logger.success(`User config migration complete. Updated ${updates.length} configs.`);
|
|
66
94
|
}
|
|
67
95
|
}
|
|
68
96
|
//# sourceMappingURL=MigrationService.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MigrationService.js","sourceRoot":"","sources":["../../src/services/MigrationService.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB,cAAc;AACd,OAAO,EAAE,EAAE,EAAE,MAAM,uBAAuB,CAAC;AAC3C,OAAO,cAAc,MAAM,0CAA0C,CAAC;
|
|
1
|
+
{"version":3,"file":"MigrationService.js","sourceRoot":"","sources":["../../src/services/MigrationService.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
// @ts-nocheck
|
|
3
3
|
import { DR } from '@aneuhold/core-ts-lib';
|
|
4
4
|
import UserRepository from '../repositories/common/UserRepository.js';
|
|
5
|
+
import DashboardUserConfigRepository from '../repositories/dashboard/DashboardUserConfigRepository.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* The default values for
|
|
8
|
-
* added here and defaulted to `false
|
|
9
|
-
* automatically.
|
|
8
|
+
* The default values for enabledFeatures. New feature flags should be
|
|
9
|
+
* added here and defaulted to `false`.
|
|
10
10
|
*/
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const ENABLED_FEATURES_DEFAULTS = {
|
|
12
|
+
financePage: false,
|
|
13
|
+
automationPage: false,
|
|
14
|
+
entertainmentPage: false,
|
|
15
|
+
homePageLinks: false,
|
|
16
|
+
useConfettiForTasks: false,
|
|
17
|
+
catImageOnHomePage: false,
|
|
18
|
+
adminPage: false
|
|
14
19
|
};
|
|
15
20
|
|
|
16
21
|
/**
|
|
@@ -31,53 +36,87 @@ export default class MigrationService {
|
|
|
31
36
|
* @param dryRun Whether or not to actually make the changes or just log them.
|
|
32
37
|
*/
|
|
33
38
|
static async migrateDb(dryRun = false): Promise<void> {
|
|
34
|
-
DR.logger.info(
|
|
39
|
+
DR.logger.info(`Starting migration... (dryRun=${dryRun})`);
|
|
35
40
|
|
|
41
|
+
await this.migrateUserConfigs(dryRun);
|
|
42
|
+
|
|
43
|
+
DR.logger.success('Migration complete.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Migrates dashboard user configs that are missing enableAdminPage
|
|
48
|
+
* or the adminPage feature flag.
|
|
49
|
+
*/
|
|
50
|
+
private static async migrateUserConfigs(dryRun: boolean): Promise<void> {
|
|
51
|
+
const configRepo = DashboardUserConfigRepository.getRepo();
|
|
52
|
+
const allConfigs = await configRepo.getAll();
|
|
53
|
+
|
|
54
|
+
// Build a userId -> userName lookup
|
|
36
55
|
const userRepo = UserRepository.getRepo();
|
|
37
|
-
const
|
|
56
|
+
const allUsers = await userRepo.getAll();
|
|
57
|
+
const userNameMap = new Map(allUsers.map((u) => [u._id, u.userName]));
|
|
58
|
+
|
|
59
|
+
// Build all updates in memory first, tracking only changed fields
|
|
60
|
+
const updates: Array<{
|
|
61
|
+
configId: string;
|
|
62
|
+
userName: string;
|
|
63
|
+
changes: string[];
|
|
64
|
+
updateDoc: Record<string, unknown>;
|
|
65
|
+
}> = [];
|
|
66
|
+
|
|
67
|
+
for (const config of allConfigs) {
|
|
68
|
+
const changes: string[] = [];
|
|
69
|
+
const updateDoc: Record<string, unknown> = { _id: config._id };
|
|
38
70
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
46
|
-
|
|
71
|
+
if (config.enableAdminPage === undefined) {
|
|
72
|
+
changes.push('enableAdminPage: undefined → false');
|
|
73
|
+
updateDoc.enableAdminPage = false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const existingFeatures = config.enabledFeatures ?? {};
|
|
77
|
+
if (!existingFeatures || existingFeatures.adminPage === undefined) {
|
|
78
|
+
changes.push('enabledFeatures.adminPage: undefined → false');
|
|
79
|
+
updateDoc.enabledFeatures = { ...ENABLED_FEATURES_DEFAULTS, ...existingFeatures };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (changes.length === 0) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const userName = userNameMap.get(config.userId) ?? 'unknown';
|
|
87
|
+
updates.push({ configId: config._id, userName, changes, updateDoc });
|
|
88
|
+
}
|
|
47
89
|
|
|
48
90
|
DR.logger.info(
|
|
49
|
-
`Found ${
|
|
91
|
+
`Found ${updates.length} of ${allConfigs.length} user configs needing admin page fields.`
|
|
50
92
|
);
|
|
51
93
|
|
|
52
|
-
if (
|
|
53
|
-
DR.logger.success('No migration needed.');
|
|
94
|
+
if (updates.length === 0) {
|
|
95
|
+
DR.logger.success('No user config migration needed.');
|
|
54
96
|
return;
|
|
55
97
|
}
|
|
56
98
|
|
|
99
|
+
// Log exactly what will change
|
|
100
|
+
for (const update of updates) {
|
|
101
|
+
DR.logger.info(` ${update.userName} (${update.configId}):`);
|
|
102
|
+
for (const change of update.changes) {
|
|
103
|
+
DR.logger.info(` ${change}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
57
107
|
if (dryRun) {
|
|
58
|
-
DR.logger.info(
|
|
59
|
-
usersToUpdate.forEach((user) => {
|
|
60
|
-
const existing = user.projectAccess ?? {};
|
|
61
|
-
const merged = { ...PROJECT_ACCESS_DEFAULTS, ...existing };
|
|
62
|
-
DR.logger.info(
|
|
63
|
-
` - ${user.userName} (${user._id}): ${JSON.stringify(existing)} → ${JSON.stringify(merged)}`
|
|
64
|
-
);
|
|
65
|
-
});
|
|
108
|
+
DR.logger.info(`Dry run complete. ${updates.length} configs would be updated.`);
|
|
66
109
|
return;
|
|
67
110
|
}
|
|
68
111
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
_id: user._id,
|
|
74
|
-
projectAccess: merged
|
|
75
|
-
});
|
|
76
|
-
DR.logger.info(
|
|
77
|
-
`Updated user ${user.userName} (${user._id}): ${JSON.stringify(existing)} → ${JSON.stringify(merged)}`
|
|
78
|
-
);
|
|
112
|
+
// Execute updates
|
|
113
|
+
for (const update of updates) {
|
|
114
|
+
await configRepo.update(update.updateDoc);
|
|
115
|
+
DR.logger.info(`Updated config for ${update.userName} (${update.configId})`);
|
|
79
116
|
}
|
|
80
117
|
|
|
81
|
-
DR.logger.success(
|
|
118
|
+
DR.logger.success(
|
|
119
|
+
`User config migration complete. Updated ${updates.length} configs.`
|
|
120
|
+
);
|
|
82
121
|
}
|
|
83
122
|
}
|
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.23",
|
|
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,8 +58,8 @@
|
|
|
58
58
|
"MongoDB"
|
|
59
59
|
],
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@aneuhold/be-ts-lib": "^3.1.
|
|
62
|
-
"@aneuhold/core-ts-db-lib": "^5.0.
|
|
61
|
+
"@aneuhold/be-ts-lib": "^3.1.10",
|
|
62
|
+
"@aneuhold/core-ts-db-lib": "^5.0.5",
|
|
63
63
|
"@aneuhold/core-ts-lib": "^2.4.3",
|
|
64
64
|
"bson": "^7.0.0",
|
|
65
65
|
"google-auth-library": "^10.6.1",
|