diff --git a/apps/server/openapi.json b/apps/server/openapi.json index dc6b280..72b023b 100644 --- a/apps/server/openapi.json +++ b/apps/server/openapi.json @@ -632,7 +632,7 @@ } }, "/v1/ledgers/{ledgerId}/availability": { - "get": { + "post": { "tags": ["Availability"], "summary": "Query resource availability", "description": "Returns a timeline of free/busy blocks for the specified resources within the given time window. Overlapping and adjacent allocations are merged into single busy blocks.", @@ -644,46 +644,40 @@ "required": true, "name": "ledgerId", "in": "path" - }, - { - "schema": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Resource IDs to query (can be repeated)", - "example": ["rsc_01abc123def456ghi789jkl012"] - }, - "required": true, - "description": "Resource IDs to query (can be repeated)", - "name": "resourceIds", - "in": "query" - }, - { - "schema": { - "type": "string", - "format": "date-time", - "description": "Start of the time window (ISO 8601)", - "example": "2026-01-04T10:00:00Z" - }, - "required": true, - "description": "Start of the time window (ISO 8601)", - "name": "startAt", - "in": "query" - }, - { - "schema": { - "type": "string", - "format": "date-time", - "description": "End of the time window (ISO 8601)", - "example": "2026-01-04T18:00:00Z" - }, - "required": true, - "description": "End of the time window (ISO 8601)", - "name": "endAt", - "in": "query" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Resource IDs to query", + "example": ["rsc_01abc123def456ghi789jkl012"] + }, + "startAt": { + "type": "string", + "format": "date-time", + "description": "Start of the time window (ISO 8601)", + "example": "2026-01-04T10:00:00Z" + }, + "endAt": { + "type": "string", + "format": "date-time", + "description": "End of the time window (ISO 8601)", + "example": "2026-01-04T18:00:00Z" + } + }, + "required": ["resourceIds", "startAt", "endAt"] + } + } + } + }, "responses": { "200": { "description": "Availability timeline for each resource", diff --git a/apps/server/src/routes/v1/availability.ts b/apps/server/src/routes/v1/availability.ts index 53140ea..8d92bb4 100644 --- a/apps/server/src/routes/v1/availability.ts +++ b/apps/server/src/routes/v1/availability.ts @@ -2,17 +2,15 @@ import { Hono } from "hono"; import { services } from "../../services/index.js"; // Nested under /v1/ledgers/:ledgerId/availability -export const availability = new Hono().get("/", async (c) => { +export const availability = new Hono().post("/", async (c) => { const ledgerId = c.req.param("ledgerId")!; - const resourceIds = c.req.queries("resourceIds") || []; - const startAt = c.req.query("startAt")!; - const endAt = c.req.query("endAt")!; + const body = await c.req.json(); const result = await services.availability.query({ ledgerId, - resourceIds, - startAt, - endAt, + resourceIds: body.resourceIds, + startAt: body.startAt, + endAt: body.endAt, }); return c.json({ data: result.items }); diff --git a/apps/server/src/scripts/generate-openapi.ts b/apps/server/src/scripts/generate-openapi.ts index 4191177..3874da3 100644 --- a/apps/server/src/scripts/generate-openapi.ts +++ b/apps/server/src/scripts/generate-openapi.ts @@ -153,7 +153,7 @@ registry.registerPath({ // Availability routes registry.registerPath({ - method: "get", + method: "post", path: "/v1/ledgers/{ledgerId}/availability", tags: ["Availability"], summary: "Query resource availability", @@ -162,20 +162,26 @@ registry.registerPath({ "Overlapping and adjacent allocations are merged into single busy blocks.", request: { params: z.object({ ledgerId: z.string() }), - query: z.object({ - resourceIds: z.array(z.string()).openapi({ - description: "Resource IDs to query (can be repeated)", - example: ["rsc_01abc123def456ghi789jkl012"], - }), - startAt: z.string().datetime().openapi({ - description: "Start of the time window (ISO 8601)", - example: "2026-01-04T10:00:00Z", - }), - endAt: z.string().datetime().openapi({ - description: "End of the time window (ISO 8601)", - example: "2026-01-04T18:00:00Z", - }), - }), + body: { + content: { + "application/json": { + schema: z.object({ + resourceIds: z.array(z.string()).openapi({ + description: "Resource IDs to query", + example: ["rsc_01abc123def456ghi789jkl012"], + }), + startAt: z.string().datetime().openapi({ + description: "Start of the time window (ISO 8601)", + example: "2026-01-04T10:00:00Z", + }), + endAt: z.string().datetime().openapi({ + description: "End of the time window (ISO 8601)", + example: "2026-01-04T18:00:00Z", + }), + }), + }, + }, + }, }, responses: { 200: { diff --git a/apps/server/test/v1/availability/query.spec.ts b/apps/server/test/v1/availability/query.spec.ts index 539cd8b..89d727b 100644 --- a/apps/server/test/v1/availability/query.spec.ts +++ b/apps/server/test/v1/availability/query.spec.ts @@ -3,7 +3,7 @@ import { client } from "../../setup/client"; import { createAllocation, createLedger, createResource } from "../../setup/factories"; import type { AvailabilityResponse } from "../../setup/types"; -describe("GET /v1/ledgers/:ledgerId/availability", () => { +describe("POST /v1/ledgers/:ledgerId/availability", () => { it("returns full window as free when no allocations", async () => { const { ledger } = await createLedger(); const { resource } = await createResource({ ledgerId: ledger.id }); @@ -11,9 +11,11 @@ describe("GET /v1/ledgers/:ledgerId/availability", () => { const startAt = "2026-01-01T10:00:00.000Z"; const endAt = "2026-01-01T12:00:00.000Z"; - const response = await client.get( - `/v1/ledgers/${ledger.id}/availability?resourceIds=${resource.id}&startAt=${startAt}&endAt=${endAt}`, - ); + const response = await client.post(`/v1/ledgers/${ledger.id}/availability`, { + resourceIds: [resource.id], + startAt, + endAt, + }); expect(response.status).toBe(200); const { data } = (await response.json()) as AvailabilityResponse; @@ -41,9 +43,11 @@ describe("GET /v1/ledgers/:ledgerId/availability", () => { const startAt = "2026-01-01T10:00:00.000Z"; const endAt = "2026-01-01T12:00:00.000Z"; - const response = await client.get( - `/v1/ledgers/${ledger.id}/availability?resourceIds=${resource.id}&startAt=${startAt}&endAt=${endAt}`, - ); + const response = await client.post(`/v1/ledgers/${ledger.id}/availability`, { + resourceIds: [resource.id], + startAt, + endAt, + }); expect(response.status).toBe(200); const { data } = (await response.json()) as AvailabilityResponse; @@ -75,9 +79,11 @@ describe("GET /v1/ledgers/:ledgerId/availability", () => { const startAt = "2026-01-01T10:00:00.000Z"; const endAt = "2026-01-01T12:00:00.000Z"; - const response = await client.get( - `/v1/ledgers/${ledger.id}/availability?resourceIds=${resource.id}&startAt=${startAt}&endAt=${endAt}`, - ); + const response = await client.post(`/v1/ledgers/${ledger.id}/availability`, { + resourceIds: [resource.id], + startAt, + endAt, + }); expect(response.status).toBe(200); const { data } = (await response.json()) as AvailabilityResponse; @@ -106,9 +112,11 @@ describe("GET /v1/ledgers/:ledgerId/availability", () => { const startAt = "2026-01-01T10:00:00.000Z"; const endAt = "2026-01-01T12:00:00.000Z"; - const response = await client.get( - `/v1/ledgers/${ledger.id}/availability?resourceIds=${resource.id}&startAt=${startAt}&endAt=${endAt}`, - ); + const response = await client.post(`/v1/ledgers/${ledger.id}/availability`, { + resourceIds: [resource.id], + startAt, + endAt, + }); expect(response.status).toBe(200); const { data } = (await response.json()) as AvailabilityResponse; @@ -132,9 +140,11 @@ describe("GET /v1/ledgers/:ledgerId/availability", () => { const startAt = "2026-01-01T10:00:00.000Z"; const endAt = "2026-01-01T12:00:00.000Z"; - const response = await client.get( - `/v1/ledgers/${ledger.id}/availability?resourceIds=${resource.id}&startAt=${startAt}&endAt=${endAt}`, - ); + const response = await client.post(`/v1/ledgers/${ledger.id}/availability`, { + resourceIds: [resource.id], + startAt, + endAt, + }); expect(response.status).toBe(200); const { data } = (await response.json()) as AvailabilityResponse; @@ -165,9 +175,11 @@ describe("GET /v1/ledgers/:ledgerId/availability", () => { const startAt = "2026-01-01T10:00:00.000Z"; const endAt = "2026-01-01T12:00:00.000Z"; - const response = await client.get( - `/v1/ledgers/${ledger.id}/availability?resourceIds=${resource.id}&startAt=${startAt}&endAt=${endAt}`, - ); + const response = await client.post(`/v1/ledgers/${ledger.id}/availability`, { + resourceIds: [resource.id], + startAt, + endAt, + }); expect(response.status).toBe(200); const { data } = (await response.json()) as AvailabilityResponse; @@ -203,9 +215,11 @@ describe("GET /v1/ledgers/:ledgerId/availability", () => { const startAt = "2026-01-01T10:00:00.000Z"; const endAt = "2026-01-01T12:00:00.000Z"; - const response = await client.get( - `/v1/ledgers/${ledger.id}/availability?resourceIds=${resource.id}&startAt=${startAt}&endAt=${endAt}`, - ); + const response = await client.post(`/v1/ledgers/${ledger.id}/availability`, { + resourceIds: [resource.id], + startAt, + endAt, + }); expect(response.status).toBe(200); const { data } = (await response.json()) as AvailabilityResponse; @@ -234,9 +248,11 @@ describe("GET /v1/ledgers/:ledgerId/availability", () => { const startAt = "2026-01-01T10:00:00.000Z"; const endAt = "2026-01-01T12:00:00.000Z"; - const response = await client.get( - `/v1/ledgers/${ledger.id}/availability?resourceIds=${resource.id}&startAt=${startAt}&endAt=${endAt}`, - ); + const response = await client.post(`/v1/ledgers/${ledger.id}/availability`, { + resourceIds: [resource.id], + startAt, + endAt, + }); expect(response.status).toBe(200); const { data } = (await response.json()) as AvailabilityResponse; @@ -262,9 +278,11 @@ describe("GET /v1/ledgers/:ledgerId/availability", () => { const startAt = "2026-01-01T10:00:00.000Z"; const endAt = "2026-01-01T12:00:00.000Z"; - const response = await client.get( - `/v1/ledgers/${ledger.id}/availability?resourceIds=${resource1.id}&resourceIds=${resource2.id}&startAt=${startAt}&endAt=${endAt}`, - ); + const response = await client.post(`/v1/ledgers/${ledger.id}/availability`, { + resourceIds: [resource1.id, resource2.id], + startAt, + endAt, + }); expect(response.status).toBe(200); const { data } = (await response.json()) as AvailabilityResponse; @@ -296,9 +314,11 @@ describe("GET /v1/ledgers/:ledgerId/availability", () => { const startAt = "2026-01-01T10:00:00.000Z"; const endAt = "2026-01-01T12:00:00.000Z"; - const response = await client.get( - `/v1/ledgers/${ledger.id}/availability?resourceIds=${resource.id}&startAt=${startAt}&endAt=${endAt}`, - ); + const response = await client.post(`/v1/ledgers/${ledger.id}/availability`, { + resourceIds: [resource.id], + startAt, + endAt, + }); expect(response.status).toBe(200); const { data } = (await response.json()) as AvailabilityResponse; diff --git a/docs/availability.md b/docs/availability.md index 3d5c390..ab8e152 100644 --- a/docs/availability.md +++ b/docs/availability.md @@ -5,7 +5,13 @@ Query free/busy timelines for resources before creating allocations. ## Query availability ```bash -curl "$FLOYD_BASE_URL/v1/ledgers/$LEDGER_ID/availability?resourceIds=$RESOURCE_ID&startAt=2026-01-04T10:00:00Z&endAt=2026-01-04T18:00:00Z" +curl -X POST "$FLOYD_BASE_URL/v1/ledgers/$LEDGER_ID/availability" \ + -H "Content-Type: application/json" \ + -d '{ + "resourceIds": ["rsc_01abc123def456ghi789jkl012"], + "startAt": "2026-01-04T10:00:00Z", + "endAt": "2026-01-04T18:00:00Z" + }' ``` Response: @@ -42,7 +48,13 @@ Response: Query multiple resources in a single request: ```bash -curl "$FLOYD_BASE_URL/v1/ledgers/$LEDGER_ID/availability?resourceIds=$RESOURCE_1&resourceIds=$RESOURCE_2&startAt=2026-01-04T10:00:00Z&endAt=2026-01-04T18:00:00Z" +curl -X POST "$FLOYD_BASE_URL/v1/ledgers/$LEDGER_ID/availability" \ + -H "Content-Type: application/json" \ + -d '{ + "resourceIds": ["rsc_resource1", "rsc_resource2"], + "startAt": "2026-01-04T10:00:00Z", + "endAt": "2026-01-04T18:00:00Z" + }' ``` Each resource gets its own timeline in the response. @@ -69,9 +81,11 @@ Expired holds and cancelled allocations do **not** block time. 3. Create a hold on the chosen slot ```javascript -const { data } = await fetch(`${baseUrl}/v1/ledgers/${ledgerId}/availability?...`).then((r) => - r.json(), -); +const { data } = await fetch(`${baseUrl}/v1/ledgers/${ledgerId}/availability`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ resourceIds, startAt, endAt }), +}).then((r) => r.json()); const freeSlots = data[0].timeline.filter((block) => block.status === "free"); const suitableSlot = freeSlots.find((slot) => {