@topogram/cli 0.3.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +67 -0
- package/CHANGELOG.md +240 -0
- package/README.md +223 -0
- package/package.json +51 -0
- package/src/adoption/index.js +3 -0
- package/src/adoption/plan.js +702 -0
- package/src/adoption/reporting.js +464 -0
- package/src/adoption/review-groups.js +313 -0
- package/src/agent-ops/query-builders.js +5012 -0
- package/src/archive/archive.js +141 -0
- package/src/archive/compact.js +26 -0
- package/src/archive/jsonl.js +70 -0
- package/src/archive/resolver-bridge.js +82 -0
- package/src/archive/schema.js +87 -0
- package/src/archive/unarchive.js +108 -0
- package/src/catalog.js +752 -0
- package/src/cli/catalog-alias.js +166 -0
- package/src/cli.js +9738 -0
- package/src/component-behavior.js +173 -0
- package/src/example-implementation.js +91 -0
- package/src/format.js +19 -0
- package/src/generator/adapters.d.ts +4 -0
- package/src/generator/adapters.js +325 -0
- package/src/generator/api.d.ts +1 -0
- package/src/generator/api.js +1196 -0
- package/src/generator/check.js +355 -0
- package/src/generator/component-conformance.js +767 -0
- package/src/generator/components.js +39 -0
- package/src/generator/context/bundle.js +291 -0
- package/src/generator/context/diff.js +256 -0
- package/src/generator/context/digest.js +182 -0
- package/src/generator/context/domain-coverage.js +94 -0
- package/src/generator/context/domain-page.js +137 -0
- package/src/generator/context/index.js +42 -0
- package/src/generator/context/report.js +121 -0
- package/src/generator/context/shared.js +1397 -0
- package/src/generator/context/slice.js +703 -0
- package/src/generator/context/task-mode.js +466 -0
- package/src/generator/docs.js +327 -0
- package/src/generator/index.js +161 -0
- package/src/generator/native/parity-bundle.js +311 -0
- package/src/generator/output.js +300 -0
- package/src/generator/registry.js +482 -0
- package/src/generator/runtime/app-bundle.js +456 -0
- package/src/generator/runtime/bundle-shared.js +166 -0
- package/src/generator/runtime/compile-check.js +163 -0
- package/src/generator/runtime/deployment.js +287 -0
- package/src/generator/runtime/environment.js +635 -0
- package/src/generator/runtime/index.js +32 -0
- package/src/generator/runtime/runtime-check.js +554 -0
- package/src/generator/runtime/shared.js +515 -0
- package/src/generator/runtime/smoke.js +219 -0
- package/src/generator/schema.js +204 -0
- package/src/generator/sdlc/board.js +66 -0
- package/src/generator/sdlc/doc-page.js +53 -0
- package/src/generator/sdlc/index.js +23 -0
- package/src/generator/sdlc/release-notes.js +62 -0
- package/src/generator/sdlc/traceability-matrix.js +65 -0
- package/src/generator/shared.js +29 -0
- package/src/generator/surfaces/contracts.js +146 -0
- package/src/generator/surfaces/databases/contract.js +40 -0
- package/src/generator/surfaces/databases/index.js +84 -0
- package/src/generator/surfaces/databases/lifecycle-shared.d.ts +1 -0
- package/src/generator/surfaces/databases/lifecycle-shared.js +612 -0
- package/src/generator/surfaces/databases/migration-plan.js +281 -0
- package/src/generator/surfaces/databases/postgres/capabilities.js +14 -0
- package/src/generator/surfaces/databases/postgres/drizzle.js +99 -0
- package/src/generator/surfaces/databases/postgres/index.js +9 -0
- package/src/generator/surfaces/databases/postgres/lifecycle.js +16 -0
- package/src/generator/surfaces/databases/postgres/prisma.js +159 -0
- package/src/generator/surfaces/databases/postgres/sql-migration.js +102 -0
- package/src/generator/surfaces/databases/postgres/sql-schema.js +34 -0
- package/src/generator/surfaces/databases/shared.d.ts +1 -0
- package/src/generator/surfaces/databases/shared.js +350 -0
- package/src/generator/surfaces/databases/snapshot.js +96 -0
- package/src/generator/surfaces/databases/sqlite/capabilities.js +14 -0
- package/src/generator/surfaces/databases/sqlite/index.js +8 -0
- package/src/generator/surfaces/databases/sqlite/lifecycle.js +16 -0
- package/src/generator/surfaces/databases/sqlite/prisma.js +143 -0
- package/src/generator/surfaces/databases/sqlite/sql-migration.js +65 -0
- package/src/generator/surfaces/databases/sqlite/sql-schema.js +27 -0
- package/src/generator/surfaces/index.js +25 -0
- package/src/generator/surfaces/native/swiftui-app.js +38 -0
- package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +20 -0
- package/src/generator/surfaces/native/swiftui-templates/README.generated.md +26 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +682 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/TodoAPIClient.swift +156 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/TodoSwiftUIApp.swift +44 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/Visibility.swift +183 -0
- package/src/generator/surfaces/services/express.d.ts +1 -0
- package/src/generator/surfaces/services/express.js +766 -0
- package/src/generator/surfaces/services/hono.d.ts +1 -0
- package/src/generator/surfaces/services/hono.js +204 -0
- package/src/generator/surfaces/services/index.js +42 -0
- package/src/generator/surfaces/services/persistence-wiring.js +240 -0
- package/src/generator/surfaces/services/runtime-helpers.js +631 -0
- package/src/generator/surfaces/services/server-contract.js +80 -0
- package/src/generator/surfaces/services/stateless.d.ts +1 -0
- package/src/generator/surfaces/services/stateless.js +97 -0
- package/src/generator/surfaces/shared.js +64 -0
- package/src/generator/surfaces/web/api-client.js +1 -0
- package/src/generator/surfaces/web/forms.js +1 -0
- package/src/generator/surfaces/web/index.d.ts +2 -0
- package/src/generator/surfaces/web/index.js +53 -0
- package/src/generator/surfaces/web/react-components.js +248 -0
- package/src/generator/surfaces/web/react.js +538 -0
- package/src/generator/surfaces/web/routes.js +1 -0
- package/src/generator/surfaces/web/screens.js +1 -0
- package/src/generator/surfaces/web/shared.js +369 -0
- package/src/generator/surfaces/web/sveltekit-actions.js +28 -0
- package/src/generator/surfaces/web/sveltekit-components.js +234 -0
- package/src/generator/surfaces/web/sveltekit.js +426 -0
- package/src/generator/surfaces/web/ui-web-contract.js +65 -0
- package/src/generator/surfaces/web/vanilla.js +239 -0
- package/src/generator/verification.js +84 -0
- package/src/generator.js +1 -0
- package/src/import/core/context.js +52 -0
- package/src/import/core/contracts.js +23 -0
- package/src/import/core/registry.js +81 -0
- package/src/import/core/runner.js +646 -0
- package/src/import/core/shared.js +910 -0
- package/src/import/enrichers/auth-session.js +18 -0
- package/src/import/enrichers/django-rest.js +226 -0
- package/src/import/enrichers/doc-linking.js +20 -0
- package/src/import/enrichers/rails-controllers.js +246 -0
- package/src/import/enrichers/rails-models.js +130 -0
- package/src/import/enrichers/workflow-target-state.js +10 -0
- package/src/import/extractors/api/aspnet-core.js +304 -0
- package/src/import/extractors/api/django-routes.js +318 -0
- package/src/import/extractors/api/express.js +154 -0
- package/src/import/extractors/api/fastify.js +371 -0
- package/src/import/extractors/api/flutter-dio.js +135 -0
- package/src/import/extractors/api/generic-route-fallback.js +90 -0
- package/src/import/extractors/api/graphql-code-first.js +565 -0
- package/src/import/extractors/api/graphql-sdl.js +309 -0
- package/src/import/extractors/api/jaxrs.js +303 -0
- package/src/import/extractors/api/micronaut.js +213 -0
- package/src/import/extractors/api/next-route.js +50 -0
- package/src/import/extractors/api/next-server-action.js +51 -0
- package/src/import/extractors/api/nextauth.js +52 -0
- package/src/import/extractors/api/openapi-code.js +242 -0
- package/src/import/extractors/api/openapi.js +232 -0
- package/src/import/extractors/api/rails-routes.js +230 -0
- package/src/import/extractors/api/react-native-repository.js +128 -0
- package/src/import/extractors/api/retrofit.js +103 -0
- package/src/import/extractors/api/spring-web.js +372 -0
- package/src/import/extractors/api/swift-webapi.js +116 -0
- package/src/import/extractors/api/trpc.js +212 -0
- package/src/import/extractors/db/django-models.js +232 -0
- package/src/import/extractors/db/dotnet-models.js +93 -0
- package/src/import/extractors/db/drizzle.js +242 -0
- package/src/import/extractors/db/ef-core.js +221 -0
- package/src/import/extractors/db/flutter-entities.js +120 -0
- package/src/import/extractors/db/jpa.js +120 -0
- package/src/import/extractors/db/liquibase.js +180 -0
- package/src/import/extractors/db/mybatis-xml.js +145 -0
- package/src/import/extractors/db/prisma.js +185 -0
- package/src/import/extractors/db/rails-schema.js +175 -0
- package/src/import/extractors/db/react-native-entities.js +95 -0
- package/src/import/extractors/db/room.js +193 -0
- package/src/import/extractors/db/snapshot.js +112 -0
- package/src/import/extractors/db/sql.js +180 -0
- package/src/import/extractors/db/swiftdata.js +137 -0
- package/src/import/extractors/ui/android-compose.js +230 -0
- package/src/import/extractors/ui/backend-only.js +70 -0
- package/src/import/extractors/ui/blazor.js +227 -0
- package/src/import/extractors/ui/flutter-screens.js +152 -0
- package/src/import/extractors/ui/maui-xaml.js +135 -0
- package/src/import/extractors/ui/next-app-router.js +83 -0
- package/src/import/extractors/ui/next-pages-router.js +141 -0
- package/src/import/extractors/ui/razor-pages.js +181 -0
- package/src/import/extractors/ui/react-native-screens.js +166 -0
- package/src/import/extractors/ui/react-router.js +139 -0
- package/src/import/extractors/ui/sveltekit.js +123 -0
- package/src/import/extractors/ui/swiftui.js +193 -0
- package/src/import/extractors/ui/uikit.js +175 -0
- package/src/import/extractors/verification/generic.js +290 -0
- package/src/import/extractors/workflows/generic.js +137 -0
- package/src/import/index.js +7 -0
- package/src/import/provenance.js +158 -0
- package/src/new-project.js +2107 -0
- package/src/parser.js +439 -0
- package/src/policy/review-boundaries.js +165 -0
- package/src/project-config.js +535 -0
- package/src/proofs/backend-parity.js +19 -0
- package/src/proofs/contract-audit.js +220 -0
- package/src/proofs/ios-parity.js +7 -0
- package/src/proofs/issues-parity.js +10 -0
- package/src/proofs/web-parity.js +50 -0
- package/src/realization/api/build-api-realization.js +5 -0
- package/src/realization/api/index.js +1 -0
- package/src/realization/backend/build-backend-runtime-realization.js +82 -0
- package/src/realization/backend/index.d.ts +1 -0
- package/src/realization/backend/index.js +4 -0
- package/src/realization/db/build-db-realization.js +17 -0
- package/src/realization/db/index.js +3 -0
- package/src/realization/db/migration-plan.js +5 -0
- package/src/realization/db/snapshot.js +5 -0
- package/src/realization/ui/build-ui-shared-realization.js +305 -0
- package/src/realization/ui/build-web-realization.js +189 -0
- package/src/realization/ui/index.js +2 -0
- package/src/reconcile/docs.js +280 -0
- package/src/reconcile/index.js +3 -0
- package/src/reconcile/journeys.js +441 -0
- package/src/resolver/docs.js +1 -0
- package/src/resolver/enrich/acceptance-criterion.js +14 -0
- package/src/resolver/enrich/bug.js +12 -0
- package/src/resolver/enrich/component.js +2 -0
- package/src/resolver/enrich/index.js +1 -0
- package/src/resolver/enrich/pitch.js +18 -0
- package/src/resolver/enrich/requirement.js +20 -0
- package/src/resolver/enrich/task.js +16 -0
- package/src/resolver/expressions.js +1 -0
- package/src/resolver/index.js +2422 -0
- package/src/resolver/normalize.js +1 -0
- package/src/resolver.js +1 -0
- package/src/sdlc/adopt.js +65 -0
- package/src/sdlc/check.js +86 -0
- package/src/sdlc/dod/acceptance-criterion.js +22 -0
- package/src/sdlc/dod/bug.js +26 -0
- package/src/sdlc/dod/document.js +23 -0
- package/src/sdlc/dod/index.js +25 -0
- package/src/sdlc/dod/pitch.js +23 -0
- package/src/sdlc/dod/requirement.js +34 -0
- package/src/sdlc/dod/task.js +39 -0
- package/src/sdlc/explain.js +116 -0
- package/src/sdlc/history.js +80 -0
- package/src/sdlc/paths.js +11 -0
- package/src/sdlc/release.js +106 -0
- package/src/sdlc/scaffold.js +89 -0
- package/src/sdlc/status-filter.js +54 -0
- package/src/sdlc/transition.js +112 -0
- package/src/sdlc/transitions/acceptance-criterion.js +28 -0
- package/src/sdlc/transitions/bug.js +31 -0
- package/src/sdlc/transitions/document.js +29 -0
- package/src/sdlc/transitions/index.js +56 -0
- package/src/sdlc/transitions/pitch.js +34 -0
- package/src/sdlc/transitions/requirement.js +31 -0
- package/src/sdlc/transitions/task.js +34 -0
- package/src/template-trust.js +597 -0
- package/src/validator/expressions.js +1 -0
- package/src/validator/index.js +3424 -0
- package/src/validator/kinds.js +346 -0
- package/src/validator/per-kind/acceptance-criterion.js +91 -0
- package/src/validator/per-kind/bug.js +77 -0
- package/src/validator/per-kind/component.js +274 -0
- package/src/validator/per-kind/domain.js +205 -0
- package/src/validator/per-kind/pitch.js +101 -0
- package/src/validator/per-kind/requirement.js +75 -0
- package/src/validator/per-kind/task.js +96 -0
- package/src/validator/registry.js +1 -0
- package/src/validator/utils.js +12 -0
- package/src/validator.js +1 -0
- package/src/workflows.js +7597 -0
- package/src/workspace-docs.js +265 -0
- package/template-helpers/react.js +5 -0
- package/template-helpers/sveltekit.js +5 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Dynamic capability client mirroring `src/lib/api/client.ts` (requestCapability over bundled api-contracts.json).
|
|
4
|
+
public final class TodoAPIClient: @unchecked Sendable {
|
|
5
|
+
private let session: URLSession
|
|
6
|
+
let contracts: [String: Any]
|
|
7
|
+
public init(session: URLSession = .shared, contractsData: Data) throws {
|
|
8
|
+
self.session = session
|
|
9
|
+
guard let root = try JSONSerialization.jsonObject(with: contractsData) as? [String: Any] else {
|
|
10
|
+
throw TodoAPIError.invalidContracts
|
|
11
|
+
}
|
|
12
|
+
self.contracts = root
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public static func loadBundledContracts() throws -> Data {
|
|
16
|
+
guard let url = Bundle.module.url(forResource: "api-contracts", withExtension: "json"),
|
|
17
|
+
let data = try? Data(contentsOf: url) else {
|
|
18
|
+
throw TodoAPIError.missingResource("api-contracts.json")
|
|
19
|
+
}
|
|
20
|
+
return data
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private func apiBase() -> String {
|
|
24
|
+
let env = ProcessInfo.processInfo.environment
|
|
25
|
+
return env["PUBLIC_TOPOGRAM_API_BASE_URL"] ?? "http://localhost:3000"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private func authToken() -> String {
|
|
29
|
+
ProcessInfo.processInfo.environment["PUBLIC_TOPOGRAM_DEMO_AUTH_TOKEN"] ?? ""
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private func buildPath(contract: [String: Any], input: [String: Any]) throws -> String {
|
|
33
|
+
guard let endpoint = contract["endpoint"] as? [String: Any],
|
|
34
|
+
let rawPath = endpoint["path"] as? String else {
|
|
35
|
+
throw TodoAPIError.invalidContracts
|
|
36
|
+
}
|
|
37
|
+
var path = rawPath
|
|
38
|
+
let requestContract = contract["requestContract"] as? [String: Any]
|
|
39
|
+
let transport = requestContract?["transport"] as? [String: Any]
|
|
40
|
+
let pathFields = transport?["path"] as? [[String: Any]] ?? []
|
|
41
|
+
for field in pathFields {
|
|
42
|
+
guard let name = field["name"] as? String else { continue }
|
|
43
|
+
let wire = (field["transport"] as? [String: Any])?["wireName"] as? String ?? name
|
|
44
|
+
let raw = input[name]
|
|
45
|
+
path = path.replacingOccurrences(of: ":\(wire)", with: encodePathSegment(raw))
|
|
46
|
+
}
|
|
47
|
+
var query: [String] = []
|
|
48
|
+
let queryFields = transport?["query"] as? [[String: Any]] ?? []
|
|
49
|
+
for field in queryFields {
|
|
50
|
+
guard let name = field["name"] as? String else { continue }
|
|
51
|
+
let wire = (field["transport"] as? [String: Any])?["wireName"] as? String ?? name
|
|
52
|
+
if let raw = input[name] {
|
|
53
|
+
if raw is NSNull { continue }
|
|
54
|
+
let s = String(describing: raw)
|
|
55
|
+
if !s.isEmpty {
|
|
56
|
+
query.append("\(wire)=\(encodeQueryComponent(s))")
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if !query.isEmpty {
|
|
61
|
+
path += "?" + query.joined(separator: "&")
|
|
62
|
+
}
|
|
63
|
+
return path
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private func encodePathSegment(_ value: Any?) -> String {
|
|
67
|
+
let s = value.map { String(describing: $0) } ?? ""
|
|
68
|
+
return s.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? s
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private func encodeQueryComponent(_ value: String) -> String {
|
|
72
|
+
value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public func requestCapability(
|
|
76
|
+
_ capabilityId: String,
|
|
77
|
+
input: [String: Any] = [:],
|
|
78
|
+
extraHeaders: [String: String] = [:]
|
|
79
|
+
) async throws -> Any {
|
|
80
|
+
guard let contract = contracts[capabilityId] as? [String: Any] else {
|
|
81
|
+
throw TodoAPIError.unknownCapability(capabilityId)
|
|
82
|
+
}
|
|
83
|
+
guard let endpoint = contract["endpoint"] as? [String: Any],
|
|
84
|
+
let method = endpoint["method"] as? String else {
|
|
85
|
+
throw TodoAPIError.invalidContracts
|
|
86
|
+
}
|
|
87
|
+
let path = try buildPath(contract: contract, input: input)
|
|
88
|
+
guard let url = URL(string: path, relativeTo: URL(string: apiBase()))?.absoluteURL else {
|
|
89
|
+
throw TodoAPIError.badURL(path)
|
|
90
|
+
}
|
|
91
|
+
var request = URLRequest(url: url)
|
|
92
|
+
request.httpMethod = method
|
|
93
|
+
|
|
94
|
+
var headers = extraHeaders
|
|
95
|
+
let authz = endpoint["authz"] as? [[String: Any]] ?? []
|
|
96
|
+
if !authz.isEmpty, !authToken().isEmpty {
|
|
97
|
+
if headers["Authorization"] == nil {
|
|
98
|
+
headers["Authorization"] = "Bearer " + authToken()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (k, v) in headers {
|
|
102
|
+
request.setValue(v, forHTTPHeaderField: k)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let requestContract = contract["requestContract"] as? [String: Any]
|
|
106
|
+
let transport = requestContract?["transport"] as? [String: Any]
|
|
107
|
+
let bodyFields = transport?["body"] as? [[String: Any]] ?? []
|
|
108
|
+
if !bodyFields.isEmpty {
|
|
109
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
110
|
+
var payload: [String: Any] = [:]
|
|
111
|
+
for field in bodyFields {
|
|
112
|
+
guard let name = field["name"] as? String else { continue }
|
|
113
|
+
let wire = (field["transport"] as? [String: Any])?["wireName"] as? String ?? name
|
|
114
|
+
if let v = input[name] {
|
|
115
|
+
payload[wire] = v
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
request.httpBody = try JSONSerialization.data(withJSONObject: payload)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let downloadable = endpoint["download"] as? [[String: Any]] ?? []
|
|
122
|
+
let (data, response) = try await session.data(for: request)
|
|
123
|
+
guard let http = response as? HTTPURLResponse else {
|
|
124
|
+
throw TodoAPIError.invalidResponse
|
|
125
|
+
}
|
|
126
|
+
guard (200 ..< 300).contains(http.statusCode) else {
|
|
127
|
+
let text = String(data: data, encoding: .utf8) ?? ""
|
|
128
|
+
throw TodoAPIError.http(http.statusCode, text)
|
|
129
|
+
}
|
|
130
|
+
if http.statusCode == 204 {
|
|
131
|
+
return NSNull()
|
|
132
|
+
}
|
|
133
|
+
if !downloadable.isEmpty {
|
|
134
|
+
return data
|
|
135
|
+
}
|
|
136
|
+
return try JSONSerialization.jsonObject(with: data)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public func extractRows(from json: Any) -> [[String: Any]] {
|
|
140
|
+
if let rows = json as? [[String: Any]] { return rows }
|
|
141
|
+
if let dict = json as? [String: Any] {
|
|
142
|
+
if let items = dict["items"] as? [[String: Any]] { return items }
|
|
143
|
+
if let data = dict["data"] as? [[String: Any]] { return data }
|
|
144
|
+
}
|
|
145
|
+
return []
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public enum TodoAPIError: Error {
|
|
150
|
+
case invalidContracts
|
|
151
|
+
case missingResource(String)
|
|
152
|
+
case unknownCapability(String)
|
|
153
|
+
case badURL(String)
|
|
154
|
+
case invalidResponse
|
|
155
|
+
case http(Int, String)
|
|
156
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
@main
|
|
4
|
+
struct TodoSwiftUIApp: App {
|
|
5
|
+
@StateObject private var contractHolder = ContractHolder()
|
|
6
|
+
|
|
7
|
+
var body: some Scene {
|
|
8
|
+
WindowGroup {
|
|
9
|
+
Group {
|
|
10
|
+
if let contract = contractHolder.contract, let client = contractHolder.client {
|
|
11
|
+
RootTabView(contract: contract, client: client)
|
|
12
|
+
} else if let err = contractHolder.error {
|
|
13
|
+
Text("Failed to load Topogram UI contract: \(err)")
|
|
14
|
+
.padding()
|
|
15
|
+
} else {
|
|
16
|
+
ProgressView("Loading Topogram Todo…")
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
.task {
|
|
20
|
+
await contractHolder.bootstrap()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@MainActor
|
|
27
|
+
final class ContractHolder: ObservableObject {
|
|
28
|
+
@Published var contract: TodoUiContract?
|
|
29
|
+
@Published var client: TodoAPIClient?
|
|
30
|
+
@Published var error: String?
|
|
31
|
+
|
|
32
|
+
func bootstrap() async {
|
|
33
|
+
do {
|
|
34
|
+
let uiData = try TodoUiContract.loadBundled()
|
|
35
|
+
let apiData = try TodoAPIClient.loadBundledContracts()
|
|
36
|
+
let ui = try TodoUiContract(data: uiData)
|
|
37
|
+
let cli = try TodoAPIClient(contractsData: apiData)
|
|
38
|
+
contract = ui
|
|
39
|
+
client = cli
|
|
40
|
+
} catch {
|
|
41
|
+
self.error = String(describing: error)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Port of web `visibility.ts` predicates used by dynamic screens.
|
|
4
|
+
public enum Visibility {
|
|
5
|
+
public struct Rule {
|
|
6
|
+
public let predicate: String?
|
|
7
|
+
public let value: String?
|
|
8
|
+
public let claimValue: String?
|
|
9
|
+
public let ownershipField: String?
|
|
10
|
+
public let capabilityId: String?
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public struct PrincipalOverride {
|
|
14
|
+
public var userId: String?
|
|
15
|
+
public var permissions: [String]?
|
|
16
|
+
public var roles: [String]?
|
|
17
|
+
public var claims: [String: Any]?
|
|
18
|
+
public var isAdmin: Bool?
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public static func canShow(
|
|
22
|
+
rule: Rule?,
|
|
23
|
+
resource: [String: Any]?,
|
|
24
|
+
overrides: PrincipalOverride? = nil
|
|
25
|
+
) -> Bool {
|
|
26
|
+
guard let rule else { return true }
|
|
27
|
+
guard let principal = currentPrincipal(overrides: overrides) else { return true }
|
|
28
|
+
|
|
29
|
+
switch rule.predicate {
|
|
30
|
+
case "permission":
|
|
31
|
+
guard let value = rule.value else { return true }
|
|
32
|
+
return principal.permissions.contains("*") || principal.permissions.contains(value)
|
|
33
|
+
case "ownership":
|
|
34
|
+
guard let value = rule.value else { return true }
|
|
35
|
+
if value == "none" { return true }
|
|
36
|
+
if value == "owner_or_admin", principal.isAdmin { return true }
|
|
37
|
+
let owner = ownerId(from: resource, field: rule.ownershipField)
|
|
38
|
+
return owner == principal.userId
|
|
39
|
+
case "claim":
|
|
40
|
+
let claim = rule.value
|
|
41
|
+
let expected = rule.claimValue
|
|
42
|
+
guard let claim else { return true }
|
|
43
|
+
let raw = principal.claims[claim]
|
|
44
|
+
if expected == nil || expected?.isEmpty == true {
|
|
45
|
+
if raw == nil { return false }
|
|
46
|
+
if let b = raw as? Bool { return b }
|
|
47
|
+
if let s = raw as? String { return !s.isEmpty }
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
return String(describing: raw ?? "") == expected
|
|
51
|
+
default:
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private static func env(_ key: String) -> String {
|
|
57
|
+
ProcessInfo.processInfo.environment[key] ?? ""
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private static func csv(_ raw: String) -> [String] {
|
|
61
|
+
raw.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private static func parseClaims(_ raw: String) -> [String: Any] {
|
|
65
|
+
guard let data = raw.data(using: .utf8),
|
|
66
|
+
let obj = try? JSONSerialization.jsonObject(with: data),
|
|
67
|
+
let dict = obj as? [String: Any] else {
|
|
68
|
+
return [:]
|
|
69
|
+
}
|
|
70
|
+
return dict
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private static func decodeJwtPayload(_ token: String) -> [String: Any]? {
|
|
74
|
+
let parts = token.split(separator: ".")
|
|
75
|
+
guard parts.count > 1 else { return nil }
|
|
76
|
+
var body = String(parts[1])
|
|
77
|
+
body = body.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
|
78
|
+
let pad = 4 - body.count % 4
|
|
79
|
+
if pad < 4 { body += String(repeating: "=", count: pad) }
|
|
80
|
+
guard let data = Data(base64Encoded: body),
|
|
81
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
82
|
+
return nil
|
|
83
|
+
}
|
|
84
|
+
return json
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private static func principalFromJwt(_ token: String) -> Principal? {
|
|
88
|
+
guard let payload = decodeJwtPayload(token) else { return nil }
|
|
89
|
+
let sub = payload["sub"] as? String ?? ""
|
|
90
|
+
let permissions = Set((payload["permissions"] as? [String]) ?? [])
|
|
91
|
+
let roles = Set((payload["roles"] as? [String]) ?? [])
|
|
92
|
+
let admin = payload["admin"] as? Bool ?? false
|
|
93
|
+
return Principal(
|
|
94
|
+
userId: sub,
|
|
95
|
+
permissions: permissions,
|
|
96
|
+
roles: roles,
|
|
97
|
+
claims: payload,
|
|
98
|
+
isAdmin: admin
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private struct Principal {
|
|
103
|
+
let userId: String
|
|
104
|
+
let permissions: Set<String>
|
|
105
|
+
let roles: Set<String>
|
|
106
|
+
let claims: [String: Any]
|
|
107
|
+
let isAdmin: Bool
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private static func currentPrincipal(overrides: PrincipalOverride?) -> Principal? {
|
|
111
|
+
let token = env("PUBLIC_TOPOGRAM_DEMO_AUTH_TOKEN")
|
|
112
|
+
let jwtPrincipal = token.isEmpty ? nil : principalFromJwt(token)
|
|
113
|
+
let envClaims = parseClaims(env("PUBLIC_TOPOGRAM_AUTH_CLAIMS"))
|
|
114
|
+
let userId = overrides?.userId
|
|
115
|
+
?? env("PUBLIC_TOPOGRAM_AUTH_USER_ID")
|
|
116
|
+
?? jwtPrincipal?.userId
|
|
117
|
+
?? ""
|
|
118
|
+
|
|
119
|
+
var permissions = Set(csv(env("PUBLIC_TOPOGRAM_AUTH_PERMISSIONS")))
|
|
120
|
+
permissions.formUnion(jwtPrincipal?.permissions ?? [])
|
|
121
|
+
permissions.formUnion(overrides?.permissions ?? [])
|
|
122
|
+
|
|
123
|
+
var roles = Set(csv(env("PUBLIC_TOPOGRAM_AUTH_ROLES")))
|
|
124
|
+
if roles.isEmpty { roles = Set(csv(env("PUBLIC_TOPOGRAM_AUTH_ROLE"))) }
|
|
125
|
+
roles.formUnion(jwtPrincipal?.roles ?? [])
|
|
126
|
+
roles.formUnion(overrides?.roles ?? [])
|
|
127
|
+
|
|
128
|
+
let adminFlag =
|
|
129
|
+
(overrides?.isAdmin ?? false)
|
|
130
|
+
|| env("PUBLIC_TOPOGRAM_AUTH_ADMIN").lowercased() == "true"
|
|
131
|
+
|| jwtPrincipal?.isAdmin == true
|
|
132
|
+
|
|
133
|
+
var claims = jwtPrincipal?.claims ?? [:]
|
|
134
|
+
claims.merge(envClaims) { _, new in new }
|
|
135
|
+
if let o = overrides?.claims { claims.merge(o) { _, new in new } }
|
|
136
|
+
|
|
137
|
+
if token.isEmpty && userId.isEmpty && permissions.isEmpty && roles.isEmpty && claims.isEmpty && !adminFlag {
|
|
138
|
+
return nil
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return Principal(
|
|
142
|
+
userId: userId,
|
|
143
|
+
permissions: permissions,
|
|
144
|
+
roles: roles,
|
|
145
|
+
claims: claims,
|
|
146
|
+
isAdmin: adminFlag
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private static func ownerId(from resource: [String: Any]?, field: String?) -> String {
|
|
151
|
+
guard let resource else { return "" }
|
|
152
|
+
if let field {
|
|
153
|
+
if let v = resource[field] as? String { return v }
|
|
154
|
+
}
|
|
155
|
+
for key in ["owner_id", "assignee_id", "author_id", "user_id", "created_by_user_id"] {
|
|
156
|
+
if let v = resource[key] as? String { return v }
|
|
157
|
+
}
|
|
158
|
+
return ""
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public static func visibilityRules(from screen: [String: Any]) -> [Rule] {
|
|
162
|
+
guard let raw = screen["visibility"] as? [[String: Any]] else { return [] }
|
|
163
|
+
return raw.map { entry in
|
|
164
|
+
let cap = entry["capability"] as? [String: Any]
|
|
165
|
+
return Rule(
|
|
166
|
+
predicate: entry["predicate"] as? String,
|
|
167
|
+
value: entry["value"] as? String,
|
|
168
|
+
claimValue: entry["claimValue"] as? String,
|
|
169
|
+
ownershipField: entry["ownershipField"] as? String,
|
|
170
|
+
capabilityId: cap?["id"] as? String
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public static func canShowAction(_ capabilityId: String?, screen: [String: Any], resource: [String: Any]?) -> Bool {
|
|
176
|
+
guard let capabilityId else { return true }
|
|
177
|
+
let rules = visibilityRules(from: screen).filter { $0.capabilityId == capabilityId }
|
|
178
|
+
for rule in rules {
|
|
179
|
+
if !canShow(rule: rule, resource: resource) { return false }
|
|
180
|
+
}
|
|
181
|
+
return true
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function generateExpressServer(graph: any, options?: any): any;
|