fix(core): complete la nullification defensive de sourceExerciseId pour #185

Delta final apres QA vert : renforce la robustesse du pull des programmes quand l'exercice source reference par sourceExerciseId est absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 18:02:38 +02:00
parent dd882f07ae
commit 933fc1121d
2 changed files with 259 additions and 24 deletions

View File

@ -4181,45 +4181,222 @@ Future<domain.ProgramExercise> _programExerciseFromRow(
db.AppDatabase database,
db.ProgramExercise row,
) async {
final imageMediaIds = _tryDecodeImageMediaIdsSnapshot(
row.exerciseImageMediaIdsSnapshotJson,
row.exerciseImageMediaIdSnapshot,
);
final steps = _tryDecodeExerciseStepsSnapshot(row.exerciseStepsSnapshotJson);
final scoreInputMode = _tryScoreInputModeFromDb(row.scoreInputModeSnapshot);
final healthServicesStrategy =
await _tryProgramExerciseHealthServicesStrategySnapshot(database, row.id);
try {
return domain.ProgramExercise(
metadata: _metadataFromRow(row),
programId: row.programId,
sourceExerciseId: row.sourceExerciseId,
position: row.position,
exerciseNameSnapshot: row.exerciseNameSnapshot,
exerciseDescriptionSnapshot: row.exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot: row.exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshot: imageMediaIds,
exerciseStepsSnapshot: steps,
autoStartNextTimedStepSnapshot: row.autoStartNextTimedStepSnapshot,
autoStartNextTimedStepOverride: row.autoStartNextTimedStepOverride,
exerciseVideoMediaIdSnapshot: row.exerciseVideoMediaIdSnapshot,
healthServicesExerciseTypeStrategySnapshot: healthServicesStrategy,
exerciseArchivedSnapshot: row.exerciseArchivedSnapshot,
availableTimeSnapshot: row.availableTimeSnapshot,
availableRepsSnapshot: row.availableRepsSnapshot,
availableScoreSnapshot: row.availableScoreSnapshot,
scoreInputModeSnapshot: scoreInputMode,
scoreLabelSnapshot: row.scoreLabelSnapshot,
scoreUnitSnapshot: row.scoreUnitSnapshot,
setsCount: row.setsCount,
timeEnabled: row.timeEnabled,
repsEnabled: row.repsEnabled,
scoreEnabled: row.scoreEnabled,
targetTimeSeconds: row.targetTimeSeconds,
targetReps: row.targetReps,
targetScore: row.targetScore,
targetScoreTimeMs: row.targetScoreTimeMs,
restSecondsOverride: row.restSecondsOverride,
);
} on domain.DomainException {
return _repairedProgramExerciseFromRow(
row,
imageMediaIds: imageMediaIds,
steps: steps,
healthServicesStrategy: healthServicesStrategy,
scoreInputMode: scoreInputMode,
);
}
}
domain.ProgramExercise _repairedProgramExerciseFromRow(
db.ProgramExercise row, {
required List<String> imageMediaIds,
required List<domain.ExerciseStep> steps,
required List<domain.HealthServicesExerciseType> healthServicesStrategy,
required domain.ScoreInputMode scoreInputMode,
}) {
var availableTime = row.availableTimeSnapshot;
var availableReps = row.availableRepsSnapshot;
var availableScore = row.availableScoreSnapshot;
var timeEnabled = row.timeEnabled && availableTime;
var repsEnabled = row.repsEnabled && availableReps;
var scoreEnabled = row.scoreEnabled && availableScore;
if (!timeEnabled && !repsEnabled && !scoreEnabled) {
if (availableReps) {
repsEnabled = true;
} else if (availableTime) {
timeEnabled = true;
} else if (availableScore) {
scoreEnabled = true;
} else {
availableReps = true;
repsEnabled = true;
}
}
final repairedScoreInputMode = scoreEnabled
? scoreInputMode
: domain.ScoreInputMode.manual;
try {
return _programExerciseFromRepairedValues(
row,
imageMediaIds: imageMediaIds,
steps: steps,
healthServicesStrategy: healthServicesStrategy,
scoreInputMode: repairedScoreInputMode,
availableTime: availableTime,
availableReps: availableReps,
availableScore: availableScore,
timeEnabled: timeEnabled,
repsEnabled: repsEnabled,
scoreEnabled: scoreEnabled,
);
} on domain.DomainException {
return _programExerciseFromRepairedValues(
row,
imageMediaIds: imageMediaIds,
steps: const [],
healthServicesStrategy: healthServicesStrategy,
scoreInputMode: repairedScoreInputMode,
availableTime: availableTime,
availableReps: availableReps,
availableScore: availableScore,
timeEnabled: timeEnabled,
repsEnabled: repsEnabled,
scoreEnabled: scoreEnabled,
);
}
}
domain.ProgramExercise _programExerciseFromRepairedValues(
db.ProgramExercise row, {
required List<String> imageMediaIds,
required List<domain.ExerciseStep> steps,
required List<domain.HealthServicesExerciseType> healthServicesStrategy,
required domain.ScoreInputMode scoreInputMode,
required bool availableTime,
required bool availableReps,
required bool availableScore,
required bool timeEnabled,
required bool repsEnabled,
required bool scoreEnabled,
}) {
return domain.ProgramExercise(
metadata: _metadataFromRow(row),
programId: row.programId,
sourceExerciseId: row.sourceExerciseId,
position: row.position,
exerciseNameSnapshot: row.exerciseNameSnapshot,
position: row.position < 0 ? 0 : row.position,
exerciseNameSnapshot: row.exerciseNameSnapshot.trim().isEmpty
? row.id
: row.exerciseNameSnapshot,
exerciseDescriptionSnapshot: row.exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot: row.exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshot: _decodeImageMediaIdsSnapshot(
row.exerciseImageMediaIdsSnapshotJson,
row.exerciseImageMediaIdSnapshot,
),
exerciseStepsSnapshot: _decodeExerciseStepsSnapshot(
row.exerciseStepsSnapshotJson,
),
exerciseImageMediaIdsSnapshot: imageMediaIds,
exerciseStepsSnapshot: steps,
autoStartNextTimedStepSnapshot: row.autoStartNextTimedStepSnapshot,
autoStartNextTimedStepOverride: row.autoStartNextTimedStepOverride,
exerciseVideoMediaIdSnapshot: row.exerciseVideoMediaIdSnapshot,
healthServicesExerciseTypeStrategySnapshot:
await _programExerciseHealthServicesStrategySnapshot(database, row.id),
healthServicesExerciseTypeStrategySnapshot: healthServicesStrategy,
exerciseArchivedSnapshot: row.exerciseArchivedSnapshot,
availableTimeSnapshot: row.availableTimeSnapshot,
availableRepsSnapshot: row.availableRepsSnapshot,
availableScoreSnapshot: row.availableScoreSnapshot,
scoreInputModeSnapshot: _scoreInputModeFromDb(row.scoreInputModeSnapshot),
availableTimeSnapshot: availableTime,
availableRepsSnapshot: availableReps,
availableScoreSnapshot: availableScore,
scoreInputModeSnapshot: scoreInputMode,
scoreLabelSnapshot: row.scoreLabelSnapshot,
scoreUnitSnapshot: row.scoreUnitSnapshot,
setsCount: row.setsCount,
timeEnabled: row.timeEnabled,
repsEnabled: row.repsEnabled,
scoreEnabled: row.scoreEnabled,
targetTimeSeconds: row.targetTimeSeconds,
targetReps: row.targetReps,
targetScore: row.targetScore,
targetScoreTimeMs: row.targetScoreTimeMs,
restSecondsOverride: row.restSecondsOverride,
setsCount: row.setsCount <= 0 ? 1 : row.setsCount,
timeEnabled: timeEnabled,
repsEnabled: repsEnabled,
scoreEnabled: scoreEnabled,
targetTimeSeconds: timeEnabled
? _positiveIntOrNull(row.targetTimeSeconds)
: null,
targetReps: repsEnabled ? _positiveIntOrNull(row.targetReps) : null,
targetScore: scoreEnabled && scoreInputMode == domain.ScoreInputMode.manual
? _nonNegativeDoubleOrNull(row.targetScore)
: null,
targetScoreTimeMs:
scoreEnabled && scoreInputMode == domain.ScoreInputMode.stopwatch
? _positiveIntOrNull(row.targetScoreTimeMs)
: null,
restSecondsOverride: _nonNegativeIntOrNull(row.restSecondsOverride),
);
}
List<String> _tryDecodeImageMediaIdsSnapshot(
String? encoded,
String? fallbackImageMediaId,
) {
try {
return _decodeImageMediaIdsSnapshot(encoded, fallbackImageMediaId);
} on Object {
return fallbackImageMediaId == null ? const [] : [fallbackImageMediaId];
}
}
List<domain.ExerciseStep> _tryDecodeExerciseStepsSnapshot(String? encoded) {
try {
return _decodeExerciseStepsSnapshot(encoded);
} on Object {
return const [];
}
}
domain.ScoreInputMode _tryScoreInputModeFromDb(String value) {
try {
return _scoreInputModeFromDb(value);
} on domain.DomainException {
return domain.ScoreInputMode.manual;
}
}
int? _positiveIntOrNull(int? value) {
return value != null && value > 0 ? value : null;
}
int? _nonNegativeIntOrNull(int? value) {
return value != null && value >= 0 ? value : null;
}
double? _nonNegativeDoubleOrNull(double? value) {
return value != null && value >= 0 ? value : null;
}
Future<List<domain.HealthServicesExerciseType>>
_tryProgramExerciseHealthServicesStrategySnapshot(
db.AppDatabase database,
String id,
) async {
try {
return _programExerciseHealthServicesStrategySnapshot(database, id);
} on Object {
return domain.legacyHealthServicesExerciseTypeStrategy;
}
}
Future<List<domain.HealthServicesExerciseType>>
_programExerciseHealthServicesStrategySnapshot(
db.AppDatabase database,