diff --git a/lib/services/__tests__/platform.test.ts b/lib/services/__tests__/platform.test.ts new file mode 100644 index 0000000..012e707 --- /dev/null +++ b/lib/services/__tests__/platform.test.ts @@ -0,0 +1,182 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import api from "@/lib/api"; +import { + getApplication, + listApplications, + listEnvironments, + listWorkspaces, +} from "@/lib/services/platform"; +import type { + Application, + ApplicationListResponse, + EnvironmentListResponse, + WorkspaceListResponse, +} from "@/lib/types/platform"; + +vi.mock("@/lib/api", () => ({ + default: { + get: vi.fn(), + }, +})); + +const mockedGet = vi.mocked(api.get); + +describe("platform read client", () => { + beforeEach(() => { + mockedGet.mockReset(); + }); + + it("lists workspaces with cursor pagination", async () => { + const data: WorkspaceListResponse = { + items: [ + { + id: "workspace-1", + name: "Platform", + description: "Production workspace", + role: "owner", + createdAt: "2026-09-01T10:00:00Z", + updatedAt: "2026-09-02T10:00:00Z", + }, + ], + pageInfo: { + nextCursor: "workspace-cursor-2", + hasMore: true, + }, + }; + mockedGet.mockResolvedValueOnce({ data } as never); + + await expect( + listWorkspaces({ cursor: "workspace-cursor-1", limit: 25 }) + ).resolves.toEqual(data); + + expect(mockedGet).toHaveBeenCalledWith("/workspaces", { + params: { + cursor: "workspace-cursor-1", + limit: 25, + }, + }); + }); + + it("omits pagination query parameters that are not supplied", async () => { + const data: WorkspaceListResponse = { + items: [], + pageInfo: { + nextCursor: null, + hasMore: false, + }, + }; + mockedGet.mockResolvedValueOnce({ data } as never); + + await listWorkspaces(); + + expect(mockedGet).toHaveBeenCalledWith("/workspaces", { + params: {}, + }); + }); + + it("lists workspace environments using the supplied workspace id", async () => { + const data: EnvironmentListResponse = { + items: [ + { + id: "environment-1", + workspaceId: "workspace-1", + name: "Production", + createdAt: "2026-09-01T10:00:00Z", + updatedAt: "2026-09-02T10:00:00Z", + }, + ], + pageInfo: { + nextCursor: null, + hasMore: false, + }, + }; + mockedGet.mockResolvedValueOnce({ data } as never); + + await expect( + listEnvironments("workspace-1", { limit: 10 }) + ).resolves.toEqual(data); + + expect(mockedGet).toHaveBeenCalledWith( + "/workspaces/workspace-1/environments", + { + params: { + limit: 10, + }, + } + ); + }); + + it("serializes application filters only when they are supplied", async () => { + const data: ApplicationListResponse = { + items: [], + pageInfo: { + nextCursor: null, + hasMore: false, + }, + }; + mockedGet.mockResolvedValueOnce({ data } as never); + + await listApplications("workspace-1", { + cursor: "application-cursor", + limit: 50, + environmentId: "environment-1", + includeArchived: false, + }); + + expect(mockedGet).toHaveBeenCalledWith( + "/workspaces/workspace-1/applications", + { + params: { + cursor: "application-cursor", + limit: 50, + environmentId: "environment-1", + includeArchived: false, + }, + } + ); + + mockedGet.mockResolvedValueOnce({ data } as never); + + await listApplications("workspace-1"); + + expect(mockedGet).toHaveBeenLastCalledWith( + "/workspaces/workspace-1/applications", + { + params: {}, + } + ); + }); + + it("gets one application and returns the Core response shape unchanged", async () => { + const data: Application = { + id: "application-1", + workspaceId: "workspace-1", + environmentId: "environment-1", + name: "checkout-api", + description: "Checkout service", + desiredState: { + image: "example/checkout:v1", + replicas: 3, + }, + status: "draft", + createdAt: "2026-09-01T10:00:00Z", + updatedAt: "2026-09-02T10:00:00Z", + }; + mockedGet.mockResolvedValueOnce({ data } as never); + + await expect( + getApplication("workspace-1", "application-1") + ).resolves.toEqual(data); + + expect(mockedGet).toHaveBeenCalledWith( + "/workspaces/workspace-1/applications/application-1" + ); + }); + + it("lets shared-client request failures reject to the caller", async () => { + const error = new Error("request failed"); + mockedGet.mockRejectedValueOnce(error); + + await expect(listWorkspaces()).rejects.toBe(error); + }); +}); diff --git a/lib/services/platform.ts b/lib/services/platform.ts new file mode 100644 index 0000000..8f407e1 --- /dev/null +++ b/lib/services/platform.ts @@ -0,0 +1,67 @@ +import api from "@/lib/api"; +import type { + Application, + ApplicationListQuery, + ApplicationListResponse, + EnvironmentListResponse, + PaginationQuery, + WorkspaceListResponse, +} from "@/lib/types/platform"; + +function paginationParams(query: PaginationQuery) { + return { + ...(query.cursor !== undefined ? { cursor: query.cursor } : {}), + ...(query.limit !== undefined ? { limit: query.limit } : {}), + }; +} + +export async function listWorkspaces( + query: PaginationQuery = {} +): Promise { + const response = await api.get("/workspaces", { + params: paginationParams(query), + }); + return response.data; +} + +export async function listEnvironments( + workspaceId: string, + query: PaginationQuery = {} +): Promise { + const response = await api.get( + `/workspaces/${workspaceId}/environments`, + { params: paginationParams(query) } + ); + return response.data; +} + +export async function listApplications( + workspaceId: string, + query: ApplicationListQuery = {} +): Promise { + const response = await api.get( + `/workspaces/${workspaceId}/applications`, + { + params: { + ...paginationParams(query), + ...(query.environmentId !== undefined + ? { environmentId: query.environmentId } + : {}), + ...(query.includeArchived !== undefined + ? { includeArchived: query.includeArchived } + : {}), + }, + } + ); + return response.data; +} + +export async function getApplication( + workspaceId: string, + applicationId: string +): Promise { + const response = await api.get( + `/workspaces/${workspaceId}/applications/${applicationId}` + ); + return response.data; +} diff --git a/lib/types/platform.ts b/lib/types/platform.ts new file mode 100644 index 0000000..18e963d --- /dev/null +++ b/lib/types/platform.ts @@ -0,0 +1,64 @@ +export type MembershipRole = "owner" | "admin" | "member"; + +export interface PageInfo { + nextCursor: string | null; + hasMore: boolean; +} + +export interface Workspace { + id: string; + name: string; + description?: string; + role: MembershipRole; + createdAt: string; + updatedAt: string; +} + +export interface WorkspaceListResponse { + items: Workspace[]; + pageInfo: PageInfo; +} + +export interface Environment { + id: string; + workspaceId: string; + name: string; + description?: string; + createdAt: string; + updatedAt: string; +} + +export interface EnvironmentListResponse { + items: Environment[]; + pageInfo: PageInfo; +} + +export type ApplicationStatus = "draft" | "archived"; + +export interface Application { + id: string; + workspaceId: string; + environmentId: string; + name: string; + description?: string; + desiredState: Record; + status: ApplicationStatus; + archivedAt?: string; + createdAt: string; + updatedAt: string; +} + +export interface ApplicationListResponse { + items: Application[]; + pageInfo: PageInfo; +} + +export interface PaginationQuery { + cursor?: string; + limit?: number; +} + +export interface ApplicationListQuery extends PaginationQuery { + environmentId?: string; + includeArchived?: boolean; +}