fix(web): register the drifted document endpoints in the api contract

The endpoint map and validation schemas have fallen behind what the
web app actually exchanges with the API:

- the memories grid and the X-bookmarks status query filter
  @post/documents/documents by `categories`, a field
  DocumentsWithMemoriesQuerySchema doesn't declare — both smuggle it
  past client validation with disableValidation, and any future caller
  that forgets the flag gets the key silently stripped and a filter
  that no-ops
- @post/documents/documents/facets was never registered:
  DocumentFacetsQuerySchema sits unused in the validation package
  while the grid calls the endpoint as an untyped raw URL and
  hand-casts the response
- @patch/documents/:id was never registered even though
  MemoryUpdateSchema exists for exactly this pairing; the
  document-edit and file-metadata mutations PATCH via template-literal
  URLs with no request/response typing

Add `categories` to the documents query schema, introduce
DocumentFacetSchema/DocumentFacetsResponseSchema (total optional — the
UI already guards its absence) and register the facets endpoint with
the previously-dead query schema, register @patch/documents/:id with
MemoryUpdateSchema/MemoryResponseSchema/params, and convert both PATCH
call sites to the typed route.

The existing disableValidation flags are deliberately left in place:
every current caller of @post/documents/documents sets one, so output
validation has never been exercised against production responses;
removing them is a follow-up that needs verification against the live
API. With the schemas now truthful, that cleanup becomes mechanical.

Tested: bun test in packages/validation (12 pass, 4 new), tsc --noEmit
on packages/validation, bun test + bun run build in apps/web, biome
clean on all touched files.
This commit is contained in:
abhay-codes07 2026-07-13 17:48:31 +05:30
parent 2cebe81512
commit ee1d50b13d
No known key found for this signature in database
4 changed files with 86 additions and 3 deletions

View file

@ -521,7 +521,8 @@ export function useDocumentMutations({
const data = (await response.json()) as { id: string }
if (applyMeta && (title || description)) {
await $fetch(`@patch/documents/${data.id}`, {
await $fetch("@patch/documents/:id", {
params: { id: data.id },
body: {
metadata: {
...(title && { title }),
@ -642,7 +643,8 @@ export function useDocumentMutations({
documentId: string
content: string
}) => {
const response = await $fetch(`@patch/documents/${documentId}`, {
const response = await $fetch("@patch/documents/:id", {
params: { id: documentId },
body: {
content,
metadata: { sm_source: "consumer" },

View file

@ -11,6 +11,8 @@ import {
CreateProjectSchema,
DeleteProjectResponseSchema,
DeleteProjectSchema,
DocumentFacetsQuerySchema,
DocumentFacetsResponseSchema,
DocumentsWithMemoriesQuerySchema,
DocumentsWithMemoriesResponseSchema,
ListContainerTagsResponseSchema,
@ -18,6 +20,7 @@ import {
ListProjectsResponseSchema,
MemoryAddSchema,
MemoryResponseSchema,
MemoryUpdateSchema,
MigrateMCPRequestSchema,
MigrateMCPResponseSchema,
ProcessingDocumentsResponseSchema,
@ -265,6 +268,10 @@ export const apiSchema = createSchema({
input: DocumentsWithMemoriesQuerySchema,
output: DocumentsWithMemoriesResponseSchema,
},
"@post/documents/documents/facets": {
input: DocumentFacetsQuerySchema,
output: DocumentFacetsResponseSchema,
},
"@post/documents/documents/by-ids": {
input: z.object({
ids: z.array(z.string()),
@ -291,6 +298,13 @@ export const apiSchema = createSchema({
output: z.any(),
},
// Update a memory
"@patch/documents/:id": {
input: MemoryUpdateSchema,
output: MemoryResponseSchema,
params: z.object({ id: z.string() }),
},
// Delete a memory
"@delete/documents/:id": {
output: z.any(), // 204 No-Content

View file

@ -1,6 +1,11 @@
import { describe, expect, it } from "bun:test"
import { readFileSync } from "node:fs"
import { SearchRequestSchema, Searchv4RequestSchema } from "./api"
import {
DocumentFacetsResponseSchema,
DocumentsWithMemoriesQuerySchema,
SearchRequestSchema,
Searchv4RequestSchema,
} from "./api"
describe("search threshold schemas", () => {
it("do not contain redundant number transforms or unreachable range guards", () => {
@ -80,3 +85,31 @@ describe("search threshold schemas", () => {
).toBe(false)
})
})
describe("documents contract schemas", () => {
it("DocumentsWithMemoriesQuerySchema accepts the categories filter", () => {
const parsed = DocumentsWithMemoriesQuerySchema.parse({
categories: ["webpage", "tweet"],
})
expect(parsed.categories).toEqual(["webpage", "tweet"])
})
it("DocumentsWithMemoriesQuerySchema leaves categories undefined when omitted", () => {
expect(
DocumentsWithMemoriesQuerySchema.parse({}).categories,
).toBeUndefined()
})
it("DocumentFacetsResponseSchema parses a facets payload", () => {
const parsed = DocumentFacetsResponseSchema.parse({
facets: [{ category: "webpage", count: 42, label: "Web pages" }],
total: 42,
})
expect(parsed.facets[0]?.category).toBe("webpage")
})
it("DocumentFacetsResponseSchema tolerates a missing total", () => {
const parsed = DocumentFacetsResponseSchema.parse({ facets: [] })
expect(parsed.total).toBeUndefined()
})
})

View file

@ -1100,6 +1100,13 @@ export const DocumentsWithMemoriesQuerySchema = z
description: "Number of items per page",
example: 10,
}),
categories: z
.array(z.string())
.optional()
.openapi({
description: "Optional document categories to filter by",
example: ["webpage", "tweet"],
}),
sort: z.enum(["createdAt", "updatedAt"]).default("createdAt").openapi({
description: "Field to sort by",
example: "createdAt",
@ -1134,6 +1141,33 @@ export const DocumentFacetsQuerySchema = z
description: "Query parameters for getting document facets",
})
export const DocumentFacetSchema = z
.object({
category: z.string().openapi({
description: "Document category identifier",
example: "webpage",
}),
count: z.number().openapi({
description: "Number of documents in this category",
example: 42,
}),
label: z.string().openapi({
description: "Human-readable category label",
example: "Web pages",
}),
})
.openapi({ description: "A single document-category facet" })
export const DocumentFacetsResponseSchema = z
.object({
facets: z.array(DocumentFacetSchema),
total: z.number().optional().openapi({
description: "Total number of documents across all categories",
example: 120,
}),
})
.openapi({ description: "Document category facets" })
export const MigrateMCPRequestSchema = z
.object({
userId: z.string().openapi({