krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

v5.0.1: DomainDig/DomainViewModel+Workflows.swift · raw

  1import Foundation
  2import SwiftUI
  3
  4/// Workflow surface of `DomainViewModel`: looking up workflows, the
  5/// collaboration permission checks for a workflow, the CRUD mutators, and
  6/// running a workflow's domains as a batch.
  7///
  8/// `runWorkflow`/`rerunCurrentDomain` drive `startBatchLookup` (the shared batch
  9/// primitive used by manual and watchlist runs too), which stays on the main
 10/// type along with `persistWorkflows` and the `normalizedDomain(s)` helpers.
 11extension DomainViewModel {
 12    func workflow(withID id: UUID) -> DomainWorkflow? {
 13        workflows.first(where: { $0.id == id })
 14    }
 15
 16    func workflowsContaining(domain: String) -> [DomainWorkflow] {
 17        let normalized = normalizedDomain(domain)
 18        guard !normalized.isEmpty else { return [] }
 19        return workflows.filter { workflow in
 20            workflow.domains.contains(where: { $0.caseInsensitiveCompare(normalized) == .orderedSame })
 21        }
 22    }
 23
 24    func canEdit(_ workflow: DomainWorkflow) -> Bool {
 25        workflow.collaboration?.canEdit ?? true
 26    }
 27
 28    func canDelete(_ workflow: DomainWorkflow) -> Bool {
 29        workflow.collaboration?.isOwner ?? true
 30    }
 31
 32    func collaborationLabel(for workflow: DomainWorkflow) -> String? {
 33        guard let collaboration = workflow.collaboration, collaboration.isShared else { return nil }
 34        return "\(collaboration.ownership.title)\(collaboration.permission.title)"
 35    }
 36
 37    @discardableResult
 38    func createWorkflow(name: String, domains: [String], notes: String? = nil) -> DomainWorkflow? {
 39        let normalizedDomains = normalizedDomains(domains)
 40        let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
 41        guard !trimmedName.isEmpty, !normalizedDomains.isEmpty else { return nil }
 42        guard FeatureAccessService.canCreateWorkflow(currentCount: workflows.count) else {
 43            upgradePrompt = FeatureAccessService.upgradePromptForWorkflows(currentCount: workflows.count)
 44            return nil
 45        }
 46
 47        let workflow = DomainWorkflow(
 48            name: trimmedName,
 49            domains: normalizedDomains,
 50            createdAt: Date(),
 51            updatedAt: Date(),
 52            notes: notes?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty,
 53            collaboration: CollaborationMetadata(
 54                scope: .privateDatabase,
 55                ownership: .owner,
 56                permission: .editable
 57            )
 58        )
 59        workflows.insert(workflow, at: 0)
 60        persistWorkflows()
 61        return workflow
 62    }
 63
 64    func updateWorkflow(_ workflow: DomainWorkflow, name: String, domains: [String], notes: String?) {
 65        guard canEdit(workflow) else { return }
 66        guard let index = workflows.firstIndex(where: { $0.id == workflow.id }) else { return }
 67        let normalizedDomains = normalizedDomains(domains)
 68        let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
 69        guard !trimmedName.isEmpty, !normalizedDomains.isEmpty else { return }
 70
 71        workflows[index].name = trimmedName
 72        workflows[index].domains = normalizedDomains
 73        workflows[index].notes = notes?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty
 74        workflows[index].updatedAt = Date()
 75        persistWorkflows()
 76    }
 77
 78    func deleteWorkflow(_ workflow: DomainWorkflow) {
 79        guard canDelete(workflow) else { return }
 80        CloudSyncService.shared.recordWorkflowDeletion(workflow)
 81        workflows.removeAll { $0.id == workflow.id }
 82        if latestWorkflowRunSummary?.workflowID == workflow.id {
 83            latestWorkflowRunSummary = nil
 84        }
 85        if activeWorkflowRunID == workflow.id {
 86            activeWorkflowRunID = nil
 87            activeWorkflowRunName = nil
 88        }
 89        persistWorkflows()
 90    }
 91
 92    func addDomains(_ domains: [String], to workflow: DomainWorkflow) {
 93        guard canEdit(workflow) else { return }
 94        guard let index = workflows.firstIndex(where: { $0.id == workflow.id }) else { return }
 95        let mergedDomains = normalizedDomains(workflows[index].domains + domains)
 96        guard mergedDomains != workflows[index].domains else { return }
 97        workflows[index].domains = mergedDomains
 98        workflows[index].updatedAt = Date()
 99        persistWorkflows()
100    }
101
102    func removeWorkflowDomains(at offsets: IndexSet, from workflow: DomainWorkflow) {
103        guard canEdit(workflow) else { return }
104        guard let index = workflows.firstIndex(where: { $0.id == workflow.id }) else { return }
105        workflows[index].domains.remove(atOffsets: offsets)
106        workflows[index].updatedAt = Date()
107        persistWorkflows()
108    }
109
110    func moveWorkflowDomains(from offsets: IndexSet, to destination: Int, in workflow: DomainWorkflow) {
111        guard canEdit(workflow) else { return }
112        guard let index = workflows.firstIndex(where: { $0.id == workflow.id }) else { return }
113        workflows[index].domains.move(fromOffsets: offsets, toOffset: destination)
114        workflows[index].updatedAt = Date()
115        persistWorkflows()
116    }
117
118    func runWorkflow(_ workflow: DomainWorkflow) {
119        guard !workflow.domains.isEmpty else { return }
120        guard FeatureAccessService.canRunBatch(domainCount: workflow.domains.count) else {
121            upgradePrompt = FeatureAccessService.upgradePromptForBatch(domainCount: workflow.domains.count)
122            return
123        }
124        startBatchLookup(domains: workflow.domains, source: .workflow, workflow: workflow)
125    }
126
127    func rerunCurrentDomain(in workflow: DomainWorkflow) {
128        guard workflow.domains.contains(where: { $0.caseInsensitiveCompare(searchedDomain) == .orderedSame }) else {
129            return
130        }
131        runWorkflow(workflow)
132    }
133
134    func refreshWorkflowList() async {
135        workflows = Self.loadWorkflows()
136        await Task.yield()
137    }
138}
139
140private extension String {
141    var nilIfEmpty: String? {
142        isEmpty ? nil : self
143    }
144}