fix: validate workspace repository browser payloads
This commit is contained in:
@@ -0,0 +1,164 @@
|
|||||||
|
// This file is generated by `cargo run -p workspace-api --features typescript --example generate_typescript | deno fmt -`.
|
||||||
|
// Do not edit this file directly.
|
||||||
|
|
||||||
|
export type WorkspaceSummary = {
|
||||||
|
workspace_id: string;
|
||||||
|
owner_account_id: string | null;
|
||||||
|
display_name: string;
|
||||||
|
state: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspaceRepositoryRecord = {
|
||||||
|
workspace_id: string;
|
||||||
|
repository_id: string;
|
||||||
|
name: string;
|
||||||
|
kind: string;
|
||||||
|
provider: string | null;
|
||||||
|
source: RepositorySource;
|
||||||
|
default_ref: string | null;
|
||||||
|
source_revision: number;
|
||||||
|
source_fingerprint: string;
|
||||||
|
observed_status: RepositoryObservedStatus;
|
||||||
|
observed_at: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspaceCreateResponse = {
|
||||||
|
workspace: WorkspaceSummary;
|
||||||
|
repository: WorkspaceRepositoryRecord;
|
||||||
|
config_revision: number;
|
||||||
|
request_fingerprint: string;
|
||||||
|
replayed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspaceAuthConfig = {
|
||||||
|
"Passkey": {
|
||||||
|
rp_id: string;
|
||||||
|
origin: string;
|
||||||
|
public_base_url: string;
|
||||||
|
cookie_name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspacePermissionSummary = {
|
||||||
|
manage_repositories: boolean;
|
||||||
|
manage_secrets: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DiagnosticSeverity = "info" | "warning" | "error";
|
||||||
|
|
||||||
|
export type Diagnostic = {
|
||||||
|
code: string;
|
||||||
|
severity: DiagnosticSeverity;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspaceExtensionPointState = {
|
||||||
|
status: string;
|
||||||
|
note: string;
|
||||||
|
diagnostics: Array<Diagnostic>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspaceExtensionPoints = {
|
||||||
|
store: string;
|
||||||
|
event_stream: WorkspaceExtensionPointState;
|
||||||
|
host_worker_bridge: WorkspaceExtensionPointState;
|
||||||
|
companion_console: WorkspaceExtensionPointState;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspaceResponse = {
|
||||||
|
workspace_id: string;
|
||||||
|
display_name: string;
|
||||||
|
record_authority: string;
|
||||||
|
schema_version: number;
|
||||||
|
auth: WorkspaceAuthConfig;
|
||||||
|
permissions: WorkspacePermissionSummary;
|
||||||
|
extension_points: WorkspaceExtensionPoints;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepositorySourceKind =
|
||||||
|
| "local_path"
|
||||||
|
| "file"
|
||||||
|
| "ssh"
|
||||||
|
| "http"
|
||||||
|
| "https"
|
||||||
|
| "invalid";
|
||||||
|
|
||||||
|
export type RepositorySource = {
|
||||||
|
kind: RepositorySourceKind;
|
||||||
|
/**
|
||||||
|
* Canonical source representation. This is an absolute local path for
|
||||||
|
* `local_path`, and a normalized URI/remote specification otherwise.
|
||||||
|
*/
|
||||||
|
uri: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepositoryObservedStatus = "unverified" | "ready" | "invalid";
|
||||||
|
|
||||||
|
export type RepositoryDiagnostic = {
|
||||||
|
severity: string;
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitRemoteSummary = { name: string; fetch_url: string };
|
||||||
|
|
||||||
|
export type GitRepositorySummary = {
|
||||||
|
status: string;
|
||||||
|
head: string | null;
|
||||||
|
branch: string | null;
|
||||||
|
dirty: boolean;
|
||||||
|
remotes: Array<GitRemoteSummary>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepositorySummary = {
|
||||||
|
id: string;
|
||||||
|
display_name: string;
|
||||||
|
kind: string;
|
||||||
|
provider: string;
|
||||||
|
source: RepositorySource;
|
||||||
|
source_revision: number;
|
||||||
|
source_fingerprint: string;
|
||||||
|
observed_status: RepositoryObservedStatus;
|
||||||
|
observed_at?: string | null;
|
||||||
|
default_selector?: string | null;
|
||||||
|
record_authority: string;
|
||||||
|
git?: GitRepositorySummary | null;
|
||||||
|
diagnostics?: Array<RepositoryDiagnostic> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitCommitSummary = {
|
||||||
|
hash: string;
|
||||||
|
short_hash: string;
|
||||||
|
summary: string;
|
||||||
|
author_name: string;
|
||||||
|
author_email: string;
|
||||||
|
author_date: string;
|
||||||
|
parents: Array<string>;
|
||||||
|
refs: Array<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepositoryListResponse = {
|
||||||
|
workspace_id: string;
|
||||||
|
items: Array<RepositorySummary>;
|
||||||
|
source: string;
|
||||||
|
diagnostics: Array<Diagnostic>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepositoryDetailResponse = {
|
||||||
|
workspace_id: string;
|
||||||
|
item: RepositorySummary;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepositoryLogResponse = {
|
||||||
|
workspace_id: string;
|
||||||
|
repository_id: string;
|
||||||
|
default_selector?: string | null;
|
||||||
|
limit: number;
|
||||||
|
items: Array<GitCommitSummary>;
|
||||||
|
diagnostics: Array<Diagnostic>;
|
||||||
|
};
|
||||||
@@ -1,41 +1,18 @@
|
|||||||
export type WorkspaceCatalogRecord = {
|
import {
|
||||||
workspace_id: string;
|
parseRepositoryListResponse,
|
||||||
owner_account_id: string | null;
|
parseWorkspaceCatalogResponse,
|
||||||
display_name: string;
|
parseWorkspaceCreateResponse,
|
||||||
state: string;
|
type RepositorySummary,
|
||||||
created_at: string;
|
type WorkspaceCreateResponse,
|
||||||
updated_at: string;
|
type WorkspaceSummary,
|
||||||
};
|
} from "$lib/workspace/api/workspace-model";
|
||||||
|
|
||||||
export type RepositorySourceKind =
|
|
||||||
| "local_path"
|
|
||||||
| "file"
|
|
||||||
| "ssh"
|
|
||||||
| "http"
|
|
||||||
| "https"
|
|
||||||
| "invalid";
|
|
||||||
|
|
||||||
export type WorkspaceRepositoryRecord = {
|
|
||||||
workspace_id: string;
|
|
||||||
repository_id: string;
|
|
||||||
name: string;
|
|
||||||
kind: string;
|
|
||||||
provider: string | null;
|
|
||||||
source: {
|
|
||||||
kind: RepositorySourceKind;
|
|
||||||
uri: string;
|
|
||||||
};
|
|
||||||
default_ref: string | null;
|
|
||||||
source_revision: number;
|
|
||||||
source_fingerprint: string;
|
|
||||||
observed_status: "unverified" | "ready" | "invalid";
|
|
||||||
observed_at: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
export type WorkspaceCatalogRecord = WorkspaceSummary;
|
||||||
export type WorkspaceCatalogItem = WorkspaceCatalogRecord & {
|
export type WorkspaceCatalogItem = WorkspaceCatalogRecord & {
|
||||||
repositories: WorkspaceRepositoryRecord[];
|
repositories: RepositorySummary[];
|
||||||
repository_error?: string;
|
repository_error?: string;
|
||||||
};
|
};
|
||||||
|
export type CreateWorkspaceResponse = WorkspaceCreateResponse;
|
||||||
|
|
||||||
export type CreateWorkspaceRequest = {
|
export type CreateWorkspaceRequest = {
|
||||||
operation_key: string;
|
operation_key: string;
|
||||||
@@ -47,14 +24,6 @@ export type CreateWorkspaceRequest = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateWorkspaceResponse = {
|
|
||||||
workspace: WorkspaceCatalogRecord;
|
|
||||||
repository: WorkspaceRepositoryRecord;
|
|
||||||
config_revision: number;
|
|
||||||
request_fingerprint: string;
|
|
||||||
replayed: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class WorkspaceCatalogError extends Error {
|
export class WorkspaceCatalogError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly status: number | null,
|
public readonly status: number | null,
|
||||||
@@ -70,20 +39,21 @@ type Fetch = typeof globalThis.fetch;
|
|||||||
export async function listWorkspaces(
|
export async function listWorkspaces(
|
||||||
fetcher: Fetch,
|
fetcher: Fetch,
|
||||||
): Promise<WorkspaceCatalogRecord[]> {
|
): Promise<WorkspaceCatalogRecord[]> {
|
||||||
return await fetchJson<WorkspaceCatalogRecord[]>(
|
return parseWorkspaceCatalogResponse(
|
||||||
fetcher,
|
await fetchJson(fetcher, "/api/workspaces?limit=200"),
|
||||||
"/api/workspaces?limit=200",
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listWorkspaceRepositories(
|
export async function listWorkspaceRepositories(
|
||||||
fetcher: Fetch,
|
fetcher: Fetch,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
): Promise<WorkspaceRepositoryRecord[]> {
|
): Promise<RepositorySummary[]> {
|
||||||
return await fetchJson<WorkspaceRepositoryRecord[]>(
|
return parseRepositoryListResponse(
|
||||||
fetcher,
|
await fetchJson(
|
||||||
`/api/w/${encodeURIComponent(workspaceId)}/repositories`,
|
fetcher,
|
||||||
);
|
`/api/w/${encodeURIComponent(workspaceId)}/repositories`,
|
||||||
|
),
|
||||||
|
).items;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadWorkspaceCatalog(
|
export async function loadWorkspaceCatalog(
|
||||||
@@ -115,11 +85,13 @@ export async function createWorkspace(
|
|||||||
fetcher: Fetch,
|
fetcher: Fetch,
|
||||||
request: CreateWorkspaceRequest,
|
request: CreateWorkspaceRequest,
|
||||||
): Promise<CreateWorkspaceResponse> {
|
): Promise<CreateWorkspaceResponse> {
|
||||||
return await fetchJson<CreateWorkspaceResponse>(fetcher, "/api/workspaces", {
|
return parseWorkspaceCreateResponse(
|
||||||
method: "POST",
|
await fetchJson(fetcher, "/api/workspaces", {
|
||||||
headers: { "content-type": "application/json" },
|
method: "POST",
|
||||||
body: JSON.stringify(request),
|
headers: { "content-type": "application/json" },
|
||||||
});
|
body: JSON.stringify(request),
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function creationErrorMessage(error: unknown): string {
|
export function creationErrorMessage(error: unknown): string {
|
||||||
@@ -150,11 +122,11 @@ export function createOperationKey(): string {
|
|||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchJson<T>(
|
async function fetchJson(
|
||||||
fetcher: Fetch,
|
fetcher: Fetch,
|
||||||
input: string,
|
input: string,
|
||||||
init?: RequestInit,
|
init?: RequestInit,
|
||||||
): Promise<T> {
|
): Promise<unknown> {
|
||||||
let response: Response;
|
let response: Response;
|
||||||
try {
|
try {
|
||||||
response = await fetcher(input, init);
|
response = await fetcher(input, init);
|
||||||
@@ -172,7 +144,7 @@ async function fetchJson<T>(
|
|||||||
}
|
}
|
||||||
throw new WorkspaceCatalogError(response.status, detail);
|
throw new WorkspaceCatalogError(response.status, detail);
|
||||||
}
|
}
|
||||||
return await response.json() as T;
|
return await response.json() as unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
function errorMessage(error: unknown): string {
|
function errorMessage(error: unknown): string {
|
||||||
|
|||||||
@@ -0,0 +1,591 @@
|
|||||||
|
import type {
|
||||||
|
Diagnostic,
|
||||||
|
GitCommitSummary,
|
||||||
|
GitRemoteSummary,
|
||||||
|
GitRepositorySummary,
|
||||||
|
RepositoryDetailResponse,
|
||||||
|
RepositoryDiagnostic,
|
||||||
|
RepositoryListResponse,
|
||||||
|
RepositoryLogResponse,
|
||||||
|
RepositorySource,
|
||||||
|
RepositorySourceKind,
|
||||||
|
RepositorySummary,
|
||||||
|
WorkspaceAuthConfig,
|
||||||
|
WorkspaceCreateResponse,
|
||||||
|
WorkspaceExtensionPoints,
|
||||||
|
WorkspaceExtensionPointState,
|
||||||
|
WorkspacePermissionSummary,
|
||||||
|
WorkspaceRepositoryRecord,
|
||||||
|
WorkspaceResponse,
|
||||||
|
WorkspaceSummary,
|
||||||
|
} from "$lib/generated/workspace-api.ts";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
GitCommitSummary,
|
||||||
|
GitRemoteSummary,
|
||||||
|
GitRepositorySummary,
|
||||||
|
RepositoryDetailResponse,
|
||||||
|
RepositoryListResponse,
|
||||||
|
RepositoryLogResponse,
|
||||||
|
RepositorySummary,
|
||||||
|
WorkspaceCreateResponse,
|
||||||
|
WorkspacePermissionSummary,
|
||||||
|
WorkspaceResponse,
|
||||||
|
WorkspaceSummary,
|
||||||
|
} from "$lib/generated/workspace-api.ts";
|
||||||
|
|
||||||
|
type JsonObject = Record<string, unknown>;
|
||||||
|
|
||||||
|
const SOURCE_KINDS = new Set<RepositorySourceKind>([
|
||||||
|
"local_path",
|
||||||
|
"file",
|
||||||
|
"ssh",
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"invalid",
|
||||||
|
]);
|
||||||
|
const OBSERVED_STATUSES = new Set(["unverified", "ready", "invalid"]);
|
||||||
|
const DIAGNOSTIC_SEVERITIES = new Set(["info", "warning", "error"]);
|
||||||
|
|
||||||
|
function object(value: unknown, path: string): JsonObject {
|
||||||
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||||
|
throw new Error(`${path} must be an object`);
|
||||||
|
}
|
||||||
|
return value as JsonObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
function array(value: unknown, path: string): unknown[] {
|
||||||
|
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function string(value: unknown, path: string): string {
|
||||||
|
if (typeof value !== "string") throw new Error(`${path} must be a string`);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function boolean(value: unknown, path: string): boolean {
|
||||||
|
if (typeof value !== "boolean") throw new Error(`${path} must be a boolean`);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function integer(value: unknown, path: string): number {
|
||||||
|
if (typeof value !== "number" || !Number.isSafeInteger(value)) {
|
||||||
|
throw new Error(`${path} must be a safe integer`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nullableString(value: unknown, path: string): string | null {
|
||||||
|
return value === null ? null : string(value, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionalNullableString(
|
||||||
|
value: unknown,
|
||||||
|
path: string,
|
||||||
|
): string | null | undefined {
|
||||||
|
return value === undefined ? undefined : nullableString(value, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exactKeys(
|
||||||
|
value: JsonObject,
|
||||||
|
keys: readonly string[],
|
||||||
|
path: string,
|
||||||
|
): void {
|
||||||
|
const allowed = new Set(keys);
|
||||||
|
const unexpected = Object.keys(value).find((key) => !allowed.has(key));
|
||||||
|
if (unexpected) {
|
||||||
|
throw new Error(`${path}.${unexpected} is not part of the wire contract`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function diagnostic(value: unknown, path: string): Diagnostic {
|
||||||
|
const item = object(value, path);
|
||||||
|
exactKeys(item, ["code", "severity", "message"], path);
|
||||||
|
const severity = string(item.severity, `${path}.severity`);
|
||||||
|
if (!DIAGNOSTIC_SEVERITIES.has(severity)) {
|
||||||
|
throw new Error(`${path}.severity is invalid`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: string(item.code, `${path}.code`),
|
||||||
|
severity: severity as Diagnostic["severity"],
|
||||||
|
message: string(item.message, `${path}.message`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function repositoryDiagnostic(
|
||||||
|
value: unknown,
|
||||||
|
path: string,
|
||||||
|
): RepositoryDiagnostic {
|
||||||
|
const item = object(value, path);
|
||||||
|
exactKeys(item, ["severity", "code", "message"], path);
|
||||||
|
return {
|
||||||
|
severity: string(item.severity, `${path}.severity`),
|
||||||
|
code: string(item.code, `${path}.code`),
|
||||||
|
message: string(item.message, `${path}.message`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function repositorySource(value: unknown, path: string): RepositorySource {
|
||||||
|
const source = object(value, path);
|
||||||
|
exactKeys(source, ["kind", "uri"], path);
|
||||||
|
const kind = string(source.kind, `${path}.kind`);
|
||||||
|
if (!SOURCE_KINDS.has(kind as RepositorySourceKind)) {
|
||||||
|
throw new Error(`${path}.kind is invalid`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind: kind as RepositorySourceKind,
|
||||||
|
uri: string(source.uri, `${path}.uri`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function gitRemote(value: unknown, path: string): GitRemoteSummary {
|
||||||
|
const remote = object(value, path);
|
||||||
|
exactKeys(remote, ["name", "fetch_url"], path);
|
||||||
|
return {
|
||||||
|
name: string(remote.name, `${path}.name`),
|
||||||
|
fetch_url: string(remote.fetch_url, `${path}.fetch_url`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function gitSummary(value: unknown, path: string): GitRepositorySummary {
|
||||||
|
const git = object(value, path);
|
||||||
|
exactKeys(git, ["status", "head", "branch", "dirty", "remotes"], path);
|
||||||
|
return {
|
||||||
|
status: string(git.status, `${path}.status`),
|
||||||
|
head: nullableString(git.head, `${path}.head`),
|
||||||
|
branch: nullableString(git.branch, `${path}.branch`),
|
||||||
|
dirty: boolean(git.dirty, `${path}.dirty`),
|
||||||
|
remotes: array(git.remotes, `${path}.remotes`).map((item, index) =>
|
||||||
|
gitRemote(item, `${path}.remotes[${index}]`)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function repositorySummary(value: unknown, path: string): RepositorySummary {
|
||||||
|
const item = object(value, path);
|
||||||
|
exactKeys(
|
||||||
|
item,
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
"display_name",
|
||||||
|
"kind",
|
||||||
|
"provider",
|
||||||
|
"source",
|
||||||
|
"source_revision",
|
||||||
|
"source_fingerprint",
|
||||||
|
"observed_status",
|
||||||
|
"observed_at",
|
||||||
|
"default_selector",
|
||||||
|
"record_authority",
|
||||||
|
"git",
|
||||||
|
"diagnostics",
|
||||||
|
],
|
||||||
|
path,
|
||||||
|
);
|
||||||
|
const observedStatus = string(
|
||||||
|
item.observed_status,
|
||||||
|
`${path}.observed_status`,
|
||||||
|
);
|
||||||
|
if (!OBSERVED_STATUSES.has(observedStatus)) {
|
||||||
|
throw new Error(`${path}.observed_status is invalid`);
|
||||||
|
}
|
||||||
|
const diagnostics =
|
||||||
|
item.diagnostics === undefined || item.diagnostics === null
|
||||||
|
? item.diagnostics
|
||||||
|
: array(item.diagnostics, `${path}.diagnostics`).map((entry, index) =>
|
||||||
|
repositoryDiagnostic(entry, `${path}.diagnostics[${index}]`)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: string(item.id, `${path}.id`),
|
||||||
|
display_name: string(item.display_name, `${path}.display_name`),
|
||||||
|
kind: string(item.kind, `${path}.kind`),
|
||||||
|
provider: string(item.provider, `${path}.provider`),
|
||||||
|
source: repositorySource(item.source, `${path}.source`),
|
||||||
|
source_revision: integer(item.source_revision, `${path}.source_revision`),
|
||||||
|
source_fingerprint: string(
|
||||||
|
item.source_fingerprint,
|
||||||
|
`${path}.source_fingerprint`,
|
||||||
|
),
|
||||||
|
observed_status: observedStatus as RepositorySummary["observed_status"],
|
||||||
|
observed_at: optionalNullableString(
|
||||||
|
item.observed_at,
|
||||||
|
`${path}.observed_at`,
|
||||||
|
),
|
||||||
|
default_selector: optionalNullableString(
|
||||||
|
item.default_selector,
|
||||||
|
`${path}.default_selector`,
|
||||||
|
),
|
||||||
|
record_authority: string(item.record_authority, `${path}.record_authority`),
|
||||||
|
git: item.git === undefined || item.git === null
|
||||||
|
? item.git
|
||||||
|
: gitSummary(item.git, `${path}.git`),
|
||||||
|
diagnostics,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function workspaceSummary(value: unknown, path: string): WorkspaceSummary {
|
||||||
|
const item = object(value, path);
|
||||||
|
exactKeys(
|
||||||
|
item,
|
||||||
|
[
|
||||||
|
"workspace_id",
|
||||||
|
"owner_account_id",
|
||||||
|
"display_name",
|
||||||
|
"state",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
],
|
||||||
|
path,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
workspace_id: string(item.workspace_id, `${path}.workspace_id`),
|
||||||
|
owner_account_id: nullableString(
|
||||||
|
item.owner_account_id,
|
||||||
|
`${path}.owner_account_id`,
|
||||||
|
),
|
||||||
|
display_name: string(item.display_name, `${path}.display_name`),
|
||||||
|
state: string(item.state, `${path}.state`),
|
||||||
|
created_at: string(item.created_at, `${path}.created_at`),
|
||||||
|
updated_at: string(item.updated_at, `${path}.updated_at`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function workspaceRepositoryRecord(
|
||||||
|
value: unknown,
|
||||||
|
path: string,
|
||||||
|
): WorkspaceRepositoryRecord {
|
||||||
|
const item = object(value, path);
|
||||||
|
exactKeys(
|
||||||
|
item,
|
||||||
|
[
|
||||||
|
"workspace_id",
|
||||||
|
"repository_id",
|
||||||
|
"name",
|
||||||
|
"kind",
|
||||||
|
"provider",
|
||||||
|
"source",
|
||||||
|
"default_ref",
|
||||||
|
"source_revision",
|
||||||
|
"source_fingerprint",
|
||||||
|
"observed_status",
|
||||||
|
"observed_at",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
],
|
||||||
|
path,
|
||||||
|
);
|
||||||
|
const observedStatus = string(
|
||||||
|
item.observed_status,
|
||||||
|
`${path}.observed_status`,
|
||||||
|
);
|
||||||
|
if (!OBSERVED_STATUSES.has(observedStatus)) {
|
||||||
|
throw new Error(`${path}.observed_status is invalid`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
workspace_id: string(item.workspace_id, `${path}.workspace_id`),
|
||||||
|
repository_id: string(item.repository_id, `${path}.repository_id`),
|
||||||
|
name: string(item.name, `${path}.name`),
|
||||||
|
kind: string(item.kind, `${path}.kind`),
|
||||||
|
provider: nullableString(item.provider, `${path}.provider`),
|
||||||
|
source: repositorySource(item.source, `${path}.source`),
|
||||||
|
default_ref: nullableString(item.default_ref, `${path}.default_ref`),
|
||||||
|
source_revision: integer(item.source_revision, `${path}.source_revision`),
|
||||||
|
source_fingerprint: string(
|
||||||
|
item.source_fingerprint,
|
||||||
|
`${path}.source_fingerprint`,
|
||||||
|
),
|
||||||
|
observed_status:
|
||||||
|
observedStatus as WorkspaceRepositoryRecord["observed_status"],
|
||||||
|
observed_at: nullableString(item.observed_at, `${path}.observed_at`),
|
||||||
|
created_at: string(item.created_at, `${path}.created_at`),
|
||||||
|
updated_at: string(item.updated_at, `${path}.updated_at`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extensionPoint(
|
||||||
|
value: unknown,
|
||||||
|
path: string,
|
||||||
|
): WorkspaceExtensionPointState {
|
||||||
|
const item = object(value, path);
|
||||||
|
exactKeys(item, ["status", "note", "diagnostics"], path);
|
||||||
|
return {
|
||||||
|
status: string(item.status, `${path}.status`),
|
||||||
|
note: string(item.note, `${path}.note`),
|
||||||
|
diagnostics: array(item.diagnostics, `${path}.diagnostics`).map((
|
||||||
|
entry,
|
||||||
|
index,
|
||||||
|
) => diagnostic(entry, `${path}.diagnostics[${index}]`)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extensionPoints(
|
||||||
|
value: unknown,
|
||||||
|
path: string,
|
||||||
|
): WorkspaceExtensionPoints {
|
||||||
|
const item = object(value, path);
|
||||||
|
exactKeys(item, [
|
||||||
|
"store",
|
||||||
|
"event_stream",
|
||||||
|
"host_worker_bridge",
|
||||||
|
"companion_console",
|
||||||
|
], path);
|
||||||
|
return {
|
||||||
|
store: string(item.store, `${path}.store`),
|
||||||
|
event_stream: extensionPoint(item.event_stream, `${path}.event_stream`),
|
||||||
|
host_worker_bridge: extensionPoint(
|
||||||
|
item.host_worker_bridge,
|
||||||
|
`${path}.host_worker_bridge`,
|
||||||
|
),
|
||||||
|
companion_console: extensionPoint(
|
||||||
|
item.companion_console,
|
||||||
|
`${path}.companion_console`,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function authConfig(value: unknown, path: string): WorkspaceAuthConfig {
|
||||||
|
const auth = object(value, path);
|
||||||
|
exactKeys(auth, ["Passkey"], path);
|
||||||
|
const passkey = object(auth.Passkey, `${path}.Passkey`);
|
||||||
|
exactKeys(
|
||||||
|
passkey,
|
||||||
|
["rp_id", "origin", "public_base_url", "cookie_name"],
|
||||||
|
`${path}.Passkey`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
Passkey: {
|
||||||
|
rp_id: string(passkey.rp_id, `${path}.Passkey.rp_id`),
|
||||||
|
origin: string(passkey.origin, `${path}.Passkey.origin`),
|
||||||
|
public_base_url: string(
|
||||||
|
passkey.public_base_url,
|
||||||
|
`${path}.Passkey.public_base_url`,
|
||||||
|
),
|
||||||
|
cookie_name: string(passkey.cookie_name, `${path}.Passkey.cookie_name`),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function permissions(value: unknown, path: string): WorkspacePermissionSummary {
|
||||||
|
const item = object(value, path);
|
||||||
|
exactKeys(item, ["manage_repositories", "manage_secrets"], path);
|
||||||
|
return {
|
||||||
|
manage_repositories: boolean(
|
||||||
|
item.manage_repositories,
|
||||||
|
`${path}.manage_repositories`,
|
||||||
|
),
|
||||||
|
manage_secrets: boolean(item.manage_secrets, `${path}.manage_secrets`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitSummary(value: unknown, path: string): GitCommitSummary {
|
||||||
|
const item = object(value, path);
|
||||||
|
exactKeys(
|
||||||
|
item,
|
||||||
|
[
|
||||||
|
"hash",
|
||||||
|
"short_hash",
|
||||||
|
"summary",
|
||||||
|
"author_name",
|
||||||
|
"author_email",
|
||||||
|
"author_date",
|
||||||
|
"parents",
|
||||||
|
"refs",
|
||||||
|
],
|
||||||
|
path,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
hash: string(item.hash, `${path}.hash`),
|
||||||
|
short_hash: string(item.short_hash, `${path}.short_hash`),
|
||||||
|
summary: string(item.summary, `${path}.summary`),
|
||||||
|
author_name: string(item.author_name, `${path}.author_name`),
|
||||||
|
author_email: string(item.author_email, `${path}.author_email`),
|
||||||
|
author_date: string(item.author_date, `${path}.author_date`),
|
||||||
|
parents: array(item.parents, `${path}.parents`).map((entry, index) =>
|
||||||
|
string(entry, `${path}.parents[${index}]`)
|
||||||
|
),
|
||||||
|
refs: array(item.refs, `${path}.refs`).map((entry, index) =>
|
||||||
|
string(entry, `${path}.refs[${index}]`)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseWorkspaceCatalogResponse(
|
||||||
|
value: unknown,
|
||||||
|
): WorkspaceSummary[] {
|
||||||
|
return array(value, "workspaces").map((item, index) =>
|
||||||
|
workspaceSummary(item, `workspaces[${index}]`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseWorkspaceCreateResponse(
|
||||||
|
value: unknown,
|
||||||
|
): WorkspaceCreateResponse {
|
||||||
|
const response = object(value, "workspace create response");
|
||||||
|
exactKeys(
|
||||||
|
response,
|
||||||
|
[
|
||||||
|
"workspace",
|
||||||
|
"repository",
|
||||||
|
"config_revision",
|
||||||
|
"request_fingerprint",
|
||||||
|
"replayed",
|
||||||
|
],
|
||||||
|
"workspace create response",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
workspace: workspaceSummary(
|
||||||
|
response.workspace,
|
||||||
|
"workspace create response.workspace",
|
||||||
|
),
|
||||||
|
repository: workspaceRepositoryRecord(
|
||||||
|
response.repository,
|
||||||
|
"workspace create response.repository",
|
||||||
|
),
|
||||||
|
config_revision: integer(
|
||||||
|
response.config_revision,
|
||||||
|
"workspace create response.config_revision",
|
||||||
|
),
|
||||||
|
request_fingerprint: string(
|
||||||
|
response.request_fingerprint,
|
||||||
|
"workspace create response.request_fingerprint",
|
||||||
|
),
|
||||||
|
replayed: boolean(response.replayed, "workspace create response.replayed"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseWorkspaceResponse(value: unknown): WorkspaceResponse {
|
||||||
|
const response = object(value, "workspace response");
|
||||||
|
exactKeys(
|
||||||
|
response,
|
||||||
|
[
|
||||||
|
"workspace_id",
|
||||||
|
"display_name",
|
||||||
|
"record_authority",
|
||||||
|
"schema_version",
|
||||||
|
"auth",
|
||||||
|
"permissions",
|
||||||
|
"extension_points",
|
||||||
|
],
|
||||||
|
"workspace response",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
workspace_id: string(
|
||||||
|
response.workspace_id,
|
||||||
|
"workspace response.workspace_id",
|
||||||
|
),
|
||||||
|
display_name: string(
|
||||||
|
response.display_name,
|
||||||
|
"workspace response.display_name",
|
||||||
|
),
|
||||||
|
record_authority: string(
|
||||||
|
response.record_authority,
|
||||||
|
"workspace response.record_authority",
|
||||||
|
),
|
||||||
|
schema_version: integer(
|
||||||
|
response.schema_version,
|
||||||
|
"workspace response.schema_version",
|
||||||
|
),
|
||||||
|
auth: authConfig(response.auth, "workspace response.auth"),
|
||||||
|
permissions: permissions(
|
||||||
|
response.permissions,
|
||||||
|
"workspace response.permissions",
|
||||||
|
),
|
||||||
|
extension_points: extensionPoints(
|
||||||
|
response.extension_points,
|
||||||
|
"workspace response.extension_points",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRepositoryListResponse(
|
||||||
|
value: unknown,
|
||||||
|
): RepositoryListResponse {
|
||||||
|
const response = object(value, "repository list response");
|
||||||
|
exactKeys(
|
||||||
|
response,
|
||||||
|
["workspace_id", "items", "source", "diagnostics"],
|
||||||
|
"repository list response",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
workspace_id: string(
|
||||||
|
response.workspace_id,
|
||||||
|
"repository list response.workspace_id",
|
||||||
|
),
|
||||||
|
items: array(response.items, "repository list response.items").map((
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
) => repositorySummary(item, `repository list response.items[${index}]`)),
|
||||||
|
source: string(response.source, "repository list response.source"),
|
||||||
|
diagnostics: array(
|
||||||
|
response.diagnostics,
|
||||||
|
"repository list response.diagnostics",
|
||||||
|
).map(
|
||||||
|
(item, index) =>
|
||||||
|
diagnostic(item, `repository list response.diagnostics[${index}]`),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRepositoryDetailResponse(
|
||||||
|
value: unknown,
|
||||||
|
): RepositoryDetailResponse {
|
||||||
|
const response = object(value, "repository detail response");
|
||||||
|
exactKeys(
|
||||||
|
response,
|
||||||
|
["workspace_id", "item", "source"],
|
||||||
|
"repository detail response",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
workspace_id: string(
|
||||||
|
response.workspace_id,
|
||||||
|
"repository detail response.workspace_id",
|
||||||
|
),
|
||||||
|
item: repositorySummary(response.item, "repository detail response.item"),
|
||||||
|
source: string(response.source, "repository detail response.source"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRepositoryLogResponse(
|
||||||
|
value: unknown,
|
||||||
|
): RepositoryLogResponse {
|
||||||
|
const response = object(value, "repository log response");
|
||||||
|
exactKeys(
|
||||||
|
response,
|
||||||
|
[
|
||||||
|
"workspace_id",
|
||||||
|
"repository_id",
|
||||||
|
"default_selector",
|
||||||
|
"limit",
|
||||||
|
"items",
|
||||||
|
"diagnostics",
|
||||||
|
],
|
||||||
|
"repository log response",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
workspace_id: string(
|
||||||
|
response.workspace_id,
|
||||||
|
"repository log response.workspace_id",
|
||||||
|
),
|
||||||
|
repository_id: string(
|
||||||
|
response.repository_id,
|
||||||
|
"repository log response.repository_id",
|
||||||
|
),
|
||||||
|
default_selector: optionalNullableString(
|
||||||
|
response.default_selector,
|
||||||
|
"repository log response.default_selector",
|
||||||
|
),
|
||||||
|
limit: integer(response.limit, "repository log response.limit"),
|
||||||
|
items: array(response.items, "repository log response.items").map((
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
) => commitSummary(item, `repository log response.items[${index}]`)),
|
||||||
|
diagnostics: array(
|
||||||
|
response.diagnostics,
|
||||||
|
"repository log response.diagnostics",
|
||||||
|
).map(
|
||||||
|
(item, index) =>
|
||||||
|
diagnostic(item, `repository log response.diagnostics[${index}]`),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,25 +3,19 @@ import type {
|
|||||||
Method as PodProtocolMethod,
|
Method as PodProtocolMethod,
|
||||||
Segment as PodProtocolSegment,
|
Segment as PodProtocolSegment,
|
||||||
} from "$lib/generated/protocol";
|
} from "$lib/generated/protocol";
|
||||||
|
import type {
|
||||||
|
GitCommitSummary as SharedGitCommitSummary,
|
||||||
|
GitRemoteSummary as SharedGitRemoteSummary,
|
||||||
|
GitRepositorySummary as SharedGitRepositorySummary,
|
||||||
|
RepositoryDetailResponse as SharedRepositoryDetailResponse,
|
||||||
|
RepositoryListResponse as SharedRepositoryListResponse,
|
||||||
|
RepositoryLogResponse as SharedRepositoryLogResponse,
|
||||||
|
RepositorySummary as SharedRepositorySummary,
|
||||||
|
WorkspaceResponse as SharedWorkspaceResponse,
|
||||||
|
} from "$lib/workspace/api/workspace-model";
|
||||||
|
|
||||||
export type { PodProtocolEvent, PodProtocolMethod, PodProtocolSegment };
|
export type { PodProtocolEvent, PodProtocolMethod, PodProtocolSegment };
|
||||||
|
export type WorkspaceResponse = SharedWorkspaceResponse;
|
||||||
export type ExtensionPoint = {
|
|
||||||
status: string;
|
|
||||||
note: string;
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkspaceResponse = {
|
|
||||||
workspace_id: string;
|
|
||||||
display_name: string;
|
|
||||||
record_authority: string;
|
|
||||||
extension_points: {
|
|
||||||
event_stream: ExtensionPoint;
|
|
||||||
host_worker_bridge: ExtensionPoint;
|
|
||||||
companion_console: ExtensionPoint;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Diagnostic = {
|
export type Diagnostic = {
|
||||||
code: string;
|
code: string;
|
||||||
@@ -257,70 +251,13 @@ export type ListResponse<T> = {
|
|||||||
diagnostics: Diagnostic[];
|
diagnostics: Diagnostic[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RepositorySummary = {
|
export type RepositorySummary = SharedRepositorySummary;
|
||||||
id: string;
|
export type GitRepositorySummary = SharedGitRepositorySummary;
|
||||||
display_name: string;
|
export type GitRemoteSummary = SharedGitRemoteSummary;
|
||||||
kind: string;
|
export type GitCommitSummary = SharedGitCommitSummary;
|
||||||
provider: string;
|
export type RepositoryListResponse = SharedRepositoryListResponse;
|
||||||
source: {
|
export type RepositoryDetailResponse = SharedRepositoryDetailResponse;
|
||||||
kind: "local_path" | "file" | "ssh" | "http" | "https" | "invalid";
|
export type RepositoryLogResponse = SharedRepositoryLogResponse;
|
||||||
uri: string;
|
|
||||||
};
|
|
||||||
source_revision: number;
|
|
||||||
source_fingerprint: string;
|
|
||||||
observed_status: "unverified" | "ready" | "invalid";
|
|
||||||
observed_at?: string | null;
|
|
||||||
default_selector?: string | null;
|
|
||||||
record_authority: string;
|
|
||||||
git?: GitRepositorySummary | null;
|
|
||||||
diagnostics?: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GitRepositorySummary = {
|
|
||||||
status: string;
|
|
||||||
branch?: string | null;
|
|
||||||
head?: string | null;
|
|
||||||
dirty: boolean;
|
|
||||||
remotes: GitRemoteSummary[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GitRemoteSummary = {
|
|
||||||
name: string;
|
|
||||||
fetch_url: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GitCommitSummary = {
|
|
||||||
hash: string;
|
|
||||||
short_hash: string;
|
|
||||||
summary: string;
|
|
||||||
author_name: string;
|
|
||||||
author_email: string;
|
|
||||||
author_date: string;
|
|
||||||
parents: string[];
|
|
||||||
refs: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RepositoryListResponse = {
|
|
||||||
workspace_id: string;
|
|
||||||
items: RepositorySummary[];
|
|
||||||
source: string;
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RepositoryDetailResponse = {
|
|
||||||
workspace_id: string;
|
|
||||||
item: RepositorySummary;
|
|
||||||
source: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RepositoryLogResponse = {
|
|
||||||
workspace_id: string;
|
|
||||||
repository_id: string;
|
|
||||||
default_selector?: string | null;
|
|
||||||
limit: number;
|
|
||||||
items: GitCommitSummary[];
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MemoryDocumentResponse = {
|
export type MemoryDocumentResponse = {
|
||||||
body_md: string;
|
body_md: string;
|
||||||
|
|||||||
@@ -132,9 +132,9 @@
|
|||||||
<code>{workspace.workspace_id}</code>
|
<code>{workspace.workspace_id}</code>
|
||||||
{#if workspace.repositories[0]}
|
{#if workspace.repositories[0]}
|
||||||
<span class="workspace-repository-summary">
|
<span class="workspace-repository-summary">
|
||||||
{workspace.repositories[0].name}
|
{workspace.repositories[0].display_name}
|
||||||
<small>
|
<small>
|
||||||
{workspace.repositories[0].default_ref ?? "repository default"} ·
|
{workspace.repositories[0].default_selector ?? "repository default"} ·
|
||||||
{workspace.repositories[0].kind}
|
{workspace.repositories[0].kind}
|
||||||
</small>
|
</small>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,34 +1,51 @@
|
|||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||||
|
import {
|
||||||
|
parseRepositoryListResponse,
|
||||||
|
parseWorkspaceResponse,
|
||||||
|
} from "$lib/workspace/api/workspace-model";
|
||||||
import type { LayoutLoad } from "./$types";
|
import type { LayoutLoad } from "./$types";
|
||||||
import type {
|
|
||||||
RepositoryListResponse,
|
|
||||||
WorkspaceResponse,
|
|
||||||
} from "$lib/workspace/sidebar/types";
|
|
||||||
|
|
||||||
export const load: LayoutLoad = async ({ fetch, params }) => {
|
export const load: LayoutLoad = async ({ fetch, params }) => {
|
||||||
const workspaceId = params.workspaceId;
|
const workspaceId = params.workspaceId;
|
||||||
const [workspace, repositories] = await Promise.all([
|
const [workspaceResult, repositoryResult] = await Promise.all([
|
||||||
loadJson<WorkspaceResponse>(
|
loadJson<unknown>(fetch, workspaceApiPath(workspaceId, "/workspace")),
|
||||||
fetch,
|
loadJson<unknown>(fetch, workspaceApiPath(workspaceId, "/repositories")),
|
||||||
workspaceApiPath(workspaceId, "/workspace"),
|
|
||||||
),
|
|
||||||
loadJson<RepositoryListResponse>(
|
|
||||||
fetch,
|
|
||||||
workspaceApiPath(workspaceId, "/repositories"),
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!workspace.data) {
|
let workspace = null;
|
||||||
|
let workspaceError = workspaceResult.error;
|
||||||
|
if (workspaceResult.data !== null) {
|
||||||
|
try {
|
||||||
|
workspace = parseWorkspaceResponse(workspaceResult.data);
|
||||||
|
} catch (cause) {
|
||||||
|
workspaceError = cause instanceof Error
|
||||||
|
? cause.message
|
||||||
|
: "invalid workspace response";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!workspace) {
|
||||||
error(404, {
|
error(404, {
|
||||||
message: workspace.error ?? `Workspace ${workspaceId} is unavailable`,
|
message: workspaceError ?? `Workspace ${workspaceId} is unavailable`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let repositories = null;
|
||||||
|
let repositoriesError = repositoryResult.error;
|
||||||
|
if (repositoryResult.data !== null) {
|
||||||
|
try {
|
||||||
|
repositories = parseRepositoryListResponse(repositoryResult.data);
|
||||||
|
} catch (cause) {
|
||||||
|
repositoriesError = cause instanceof Error
|
||||||
|
? cause.message
|
||||||
|
: "invalid repository list response";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workspace: workspace.data,
|
workspace,
|
||||||
workspaceError: null,
|
workspaceError: null,
|
||||||
repositories: repositories.data,
|
repositories,
|
||||||
repositoriesError: repositories.error,
|
repositoriesError,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,29 +1,59 @@
|
|||||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||||
import type {
|
import {
|
||||||
RepositoryDetailResponse,
|
parseRepositoryDetailResponse,
|
||||||
RepositoryLogResponse,
|
parseRepositoryLogResponse,
|
||||||
} from "$lib/workspace/sidebar/types";
|
} from "$lib/workspace/api/workspace-model";
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
export const load: PageLoad = async ({ fetch, params }) => {
|
export const load: PageLoad = async ({ fetch, params }) => {
|
||||||
const apiPath = (path: string) => workspaceApiPath(params.workspaceId, path);
|
const workspaceId = params.workspaceId;
|
||||||
const repositoryId = params.repositoryId;
|
const repositoryId = params.repositoryId;
|
||||||
const [repository, log] = await Promise.all([
|
const [repositoryResult, logResult] = await Promise.all([
|
||||||
loadJson<RepositoryDetailResponse>(
|
loadJson<unknown>(
|
||||||
fetch,
|
fetch,
|
||||||
apiPath(`/repositories/${encodeURIComponent(repositoryId)}`),
|
workspaceApiPath(
|
||||||
|
workspaceId,
|
||||||
|
`/repositories/${encodeURIComponent(repositoryId)}`,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
loadJson<RepositoryLogResponse>(
|
loadJson<unknown>(
|
||||||
fetch,
|
fetch,
|
||||||
apiPath(`/repositories/${encodeURIComponent(repositoryId)}/log`),
|
workspaceApiPath(
|
||||||
|
workspaceId,
|
||||||
|
`/repositories/${encodeURIComponent(repositoryId)}/log`,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
let repository = null;
|
||||||
|
let repositoryError = repositoryResult.error;
|
||||||
|
if (repositoryResult.data !== null) {
|
||||||
|
try {
|
||||||
|
repository = parseRepositoryDetailResponse(repositoryResult.data);
|
||||||
|
} catch (cause) {
|
||||||
|
repositoryError = cause instanceof Error
|
||||||
|
? cause.message
|
||||||
|
: "invalid repository detail response";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let log = null;
|
||||||
|
let logError = logResult.error;
|
||||||
|
if (logResult.data !== null) {
|
||||||
|
try {
|
||||||
|
log = parseRepositoryLogResponse(logResult.data);
|
||||||
|
} catch (cause) {
|
||||||
|
logError = cause instanceof Error
|
||||||
|
? cause.message
|
||||||
|
: "invalid repository log response";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
repositoryId,
|
repositoryId,
|
||||||
repository: repository.data,
|
repository,
|
||||||
repositoryError: repository.error,
|
repositoryError,
|
||||||
repositoryLog: log.data,
|
repositoryLog: log,
|
||||||
repositoryLogError: log.error,
|
repositoryLogError: logError,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,21 +55,30 @@ Deno.test("workspace catalog enriches each visible workspace without dropping si
|
|||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
if (url.includes("w-a")) {
|
if (url.includes("w-a")) {
|
||||||
return Promise.resolve(Response.json([{
|
return Promise.resolve(Response.json({
|
||||||
workspace_id: "w-a",
|
workspace_id: "w-a",
|
||||||
repository_id: "main",
|
items: [{
|
||||||
name: "Main",
|
id: "main",
|
||||||
kind: "local_path",
|
display_name: "Main",
|
||||||
uri: "/srv/alpha",
|
kind: "git",
|
||||||
default_ref: "develop",
|
provider: "git",
|
||||||
}]));
|
source: { kind: "local_path", uri: "/srv/alpha" },
|
||||||
|
source_revision: 1,
|
||||||
|
source_fingerprint: "sha256:alpha",
|
||||||
|
observed_status: "ready",
|
||||||
|
default_selector: "develop",
|
||||||
|
record_authority: "workspace-control-plane",
|
||||||
|
}],
|
||||||
|
source: "workspace-control-plane",
|
||||||
|
diagnostics: [],
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
return Promise.resolve(new Response("unavailable", { status: 503 }));
|
return Promise.resolve(new Response("unavailable", { status: 503 }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const items = await loadWorkspaceCatalog(fetcher as typeof fetch);
|
const items = await loadWorkspaceCatalog(fetcher as typeof fetch);
|
||||||
assertEquals(items.length, 2);
|
assertEquals(items.length, 2);
|
||||||
assertEquals(items[0].repositories[0].repository_id, "main");
|
assertEquals(items[0].repositories[0].id, "main");
|
||||||
assertEquals(items[1].repositories, []);
|
assertEquals(items[1].repositories, []);
|
||||||
assertEquals(typeof items[1].repository_error, "string");
|
assertEquals(typeof items[1].repository_error, "string");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
declare const Deno: {
|
||||||
|
test(name: string, fn: () => void | Promise<void>): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
import {
|
||||||
|
parseRepositoryListResponse,
|
||||||
|
parseWorkspaceResponse,
|
||||||
|
} from "../src/lib/workspace/api/workspace-model.ts";
|
||||||
|
|
||||||
|
function assertThrows(operation: () => unknown, expected: string): void {
|
||||||
|
try {
|
||||||
|
operation();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message.includes(expected)) return;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new Error("expected operation to throw");
|
||||||
|
}
|
||||||
|
|
||||||
|
const repositoryList = {
|
||||||
|
workspace_id: "w-a",
|
||||||
|
items: [{
|
||||||
|
id: "main",
|
||||||
|
display_name: "Main",
|
||||||
|
kind: "git",
|
||||||
|
provider: "git",
|
||||||
|
source: { kind: "local_path", uri: "/srv/alpha" },
|
||||||
|
source_revision: 1,
|
||||||
|
source_fingerprint: "sha256:alpha",
|
||||||
|
observed_status: "ready",
|
||||||
|
record_authority: "workspace-control-plane",
|
||||||
|
}],
|
||||||
|
source: "workspace-control-plane",
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
Deno.test("generated repository wrapper validates current Backend JSON", () => {
|
||||||
|
const parsed = parseRepositoryListResponse(repositoryList);
|
||||||
|
if (parsed.items[0]?.id !== "main") {
|
||||||
|
throw new Error("repository id was not preserved");
|
||||||
|
}
|
||||||
|
if (parsed.items[0]?.source.kind !== "local_path") {
|
||||||
|
throw new Error("repository source kind was not preserved");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("stale repository aliases fail closed at the JSON boundary", () => {
|
||||||
|
const stale = structuredClone(repositoryList) as Record<string, unknown>;
|
||||||
|
const items = stale.items as Array<Record<string, unknown>>;
|
||||||
|
items[0].repository_id = items[0].id;
|
||||||
|
delete items[0].id;
|
||||||
|
assertThrows(
|
||||||
|
() => parseRepositoryListResponse(stale),
|
||||||
|
".repository_id is not part",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("workspace response requires the permission projection", () => {
|
||||||
|
const stale = {
|
||||||
|
workspace_id: "w-a",
|
||||||
|
display_name: "Alpha",
|
||||||
|
record_authority: "workspace-control-plane",
|
||||||
|
schema_version: 46,
|
||||||
|
auth: {
|
||||||
|
Passkey: {
|
||||||
|
rp_id: "example.test",
|
||||||
|
origin: "https://example.test",
|
||||||
|
public_base_url: "https://example.test",
|
||||||
|
cookie_name: "yoi_session",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extension_points: {
|
||||||
|
store: "sqlite",
|
||||||
|
event_stream: { status: "available", note: "ready", diagnostics: [] },
|
||||||
|
host_worker_bridge: {
|
||||||
|
status: "available",
|
||||||
|
note: "ready",
|
||||||
|
diagnostics: [],
|
||||||
|
},
|
||||||
|
companion_console: {
|
||||||
|
status: "available",
|
||||||
|
note: "ready",
|
||||||
|
diagnostics: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assertThrows(
|
||||||
|
() => parseWorkspaceResponse(stale),
|
||||||
|
"permissions must be an object",
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user