mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-23 04:26:05 +00:00
fix(httpapi): return project not found errors (#28856)
This commit is contained in:
parent
b368e5adbe
commit
3e1972fd92
8 changed files with 76 additions and 10 deletions
|
|
@ -101,6 +101,10 @@ export const UpdatePayload = Schema.Struct({
|
|||
}).annotate({ identifier: "ProjectUpdateInput" })
|
||||
export type UpdatePayload = Types.DeepMutable<Schema.Schema.Type<typeof UpdatePayload>>
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Project.NotFoundError", {
|
||||
projectID: ProjectID,
|
||||
}) {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Effect service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -116,7 +120,7 @@ export interface Interface {
|
|||
readonly discover: (input: Info) => Effect.Effect<void>
|
||||
readonly list: () => Effect.Effect<Info[]>
|
||||
readonly get: (id: ProjectID) => Effect.Effect<Info | undefined>
|
||||
readonly update: (input: UpdateInput) => Effect.Effect<Info>
|
||||
readonly update: (input: UpdateInput) => Effect.Effect<Info, NotFoundError>
|
||||
readonly initGit: (input: { directory: string; project: Info }) => Effect.Effect<Info>
|
||||
readonly setInitialized: (id: ProjectID) => Effect.Effect<void>
|
||||
readonly sandboxes: (id: ProjectID) => Effect.Effect<string[]>
|
||||
|
|
@ -372,7 +376,9 @@ export const layer: Layer.Layer<
|
|||
const base64 = Buffer.from(buffer).toString("base64")
|
||||
const mime = AppFileSystem.mimeType(shortest)
|
||||
const url = `data:${mime};base64,${base64}`
|
||||
yield* update({ projectID: input.id, icon: { url } })
|
||||
yield* update({ projectID: input.id, icon: { url } }).pipe(
|
||||
Effect.catchTag("Project.NotFoundError", () => Effect.void),
|
||||
)
|
||||
})
|
||||
|
||||
const list = Effect.fn("Project.list")(function* () {
|
||||
|
|
@ -400,7 +406,7 @@ export const layer: Layer.Layer<
|
|||
.returning()
|
||||
.get(),
|
||||
)
|
||||
if (!result) throw new Error(`Project not found: ${input.projectID}`)
|
||||
if (!result) return yield* new NotFoundError({ projectID: input.projectID })
|
||||
const data = fromRow(result)
|
||||
yield* emitUpdated(data)
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -166,6 +166,15 @@ export class PtyForbiddenError extends Schema.TaggedErrorClass<PtyForbiddenError
|
|||
{ httpApiStatus: 403 },
|
||||
) {}
|
||||
|
||||
export class ProjectNotFoundError extends Schema.TaggedErrorClass<ProjectNotFoundError>()(
|
||||
"ProjectNotFoundError",
|
||||
{
|
||||
projectID: Schema.String,
|
||||
message: Schema.String,
|
||||
},
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class ApiNotFoundError extends Schema.ErrorClass<ApiNotFoundError>("NotFoundError")(
|
||||
{
|
||||
name: Schema.Literal("NotFoundError"),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Project } from "@/project/project"
|
|||
import { ProjectID } from "@/project/schema"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { ProjectNotFoundError } from "../errors"
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||
|
|
@ -53,7 +54,7 @@ export const ProjectApi = HttpApi.make("project")
|
|||
query: WorkspaceRoutingQuery,
|
||||
payload: UpdatePayload,
|
||||
success: described(Project.Info, "Updated project information"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, ProjectNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "project.update",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { ProjectID } from "@/project/schema"
|
|||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { InstanceHttpApi } from "../api"
|
||||
import { ProjectNotFoundError } from "../errors"
|
||||
import { markInstanceForReload } from "../lifecycle"
|
||||
|
||||
export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project", (handlers) =>
|
||||
|
|
@ -35,7 +36,16 @@ export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project",
|
|||
params: { projectID: ProjectID }
|
||||
payload: Project.UpdatePayload
|
||||
}) {
|
||||
return yield* svc.update({ ...ctx.payload, projectID: ctx.params.projectID })
|
||||
return yield* svc.update({ ...ctx.payload, projectID: ctx.params.projectID }).pipe(
|
||||
Effect.catchTag("Project.NotFoundError", (error) =>
|
||||
Effect.fail(
|
||||
new ProjectNotFoundError({
|
||||
projectID: error.projectID,
|
||||
message: `Project not found: ${error.projectID}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
return handlers.handle("list", list).handle("current", current).handle("initGit", initGit).handle("update", update)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const encoder = new TextEncoder()
|
|||
const layer = Layer.mergeAll(Project.defaultLayer, CrossSpawnSpawner.defaultLayer)
|
||||
const it = testEffect(layer)
|
||||
|
||||
function run<A>(fn: (svc: Project.Interface) => Effect.Effect<A>) {
|
||||
function run<A, E>(fn: (svc: Project.Interface) => Effect.Effect<A, E>) {
|
||||
return Effect.gen(function* () {
|
||||
const svc = yield* Project.Service
|
||||
return yield* fn(svc)
|
||||
|
|
@ -481,7 +481,7 @@ describe("Project.update", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.live("should throw error when project not found", () =>
|
||||
it.live("should fail when project not found", () =>
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* run((svc) =>
|
||||
svc.update({
|
||||
|
|
@ -492,9 +492,7 @@ describe("Project.update", () => {
|
|||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) {
|
||||
const error = Cause.squash(exit.cause)
|
||||
expect(error instanceof Error ? error.message : String(error)).toContain(
|
||||
"Project not found: nonexistent-project-id",
|
||||
)
|
||||
expect(error).toMatchObject({ _tag: "Project.NotFoundError", projectID: "nonexistent-project-id" })
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -177,6 +177,15 @@ const scenarios: Scenario[] = [
|
|||
},
|
||||
"status",
|
||||
),
|
||||
http.protected
|
||||
.patch("/project/{projectID}", "project.update.missing")
|
||||
.mutating()
|
||||
.at((ctx) => ({
|
||||
path: route("/project/{projectID}", { projectID: "project_httpapi_missing" }),
|
||||
headers: ctx.headers(),
|
||||
body: { name: "Missing Project" },
|
||||
}))
|
||||
.json(404, object, "status"),
|
||||
http.protected
|
||||
.post("/project/git/init", "project.initGit")
|
||||
.mutating()
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { ControlPaths } from "../../src/server/routes/instance/httpapi/groups/co
|
|||
import { InstancePaths } from "../../src/server/routes/instance/httpapi/groups/instance"
|
||||
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
|
||||
import { PermissionID } from "../../src/permission/schema"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { QuestionID } from "../../src/question/schema"
|
||||
import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server"
|
||||
import { HEADER as FenceHeader } from "../../src/server/shared/fence"
|
||||
|
|
@ -205,6 +206,30 @@ describe("instance HttpApi", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.live("returns typed not found bodies for missing projects", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
const projectID = ProjectID.make("project_missing")
|
||||
const response = yield* Effect.promise(() =>
|
||||
HttpApiApp.webHandler().handler(
|
||||
new Request(`http://localhost/project/${projectID}`, {
|
||||
method: "PATCH",
|
||||
headers: { "x-opencode-directory": dir, "content-type": "application/json" },
|
||||
body: JSON.stringify({ name: "Missing" }),
|
||||
}),
|
||||
handlerContext,
|
||||
),
|
||||
)
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
expect(yield* Effect.promise(() => response.json())).toEqual({
|
||||
_tag: "ProjectNotFoundError",
|
||||
projectID,
|
||||
message: `Project not found: ${projectID}`,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("serves path and VCS read endpoints", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
|
|
|
|||
|
|
@ -208,4 +208,12 @@ describe("PublicApi OpenAPI v2 errors", () => {
|
|||
"PtyForbiddenError",
|
||||
)
|
||||
})
|
||||
|
||||
test("documents project not-found errors", () => {
|
||||
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
|
||||
|
||||
expect(componentName(responseRef(spec.paths["/project/{projectID}"]?.patch?.responses?.["404"]) ?? "")).toBe(
|
||||
"ProjectNotFoundError",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue