forked from eva/focus-flow
feat(data): add MongoDB document mapping helpers
This commit is contained in:
parent
9537e06c3c
commit
b0e790f8a6
5 changed files with 479 additions and 2 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# V1 Block 09 — Persistence Preparation
|
||||
|
||||
Status: In progress — Chunks 9.1 and 9.2 complete
|
||||
Status: In progress — Chunks 9.1, 9.2, and 9.3 complete
|
||||
|
||||
Purpose: Prepare the domain layer for future MongoDB-backed persistence without adding a MongoDB driver, network behavior, sync behavior, or database runtime dependency too early.
|
||||
|
||||
|
|
@ -133,6 +133,16 @@ Acceptance criteria:
|
|||
- Tests cover intentional clearing of nullable fields if helper behavior is added.
|
||||
- Tests confirm `not set` reward remains distinct from very low reward through mapping.
|
||||
|
||||
Completed:
|
||||
|
||||
- Added pure Dart task and task-statistics document mapping helpers using MongoDB-friendly map shapes.
|
||||
- Added task round-trip tests covering scheduling fields, child ownership, backlog tags, timestamps, and nested statistics.
|
||||
- Added enum decode helpers with tests for stable persisted names and unknown-name rejection.
|
||||
- Added explicit schedule-clearing document mapping behavior through `Task.toDocument(clearSchedule: true)`.
|
||||
- Confirmed `RewardLevel.notSet` and `RewardLevel.veryLow` remain distinct through document mapping.
|
||||
- Kept mapping helpers independent from MongoDB client libraries and database runtime requirements.
|
||||
- Verified with `dart format lib test`, `dart analyze`, and `dart test`.
|
||||
|
||||
BREAKPOINT: Stop here. Confirm `low` mode before documenting the explicit non-sync boundary.
|
||||
|
||||
## Chunk 9.4 — Explicit non-sync / no-database-runtime boundary
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# V1 Block 09 — Persistence Preparation
|
||||
|
||||
Status: In progress — Chunks 9.1 and 9.2 complete
|
||||
Status: In progress — Chunks 9.1, 9.2, and 9.3 complete
|
||||
|
||||
Purpose: Prepare the domain layer for future MongoDB-backed persistence without adding a MongoDB driver, network behavior, sync behavior, or database runtime dependency too early.
|
||||
|
||||
|
|
@ -133,6 +133,16 @@ Acceptance criteria:
|
|||
- Tests cover intentional clearing of nullable fields if helper behavior is added.
|
||||
- Tests confirm `not set` reward remains distinct from very low reward through mapping.
|
||||
|
||||
Completed:
|
||||
|
||||
- Added pure Dart task and task-statistics document mapping helpers using MongoDB-friendly map shapes.
|
||||
- Added task round-trip tests covering scheduling fields, child ownership, backlog tags, timestamps, and nested statistics.
|
||||
- Added enum decode helpers with tests for stable persisted names and unknown-name rejection.
|
||||
- Added explicit schedule-clearing document mapping behavior through `Task.toDocument(clearSchedule: true)`.
|
||||
- Confirmed `RewardLevel.notSet` and `RewardLevel.veryLow` remain distinct through document mapping.
|
||||
- Kept mapping helpers independent from MongoDB client libraries and database runtime requirements.
|
||||
- Verified with `dart format lib test`, `dart analyze`, and `dart test`.
|
||||
|
||||
BREAKPOINT: Stop here. Confirm `low` mode before documenting the explicit non-sync boundary.
|
||||
|
||||
## Chunk 9.4 — Explicit non-sync / no-database-runtime boundary
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
export 'src/models.dart';
|
||||
export 'src/backlog.dart';
|
||||
export 'src/child_tasks.dart';
|
||||
export 'src/document_mapping.dart';
|
||||
export 'src/locked_time.dart';
|
||||
export 'src/persistence_contract.dart';
|
||||
export 'src/quick_capture.dart';
|
||||
|
|
|
|||
305
lib/src/document_mapping.dart
Normal file
305
lib/src/document_mapping.dart
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
// MongoDB-friendly document mapping helpers.
|
||||
//
|
||||
// These helpers use plain Dart map shapes only. They do not import MongoDB APIs,
|
||||
// create clients, or perform database I/O.
|
||||
|
||||
import 'models.dart';
|
||||
import 'persistence_contract.dart';
|
||||
import 'task_statistics.dart';
|
||||
|
||||
/// Stable enum encode/decode helpers for document maps.
|
||||
abstract final class PersistenceEnumMapping {
|
||||
static String? encodeNullable(Enum? value) {
|
||||
return value?.persistenceName;
|
||||
}
|
||||
|
||||
static TaskType decodeTaskType(String value) {
|
||||
return _decodeEnum(TaskType.values, value, 'TaskType');
|
||||
}
|
||||
|
||||
static TaskStatus decodeTaskStatus(String value) {
|
||||
return _decodeEnum(TaskStatus.values, value, 'TaskStatus');
|
||||
}
|
||||
|
||||
static PriorityLevel? decodeNullablePriority(String? value) {
|
||||
return value == null
|
||||
? null
|
||||
: _decodeEnum(PriorityLevel.values, value, 'PriorityLevel');
|
||||
}
|
||||
|
||||
static RewardLevel decodeReward(String value) {
|
||||
return _decodeEnum(RewardLevel.values, value, 'RewardLevel');
|
||||
}
|
||||
|
||||
static DifficultyLevel decodeDifficulty(String value) {
|
||||
return _decodeEnum(DifficultyLevel.values, value, 'DifficultyLevel');
|
||||
}
|
||||
|
||||
static BacklogTag decodeBacklogTag(String value) {
|
||||
return _decodeEnum(BacklogTag.values, value, 'BacklogTag');
|
||||
}
|
||||
|
||||
static T _decodeEnum<T extends Enum>(
|
||||
Iterable<T> values,
|
||||
String persistedName,
|
||||
String enumName,
|
||||
) {
|
||||
for (final value in values) {
|
||||
if (value.persistenceName == persistedName) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
throw ArgumentError.value(
|
||||
persistedName,
|
||||
enumName,
|
||||
'Unknown persistence enum name.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Document mapping for [Task].
|
||||
abstract final class TaskDocumentMapping {
|
||||
/// Convert [task] to a MongoDB-friendly document-shaped map.
|
||||
static Map<String, Object?> toDocument(
|
||||
Task task, {
|
||||
bool clearSchedule = false,
|
||||
}) {
|
||||
final scheduledStart = clearSchedule ? null : task.scheduledStart;
|
||||
final scheduledEnd = clearSchedule ? null : task.scheduledEnd;
|
||||
|
||||
return {
|
||||
TaskDocumentFields.id: task.id,
|
||||
TaskDocumentFields.title: task.title,
|
||||
TaskDocumentFields.projectId: task.projectId,
|
||||
TaskDocumentFields.type: task.type.persistenceName,
|
||||
TaskDocumentFields.status: task.status.persistenceName,
|
||||
TaskDocumentFields.priority:
|
||||
PersistenceEnumMapping.encodeNullable(task.priority),
|
||||
TaskDocumentFields.reward: task.reward.persistenceName,
|
||||
TaskDocumentFields.difficulty: task.difficulty.persistenceName,
|
||||
TaskDocumentFields.durationMinutes: task.durationMinutes,
|
||||
TaskDocumentFields.scheduledStart: _dateTimeToStored(scheduledStart),
|
||||
TaskDocumentFields.scheduledEnd: _dateTimeToStored(scheduledEnd),
|
||||
TaskDocumentFields.parentTaskId: task.parentTaskId,
|
||||
TaskDocumentFields.backlogTags:
|
||||
task.backlogTags.map((tag) => tag.persistenceName).toList(),
|
||||
TaskDocumentFields.createdAt:
|
||||
PersistenceDateTimeConvention.toStoredString(task.createdAt),
|
||||
TaskDocumentFields.updatedAt:
|
||||
PersistenceDateTimeConvention.toStoredString(task.updatedAt),
|
||||
TaskDocumentFields.stats: task.stats.toDocument(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Rebuild a [Task] from a document-shaped map.
|
||||
static Task fromDocument(Map<String, Object?> document) {
|
||||
return Task(
|
||||
id: _requiredString(document, TaskDocumentFields.id),
|
||||
title: _requiredString(document, TaskDocumentFields.title),
|
||||
projectId: _requiredString(document, TaskDocumentFields.projectId),
|
||||
type: PersistenceEnumMapping.decodeTaskType(
|
||||
_requiredString(document, TaskDocumentFields.type),
|
||||
),
|
||||
status: PersistenceEnumMapping.decodeTaskStatus(
|
||||
_requiredString(document, TaskDocumentFields.status),
|
||||
),
|
||||
priority: PersistenceEnumMapping.decodeNullablePriority(
|
||||
_nullableString(document, TaskDocumentFields.priority),
|
||||
),
|
||||
reward: PersistenceEnumMapping.decodeReward(
|
||||
_requiredString(document, TaskDocumentFields.reward),
|
||||
),
|
||||
difficulty: PersistenceEnumMapping.decodeDifficulty(
|
||||
_requiredString(document, TaskDocumentFields.difficulty),
|
||||
),
|
||||
durationMinutes:
|
||||
_nullableInt(document, TaskDocumentFields.durationMinutes),
|
||||
scheduledStart:
|
||||
_nullableDateTime(document, TaskDocumentFields.scheduledStart),
|
||||
scheduledEnd:
|
||||
_nullableDateTime(document, TaskDocumentFields.scheduledEnd),
|
||||
parentTaskId: _nullableString(document, TaskDocumentFields.parentTaskId),
|
||||
backlogTags: _backlogTagsFromDocument(document),
|
||||
createdAt: _requiredDateTime(document, TaskDocumentFields.createdAt),
|
||||
updatedAt: _requiredDateTime(document, TaskDocumentFields.updatedAt),
|
||||
stats: TaskStatisticsDocumentMapping.fromDocument(
|
||||
_requiredMap(document, TaskDocumentFields.stats),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Set<BacklogTag> _backlogTagsFromDocument(
|
||||
Map<String, Object?> document,
|
||||
) {
|
||||
final rawTags = document[TaskDocumentFields.backlogTags];
|
||||
if (rawTags == null) {
|
||||
return const <BacklogTag>{};
|
||||
}
|
||||
if (rawTags is! Iterable) {
|
||||
throw ArgumentError.value(
|
||||
rawTags,
|
||||
TaskDocumentFields.backlogTags,
|
||||
'Expected an iterable of persisted backlog tag names.',
|
||||
);
|
||||
}
|
||||
return rawTags
|
||||
.map((tag) => PersistenceEnumMapping.decodeBacklogTag(tag as String))
|
||||
.toSet();
|
||||
}
|
||||
}
|
||||
|
||||
/// Document mapping for [TaskStatistics].
|
||||
abstract final class TaskStatisticsDocumentMapping {
|
||||
/// Convert [stats] to a MongoDB-friendly embedded document-shaped map.
|
||||
static Map<String, Object?> toDocument(TaskStatistics stats) {
|
||||
return {
|
||||
TaskStatisticsDocumentFields.skippedDuringBurnoutCount:
|
||||
stats.skippedDuringBurnoutCount,
|
||||
TaskStatisticsDocumentFields.manuallyPushedCount:
|
||||
stats.manuallyPushedCount,
|
||||
TaskStatisticsDocumentFields.autoPushedCount: stats.autoPushedCount,
|
||||
TaskStatisticsDocumentFields.movedToBacklogCount:
|
||||
stats.movedToBacklogCount,
|
||||
TaskStatisticsDocumentFields.restoredFromBacklogCount:
|
||||
stats.restoredFromBacklogCount,
|
||||
TaskStatisticsDocumentFields.missedCount: stats.missedCount,
|
||||
TaskStatisticsDocumentFields.cancelledCount: stats.cancelledCount,
|
||||
TaskStatisticsDocumentFields.completedLateCount: stats.completedLateCount,
|
||||
TaskStatisticsDocumentFields.completedDuringLockedHoursCount:
|
||||
stats.completedDuringLockedHoursCount,
|
||||
TaskStatisticsDocumentFields.completedDuringLockedHoursMinutes:
|
||||
stats.completedDuringLockedHoursMinutes,
|
||||
};
|
||||
}
|
||||
|
||||
/// Rebuild [TaskStatistics] from an embedded document-shaped map.
|
||||
static TaskStatistics fromDocument(Map<String, Object?> document) {
|
||||
return TaskStatistics(
|
||||
skippedDuringBurnoutCount: _intOrZero(
|
||||
document,
|
||||
TaskStatisticsDocumentFields.skippedDuringBurnoutCount,
|
||||
),
|
||||
manuallyPushedCount: _intOrZero(
|
||||
document,
|
||||
TaskStatisticsDocumentFields.manuallyPushedCount,
|
||||
),
|
||||
autoPushedCount: _intOrZero(
|
||||
document,
|
||||
TaskStatisticsDocumentFields.autoPushedCount,
|
||||
),
|
||||
movedToBacklogCount: _intOrZero(
|
||||
document,
|
||||
TaskStatisticsDocumentFields.movedToBacklogCount,
|
||||
),
|
||||
restoredFromBacklogCount: _intOrZero(
|
||||
document,
|
||||
TaskStatisticsDocumentFields.restoredFromBacklogCount,
|
||||
),
|
||||
missedCount: _intOrZero(
|
||||
document,
|
||||
TaskStatisticsDocumentFields.missedCount,
|
||||
),
|
||||
cancelledCount: _intOrZero(
|
||||
document,
|
||||
TaskStatisticsDocumentFields.cancelledCount,
|
||||
),
|
||||
completedLateCount: _intOrZero(
|
||||
document,
|
||||
TaskStatisticsDocumentFields.completedLateCount,
|
||||
),
|
||||
completedDuringLockedHoursCount: _intOrZero(
|
||||
document,
|
||||
TaskStatisticsDocumentFields.completedDuringLockedHoursCount,
|
||||
),
|
||||
completedDuringLockedHoursMinutes: _intOrZero(
|
||||
document,
|
||||
TaskStatisticsDocumentFields.completedDuringLockedHoursMinutes,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience extension for converting a [Task] into a document map.
|
||||
extension TaskDocumentExtension on Task {
|
||||
/// Convert this task to a MongoDB-friendly document-shaped map.
|
||||
Map<String, Object?> toDocument({bool clearSchedule = false}) {
|
||||
return TaskDocumentMapping.toDocument(this, clearSchedule: clearSchedule);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience extension for converting [TaskStatistics] into an embedded map.
|
||||
extension TaskStatisticsDocumentExtension on TaskStatistics {
|
||||
/// Convert this statistics value to a MongoDB-friendly embedded document.
|
||||
Map<String, Object?> toDocument() {
|
||||
return TaskStatisticsDocumentMapping.toDocument(this);
|
||||
}
|
||||
}
|
||||
|
||||
String? _dateTimeToStored(DateTime? value) {
|
||||
return value == null
|
||||
? null
|
||||
: PersistenceDateTimeConvention.toStoredString(value);
|
||||
}
|
||||
|
||||
String _requiredString(Map<String, Object?> document, String fieldName) {
|
||||
final value = document[fieldName];
|
||||
if (value is String) {
|
||||
return value;
|
||||
}
|
||||
throw ArgumentError.value(value, fieldName, 'Expected a string.');
|
||||
}
|
||||
|
||||
String? _nullableString(Map<String, Object?> document, String fieldName) {
|
||||
final value = document[fieldName];
|
||||
if (value == null || value is String) {
|
||||
return value as String?;
|
||||
}
|
||||
throw ArgumentError.value(value, fieldName, 'Expected a string or null.');
|
||||
}
|
||||
|
||||
int? _nullableInt(Map<String, Object?> document, String fieldName) {
|
||||
final value = document[fieldName];
|
||||
if (value == null || value is int) {
|
||||
return value as int?;
|
||||
}
|
||||
throw ArgumentError.value(value, fieldName, 'Expected an int or null.');
|
||||
}
|
||||
|
||||
int _intOrZero(Map<String, Object?> document, String fieldName) {
|
||||
final value = document[fieldName];
|
||||
if (value == null) {
|
||||
return 0;
|
||||
}
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
throw ArgumentError.value(value, fieldName, 'Expected an int.');
|
||||
}
|
||||
|
||||
DateTime _requiredDateTime(Map<String, Object?> document, String fieldName) {
|
||||
return PersistenceDateTimeConvention.fromStoredString(
|
||||
_requiredString(document, fieldName),
|
||||
);
|
||||
}
|
||||
|
||||
DateTime? _nullableDateTime(Map<String, Object?> document, String fieldName) {
|
||||
final value = _nullableString(document, fieldName);
|
||||
return value == null
|
||||
? null
|
||||
: PersistenceDateTimeConvention.fromStoredString(value);
|
||||
}
|
||||
|
||||
Map<String, Object?> _requiredMap(
|
||||
Map<String, Object?> document,
|
||||
String fieldName,
|
||||
) {
|
||||
final value = document[fieldName];
|
||||
if (value is Map<String, Object?>) {
|
||||
return value;
|
||||
}
|
||||
if (value is Map) {
|
||||
return Map<String, Object?>.from(value);
|
||||
}
|
||||
throw ArgumentError.value(value, fieldName, 'Expected a document map.');
|
||||
}
|
||||
151
test/document_mapping_test.dart
Normal file
151
test/document_mapping_test.dart
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import 'package:adhd_scheduler_core/scheduler_core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('MongoDB document mapping helpers', () {
|
||||
final createdAt = DateTime.utc(2026, 6, 23, 8);
|
||||
|
||||
test('task documents round-trip every scheduling field', () {
|
||||
final task = scheduledTask(
|
||||
id: 'task-1',
|
||||
createdAt: createdAt,
|
||||
reward: RewardLevel.high,
|
||||
difficulty: DifficultyLevel.hard,
|
||||
).copyWith(
|
||||
stats: const TaskStatistics().incrementManualPush(),
|
||||
);
|
||||
|
||||
final document = task.toDocument();
|
||||
final restored = TaskDocumentMapping.fromDocument(document);
|
||||
|
||||
expect(document[TaskDocumentFields.id], task.id);
|
||||
expect(document[TaskDocumentFields.type], 'flexible');
|
||||
expect(document[TaskDocumentFields.status], 'planned');
|
||||
expect(document[TaskDocumentFields.reward], 'high');
|
||||
expect(document[TaskDocumentFields.difficulty], 'hard');
|
||||
expect(document[TaskDocumentFields.scheduledStart],
|
||||
'2026-06-23T09:00:00.000Z');
|
||||
expect(restored.id, task.id);
|
||||
expect(restored.title, task.title);
|
||||
expect(restored.projectId, task.projectId);
|
||||
expect(restored.type, task.type);
|
||||
expect(restored.status, task.status);
|
||||
expect(restored.priority, task.priority);
|
||||
expect(restored.reward, task.reward);
|
||||
expect(restored.difficulty, task.difficulty);
|
||||
expect(restored.durationMinutes, task.durationMinutes);
|
||||
expect(restored.scheduledStart, task.scheduledStart);
|
||||
expect(restored.scheduledEnd, task.scheduledEnd);
|
||||
expect(restored.parentTaskId, task.parentTaskId);
|
||||
expect(restored.backlogTags, task.backlogTags);
|
||||
expect(restored.createdAt, task.createdAt);
|
||||
expect(restored.updatedAt, task.updatedAt);
|
||||
expect(restored.stats.manuallyPushedCount, 1);
|
||||
});
|
||||
|
||||
test('task documents preserve not-set reward distinctly from very low', () {
|
||||
final noReward = scheduledTask(
|
||||
id: 'no-reward',
|
||||
createdAt: createdAt,
|
||||
reward: RewardLevel.notSet,
|
||||
);
|
||||
final veryLowReward = scheduledTask(
|
||||
id: 'very-low',
|
||||
createdAt: createdAt,
|
||||
reward: RewardLevel.veryLow,
|
||||
);
|
||||
|
||||
final noRewardDocument = noReward.toDocument();
|
||||
final veryLowRewardDocument = veryLowReward.toDocument();
|
||||
|
||||
expect(noRewardDocument[TaskDocumentFields.reward], 'notSet');
|
||||
expect(veryLowRewardDocument[TaskDocumentFields.reward], 'veryLow');
|
||||
expect(
|
||||
TaskDocumentMapping.fromDocument(noRewardDocument).reward,
|
||||
RewardLevel.notSet,
|
||||
);
|
||||
expect(
|
||||
TaskDocumentMapping.fromDocument(veryLowRewardDocument).reward,
|
||||
RewardLevel.veryLow,
|
||||
);
|
||||
});
|
||||
|
||||
test('task documents support explicit nullable schedule clearing', () {
|
||||
final task = scheduledTask(
|
||||
id: 'scheduled',
|
||||
createdAt: createdAt,
|
||||
);
|
||||
|
||||
final clearedDocument = task.toDocument(clearSchedule: true);
|
||||
final restored = TaskDocumentMapping.fromDocument(clearedDocument);
|
||||
|
||||
expect(clearedDocument[TaskDocumentFields.scheduledStart], isNull);
|
||||
expect(clearedDocument[TaskDocumentFields.scheduledEnd], isNull);
|
||||
expect(restored.scheduledStart, isNull);
|
||||
expect(restored.scheduledEnd, isNull);
|
||||
expect(restored.id, task.id);
|
||||
});
|
||||
|
||||
test('task statistics document mapping preserves every counter', () {
|
||||
const stats = TaskStatistics(
|
||||
skippedDuringBurnoutCount: 1,
|
||||
manuallyPushedCount: 2,
|
||||
autoPushedCount: 3,
|
||||
movedToBacklogCount: 4,
|
||||
restoredFromBacklogCount: 5,
|
||||
missedCount: 6,
|
||||
cancelledCount: 7,
|
||||
completedLateCount: 8,
|
||||
completedDuringLockedHoursCount: 9,
|
||||
completedDuringLockedHoursMinutes: 10,
|
||||
);
|
||||
|
||||
final document = stats.toDocument();
|
||||
final restored = TaskStatisticsDocumentMapping.fromDocument(document);
|
||||
|
||||
expect(document.keys.toSet(), TaskStatisticsDocumentFields.all);
|
||||
expect(restored.skippedDuringBurnoutCount, 1);
|
||||
expect(restored.manuallyPushedCount, 2);
|
||||
expect(restored.autoPushedCount, 3);
|
||||
expect(restored.movedToBacklogCount, 4);
|
||||
expect(restored.restoredFromBacklogCount, 5);
|
||||
expect(restored.missedCount, 6);
|
||||
expect(restored.cancelledCount, 7);
|
||||
expect(restored.completedLateCount, 8);
|
||||
expect(restored.completedDuringLockedHoursCount, 9);
|
||||
expect(restored.completedDuringLockedHoursMinutes, 10);
|
||||
});
|
||||
|
||||
test('enum document mapping rejects unknown persistence names', () {
|
||||
expect(
|
||||
() => PersistenceEnumMapping.decodeTaskType('unknown'),
|
||||
throwsArgumentError,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Task scheduledTask({
|
||||
required String id,
|
||||
required DateTime createdAt,
|
||||
RewardLevel reward = RewardLevel.notSet,
|
||||
DifficultyLevel difficulty = DifficultyLevel.medium,
|
||||
}) {
|
||||
return Task(
|
||||
id: id,
|
||||
title: 'Scheduled task',
|
||||
projectId: 'project-1',
|
||||
type: TaskType.flexible,
|
||||
status: TaskStatus.planned,
|
||||
priority: PriorityLevel.medium,
|
||||
reward: reward,
|
||||
difficulty: difficulty,
|
||||
durationMinutes: 30,
|
||||
scheduledStart: DateTime.utc(2026, 6, 23, 9),
|
||||
scheduledEnd: DateTime.utc(2026, 6, 23, 9, 30),
|
||||
parentTaskId: 'parent-1',
|
||||
backlogTags: const {BacklogTag.wishlist},
|
||||
createdAt: createdAt,
|
||||
updatedAt: createdAt.add(const Duration(minutes: 5)),
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue