Skip to content

Commit 30c1e10

Browse files
committed
refactor(openapi): expose direct effect client
1 parent 25cb31e commit 30c1e10

16 files changed

Lines changed: 773 additions & 729 deletions

bun.lock

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/app/src/web/api-create-project.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import type { Effect } from "effect"
1+
import { Effect, Match } from "effect"
22

3-
import { dockerGitOpenApi } from "./api-http.js"
3+
import { dockerGitOpenApi, renderDockerGitOpenApiFailure } from "./api-http.js"
44
import {
55
type BaseCreateProjectBody,
66
baseCreateProjectBody,
77
type CreateProjectRequestDraft,
88
optionalProjectResourceFields,
99
type OptionalProjectResourceFieldsBody
1010
} from "./api-project-create-body.js"
11-
import { CreateProjectAcceptedResponseSchema } from "./api-schema.js"
1211
import type { CreateProjectAcceptedResponse } from "./api-schema.js"
1312

1413
type CreateProjectAcceptedBody = Readonly<
@@ -42,7 +41,15 @@ export const createProjectAcceptedBody = (draft: CreateProjectRequestDraft): Cre
4241
export const startCreateProject = (
4342
draft: CreateProjectRequestDraft
4443
): Effect.Effect<CreateProjectAcceptedResponse, string> =>
45-
dockerGitOpenApi.openApiJsonSchema(CreateProjectAcceptedResponseSchema, (client) =>
46-
client.POST("/projects", {
47-
body: createProjectAcceptedBody(draft)
48-
}))
44+
dockerGitOpenApi.POST("/projects", {
45+
body: createProjectAcceptedBody(draft)
46+
}).pipe(
47+
Effect.mapError(renderDockerGitOpenApiFailure),
48+
Effect.flatMap((success) =>
49+
Match.value(success).pipe(
50+
Match.when({ status: 202 }, ({ body }) => Effect.succeed(body)),
51+
Match.when({ status: 201 }, () => Effect.fail("HTTP 201: unexpected synchronous project creation response")),
52+
Match.exhaustive
53+
)
54+
)
55+
)
Lines changed: 47 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
import { Effect } from "effect"
22

3-
import { dockerGitOpenApi } from "./api-http.js"
4-
import {
5-
ProjectDatabaseForwardResponseSchema,
6-
ProjectDatabaseForwardsResponseSchema,
7-
ProjectDatabaseProfileResponseSchema,
8-
ProjectDatabaseProfilesResponseSchema,
9-
ProjectDatabaseSessionResponseSchema
10-
} from "./api-schema.js"
3+
import { dockerGitOpenApi, renderDockerGitOpenApiFailure } from "./api-http.js"
114
import type { ProjectDatabaseForward, ProjectDatabaseSession } from "./api-schema.js"
125

136
export const projectDatabaseEditorUrl = (session: ProjectDatabaseSession): string => session.editorPath
@@ -16,106 +9,87 @@ export const projectDatabaseExternalUrl = (forward: ProjectDatabaseForward): str
169
`${forward.publicHost}:${forward.hostPort}`
1710

