krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v5.0.2: 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}