Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions packages/api/src/api/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,60 @@ export type ProjectBrowserSession = {
readonly cdpUrl: string
}

export type ProjectDatabaseEngine = "postgres" | "mysql" | "mariadb"

export type ProjectDatabaseProfile = {
readonly createdAt: string
readonly database: string
readonly engine: ProjectDatabaseEngine
readonly host: string
readonly id: string
readonly label: string
readonly maskedConnectionString: string
readonly port: number
readonly updatedAt: string
readonly user: string
}

export type ProjectDatabaseProfileRequest = {
readonly connectionString: string
readonly label?: string | null | undefined
}

export type ProjectDatabaseSessionStatus = "running" | "stopped" | "missing" | "unknown"

export type ProjectDatabaseSession = {
readonly configHash: string
readonly containerName: string
readonly editorPath: string
readonly editorUrl: string
readonly projectId: string
readonly projectKey: string
readonly status: ProjectDatabaseSessionStatus
}

export type ProjectDatabaseForwardStatus = "running" | "stopped" | "unknown"

export type ProjectDatabaseForward = {
readonly bindHost: string
readonly containerName: string
readonly createdAt: string | null
readonly database: string
readonly engine: ProjectDatabaseEngine
readonly externalConnectionString: string
readonly hostPort: number
readonly id: string
readonly maskedExternalConnectionString: string
readonly profileId: string
readonly profileLabel: string
readonly projectId: string
readonly projectKey: string
readonly publicHost: string
readonly status: ProjectDatabaseForwardStatus
readonly targetHost: string
readonly targetPort: number
}