1811
export const loadProjectDatabaseProfiles = (projectId: string) =>
19-
dockerGitOpenApi.openApiJsonSchema(
20-
ProjectDatabaseProfilesResponseSchema,
21-
(client) =>
22-
client.GET("/projects/{projectId}/databases/profiles", {
23-
params: { path: { projectId } }
24-
})
25-
).pipe(
26-
Effect.map((response) => response.profiles)
12+
dockerGitOpenApi.GET("/projects/{projectId}/databases/profiles", {
13+
params: { path: { projectId } }
14+
}).pipe(
15+
Effect.map(({ body }) => body.profiles),
16+
Effect.mapError(renderDockerGitOpenApiFailure)
2717
)
2818

2919
export const loadProjectDatabaseForwards = (projectId: string) =>
30-
dockerGitOpenApi.openApiJsonSchema(
31-
ProjectDatabaseForwardsResponseSchema,
32-
(client) =>
33-
client.GET("/projects/{projectId}/databases/forwards", {
34-
params: { path: { projectId } }
35-
})
36-
).pipe(
37-
Effect.map((response) => response.forwards)
20+
dockerGitOpenApi.GET("/projects/{projectId}/databases/forwards", {
21+
params: { path: { projectId } }
22+
}).pipe(
23+
Effect.map(({ body }) => body.forwards),
24+
Effect.mapError(renderDockerGitOpenApiFailure)
3825
)
3926

4027
export const saveProjectDatabaseProfile = (
4128
projectId: string,
4229
connectionString: string,
4330
label: string | null
4431
) =>
45-
dockerGitOpenApi.openApiJsonSchema(
46-
ProjectDatabaseProfileResponseSchema,
47-
(client) =>
48-
client.POST("/projects/{projectId}/databases/profiles", {
49-
body: { connectionString, label },
50-
params: { path: { projectId } }
51-
})
52-
).pipe(
53-
Effect.map((response) => response.profile)
32+
dockerGitOpenApi.POST("/projects/{projectId}/databases/profiles", {
33+
body: { connectionString, label },
34+
params: { path: { projectId } }
35+
}).pipe(
36+
Effect.map(({ body }) => body.profile),
37+
Effect.mapError(renderDockerGitOpenApiFailure)
5438
)
5539

5640
export const deleteProjectDatabaseProfile = (
5741
projectId: string,
5842
profileId: string
5943
) =>
60-
dockerGitOpenApi.openApiVoid((client) =>
61-
client.DELETE("/projects/{projectId}/databases/profiles/{profileId}", {
62-
params: { path: { profileId, projectId } }
63-
})
44+
dockerGitOpenApi.DELETE("/projects/{projectId}/databases/profiles/{profileId}", {
45+
params: { path: { profileId, projectId } }
46+
}).pipe(
47+
Effect.asVoid,
48+
Effect.mapError(renderDockerGitOpenApiFailure)
6449
)
6550

6651
export const exposeProjectDatabaseProfile = (
6752
projectId: string,
6853
profileId: string
6954
) =>
70-
dockerGitOpenApi.openApiJsonSchema(
71-
ProjectDatabaseForwardResponseSchema,
72-
(client) =>
73-
client.POST("/projects/{projectId}/databases/profiles/{profileId}/expose", {
74-
params: { path: { profileId, projectId } }
75-
})
76-
).pipe(
77-
Effect.map((response) => response.forward)
55+
dockerGitOpenApi.POST("/projects/{projectId}/databases/profiles/{profileId}/expose", {
56+
params: { path: { profileId, projectId } }
57+
}).pipe(
58+
Effect.map(({ body }) => body.forward),
59+
Effect.mapError(renderDockerGitOpenApiFailure)
7860
)
7961

8062
export const deleteProjectDatabaseForward = (
8163
projectId: string,
8264
profileId: string
8365
) =>
84-
dockerGitOpenApi.openApiVoid((client) =>
85-
client.DELETE("/projects/{projectId}/databases/profiles/{profileId}/expose", {
86-
params: { path: { profileId, projectId } }
87-
})
66+
dockerGitOpenApi.DELETE("/projects/{projectId}/databases/profiles/{profileId}/expose", {
67+
params: { path: { profileId, projectId } }
68+
}).pipe(
69+
Effect.asVoid,
70+
Effect.mapError(renderDockerGitOpenApiFailure)
8871
)
8972

9073
export const loadProjectDatabaseSession = (projectId: string) =>
91-
dockerGitOpenApi.openApiJsonSchema(
92-
ProjectDatabaseSessionResponseSchema,
93-
(client) =>
94-
client.GET("/projects/{projectId}/databases/session", {
95-
params: { path: { projectId } }
96-
})
97-
).pipe(
98-
Effect.map((response) => response.session)
74+
dockerGitOpenApi.GET("/projects/{projectId}/databases/session", {
75+
params: { path: { projectId } }
76+
}).pipe(
77+
Effect.map(({ body }) => body.session),
78+
Effect.mapError(renderDockerGitOpenApiFailure)
9979
)
10080

10181
export const openProjectDatabaseEditor = (projectId: string) =>
102-
dockerGitOpenApi.openApiJsonSchema(
103-
ProjectDatabaseSessionResponseSchema,
104-
(client) =>
105-
client.POST("/projects/{projectId}/databases/open", {
106-
params: { path: { projectId } }
107-
})
108-
).pipe(
109-
Effect.map((response) => response.session)
82+
dockerGitOpenApi.POST("/projects/{projectId}/databases/open", {
83+
params: { path: { projectId } }
84+
}).pipe(
85+
Effect.map(({ body }) => body.session),
86+
Effect.mapError(renderDockerGitOpenApiFailure)
11087
)
11188

11289
export const restartProjectDatabaseEditor = (projectId: string) =>
113-
dockerGitOpenApi.openApiJsonSchema(
114-
ProjectDatabaseSessionResponseSchema,
115-
(client) =>
116-
client.POST("/projects/{projectId}/databases/restart", {
117-
params: { path: { projectId } }
118-
})
119-
).pipe(
120-
Effect.map((response) => response.session)
90+
dockerGitOpenApi.POST("/projects/{projectId}/databases/restart", {
91+
params: { path: { projectId } }
92+
}).pipe(
93+
Effect.map(({ body }) => body.session),
94+
Effect.mapError(renderDockerGitOpenApiFailure)
12195
)

packages/app/src/web/api-http.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import * as ParseResult from "@effect/schema/ParseResult"
33
import * as Schema from "@effect/schema/Schema"
44
import * as TreeFormatter from "@effect/schema/TreeFormatter"
55
import { createClient } from "@prover-coder-ai/docker-git-openapi"
6-
import { Effect, Either } from "effect"
6+
import type { BoundaryError } from "@prover-coder-ai/docker-git-openapi"
7+
import { Effect, Either, Match } from "effect"
78

89
import { type JsonRequest, parseResponseBody, renderJsonPayload } from "../docker-git/api-json.js"
910
import { readHttpResponseTextStream } from "../shared/http-response-stream.js"
@@ -19,6 +20,17 @@ type TextStreamRequest = {
1920
readonly path: string
2021
}
2122

23+
type RenderableOpenApiBody = boolean | number | object | string | null | undefined
24+
25+
type RenderableOpenApiHttpError = {
26+
readonly _tag: "HttpError"
27+
readonly body: RenderableOpenApiBody
28+
readonly contentType: string
29+
readonly status: number | string
30+
}
31+
32+
export type RenderableOpenApiFailure = RenderableOpenApiHttpError | BoundaryError
33+
2234
const noCacheHeaders: Readonly<Record<string, string>> = {
2335
"cache-control": "no-cache, no-store, max-age=0",
2436
pragma: "no-cache"
@@ -85,6 +97,57 @@ export const resolveApiBaseUrl = (): string => {
8597
: trimTrailingSlash(configured.trim())
8698
}
8799

100+
const renderOpenApiBody = (body: RenderableOpenApiBody): string => {
101+
if (body === undefined) {
102+
return "empty response"
103+
}
104+
if (typeof body === "string") {
105+
return body
106+
}
107+
return JSON.stringify(body, null, 2)
108+
}
109+
110+
// CHANGE: Convert direct openapi-effect failures to legacy UI string errors at the web boundary.
111+
// WHY: app API functions still expose `Effect<_, string>` while transport typing is owned by openapi-effect.
112+
// QUOTE(ТЗ): "client сам возвращает нужную схему рабочую"
113+
// REF: user-openapi-effect-direct-client
114+
// SOURCE: n/a
115+
// FORMAT THEOREM: forall failure f: render(f) is total and does not alter success values.
116+
// PURITY: SHELL
117+
// EFFECT: none
118+
// INVARIANT: only the error channel is collapsed to string for existing UI callers.
119+
// COMPLEXITY: O(n)/O(n), where n is rendered body size.
120+
/**
121+
* Renders typed openapi-effect failures for existing UI string error channels.
122+
*
123+
* @param failure - Typed OpenAPI transport or HTTP failure.
124+
* @returns User-facing diagnostic string.
125+
*
126+
* @pure true - deterministic formatting of immutable failure data.
127+
* @effect none.
128+
* @invariant success values are never inspected or changed; only failure values are rendered.
129+
* @precondition failure was produced by the docker-git OpenAPI client.
130+
* @postcondition return value is non-throwing and suitable for legacy `Effect<_, string>` callers.
131+
* @complexity O(n)/O(n), where n is rendered response body size.
132+
* @throws Never.
133+
*/
134+
export const renderDockerGitOpenApiFailure = (failure: RenderableOpenApiFailure): string =>
135+
Match.value(failure).pipe(
136+
Match.when({ _tag: "HttpError" }, (error) =>
137+
String(error.status) === "429"
138+
? "HTTP 429: tunnel or proxy rate limited the request. Retry or request a fresh tunnel URL."
139+
: renderOpenApiBody(error.body)),
140+
Match.when({ _tag: "TransportError" }, (error) => error.error.message),
141+
Match.when({ _tag: "UnexpectedStatus" }, (error) => `HTTP ${error.status}: ${error.body}`),
142+
Match.when({ _tag: "UnexpectedContentType" }, (error) =>
143+
`HTTP ${error.status}: unexpected content type ${error.actual ?? "none"}: ${error.body}`),
144+
Match.when({ _tag: "ParseError" }, (error) =>
145+
`HTTP ${error.status}: invalid ${error.contentType} response: ${error.error.message}`),
146+
Match.when({ _tag: "DecodeError" }, (error) =>
147+
`HTTP ${error.status}: invalid decoded response: ${error.error.message}`),
148+
Match.exhaustive
149+
)
150+
88151
/**
89152
* Configured docker-git OpenAPI client for the web HTTP boundary.
90153
*
@@ -97,7 +160,7 @@ export const resolveApiBaseUrl = (): string => {
97160
* @throws Never.
98161
*/
99162
export const dockerGitOpenApi = createClient({
100-
resolveBaseUrl: resolveApiBaseUrl
163+
baseUrl: resolveApiBaseUrl()
101164
})
102165

103166
export const requestText = (

0 commit comments

Comments
 (0)