forked from eva/focus-flow
1981 lines
62 KiB
Dart
1981 lines
62 KiB
Dart
// Application orchestration contracts for UI-facing use cases.
|
|
//
|
|
// This layer sits above the pure domain services and repository interfaces. It
|
|
// gives future Flutter/use-case code one atomic boundary per user command
|
|
// without importing widgets, MongoDB clients, or platform notification APIs.
|
|
|
|
import 'backlog.dart';
|
|
import 'locked_time.dart';
|
|
import 'models.dart';
|
|
import 'project_statistics.dart';
|
|
import 'repositories.dart';
|
|
import 'scheduling_engine.dart';
|
|
import 'task_lifecycle.dart';
|
|
import 'time_contracts.dart';
|
|
|
|
/// Owner-local time context supplied to application operations.
|
|
class OwnerTimeZoneContext {
|
|
OwnerTimeZoneContext({
|
|
required String ownerId,
|
|
required String timeZoneId,
|
|
}) : ownerId = _requiredTrimmed(ownerId, 'ownerId'),
|
|
timeZoneId = _requiredTrimmed(timeZoneId, 'timeZoneId');
|
|
|
|
/// Authorization-neutral owner scope. V1 does not implement accounts.
|
|
final String ownerId;
|
|
|
|
/// IANA-style time-zone identifier chosen by the app boundary.
|
|
final String timeZoneId;
|
|
}
|
|
|
|
/// Per-operation application context.
|
|
class ApplicationOperationContext {
|
|
ApplicationOperationContext({
|
|
required String operationId,
|
|
required this.now,
|
|
required this.ownerTimeZone,
|
|
required this.clock,
|
|
required this.idGenerator,
|
|
}) : operationId = _requiredTrimmed(operationId, 'operationId');
|
|
|
|
/// Convenience constructor that captures [Clock.now] once.
|
|
factory ApplicationOperationContext.start({
|
|
required String operationId,
|
|
required OwnerTimeZoneContext ownerTimeZone,
|
|
required Clock clock,
|
|
required IdGenerator idGenerator,
|
|
}) {
|
|
return ApplicationOperationContext(
|
|
operationId: operationId,
|
|
now: clock.now(),
|
|
ownerTimeZone: ownerTimeZone,
|
|
clock: clock,
|
|
idGenerator: idGenerator,
|
|
);
|
|
}
|
|
|
|
/// Exactly-once key for this user command.
|
|
final String operationId;
|
|
|
|
/// Instant used by the operation. Use this instead of repeatedly reading the
|
|
/// clock during one command.
|
|
final DateTime now;
|
|
|
|
/// Owner scope and time-zone data for date-sensitive rules.
|
|
final OwnerTimeZoneContext ownerTimeZone;
|
|
|
|
/// Injected clock available to lower-level services that need one.
|
|
final Clock clock;
|
|
|
|
/// Injected ID generator for records created by the operation.
|
|
final IdGenerator idGenerator;
|
|
}
|
|
|
|
/// Stable application-layer result categories.
|
|
enum ApplicationFailureCode {
|
|
validation,
|
|
notFound,
|
|
conflict,
|
|
staleRevision,
|
|
noSlot,
|
|
duplicateOperation,
|
|
persistenceFailure,
|
|
unexpected,
|
|
}
|
|
|
|
/// Typed failure returned by application commands and queries.
|
|
class ApplicationFailure {
|
|
const ApplicationFailure({
|
|
required this.code,
|
|
this.entityId,
|
|
this.detailCode,
|
|
});
|
|
|
|
/// Stable machine-readable category.
|
|
final ApplicationFailureCode code;
|
|
|
|
/// Optional stable entity id related to the failure.
|
|
final String? entityId;
|
|
|
|
/// Optional narrower stable reason, such as a domain validation code name.
|
|
final String? detailCode;
|
|
}
|
|
|
|
/// Result wrapper for expected application success/failure paths.
|
|
class ApplicationResult<T> {
|
|
const ApplicationResult._({
|
|
this.value,
|
|
this.failure,
|
|
});
|
|
|
|
/// Successful result carrying [value].
|
|
factory ApplicationResult.success(T value) {
|
|
return ApplicationResult._(value: value);
|
|
}
|
|
|
|
/// Expected failure result.
|
|
factory ApplicationResult.failure(ApplicationFailure failure) {
|
|
return ApplicationResult._(failure: failure);
|
|
}
|
|
|
|
/// Successful value, if any.
|
|
final T? value;
|
|
|
|
/// Typed failure, if the operation did not succeed.
|
|
final ApplicationFailure? failure;
|
|
|
|
/// Whether the operation succeeded.
|
|
bool get isSuccess => failure == null;
|
|
|
|
/// Whether the operation failed with a typed application failure.
|
|
bool get isFailure => failure != null;
|
|
|
|
/// Return the value or throw if this result is a failure.
|
|
T get requireValue {
|
|
if (failure != null) {
|
|
throw StateError(
|
|
'Application result is a failure: ${failure!.code.name}');
|
|
}
|
|
return value as T;
|
|
}
|
|
}
|
|
|
|
/// Exception for expected failures raised inside a unit-of-work callback.
|
|
class ApplicationFailureException implements Exception {
|
|
const ApplicationFailureException(this.failure);
|
|
|
|
/// Failure to return from the application boundary.
|
|
final ApplicationFailure failure;
|
|
}
|
|
|
|
/// Exception representing persistence/driver failures at repository boundaries.
|
|
class ApplicationPersistenceException implements Exception {
|
|
const ApplicationPersistenceException();
|
|
}
|
|
|
|
/// Persisted exactly-once operation record.
|
|
class ApplicationOperationRecord {
|
|
ApplicationOperationRecord({
|
|
required String operationId,
|
|
required String ownerId,
|
|
required String operationName,
|
|
required this.committedAt,
|
|
}) : operationId = _requiredTrimmed(operationId, 'operationId'),
|
|
ownerId = _requiredTrimmed(ownerId, 'ownerId'),
|
|
operationName = _requiredTrimmed(operationName, 'operationName');
|
|
|
|
/// Exactly-once operation id.
|
|
final String operationId;
|
|
|
|
/// Authorization-neutral owner scope.
|
|
final String ownerId;
|
|
|
|
/// Stable use-case/command label.
|
|
final String operationName;
|
|
|
|
/// Commit instant.
|
|
final DateTime committedAt;
|
|
}
|
|
|
|
/// Owner-scoped acknowledgement for a one-time scheduling notice.
|
|
class NoticeAcknowledgementRecord {
|
|
NoticeAcknowledgementRecord({
|
|
required String noticeId,
|
|
required String ownerId,
|
|
required this.acknowledgedAt,
|
|
}) : noticeId = _requiredTrimmed(noticeId, 'noticeId'),
|
|
ownerId = _requiredTrimmed(ownerId, 'ownerId');
|
|
|
|
/// Stable notice id generated by the read/use-case boundary.
|
|
final String noticeId;
|
|
|
|
/// Authorization-neutral owner scope.
|
|
final String ownerId;
|
|
|
|
/// Instant the notice was consumed.
|
|
final DateTime acknowledgedAt;
|
|
}
|
|
|
|
/// Owner-level settings needed by application use cases.
|
|
class OwnerSettings {
|
|
OwnerSettings({
|
|
required String ownerId,
|
|
required String timeZoneId,
|
|
WallTime? dayStart,
|
|
WallTime? dayEnd,
|
|
this.compactModeEnabled = false,
|
|
this.backlogStalenessSettings = const BacklogStalenessSettings(),
|
|
}) : ownerId = _requiredTrimmed(ownerId, 'ownerId'),
|
|
timeZoneId = _requiredTrimmed(timeZoneId, 'timeZoneId'),
|
|
dayStart = dayStart ?? WallTime(hour: 0, minute: 0),
|
|
dayEnd = dayEnd ?? WallTime(hour: 23, minute: 59) {
|
|
if (this.dayStart.compareTo(this.dayEnd) >= 0) {
|
|
throw ArgumentError.value(
|
|
{'dayStart': this.dayStart, 'dayEnd': this.dayEnd},
|
|
'dayEnd',
|
|
'Day end must be after day start.',
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Authorization-neutral owner scope.
|
|
final String ownerId;
|
|
|
|
/// Owner time-zone id used by date-sensitive application use cases.
|
|
final String timeZoneId;
|
|
|
|
/// Default local day start.
|
|
final WallTime dayStart;
|
|
|
|
/// Default local day end.
|
|
final WallTime dayEnd;
|
|
|
|
/// Persisted compact-mode preference.
|
|
final bool compactModeEnabled;
|
|
|
|
/// Configurable backlog age thresholds.
|
|
final BacklogStalenessSettings backlogStalenessSettings;
|
|
|
|
/// Return a copy with selected settings changed.
|
|
OwnerSettings copyWith({
|
|
String? ownerId,
|
|
String? timeZoneId,
|
|
WallTime? dayStart,
|
|
WallTime? dayEnd,
|
|
bool? compactModeEnabled,
|
|
BacklogStalenessSettings? backlogStalenessSettings,
|
|
}) {
|
|
return OwnerSettings(
|
|
ownerId: ownerId ?? this.ownerId,
|
|
timeZoneId: timeZoneId ?? this.timeZoneId,
|
|
dayStart: dayStart ?? this.dayStart,
|
|
dayEnd: dayEnd ?? this.dayEnd,
|
|
compactModeEnabled: compactModeEnabled ?? this.compactModeEnabled,
|
|
backlogStalenessSettings:
|
|
backlogStalenessSettings ?? this.backlogStalenessSettings,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Repository contract for internal task activities.
|
|
abstract interface class TaskActivityRepository {
|
|
Future<TaskActivity?> findById(String id);
|
|
|
|
Future<List<TaskActivity>> findByOperationId(String operationId);
|
|
|
|
Future<List<TaskActivity>> findByTaskId(String taskId);
|
|
|
|
Future<List<TaskActivity>> findByProjectId(String projectId);
|
|
|
|
Future<List<TaskActivity>> findAll();
|
|
|
|
Future<RepositoryPage<TaskActivity>> findByOwner({
|
|
required String ownerId,
|
|
RepositoryPageRequest page = const RepositoryPageRequest(),
|
|
});
|
|
|
|
Future<void> save(TaskActivity activity);
|
|
|
|
Future<void> saveAll(Iterable<TaskActivity> activities);
|
|
|
|
Future<RepositoryMutationResult> append({
|
|
required TaskActivity activity,
|
|
required String ownerId,
|
|
});
|
|
}
|
|
|
|
/// Repository contract for project statistics aggregates.
|
|
abstract interface class ProjectStatisticsRepository {
|
|
Future<ProjectStatistics?> findByProjectId(String projectId);
|
|
|
|
Future<RepositoryRecord<ProjectStatistics>?> findRecordByProjectId(
|
|
String projectId,
|
|
);
|
|
|
|
Future<List<ProjectStatistics>> findAll();
|
|
|
|
Future<void> save(ProjectStatistics statistics);
|
|
|
|
Future<RepositoryMutationResult> saveIfRevision({
|
|
required ProjectStatistics statistics,
|
|
required String ownerId,
|
|
required int expectedRevision,
|
|
});
|
|
}
|
|
|
|
/// Repository contract for owner settings.
|
|
abstract interface class OwnerSettingsRepository {
|
|
Future<OwnerSettings?> findByOwnerId(String ownerId);
|
|
|
|
Future<RepositoryRecord<OwnerSettings>?> findRecordByOwnerId(String ownerId);
|
|
|
|
Future<void> save(OwnerSettings settings);
|
|
|
|
Future<RepositoryMutationResult> saveIfRevision({
|
|
required OwnerSettings settings,
|
|
required int expectedRevision,
|
|
});
|
|
}
|
|
|
|
/// Repository contract for consumed one-time notices.
|
|
abstract interface class NoticeAcknowledgementRepository {
|
|
Future<NoticeAcknowledgementRecord?> findById({
|
|
required String ownerId,
|
|
required String noticeId,
|
|
});
|
|
|
|
Future<List<NoticeAcknowledgementRecord>> findByOwnerId(String ownerId);
|
|
|
|
Future<RepositoryPage<NoticeAcknowledgementRecord>> findByOwner({
|
|
required String ownerId,
|
|
RepositoryPageRequest page = const RepositoryPageRequest(),
|
|
});
|
|
|
|
Future<void> save(NoticeAcknowledgementRecord record);
|
|
|
|
Future<RepositoryMutationResult> insert(
|
|
NoticeAcknowledgementRecord record,
|
|
);
|
|
}
|
|
|
|
/// Repository contract for exactly-once operation records.
|
|
abstract interface class ApplicationOperationRepository {
|
|
Future<ApplicationOperationRecord?> findById(String operationId);
|
|
|
|
Future<ApplicationOperationRecord?> findByIdempotencyKey({
|
|
required String ownerId,
|
|
required String operationId,
|
|
});
|
|
|
|
Future<void> save(ApplicationOperationRecord operation);
|
|
|
|
Future<RepositoryMutationResult> insertIfAbsent(
|
|
ApplicationOperationRecord operation,
|
|
);
|
|
}
|
|
|
|
/// Repository set available inside one application unit of work.
|
|
abstract interface class ApplicationUnitOfWorkRepositories {
|
|
TaskRepository get tasks;
|
|
|
|
ProjectRepository get projects;
|
|
|
|
LockedBlockRepository get lockedBlocks;
|
|
|
|
SchedulingSnapshotRepository get schedulingSnapshots;
|
|
|
|
TaskActivityRepository get taskActivities;
|
|
|
|
ProjectStatisticsRepository get projectStatistics;
|
|
|
|
OwnerSettingsRepository get ownerSettings;
|
|
|
|
NoticeAcknowledgementRepository get noticeAcknowledgements;
|
|
|
|
ApplicationOperationRepository get operations;
|
|
}
|
|
|
|
/// Atomic application transaction boundary.
|
|
abstract interface class ApplicationUnitOfWork {
|
|
Future<ApplicationResult<T>> run<T>({
|
|
required ApplicationOperationContext context,
|
|
required String operationName,
|
|
required Future<ApplicationResult<T>> Function(
|
|
ApplicationUnitOfWorkRepositories repositories,
|
|
) action,
|
|
});
|
|
|
|
Future<ApplicationResult<T>> read<T>({
|
|
required Future<ApplicationResult<T>> Function(
|
|
ApplicationUnitOfWorkRepositories repositories,
|
|
) action,
|
|
});
|
|
}
|
|
|
|
/// Repository loader for common scheduler input assembly.
|
|
class ApplicationSchedulingLoader {
|
|
const ApplicationSchedulingLoader(this.repositories);
|
|
|
|
/// Repositories for the current application operation.
|
|
final ApplicationUnitOfWorkRepositories repositories;
|
|
|
|
/// Load scheduler input for [window] from the repository boundary.
|
|
///
|
|
/// UI code should not persist partial [SchedulingResult] objects. Commands
|
|
/// should load authoritative state here, invoke domain services, then commit
|
|
/// resulting entities through a unit of work.
|
|
Future<SchedulingInput> loadInputForWindow({
|
|
required SchedulingWindow window,
|
|
List<TimeInterval> lockedIntervals = const <TimeInterval>[],
|
|
List<TimeInterval> requiredVisibleIntervals = const <TimeInterval>[],
|
|
}) async {
|
|
final scheduledTasks = await repositories.tasks.findScheduledInWindow(
|
|
window,
|
|
);
|
|
return SchedulingInput(
|
|
tasks: scheduledTasks,
|
|
window: window,
|
|
lockedIntervals: lockedIntervals,
|
|
requiredVisibleIntervals: requiredVisibleIntervals,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// In-memory unit-of-work implementation for deterministic application tests.
|
|
class InMemoryApplicationUnitOfWork implements ApplicationUnitOfWork {
|
|
InMemoryApplicationUnitOfWork({
|
|
Iterable<Task> initialTasks = const <Task>[],
|
|
Iterable<ProjectProfile> initialProjects = const <ProjectProfile>[],
|
|
Iterable<LockedBlock> initialLockedBlocks = const <LockedBlock>[],
|
|
Iterable<LockedBlockOverride> initialLockedOverrides =
|
|
const <LockedBlockOverride>[],
|
|
Iterable<SchedulingStateSnapshot> initialSchedulingSnapshots =
|
|
const <SchedulingStateSnapshot>[],
|
|
Iterable<TaskActivity> initialTaskActivities = const <TaskActivity>[],
|
|
Iterable<ProjectStatistics> initialProjectStatistics =
|
|
const <ProjectStatistics>[],
|
|
Iterable<OwnerSettings> initialOwnerSettings = const <OwnerSettings>[],
|
|
Iterable<NoticeAcknowledgementRecord> initialNoticeAcknowledgements =
|
|
const <NoticeAcknowledgementRecord>[],
|
|
Iterable<ApplicationOperationRecord> initialOperations =
|
|
const <ApplicationOperationRecord>[],
|
|
}) : _state = _InMemoryApplicationState(
|
|
tasksById: {
|
|
for (final task in initialTasks) task.id: task,
|
|
},
|
|
taskOwnersById: {
|
|
for (final task in initialTasks) task.id: 'owner-1',
|
|
},
|
|
taskRevisionsById: {
|
|
for (final task in initialTasks) task.id: 1,
|
|
},
|
|
projectsById: {
|
|
for (final project in initialProjects) project.id: project,
|
|
},
|
|
projectOwnersById: {
|
|
for (final project in initialProjects) project.id: 'owner-1',
|
|
},
|
|
projectRevisionsById: {
|
|
for (final project in initialProjects) project.id: 1,
|
|
},
|
|
lockedBlocksById: {
|
|
for (final block in initialLockedBlocks) block.id: block,
|
|
},
|
|
lockedBlockOwnersById: {
|
|
for (final block in initialLockedBlocks) block.id: 'owner-1',
|
|
},
|
|
lockedBlockRevisionsById: {
|
|
for (final block in initialLockedBlocks) block.id: 1,
|
|
},
|
|
lockedOverridesById: {
|
|
for (final override in initialLockedOverrides)
|
|
override.id: override,
|
|
},
|
|
lockedOverrideOwnersById: {
|
|
for (final override in initialLockedOverrides)
|
|
override.id: 'owner-1',
|
|
},
|
|
lockedOverrideRevisionsById: {
|
|
for (final override in initialLockedOverrides) override.id: 1,
|
|
},
|
|
schedulingSnapshotsById: {
|
|
for (final snapshot in initialSchedulingSnapshots)
|
|
snapshot.id: snapshot,
|
|
},
|
|
taskActivitiesById: {
|
|
for (final activity in initialTaskActivities) activity.id: activity,
|
|
},
|
|
taskActivityOwnersById: {
|
|
for (final activity in initialTaskActivities)
|
|
activity.id: 'owner-1',
|
|
},
|
|
projectStatisticsByProjectId: {
|
|
for (final statistics in initialProjectStatistics)
|
|
statistics.projectId: statistics,
|
|
},
|
|
projectStatisticsOwnersByProjectId: {
|
|
for (final statistics in initialProjectStatistics)
|
|
statistics.projectId: 'owner-1',
|
|
},
|
|
projectStatisticsRevisionsByProjectId: {
|
|
for (final statistics in initialProjectStatistics)
|
|
statistics.projectId: 1,
|
|
},
|
|
ownerSettingsByOwnerId: {
|
|
for (final settings in initialOwnerSettings)
|
|
settings.ownerId: settings,
|
|
},
|
|
ownerSettingsRevisionsByOwnerId: {
|
|
for (final settings in initialOwnerSettings) settings.ownerId: 1,
|
|
},
|
|
noticeAcknowledgementsByScopedId: {
|
|
for (final acknowledgement in initialNoticeAcknowledgements)
|
|
_noticeAcknowledgementKey(
|
|
ownerId: acknowledgement.ownerId,
|
|
noticeId: acknowledgement.noticeId,
|
|
): acknowledgement,
|
|
},
|
|
noticeAcknowledgementRevisionsByScopedId: {
|
|
for (final acknowledgement in initialNoticeAcknowledgements)
|
|
_noticeAcknowledgementKey(
|
|
ownerId: acknowledgement.ownerId,
|
|
noticeId: acknowledgement.noticeId,
|
|
): 1,
|
|
},
|
|
operationsById: {
|
|
for (final operation in initialOperations)
|
|
operation.operationId: operation,
|
|
},
|
|
);
|
|
|
|
_InMemoryApplicationState _state;
|
|
bool _failNextCommit = false;
|
|
|
|
/// Current committed tasks, useful for contract tests.
|
|
List<Task> get currentTasks =>
|
|
List<Task>.unmodifiable(_state.tasksById.values);
|
|
|
|
/// Current committed projects, useful for contract tests.
|
|
List<ProjectProfile> get currentProjects {
|
|
return List<ProjectProfile>.unmodifiable(_state.projectsById.values);
|
|
}
|
|
|
|
/// Current committed locked blocks, useful for contract tests.
|
|
List<LockedBlock> get currentLockedBlocks {
|
|
return List<LockedBlock>.unmodifiable(_state.lockedBlocksById.values);
|
|
}
|
|
|
|
/// Current committed locked overrides, useful for contract tests.
|
|
List<LockedBlockOverride> get currentLockedOverrides {
|
|
return List<LockedBlockOverride>.unmodifiable(
|
|
_state.lockedOverridesById.values,
|
|
);
|
|
}
|
|
|
|
/// Current committed scheduling snapshots, useful for contract tests.
|
|
List<SchedulingStateSnapshot> get currentSchedulingSnapshots {
|
|
return List<SchedulingStateSnapshot>.unmodifiable(
|
|
_state.schedulingSnapshotsById.values,
|
|
);
|
|
}
|
|
|
|
/// Current committed activities, useful for contract tests.
|
|
List<TaskActivity> get currentTaskActivities {
|
|
return List<TaskActivity>.unmodifiable(_state.taskActivitiesById.values);
|
|
}
|
|
|
|
/// Current committed project statistics, useful for contract tests.
|
|
List<ProjectStatistics> get currentProjectStatistics {
|
|
return List<ProjectStatistics>.unmodifiable(
|
|
_state.projectStatisticsByProjectId.values,
|
|
);
|
|
}
|
|
|
|
/// Current committed owner settings, useful for contract tests.
|
|
List<OwnerSettings> get currentOwnerSettings {
|
|
return List<OwnerSettings>.unmodifiable(
|
|
_state.ownerSettingsByOwnerId.values);
|
|
}
|
|
|
|
/// Current committed notice acknowledgements, useful for contract tests.
|
|
List<NoticeAcknowledgementRecord> get currentNoticeAcknowledgements {
|
|
return List<NoticeAcknowledgementRecord>.unmodifiable(
|
|
_state.noticeAcknowledgementsByScopedId.values,
|
|
);
|
|
}
|
|
|
|
/// Current committed operation records, useful for contract tests.
|
|
List<ApplicationOperationRecord> get currentOperations {
|
|
return List<ApplicationOperationRecord>.unmodifiable(
|
|
_state.operationsById.values,
|
|
);
|
|
}
|
|
|
|
/// Cause the next successful transaction to fail at commit time.
|
|
void failNextCommitForTesting() {
|
|
_failNextCommit = true;
|
|
}
|
|
|
|
@override
|
|
Future<ApplicationResult<T>> run<T>({
|
|
required ApplicationOperationContext context,
|
|
required String operationName,
|
|
required Future<ApplicationResult<T>> Function(
|
|
ApplicationUnitOfWorkRepositories repositories,
|
|
) action,
|
|
}) async {
|
|
if (_state.operationsById.containsKey(context.operationId)) {
|
|
return ApplicationResult.failure(
|
|
ApplicationFailure(
|
|
code: ApplicationFailureCode.duplicateOperation,
|
|
entityId: context.operationId,
|
|
),
|
|
);
|
|
}
|
|
|
|
final staged = _state.copy();
|
|
final repositories = _InMemoryApplicationRepositories(staged);
|
|
|
|
try {
|
|
final result = await action(repositories);
|
|
if (result.isFailure) {
|
|
return result;
|
|
}
|
|
|
|
staged.operationsById[context.operationId] = ApplicationOperationRecord(
|
|
operationId: context.operationId,
|
|
ownerId: context.ownerTimeZone.ownerId,
|
|
operationName: operationName,
|
|
committedAt: context.now,
|
|
);
|
|
|
|
if (_failNextCommit) {
|
|
_failNextCommit = false;
|
|
throw const ApplicationPersistenceException();
|
|
}
|
|
|
|
_state = staged;
|
|
return result;
|
|
} on ApplicationFailureException catch (error) {
|
|
return ApplicationResult.failure(error.failure);
|
|
} on DomainValidationException catch (error) {
|
|
return ApplicationResult.failure(
|
|
ApplicationFailure(
|
|
code: ApplicationFailureCode.validation,
|
|
detailCode: error.code.name,
|
|
),
|
|
);
|
|
} on ArgumentError catch (error) {
|
|
return ApplicationResult.failure(
|
|
ApplicationFailure(
|
|
code: ApplicationFailureCode.validation,
|
|
detailCode: error.name,
|
|
),
|
|
);
|
|
} on ApplicationPersistenceException {
|
|
return ApplicationResult.failure(
|
|
const ApplicationFailure(
|
|
code: ApplicationFailureCode.persistenceFailure,
|
|
),
|
|
);
|
|
} catch (_) {
|
|
return ApplicationResult.failure(
|
|
const ApplicationFailure(code: ApplicationFailureCode.unexpected),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<ApplicationResult<T>> read<T>({
|
|
required Future<ApplicationResult<T>> Function(
|
|
ApplicationUnitOfWorkRepositories repositories,
|
|
) action,
|
|
}) async {
|
|
final staged = _state.copy();
|
|
final repositories = _InMemoryApplicationRepositories(staged);
|
|
|
|
try {
|
|
return await action(repositories);
|
|
} on ApplicationFailureException catch (error) {
|
|
return ApplicationResult.failure(error.failure);
|
|
} on DomainValidationException catch (error) {
|
|
return ApplicationResult.failure(
|
|
ApplicationFailure(
|
|
code: ApplicationFailureCode.validation,
|
|
detailCode: error.code.name,
|
|
),
|
|
);
|
|
} on ArgumentError catch (error) {
|
|
return ApplicationResult.failure(
|
|
ApplicationFailure(
|
|
code: ApplicationFailureCode.validation,
|
|
detailCode: error.name,
|
|
),
|
|
);
|
|
} on ApplicationPersistenceException {
|
|
return ApplicationResult.failure(
|
|
const ApplicationFailure(
|
|
code: ApplicationFailureCode.persistenceFailure,
|
|
),
|
|
);
|
|
} catch (_) {
|
|
return ApplicationResult.failure(
|
|
const ApplicationFailure(code: ApplicationFailureCode.unexpected),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
class _InMemoryApplicationState {
|
|
_InMemoryApplicationState({
|
|
required this.tasksById,
|
|
required this.taskOwnersById,
|
|
required this.taskRevisionsById,
|
|
required this.projectsById,
|
|
required this.projectOwnersById,
|
|
required this.projectRevisionsById,
|
|
required this.lockedBlocksById,
|
|
required this.lockedBlockOwnersById,
|
|
required this.lockedBlockRevisionsById,
|
|
required this.lockedOverridesById,
|
|
required this.lockedOverrideOwnersById,
|
|
required this.lockedOverrideRevisionsById,
|
|
required this.schedulingSnapshotsById,
|
|
required this.taskActivitiesById,
|
|
required this.taskActivityOwnersById,
|
|
required this.projectStatisticsByProjectId,
|
|
required this.projectStatisticsOwnersByProjectId,
|
|
required this.projectStatisticsRevisionsByProjectId,
|
|
required this.ownerSettingsByOwnerId,
|
|
required this.ownerSettingsRevisionsByOwnerId,
|
|
required this.noticeAcknowledgementsByScopedId,
|
|
required this.noticeAcknowledgementRevisionsByScopedId,
|
|
required this.operationsById,
|
|
});
|
|
|
|
final Map<String, Task> tasksById;
|
|
final Map<String, String> taskOwnersById;
|
|
final Map<String, int> taskRevisionsById;
|
|
final Map<String, ProjectProfile> projectsById;
|
|
final Map<String, String> projectOwnersById;
|
|
final Map<String, int> projectRevisionsById;
|
|
final Map<String, LockedBlock> lockedBlocksById;
|
|
final Map<String, String> lockedBlockOwnersById;
|
|
final Map<String, int> lockedBlockRevisionsById;
|
|
final Map<String, LockedBlockOverride> lockedOverridesById;
|
|
final Map<String, String> lockedOverrideOwnersById;
|
|
final Map<String, int> lockedOverrideRevisionsById;
|
|
final Map<String, SchedulingStateSnapshot> schedulingSnapshotsById;
|
|
final Map<String, TaskActivity> taskActivitiesById;
|
|
final Map<String, String> taskActivityOwnersById;
|
|
final Map<String, ProjectStatistics> projectStatisticsByProjectId;
|
|
final Map<String, String> projectStatisticsOwnersByProjectId;
|
|
final Map<String, int> projectStatisticsRevisionsByProjectId;
|
|
final Map<String, OwnerSettings> ownerSettingsByOwnerId;
|
|
final Map<String, int> ownerSettingsRevisionsByOwnerId;
|
|
final Map<String, NoticeAcknowledgementRecord>
|
|
noticeAcknowledgementsByScopedId;
|
|
final Map<String, int> noticeAcknowledgementRevisionsByScopedId;
|
|
final Map<String, ApplicationOperationRecord> operationsById;
|
|
|
|
_InMemoryApplicationState copy() {
|
|
return _InMemoryApplicationState(
|
|
tasksById: Map<String, Task>.of(tasksById),
|
|
taskOwnersById: Map<String, String>.of(taskOwnersById),
|
|
taskRevisionsById: Map<String, int>.of(taskRevisionsById),
|
|
projectsById: Map<String, ProjectProfile>.of(projectsById),
|
|
projectOwnersById: Map<String, String>.of(projectOwnersById),
|
|
projectRevisionsById: Map<String, int>.of(projectRevisionsById),
|
|
lockedBlocksById: Map<String, LockedBlock>.of(lockedBlocksById),
|
|
lockedBlockOwnersById: Map<String, String>.of(lockedBlockOwnersById),
|
|
lockedBlockRevisionsById: Map<String, int>.of(
|
|
lockedBlockRevisionsById,
|
|
),
|
|
lockedOverridesById:
|
|
Map<String, LockedBlockOverride>.of(lockedOverridesById),
|
|
lockedOverrideOwnersById: Map<String, String>.of(
|
|
lockedOverrideOwnersById,
|
|
),
|
|
lockedOverrideRevisionsById: Map<String, int>.of(
|
|
lockedOverrideRevisionsById,
|
|
),
|
|
schedulingSnapshotsById:
|
|
Map<String, SchedulingStateSnapshot>.of(schedulingSnapshotsById),
|
|
taskActivitiesById: Map<String, TaskActivity>.of(taskActivitiesById),
|
|
taskActivityOwnersById: Map<String, String>.of(taskActivityOwnersById),
|
|
projectStatisticsByProjectId:
|
|
Map<String, ProjectStatistics>.of(projectStatisticsByProjectId),
|
|
projectStatisticsOwnersByProjectId: Map<String, String>.of(
|
|
projectStatisticsOwnersByProjectId,
|
|
),
|
|
projectStatisticsRevisionsByProjectId: Map<String, int>.of(
|
|
projectStatisticsRevisionsByProjectId,
|
|
),
|
|
ownerSettingsByOwnerId: Map<String, OwnerSettings>.of(
|
|
ownerSettingsByOwnerId,
|
|
),
|
|
ownerSettingsRevisionsByOwnerId: Map<String, int>.of(
|
|
ownerSettingsRevisionsByOwnerId,
|
|
),
|
|
noticeAcknowledgementsByScopedId:
|
|
Map<String, NoticeAcknowledgementRecord>.of(
|
|
noticeAcknowledgementsByScopedId,
|
|
),
|
|
noticeAcknowledgementRevisionsByScopedId: Map<String, int>.of(
|
|
noticeAcknowledgementRevisionsByScopedId,
|
|
),
|
|
operationsById: Map<String, ApplicationOperationRecord>.of(
|
|
operationsById,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _InMemoryApplicationRepositories
|
|
implements ApplicationUnitOfWorkRepositories {
|
|
_InMemoryApplicationRepositories(_InMemoryApplicationState state)
|
|
: tasks = _UnitOfWorkTaskRepository(
|
|
tasksById: state.tasksById,
|
|
ownersById: state.taskOwnersById,
|
|
revisionsById: state.taskRevisionsById,
|
|
),
|
|
projects = _UnitOfWorkProjectRepository(
|
|
projectsById: state.projectsById,
|
|
ownersById: state.projectOwnersById,
|
|
revisionsById: state.projectRevisionsById,
|
|
),
|
|
lockedBlocks = _UnitOfWorkLockedBlockRepository(
|
|
blocksById: state.lockedBlocksById,
|
|
blockOwnersById: state.lockedBlockOwnersById,
|
|
blockRevisionsById: state.lockedBlockRevisionsById,
|
|
overridesById: state.lockedOverridesById,
|
|
overrideOwnersById: state.lockedOverrideOwnersById,
|
|
overrideRevisionsById: state.lockedOverrideRevisionsById,
|
|
),
|
|
schedulingSnapshots = _UnitOfWorkSchedulingSnapshotRepository(
|
|
state.schedulingSnapshotsById,
|
|
),
|
|
taskActivities = _UnitOfWorkTaskActivityRepository(
|
|
activitiesById: state.taskActivitiesById,
|
|
ownersById: state.taskActivityOwnersById,
|
|
),
|
|
projectStatistics = _UnitOfWorkProjectStatisticsRepository(
|
|
statisticsByProjectId: state.projectStatisticsByProjectId,
|
|
ownersByProjectId: state.projectStatisticsOwnersByProjectId,
|
|
revisionsByProjectId: state.projectStatisticsRevisionsByProjectId,
|
|
),
|
|
ownerSettings = _UnitOfWorkOwnerSettingsRepository(
|
|
settingsByOwnerId: state.ownerSettingsByOwnerId,
|
|
revisionsByOwnerId: state.ownerSettingsRevisionsByOwnerId,
|
|
),
|
|
noticeAcknowledgements = _UnitOfWorkNoticeAcknowledgementRepository(
|
|
recordsByScopedId: state.noticeAcknowledgementsByScopedId,
|
|
revisionsByScopedId: state.noticeAcknowledgementRevisionsByScopedId,
|
|
),
|
|
operations = _UnitOfWorkOperationRepository(state.operationsById);
|
|
|
|
@override
|
|
final TaskRepository tasks;
|
|
|
|
@override
|
|
final ProjectRepository projects;
|
|
|
|
@override
|
|
final LockedBlockRepository lockedBlocks;
|
|
|
|
@override
|
|
final SchedulingSnapshotRepository schedulingSnapshots;
|
|
|
|
@override
|
|
final TaskActivityRepository taskActivities;
|
|
|
|
@override
|
|
final ProjectStatisticsRepository projectStatistics;
|
|
|
|
@override
|
|
final OwnerSettingsRepository ownerSettings;
|
|
|
|
@override
|
|
final NoticeAcknowledgementRepository noticeAcknowledgements;
|
|
|
|
@override
|
|
final ApplicationOperationRepository operations;
|
|
}
|
|
|
|
class _UnitOfWorkTaskRepository implements TaskRepository {
|
|
_UnitOfWorkTaskRepository({
|
|
required Map<String, Task> tasksById,
|
|
required Map<String, String> ownersById,
|
|
required Map<String, int> revisionsById,
|
|
}) : _tasksById = tasksById,
|
|
_ownersById = ownersById,
|
|
_revisionsById = revisionsById;
|
|
|
|
final Map<String, Task> _tasksById;
|
|
final Map<String, String> _ownersById;
|
|
final Map<String, int> _revisionsById;
|
|
|
|
@override
|
|
Future<Task?> findById(String id) async => _tasksById[id];
|
|
|
|
@override
|
|
Future<RepositoryRecord<Task>?> findRecordById(String id) async {
|
|
final task = _tasksById[id];
|
|
if (task == null) {
|
|
return null;
|
|
}
|
|
return RepositoryRecord(
|
|
value: task,
|
|
ownerId: _ownerFor(id),
|
|
revision: _revisionFor(id),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<Task>> findAll() async {
|
|
return List<Task>.unmodifiable(_tasksById.values);
|
|
}
|
|
|
|
@override
|
|
Future<List<Task>> findByStatus(TaskStatus status) async {
|
|
return List<Task>.unmodifiable(
|
|
_tasksById.values.where((task) => task.status == status),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryPage<Task>> findByOwner({
|
|
required String ownerId,
|
|
RepositoryPageRequest page = const RepositoryPageRequest(),
|
|
}) async {
|
|
final tasks = _tasksById.values
|
|
.where((task) => _ownerFor(task.id) == ownerId)
|
|
.toList(growable: false)
|
|
..sort(_compareTaskIds);
|
|
return _page(tasks, page);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryPage<Task>> findByProjectId({
|
|
required String ownerId,
|
|
required String projectId,
|
|
RepositoryPageRequest page = const RepositoryPageRequest(),
|
|
}) async {
|
|
final tasks = _tasksById.values
|
|
.where(
|
|
(task) =>
|
|
_ownerFor(task.id) == ownerId && task.projectId == projectId,
|
|
)
|
|
.toList(growable: false)
|
|
..sort(_compareTaskIds);
|
|
return _page(tasks, page);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryPage<Task>> findByParentTaskId({
|
|
required String ownerId,
|
|
required String parentTaskId,
|
|
RepositoryPageRequest page = const RepositoryPageRequest(),
|
|
}) async {
|
|
final tasks = _tasksById.values
|
|
.where(
|
|
(task) =>
|
|
_ownerFor(task.id) == ownerId &&
|
|
task.parentTaskId == parentTaskId,
|
|
)
|
|
.toList(growable: false)
|
|
..sort(_compareTaskIds);
|
|
return _page(tasks, page);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryPage<Task>> findBacklogCandidates(
|
|
BacklogCandidateQuery query,
|
|
) async {
|
|
final tasks = _tasksById.values
|
|
.where(
|
|
(task) =>
|
|
_ownerFor(task.id) == query.ownerId &&
|
|
task.status == TaskStatus.backlog &&
|
|
(query.projectId == null || task.projectId == query.projectId) &&
|
|
query.tags.every(task.backlogTags.contains),
|
|
)
|
|
.toList(growable: false)
|
|
..sort(_compareBacklogCandidates);
|
|
return _page(tasks, query.page);
|
|
}
|
|
|
|
@override
|
|
Future<List<Task>> findScheduledInWindow(SchedulingWindow window) async {
|
|
return List<Task>.unmodifiable(
|
|
_tasksById.values.where((task) {
|
|
return _taskOverlapsWindow(task, window);
|
|
}),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryPage<Task>> findScheduledInWindowForOwner({
|
|
required String ownerId,
|
|
required SchedulingWindow window,
|
|
RepositoryPageRequest page = const RepositoryPageRequest(),
|
|
}) async {
|
|
final tasks = _tasksById.values
|
|
.where(
|
|
(task) =>
|
|
_ownerFor(task.id) == ownerId &&
|
|
_taskOverlapsWindow(task, window),
|
|
)
|
|
.toList(growable: false)
|
|
..sort(_compareScheduledTasks);
|
|
return _page(tasks, page);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryPage<Task>> findForLocalDay({
|
|
required String ownerId,
|
|
required CivilDate localDate,
|
|
required SchedulingWindow window,
|
|
RepositoryPageRequest page = const RepositoryPageRequest(),
|
|
}) {
|
|
return findScheduledInWindowForOwner(
|
|
ownerId: ownerId,
|
|
window: window,
|
|
page: page,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> save(Task task) async {
|
|
_tasksById[task.id] = task;
|
|
_ownersById.putIfAbsent(task.id, () => 'owner-1');
|
|
_revisionsById[task.id] = _revisionFor(task.id) + 1;
|
|
}
|
|
|
|
@override
|
|
Future<void> saveAll(Iterable<Task> tasks) async {
|
|
for (final task in tasks) {
|
|
await save(task);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryMutationResult> insert({
|
|
required Task task,
|
|
required String ownerId,
|
|
}) async {
|
|
if (_tasksById.containsKey(task.id)) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.duplicateId,
|
|
entityId: task.id,
|
|
),
|
|
);
|
|
}
|
|
_tasksById[task.id] = task;
|
|
_ownersById[task.id] = ownerId;
|
|
_revisionsById[task.id] = 1;
|
|
return RepositoryMutationResult.success(revision: 1);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryMutationResult> saveIfRevision({
|
|
required Task task,
|
|
required String ownerId,
|
|
required int expectedRevision,
|
|
}) async {
|
|
if (!_tasksById.containsKey(task.id) || _ownerFor(task.id) != ownerId) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.notFound, entityId: task.id),
|
|
);
|
|
}
|
|
final actualRevision = _revisionFor(task.id);
|
|
if (actualRevision != expectedRevision) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.staleRevision,
|
|
entityId: task.id,
|
|
expectedRevision: expectedRevision,
|
|
actualRevision: actualRevision,
|
|
),
|
|
);
|
|
}
|
|
final nextRevision = actualRevision + 1;
|
|
_tasksById[task.id] = task;
|
|
_ownersById[task.id] = ownerId;
|
|
_revisionsById[task.id] = nextRevision;
|
|
return RepositoryMutationResult.success(revision: nextRevision);
|
|
}
|
|
|
|
String _ownerFor(String id) => _ownersById[id] ?? 'owner-1';
|
|
|
|
int _revisionFor(String id) => _revisionsById[id] ?? 1;
|
|
}
|
|
|
|
class _UnitOfWorkProjectRepository implements ProjectRepository {
|
|
_UnitOfWorkProjectRepository({
|
|
required Map<String, ProjectProfile> projectsById,
|
|
required Map<String, String> ownersById,
|
|
required Map<String, int> revisionsById,
|
|
}) : _projectsById = projectsById,
|
|
_ownersById = ownersById,
|
|
_revisionsById = revisionsById;
|
|
|
|
final Map<String, ProjectProfile> _projectsById;
|
|
final Map<String, String> _ownersById;
|
|
final Map<String, int> _revisionsById;
|
|
|
|
@override
|
|
Future<ProjectProfile?> findById(String id) async => _projectsById[id];
|
|
|
|
@override
|
|
Future<RepositoryRecord<ProjectProfile>?> findRecordById(String id) async {
|
|
final project = _projectsById[id];
|
|
if (project == null) {
|
|
return null;
|
|
}
|
|
return RepositoryRecord(
|
|
value: project,
|
|
ownerId: _ownerFor(id),
|
|
revision: _revisionFor(id),
|
|
archivedAt: project.archivedAt,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<ProjectProfile>> findAll() async {
|
|
return List<ProjectProfile>.unmodifiable(_projectsById.values);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryPage<ProjectProfile>> findByOwner({
|
|
required String ownerId,
|
|
bool includeArchived = false,
|
|
RepositoryPageRequest page = const RepositoryPageRequest(),
|
|
}) async {
|
|
final projects = _projectsById.values
|
|
.where(
|
|
(project) =>
|
|
_ownerFor(project.id) == ownerId &&
|
|
(includeArchived || !project.isArchived),
|
|
)
|
|
.toList(growable: false)
|
|
..sort(_compareProjects);
|
|
return _page(projects, page);
|
|
}
|
|
|
|
@override
|
|
Future<void> save(ProjectProfile project) async {
|
|
_projectsById[project.id] = project;
|
|
_ownersById.putIfAbsent(project.id, () => 'owner-1');
|
|
_revisionsById[project.id] = _revisionFor(project.id) + 1;
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryMutationResult> insert({
|
|
required ProjectProfile project,
|
|
required String ownerId,
|
|
}) async {
|
|
if (_projectsById.containsKey(project.id)) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.duplicateId,
|
|
entityId: project.id,
|
|
),
|
|
);
|
|
}
|
|
_projectsById[project.id] = project;
|
|
_ownersById[project.id] = ownerId;
|
|
_revisionsById[project.id] = 1;
|
|
return RepositoryMutationResult.success(revision: 1);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryMutationResult> saveIfRevision({
|
|
required ProjectProfile project,
|
|
required String ownerId,
|
|
required int expectedRevision,
|
|
}) async {
|
|
if (!_projectsById.containsKey(project.id) ||
|
|
_ownerFor(project.id) != ownerId) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.notFound,
|
|
entityId: project.id,
|
|
),
|
|
);
|
|
}
|
|
final actualRevision = _revisionFor(project.id);
|
|
if (actualRevision != expectedRevision) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.staleRevision,
|
|
entityId: project.id,
|
|
expectedRevision: expectedRevision,
|
|
actualRevision: actualRevision,
|
|
),
|
|
);
|
|
}
|
|
final nextRevision = actualRevision + 1;
|
|
_projectsById[project.id] = project;
|
|
_ownersById[project.id] = ownerId;
|
|
_revisionsById[project.id] = nextRevision;
|
|
return RepositoryMutationResult.success(revision: nextRevision);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryMutationResult> archive({
|
|
required String projectId,
|
|
required String ownerId,
|
|
required int expectedRevision,
|
|
required DateTime archivedAt,
|
|
}) async {
|
|
final project = _projectsById[projectId];
|
|
if (project == null) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.notFound,
|
|
entityId: projectId,
|
|
),
|
|
);
|
|
}
|
|
return saveIfRevision(
|
|
project: project.copyWith(archivedAt: archivedAt),
|
|
ownerId: ownerId,
|
|
expectedRevision: expectedRevision,
|
|
);
|
|
}
|
|
|
|
String _ownerFor(String id) => _ownersById[id] ?? 'owner-1';
|
|
|
|
int _revisionFor(String id) => _revisionsById[id] ?? 1;
|
|
}
|
|
|
|
class _UnitOfWorkLockedBlockRepository implements LockedBlockRepository {
|
|
_UnitOfWorkLockedBlockRepository({
|
|
required Map<String, LockedBlock> blocksById,
|
|
required Map<String, String> blockOwnersById,
|
|
required Map<String, int> blockRevisionsById,
|
|
required Map<String, LockedBlockOverride> overridesById,
|
|
required Map<String, String> overrideOwnersById,
|
|
required Map<String, int> overrideRevisionsById,
|
|
}) : _blocksById = blocksById,
|
|
_blockOwnersById = blockOwnersById,
|
|
_blockRevisionsById = blockRevisionsById,
|
|
_overridesById = overridesById,
|
|
_overrideOwnersById = overrideOwnersById,
|
|
_overrideRevisionsById = overrideRevisionsById;
|
|
|
|
final Map<String, LockedBlock> _blocksById;
|
|
final Map<String, String> _blockOwnersById;
|
|
final Map<String, int> _blockRevisionsById;
|
|
final Map<String, LockedBlockOverride> _overridesById;
|
|
final Map<String, String> _overrideOwnersById;
|
|
final Map<String, int> _overrideRevisionsById;
|
|
|
|
@override
|
|
Future<LockedBlock?> findBlockById(String id) async => _blocksById[id];
|
|
|
|
@override
|
|
Future<RepositoryRecord<LockedBlock>?> findBlockRecordById(String id) async {
|
|
final block = _blocksById[id];
|
|
if (block == null) {
|
|
return null;
|
|
}
|
|
return RepositoryRecord(
|
|
value: block,
|
|
ownerId: _blockOwnerFor(id),
|
|
revision: _blockRevisionFor(id),
|
|
archivedAt: block.archivedAt,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<LockedBlock>> findAllBlocks() async {
|
|
return List<LockedBlock>.unmodifiable(_blocksById.values);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryPage<LockedBlock>> findBlocksByOwner({
|
|
required String ownerId,
|
|
bool includeArchived = false,
|
|
RepositoryPageRequest page = const RepositoryPageRequest(),
|
|
}) async {
|
|
final blocks = _blocksById.values
|
|
.where(
|
|
(block) =>
|
|
_blockOwnerFor(block.id) == ownerId &&
|
|
(includeArchived || !block.isArchived),
|
|
)
|
|
.toList(growable: false)
|
|
..sort(_compareLockedBlocks);
|
|
return _page(blocks, page);
|
|
}
|
|
|
|
@override
|
|
Future<LockedBlockOverride?> findOverrideById(String id) async {
|
|
return _overridesById[id];
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryRecord<LockedBlockOverride>?> findOverrideRecordById(
|
|
String id,
|
|
) async {
|
|
final override = _overridesById[id];
|
|
if (override == null) {
|
|
return null;
|
|
}
|
|
return RepositoryRecord(
|
|
value: override,
|
|
ownerId: _overrideOwnerFor(id),
|
|
revision: _overrideRevisionFor(id),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<LockedBlockOverride>> findAllOverrides() async {
|
|
return List<LockedBlockOverride>.unmodifiable(_overridesById.values);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryPage<LockedBlockOverride>> findOverridesForDate({
|
|
required String ownerId,
|
|
required CivilDate date,
|
|
RepositoryPageRequest page = const RepositoryPageRequest(),
|
|
}) async {
|
|
final overrides = _overridesById.values
|
|
.where(
|
|
(override) =>
|
|
_overrideOwnerFor(override.id) == ownerId &&
|
|
override.date == date,
|
|
)
|
|
.toList(growable: false)
|
|
..sort(_compareLockedOverrides);
|
|
return _page(overrides, page);
|
|
}
|
|
|
|
@override
|
|
Future<void> saveBlock(LockedBlock block) async {
|
|
_blocksById[block.id] = block;
|
|
_blockOwnersById.putIfAbsent(block.id, () => 'owner-1');
|
|
_blockRevisionsById[block.id] = _blockRevisionFor(block.id) + 1;
|
|
}
|
|
|
|
@override
|
|
Future<void> saveOverride(LockedBlockOverride override) async {
|
|
_overridesById[override.id] = override;
|
|
_overrideOwnersById.putIfAbsent(override.id, () => 'owner-1');
|
|
_overrideRevisionsById[override.id] = _overrideRevisionFor(override.id) + 1;
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryMutationResult> insertBlock({
|
|
required LockedBlock block,
|
|
required String ownerId,
|
|
}) async {
|
|
if (_blocksById.containsKey(block.id)) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.duplicateId,
|
|
entityId: block.id,
|
|
),
|
|
);
|
|
}
|
|
_blocksById[block.id] = block;
|
|
_blockOwnersById[block.id] = ownerId;
|
|
_blockRevisionsById[block.id] = 1;
|
|
return RepositoryMutationResult.success(revision: 1);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryMutationResult> saveBlockIfRevision({
|
|
required LockedBlock block,
|
|
required String ownerId,
|
|
required int expectedRevision,
|
|
}) async {
|
|
if (!_blocksById.containsKey(block.id) ||
|
|
_blockOwnerFor(block.id) != ownerId) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.notFound,
|
|
entityId: block.id,
|
|
),
|
|
);
|
|
}
|
|
final actualRevision = _blockRevisionFor(block.id);
|
|
if (actualRevision != expectedRevision) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.staleRevision,
|
|
entityId: block.id,
|
|
expectedRevision: expectedRevision,
|
|
actualRevision: actualRevision,
|
|
),
|
|
);
|
|
}
|
|
final nextRevision = actualRevision + 1;
|
|
_blocksById[block.id] = block;
|
|
_blockOwnersById[block.id] = ownerId;
|
|
_blockRevisionsById[block.id] = nextRevision;
|
|
return RepositoryMutationResult.success(revision: nextRevision);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryMutationResult> archiveBlock({
|
|
required String blockId,
|
|
required String ownerId,
|
|
required int expectedRevision,
|
|
required DateTime archivedAt,
|
|
}) async {
|
|
final block = _blocksById[blockId];
|
|
if (block == null) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.notFound,
|
|
entityId: blockId,
|
|
),
|
|
);
|
|
}
|
|
return saveBlockIfRevision(
|
|
block: block.copyWith(updatedAt: archivedAt, archivedAt: archivedAt),
|
|
ownerId: ownerId,
|
|
expectedRevision: expectedRevision,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryMutationResult> insertOverride({
|
|
required LockedBlockOverride override,
|
|
required String ownerId,
|
|
}) async {
|
|
if (_overridesById.containsKey(override.id)) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.duplicateId,
|
|
entityId: override.id,
|
|
),
|
|
);
|
|
}
|
|
_overridesById[override.id] = override;
|
|
_overrideOwnersById[override.id] = ownerId;
|
|
_overrideRevisionsById[override.id] = 1;
|
|
return RepositoryMutationResult.success(revision: 1);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryMutationResult> saveOverrideIfRevision({
|
|
required LockedBlockOverride override,
|
|
required String ownerId,
|
|
required int expectedRevision,
|
|
}) async {
|
|
if (!_overridesById.containsKey(override.id) ||
|
|
_overrideOwnerFor(override.id) != ownerId) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.notFound,
|
|
entityId: override.id,
|
|
),
|
|
);
|
|
}
|
|
final actualRevision = _overrideRevisionFor(override.id);
|
|
if (actualRevision != expectedRevision) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.staleRevision,
|
|
entityId: override.id,
|
|
expectedRevision: expectedRevision,
|
|
actualRevision: actualRevision,
|
|
),
|
|
);
|
|
}
|
|
final nextRevision = actualRevision + 1;
|
|
_overridesById[override.id] = override;
|
|
_overrideOwnersById[override.id] = ownerId;
|
|
_overrideRevisionsById[override.id] = nextRevision;
|
|
return RepositoryMutationResult.success(revision: nextRevision);
|
|
}
|
|
|
|
String _blockOwnerFor(String id) => _blockOwnersById[id] ?? 'owner-1';
|
|
|
|
int _blockRevisionFor(String id) => _blockRevisionsById[id] ?? 1;
|
|
|
|
String _overrideOwnerFor(String id) => _overrideOwnersById[id] ?? 'owner-1';
|
|
|
|
int _overrideRevisionFor(String id) => _overrideRevisionsById[id] ?? 1;
|
|
}
|
|
|
|
class _UnitOfWorkSchedulingSnapshotRepository
|
|
implements SchedulingSnapshotRepository {
|
|
_UnitOfWorkSchedulingSnapshotRepository(this._snapshotsById);
|
|
|
|
final Map<String, SchedulingStateSnapshot> _snapshotsById;
|
|
|
|
@override
|
|
Future<SchedulingStateSnapshot?> findById(String id) async {
|
|
return _snapshotsById[id];
|
|
}
|
|
|
|
@override
|
|
Future<List<SchedulingStateSnapshot>> findInWindow(
|
|
SchedulingWindow window,
|
|
) async {
|
|
return List<SchedulingStateSnapshot>.unmodifiable(
|
|
_snapshotsById.values.where(
|
|
(snapshot) => snapshot.window.interval.overlaps(window.interval),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> save(SchedulingStateSnapshot snapshot) async {
|
|
_snapshotsById[snapshot.id] = snapshot;
|
|
}
|
|
}
|
|
|
|
class _UnitOfWorkTaskActivityRepository implements TaskActivityRepository {
|
|
_UnitOfWorkTaskActivityRepository({
|
|
required Map<String, TaskActivity> activitiesById,
|
|
required Map<String, String> ownersById,
|
|
}) : _activitiesById = activitiesById,
|
|
_ownersById = ownersById;
|
|
|
|
final Map<String, TaskActivity> _activitiesById;
|
|
final Map<String, String> _ownersById;
|
|
|
|
@override
|
|
Future<TaskActivity?> findById(String id) async => _activitiesById[id];
|
|
|
|
@override
|
|
Future<List<TaskActivity>> findByOperationId(String operationId) async {
|
|
return List<TaskActivity>.unmodifiable(
|
|
_activitiesById.values.where(
|
|
(activity) => activity.operationId == operationId,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<TaskActivity>> findByTaskId(String taskId) async {
|
|
return List<TaskActivity>.unmodifiable(
|
|
_activitiesById.values.where((activity) => activity.taskId == taskId),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<TaskActivity>> findByProjectId(String projectId) async {
|
|
return List<TaskActivity>.unmodifiable(
|
|
_activitiesById.values.where(
|
|
(activity) => activity.projectId == projectId,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<TaskActivity>> findAll() async {
|
|
return List<TaskActivity>.unmodifiable(_activitiesById.values);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryPage<TaskActivity>> findByOwner({
|
|
required String ownerId,
|
|
RepositoryPageRequest page = const RepositoryPageRequest(),
|
|
}) async {
|
|
final activities = _activitiesById.values
|
|
.where((activity) => _ownerFor(activity.id) == ownerId)
|
|
.toList(growable: false)
|
|
..sort(_compareActivities);
|
|
return _page(activities, page);
|
|
}
|
|
|
|
@override
|
|
Future<void> save(TaskActivity activity) async {
|
|
_activitiesById[activity.id] = activity;
|
|
_ownersById.putIfAbsent(activity.id, () => 'owner-1');
|
|
}
|
|
|
|
@override
|
|
Future<void> saveAll(Iterable<TaskActivity> activities) async {
|
|
for (final activity in activities) {
|
|
_activitiesById[activity.id] = activity;
|
|
_ownersById.putIfAbsent(activity.id, () => 'owner-1');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryMutationResult> append({
|
|
required TaskActivity activity,
|
|
required String ownerId,
|
|
}) async {
|
|
if (_activitiesById.containsKey(activity.id)) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.duplicateId,
|
|
entityId: activity.id,
|
|
),
|
|
);
|
|
}
|
|
_activitiesById[activity.id] = activity;
|
|
_ownersById[activity.id] = ownerId;
|
|
return RepositoryMutationResult.success(revision: 1);
|
|
}
|
|
|
|
String _ownerFor(String id) => _ownersById[id] ?? 'owner-1';
|
|
}
|
|
|
|
class _UnitOfWorkProjectStatisticsRepository
|
|
implements ProjectStatisticsRepository {
|
|
_UnitOfWorkProjectStatisticsRepository({
|
|
required Map<String, ProjectStatistics> statisticsByProjectId,
|
|
required Map<String, String> ownersByProjectId,
|
|
required Map<String, int> revisionsByProjectId,
|
|
}) : _statisticsByProjectId = statisticsByProjectId,
|
|
_ownersByProjectId = ownersByProjectId,
|
|
_revisionsByProjectId = revisionsByProjectId;
|
|
|
|
final Map<String, ProjectStatistics> _statisticsByProjectId;
|
|
final Map<String, String> _ownersByProjectId;
|
|
final Map<String, int> _revisionsByProjectId;
|
|
|
|
@override
|
|
Future<ProjectStatistics?> findByProjectId(String projectId) async {
|
|
return _statisticsByProjectId[projectId];
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryRecord<ProjectStatistics>?> findRecordByProjectId(
|
|
String projectId,
|
|
) async {
|
|
final statistics = _statisticsByProjectId[projectId];
|
|
if (statistics == null) {
|
|
return null;
|
|
}
|
|
return RepositoryRecord(
|
|
value: statistics,
|
|
ownerId: _ownersByProjectId[projectId] ?? 'owner-1',
|
|
revision: _revisionsByProjectId[projectId] ?? 1,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<ProjectStatistics>> findAll() async {
|
|
return List<ProjectStatistics>.unmodifiable(_statisticsByProjectId.values);
|
|
}
|
|
|
|
@override
|
|
Future<void> save(ProjectStatistics statistics) async {
|
|
_statisticsByProjectId[statistics.projectId] = statistics;
|
|
_ownersByProjectId.putIfAbsent(statistics.projectId, () => 'owner-1');
|
|
_revisionsByProjectId[statistics.projectId] =
|
|
(_revisionsByProjectId[statistics.projectId] ?? 1) + 1;
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryMutationResult> saveIfRevision({
|
|
required ProjectStatistics statistics,
|
|
required String ownerId,
|
|
required int expectedRevision,
|
|
}) async {
|
|
final projectId = statistics.projectId;
|
|
if (!_statisticsByProjectId.containsKey(projectId) ||
|
|
(_ownersByProjectId[projectId] ?? 'owner-1') != ownerId) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.notFound,
|
|
entityId: projectId,
|
|
),
|
|
);
|
|
}
|
|
final actualRevision = _revisionsByProjectId[projectId] ?? 1;
|
|
if (actualRevision != expectedRevision) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.staleRevision,
|
|
entityId: projectId,
|
|
expectedRevision: expectedRevision,
|
|
actualRevision: actualRevision,
|
|
),
|
|
);
|
|
}
|
|
final nextRevision = actualRevision + 1;
|
|
_statisticsByProjectId[projectId] = statistics;
|
|
_ownersByProjectId[projectId] = ownerId;
|
|
_revisionsByProjectId[projectId] = nextRevision;
|
|
return RepositoryMutationResult.success(revision: nextRevision);
|
|
}
|
|
}
|
|
|
|
class _UnitOfWorkOwnerSettingsRepository implements OwnerSettingsRepository {
|
|
_UnitOfWorkOwnerSettingsRepository({
|
|
required Map<String, OwnerSettings> settingsByOwnerId,
|
|
required Map<String, int> revisionsByOwnerId,
|
|
}) : _settingsByOwnerId = settingsByOwnerId,
|
|
_revisionsByOwnerId = revisionsByOwnerId;
|
|
|
|
final Map<String, OwnerSettings> _settingsByOwnerId;
|
|
final Map<String, int> _revisionsByOwnerId;
|
|
|
|
@override
|
|
Future<OwnerSettings?> findByOwnerId(String ownerId) async {
|
|
return _settingsByOwnerId[ownerId];
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryRecord<OwnerSettings>?> findRecordByOwnerId(
|
|
String ownerId,
|
|
) async {
|
|
final settings = _settingsByOwnerId[ownerId];
|
|
if (settings == null) {
|
|
return null;
|
|
}
|
|
return RepositoryRecord(
|
|
value: settings,
|
|
ownerId: ownerId,
|
|
revision: _revisionsByOwnerId[ownerId] ?? 1,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> save(OwnerSettings settings) async {
|
|
_settingsByOwnerId[settings.ownerId] = settings;
|
|
_revisionsByOwnerId[settings.ownerId] =
|
|
(_revisionsByOwnerId[settings.ownerId] ?? 1) + 1;
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryMutationResult> saveIfRevision({
|
|
required OwnerSettings settings,
|
|
required int expectedRevision,
|
|
}) async {
|
|
final ownerId = settings.ownerId;
|
|
if (!_settingsByOwnerId.containsKey(ownerId)) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.notFound,
|
|
entityId: ownerId,
|
|
),
|
|
);
|
|
}
|
|
final actualRevision = _revisionsByOwnerId[ownerId] ?? 1;
|
|
if (actualRevision != expectedRevision) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.staleRevision,
|
|
entityId: ownerId,
|
|
expectedRevision: expectedRevision,
|
|
actualRevision: actualRevision,
|
|
),
|
|
);
|
|
}
|
|
final nextRevision = actualRevision + 1;
|
|
_settingsByOwnerId[ownerId] = settings;
|
|
_revisionsByOwnerId[ownerId] = nextRevision;
|
|
return RepositoryMutationResult.success(revision: nextRevision);
|
|
}
|
|
}
|
|
|
|
class _UnitOfWorkNoticeAcknowledgementRepository
|
|
implements NoticeAcknowledgementRepository {
|
|
_UnitOfWorkNoticeAcknowledgementRepository({
|
|
required Map<String, NoticeAcknowledgementRecord> recordsByScopedId,
|
|
required Map<String, int> revisionsByScopedId,
|
|
}) : _recordsByScopedId = recordsByScopedId,
|
|
_revisionsByScopedId = revisionsByScopedId;
|
|
|
|
final Map<String, NoticeAcknowledgementRecord> _recordsByScopedId;
|
|
final Map<String, int> _revisionsByScopedId;
|
|
|
|
@override
|
|
Future<NoticeAcknowledgementRecord?> findById({
|
|
required String ownerId,
|
|
required String noticeId,
|
|
}) async {
|
|
return _recordsByScopedId[
|
|
_noticeAcknowledgementKey(ownerId: ownerId, noticeId: noticeId)];
|
|
}
|
|
|
|
@override
|
|
Future<List<NoticeAcknowledgementRecord>> findByOwnerId(
|
|
String ownerId,
|
|
) async {
|
|
return List<NoticeAcknowledgementRecord>.unmodifiable(
|
|
_recordsByScopedId.values.where((record) => record.ownerId == ownerId),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryPage<NoticeAcknowledgementRecord>> findByOwner({
|
|
required String ownerId,
|
|
RepositoryPageRequest page = const RepositoryPageRequest(),
|
|
}) async {
|
|
final records = _recordsByScopedId.values
|
|
.where((record) => record.ownerId == ownerId)
|
|
.toList(growable: false)
|
|
..sort(_compareNoticeAcknowledgements);
|
|
return _page(records, page);
|
|
}
|
|
|
|
@override
|
|
Future<void> save(NoticeAcknowledgementRecord record) async {
|
|
final key = _noticeAcknowledgementKey(
|
|
ownerId: record.ownerId,
|
|
noticeId: record.noticeId,
|
|
);
|
|
_recordsByScopedId[key] = record;
|
|
_revisionsByScopedId[key] = (_revisionsByScopedId[key] ?? 1) + 1;
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryMutationResult> insert(
|
|
NoticeAcknowledgementRecord record,
|
|
) async {
|
|
final key = _noticeAcknowledgementKey(
|
|
ownerId: record.ownerId,
|
|
noticeId: record.noticeId,
|
|
);
|
|
if (_recordsByScopedId.containsKey(key)) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.duplicateId,
|
|
entityId: record.noticeId,
|
|
),
|
|
);
|
|
}
|
|
_recordsByScopedId[key] = record;
|
|
_revisionsByScopedId[key] = 1;
|
|
return RepositoryMutationResult.success(revision: 1);
|
|
}
|
|
}
|
|
|
|
class _UnitOfWorkOperationRepository implements ApplicationOperationRepository {
|
|
_UnitOfWorkOperationRepository(this._operationsById);
|
|
|
|
final Map<String, ApplicationOperationRecord> _operationsById;
|
|
|
|
@override
|
|
Future<ApplicationOperationRecord?> findById(String operationId) async {
|
|
return _operationsById[operationId];
|
|
}
|
|
|
|
@override
|
|
Future<ApplicationOperationRecord?> findByIdempotencyKey({
|
|
required String ownerId,
|
|
required String operationId,
|
|
}) async {
|
|
final operation = _operationsById[operationId];
|
|
if (operation == null || operation.ownerId != ownerId) {
|
|
return null;
|
|
}
|
|
return operation;
|
|
}
|
|
|
|
@override
|
|
Future<void> save(ApplicationOperationRecord operation) async {
|
|
_operationsById[operation.operationId] = operation;
|
|
}
|
|
|
|
@override
|
|
Future<RepositoryMutationResult> insertIfAbsent(
|
|
ApplicationOperationRecord operation,
|
|
) async {
|
|
if (_operationsById.containsKey(operation.operationId)) {
|
|
return RepositoryMutationResult.failure(
|
|
RepositoryFailure(
|
|
code: RepositoryFailureCode.duplicateOperation,
|
|
entityId: operation.operationId,
|
|
),
|
|
);
|
|
}
|
|
_operationsById[operation.operationId] = operation;
|
|
return RepositoryMutationResult.success(revision: 1);
|
|
}
|
|
}
|
|
|
|
String _noticeAcknowledgementKey({
|
|
required String ownerId,
|
|
required String noticeId,
|
|
}) {
|
|
return '$ownerId::$noticeId';
|
|
}
|
|
|
|
RepositoryPage<T> _page<T>(List<T> sortedItems, RepositoryPageRequest request) {
|
|
final offset = int.tryParse(request.cursor ?? '') ?? 0;
|
|
final safeOffset = offset.clamp(0, sortedItems.length);
|
|
final end = (safeOffset + request.limit).clamp(0, sortedItems.length);
|
|
final items = sortedItems.sublist(safeOffset, end);
|
|
final nextCursor = end < sortedItems.length ? end.toString() : null;
|
|
return RepositoryPage<T>(
|
|
items: List<T>.unmodifiable(items),
|
|
nextCursor: nextCursor,
|
|
);
|
|
}
|
|
|
|
bool _taskOverlapsWindow(Task task, SchedulingWindow window) {
|
|
final start = task.scheduledStart;
|
|
final end = task.scheduledEnd;
|
|
if (start == null || end == null) {
|
|
return false;
|
|
}
|
|
return TimeInterval(start: start, end: end).overlaps(window.interval);
|
|
}
|
|
|
|
int _compareTaskIds(Task left, Task right) => left.id.compareTo(right.id);
|
|
|
|
int _compareScheduledTasks(Task left, Task right) {
|
|
final leftStart = left.scheduledStart;
|
|
final rightStart = right.scheduledStart;
|
|
if (leftStart != null && rightStart != null) {
|
|
final startComparison = leftStart.compareTo(rightStart);
|
|
if (startComparison != 0) {
|
|
return startComparison;
|
|
}
|
|
}
|
|
return left.id.compareTo(right.id);
|
|
}
|
|
|
|
int _compareBacklogCandidates(Task left, Task right) {
|
|
final createdComparison = left.createdAt.compareTo(right.createdAt);
|
|
if (createdComparison != 0) {
|
|
return createdComparison;
|
|
}
|
|
return left.id.compareTo(right.id);
|
|
}
|
|
|
|
int _compareProjects(ProjectProfile left, ProjectProfile right) {
|
|
final nameComparison = left.name.compareTo(right.name);
|
|
if (nameComparison != 0) {
|
|
return nameComparison;
|
|
}
|
|
return left.id.compareTo(right.id);
|
|
}
|
|
|
|
int _compareLockedBlocks(LockedBlock left, LockedBlock right) {
|
|
final nameComparison = left.name.compareTo(right.name);
|
|
if (nameComparison != 0) {
|
|
return nameComparison;
|
|
}
|
|
return left.id.compareTo(right.id);
|
|
}
|
|
|
|
int _compareLockedOverrides(
|
|
LockedBlockOverride left,
|
|
LockedBlockOverride right,
|
|
) {
|
|
final dateComparison = left.date.compareTo(right.date);
|
|
if (dateComparison != 0) {
|
|
return dateComparison;
|
|
}
|
|
final createdComparison = left.createdAt.compareTo(right.createdAt);
|
|
if (createdComparison != 0) {
|
|
return createdComparison;
|
|
}
|
|
return left.id.compareTo(right.id);
|
|
}
|
|
|
|
int _compareActivities(TaskActivity left, TaskActivity right) {
|
|
final occurredComparison = left.occurredAt.compareTo(right.occurredAt);
|
|
if (occurredComparison != 0) {
|
|
return occurredComparison;
|
|
}
|
|
return left.id.compareTo(right.id);
|
|
}
|
|
|
|
int _compareNoticeAcknowledgements(
|
|
NoticeAcknowledgementRecord left,
|
|
NoticeAcknowledgementRecord right,
|
|
) {
|
|
final acknowledgedComparison =
|
|
left.acknowledgedAt.compareTo(right.acknowledgedAt);
|
|
if (acknowledgedComparison != 0) {
|
|
return acknowledgedComparison;
|
|
}
|
|
return left.noticeId.compareTo(right.noticeId);
|
|
}
|
|
|
|
String _requiredTrimmed(String value, String name) {
|
|
final trimmed = value.trim();
|
|
if (trimmed.isEmpty) {
|
|
throw ArgumentError.value(value, name, 'Value is required.');
|
|
}
|
|
return trimmed;
|
|
}
|