export type GithubAuthTokenStatus = {
readonly key: string
readonly label: string
Expand Down
55 changes: 55 additions & 0 deletions packages/api/src/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,60 @@ export const ProjectBrowserSessionSchema = Schema.Struct({
status: ProjectBrowserStatusSchema
})

export const ProjectDatabaseEngineSchema = Schema.Literal("postgres", "mysql", "mariadb")

export const ProjectDatabaseProfileSchema = Schema.Struct({
createdAt: Schema.String,
database: Schema.String,
engine: ProjectDatabaseEngineSchema,
host: Schema.String,
id: Schema.String,
label: Schema.String,
maskedConnectionString: Schema.String,
port: Schema.Number,
updatedAt: Schema.String,
user: Schema.String
})

export const ProjectDatabaseProfileRequestSchema = Schema.Struct({
connectionString: Schema.String,
label: OptionalNullableString
})

export const ProjectDatabaseSessionStatusSchema = Schema.Literal("running", "stopped", "missing", "unknown")

export const ProjectDatabaseSessionSchema = Schema.Struct({
configHash: Schema.String,
containerName: Schema.String,
editorPath: Schema.String,
editorUrl: Schema.String,
projectId: Schema.String,
projectKey: Schema.String,
status: ProjectDatabaseSessionStatusSchema
})

export const ProjectDatabaseForwardStatusSchema = Schema.Literal("running", "stopped", "unknown")

export const ProjectDatabaseForwardSchema = Schema.Struct({
bindHost: Schema.String,
containerName: Schema.String,
createdAt: Schema.NullOr(Schema.String),
database: Schema.String,
engine: ProjectDatabaseEngineSchema,
externalConnectionString: Schema.String,
hostPort: Schema.Number,
id: Schema.String,
maskedExternalConnectionString: Schema.String,
profileId: Schema.String,
profileLabel: Schema.String,
projectId: Schema.String,
projectKey: Schema.String,
publicHost: Schema.String,
status: ProjectDatabaseForwardStatusSchema,
targetHost: Schema.String,
targetPort: Schema.Number
})

export const AgentProviderSchema = Schema.Literal("codex", "opencode", "claude", "custom")

export const AgentEnvVarSchema = Schema.Struct({
Expand Down Expand Up @@ -220,5 +274,6 @@ export type StateSyncRequestInput = Schema.Schema.Type<typeof StateSyncRequestSc
export type ApplyAllRequestInput = Schema.Schema.Type<typeof ApplyAllRequestSchema>
export type UpProjectRequestInput = Schema.Schema.Type<typeof UpProjectRequestSchema>
export type ProjectPortForwardRequestInput = Schema.Schema.Type<typeof ProjectPortForwardRequestSchema>
export type ProjectDatabaseProfileRequestInput = Schema.Schema.Type<typeof ProjectDatabaseProfileRequestSchema>
export type CreateAgentRequestInput = Schema.Schema.Type<typeof CreateAgentRequestSchema>
export type CreateFollowRequestInput = Schema.Schema.Type<typeof CreateFollowRequestSchema>
122 changes: 121 additions & 1 deletion packages/api/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
CreateProjectRequestSchema,
GithubAuthLoginRequestSchema,
GithubAuthLogoutRequestSchema,
ProjectDatabaseProfileRequestSchema,
ProjectAuthRequestSchema,
ProjectPortForwardRequestSchema,
StateCommitRequestSchema,
Expand Down Expand Up @@ -76,6 +77,22 @@ import {
import { readProjectAuthSnapshot, runProjectAuthFlow } from "./services/project-auth.js"
import { readProjectBrowserSession, proxyProjectBrowser } from "./services/project-browser.js"
import { parseProjectBrowserProxyPath } from "./services/project-browser-core.js"
import {
deleteProjectDatabaseForward,
deleteProjectDatabaseProfile,
exposeProjectDatabaseProfile,
listProjectDatabaseForwards,
listProjectDatabaseProfiles,
openProjectDatabaseEditor,
proxyProjectDatabase,
readProjectDatabaseSession,
restartProjectDatabaseEditor,
saveProjectDatabaseProfile
} from "./services/project-databases.js"
import {
parseProjectDatabaseProxyPath,
parseProjectDatabaseStatefulProxyPath
} from "./services/project-databases-core.js"
import {
createProjectPortForward,
deleteProjectPortForward,
Expand Down Expand Up @@ -103,6 +120,11 @@ const ProjectPortForwardParamsSchema = Schema.Struct({
targetPort: Schema.String
})

const ProjectDatabaseProfileParamsSchema = Schema.Struct({
projectId: Schema.String,
profileId: Schema.String
})

const AgentParamsSchema = Schema.Struct({
projectId: Schema.String,
agentId: Schema.String
Expand Down Expand Up @@ -264,6 +286,7 @@ const errorResponse = (error: ApiError | unknown) => {

const projectParams = HttpRouter.schemaParams(ProjectParamsSchema)
const projectPortForwardParams = HttpRouter.schemaParams(ProjectPortForwardParamsSchema)
const projectDatabaseProfileParams = HttpRouter.schemaParams(ProjectDatabaseProfileParamsSchema)
const agentParams = HttpRouter.schemaParams(AgentParamsSchema)
const terminalSessionParams = HttpRouter.schemaParams(TerminalSessionParamsSchema)
const authTerminalSessionParams = HttpRouter.schemaParams(AuthTerminalSessionParamsSchema)
Expand All @@ -279,6 +302,7 @@ const readCodexAuthLoginRequest = () => HttpServerRequest.schemaBodyJson(CodexAu
const readCodexAuthLogoutRequest = () => HttpServerRequest.schemaBodyJson(CodexAuthLogoutRequestSchema)
const readProjectAuthRequest = () => HttpServerRequest.schemaBodyJson(ProjectAuthRequestSchema)
const readProjectPortForwardRequest = () => HttpServerRequest.schemaBodyJson(ProjectPortForwardRequestSchema)
const readProjectDatabaseProfileRequest = () => HttpServerRequest.schemaBodyJson(ProjectDatabaseProfileRequestSchema)
const readStateInitRequest = () => HttpServerRequest.schemaBodyJson(StateInitRequestSchema)
const readStateCommitRequest = () => HttpServerRequest.schemaBodyJson(StateCommitRequestSchema)
const readStateSyncRequest = () => HttpServerRequest.schemaBodyJson(StateSyncRequestSchema)
Expand Down Expand Up @@ -367,8 +391,20 @@ const projectProxyResponse = Effect.gen(function*(_) {
if (browserTarget !== null) {
return yield* _(proxyProjectBrowser(request, browserTarget, resolveRequestOrigin(request)))
}
const databaseTarget = parseProjectDatabaseProxyPath(pathname)
if (databaseTarget !== null) {
return yield* _(proxyProjectDatabase(request, databaseTarget))
}
const target = parseProjectPortProxyPath(pathname)
if (target === null) {
const statefulDatabaseTarget = parseProjectDatabaseStatefulProxyPath(
pathname,
readHeader(request, "referer"),
readHeader(request, "cookie")
)
if (statefulDatabaseTarget !== null) {
return yield* _(proxyProjectDatabase(request, statefulDatabaseTarget))
}
return yield* _(Effect.fail(new ApiNotFoundError({ message: `Route not found: ${pathname}` })))
}
return yield* _(proxyProjectPortForward(request, target))
Expand Down Expand Up @@ -740,7 +776,91 @@ export const makeRouter = () => {
const browser = yield* _(readProjectBrowserSession(projectId, resolveRequestOrigin(request)))
return yield* _(jsonResponse({ browser }, 200))
}).pipe(Effect.catchAll(errorResponse))
)
)

const withProjectDatabases = withProjects.pipe(
HttpRouter.get(
"/projects/:projectId/databases/profiles",
projectParams.pipe(
Effect.flatMap(({ projectId }) => listProjectDatabaseProfiles(projectId)),
Effect.flatMap((profiles) => jsonResponse({ profiles }, 200)),
Effect.catchAll(errorResponse)
)
),
HttpRouter.get(
"/projects/:projectId/databases/forwards",
projectParams.pipe(
Effect.flatMap(({ projectId }) => listProjectDatabaseForwards(projectId)),
Effect.flatMap((forwards) => jsonResponse({ forwards }, 200)),
Effect.catchAll(errorResponse)
)
),
HttpRouter.post(
"/projects/:projectId/databases/profiles",
Effect.gen(function*(_) {
const { projectId } = yield* _(projectParams)
const request = yield* _(readProjectDatabaseProfileRequest())
const profile = yield* _(saveProjectDatabaseProfile(projectId, request))
return yield* _(jsonResponse({ profile }, 201))
}).pipe(Effect.catchAll(errorResponse))
),
HttpRouter.del(
"/projects/:projectId/databases/profiles/:profileId",
projectDatabaseProfileParams.pipe(
Effect.flatMap(({ projectId, profileId }) => deleteProjectDatabaseProfile(projectId, profileId)),
Effect.flatMap(() => jsonResponse({ ok: true }, 200)),
Effect.catchAll(errorResponse)
)
),
HttpRouter.post(
"/projects/:projectId/databases/profiles/:profileId/expose",
Effect.gen(function*(_) {
const { projectId, profileId } = yield* _(projectDatabaseProfileParams)
const serverRequest = yield* _(HttpServerRequest.HttpServerRequest)
const forward = yield* _(exposeProjectDatabaseProfile(
projectId,
profileId,
resolvePortPublicHost(serverRequest)
))
return yield* _(jsonResponse({ forward }, 201))
}).pipe(Effect.catchAll(errorResponse))
),
HttpRouter.del(
"/projects/:projectId/databases/profiles/:profileId/expose",
projectDatabaseProfileParams.pipe(
Effect.flatMap(({ projectId, profileId }) => deleteProjectDatabaseForward(projectId, profileId)),
Effect.flatMap(() => jsonResponse({ ok: true }, 200)),
Effect.catchAll(errorResponse)
)
),
HttpRouter.get(
"/projects/:projectId/databases/session",
projectParams.pipe(
Effect.flatMap(({ projectId }) => readProjectDatabaseSession(projectId)),
Effect.flatMap((session) => jsonResponse({ session }, 200)),
Effect.catchAll(errorResponse)
)
),
HttpRouter.post(
"/projects/:projectId/databases/open",
projectParams.pipe(
Effect.flatMap(({ projectId }) => openProjectDatabaseEditor(projectId)),
Effect.flatMap((session) => jsonResponse({ session }, 200)),
Effect.catchAll(errorResponse)
)
),
HttpRouter.post(
"/projects/:projectId/databases/restart",
projectParams.pipe(
Effect.flatMap(({ projectId }) => restartProjectDatabaseEditor(projectId)),
Effect.flatMap((session) => jsonResponse({ session }, 200)),
Effect.catchAll(errorResponse)
)
)
)

const withProjectLifecycle = withProjectDatabases.pipe(
HttpRouter.del(
"/projects/:projectId",
projectParams.pipe(
Expand Down Expand Up @@ -816,7 +936,7 @@ export const makeRouter = () => {
)
)

const withAgents = withProjects.pipe(
const withAgents = withProjectLifecycle.pipe(
HttpRouter.post(
"/projects/:projectId/agents",
Effect.gen(function*(_) {
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { initializeAgentState } from "./services/agents.js"
import { attachAuthTerminalWebSocketServer } from "./services/auth-terminal-sessions.js"
import { startOutboxPolling } from "./services/federation.js"
import { attachProjectBrowserWebSocketServer } from "./services/project-browser.js"
import { attachProjectDatabaseWebSocketServer } from "./services/project-databases.js"
import { attachTerminalWebSocketServer } from "./services/terminal-sessions.js"

const resolvePort = (env: Record<string, string | undefined>): number => {
Expand Down Expand Up @@ -48,6 +49,7 @@ export const program = (() => {
attachAuthTerminalWebSocketServer(server)
attachTerminalWebSocketServer(server)
attachProjectBrowserWebSocketServer(server)
attachProjectDatabaseWebSocketServer(server)
const serverLayer = NodeHttpServer.layer(() => server, { port })

const pollingInterval = parseInt(process.env["DOCKER_GIT_OUTBOX_POLLING_INTERVAL_MS"] ?? "5000", 10)
Expand Down
Loading