dorfteich/packages/shared/src/public-api.ts
Claude Fable 5 ffcc337ed0
Some checks failed
CD / Build and push images (push) Successful in 3m57s
CD / Deploy to Test (push) Successful in 9s
CI / Lint, typecheck, test (push) Failing after 4m16s
CI / Auth e2e pack (push) Has been skipped
CI / Import/export fidelity gate (push) Has been skipped
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m19s
CD / Promote to Int (push) Successful in 11s
Page-tree parity for the public REST API and MCP (#110)
The slug-based machine surfaces now see and shape the hierarchy:

- REST: page list/detail carry parent (the parent page's slug, nulled
  when the token's user may not read it — same no-leak rule as the
  internal list); create accepts parent; PATCH accepts parent
  (slug nests, null moves to the top level, appended at the end of the
  new sibling group via the new PagesService.moveToEnd). Cycle/depth
  refusals keep their regular error codes. OpenAPI updated.
- MCP: list_pages returns parent, create_page takes an optional parent
  slug, update_page moves with parent (slug|null); tool errors carry
  the api code (page_cycle covered in the e2e pack).
- ZIP export deliberately stays flat — noted in features.md; the
  hierarchy is organizational only.

e2e: REST pack covers nested create, list shape, move/root-move, 409
page_cycle, 404 unknown parent; MCP pack covers nested create, list
parent, and the cycle tool error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 10:49:52 +02:00

113 lines
3.8 KiB
TypeScript

import { z } from 'zod';
import type { ApiTokenScope } from './api-tokens';
import type { CommentView } from './comments';
import type { LabelTreeNode, LabelView } from './labels';
/**
* Wire types of the public REST API (`/api/public/v1`, issue #104). The
* shapes are deliberately independent of the internal views: this surface
* is versioned and consumed by scripts and MCP clients, so it exposes
* slugs and stable ids, never internal implementation details.
*/
export interface PublicMeView {
user: { id: string; username: string; displayName: string };
scope: ApiTokenScope;
/** Pond restriction (slugs); empty = every pond the user may access. */
pondSlugs: string[];
}
export interface PublicPondView {
slug: string;
name: string;
description: string;
type: 'personal' | 'shared';
createdAt: string;
}
export interface PublicPageListItemView {
slug: string;
title: string;
/** Parent page slug in the tree (issue #110), or null at the root — nulled
* as well when the token's user may not read the parent (no existence leak). */
parent: string | null;
labels: string[];
createdAt: string;
updatedAt: string;
}
export interface PublicPageView {
slug: string;
title: string;
pondSlug: string;
/** Parent page slug (issue #110); see {@link PublicPageListItemView.parent}. */
parent: string | null;
markdown: string;
html: string;
labels: string[];
createdAt: string;
updatedAt: string;
}
/** Bounded well above every realistic page (the editor caps documents far
* lower); the limit only stops abuse of the raw endpoint. */
const MARKDOWN_MAX_BYTES = 2 * 1024 * 1024;
export const publicCreatePageInputSchema = z.object({
title: z.string().trim().min(1, 'validation.required').max(200, 'validation.tooLong'),
markdown: z.string().max(MARKDOWN_MAX_BYTES).default(''),
/** Parent page slug (issue #110) — nests the new page under it. */
parent: z.string().min(1).optional(),
});
export type PublicCreatePageInput = z.infer<typeof publicCreatePageInputSchema>;
export const publicUpdatePageInputSchema = z
.object({
title: z.string().trim().min(1, 'validation.required').max(200, 'validation.tooLong'),
/** Replace semantics: the whole content becomes this Markdown. */
markdown: z.string().max(MARKDOWN_MAX_BYTES),
/** Move in the tree (issue #110): a page slug nests, `null` moves to the
* root; the page lands at the end of its new sibling group. */
parent: z.string().min(1).nullable(),
})
.partial()
.refine(
(input) =>
input.title !== undefined || input.markdown !== undefined || input.parent !== undefined,
{ message: 'validation.required' },
);
export type PublicUpdatePageInput = z.infer<typeof publicUpdatePageInputSchema>;
/**
* One PATCH covers rename, recolour, and move (the internal API splits
* update and move); `parentId: null` moves the label to the root.
*/
export const publicUpdateLabelInputSchema = z
.object({
name: z.string().trim().min(1, 'validation.required').max(60, 'validation.tooLong'),
color: z.string().regex(/^#[0-9a-fA-F]{6}$/, 'validation.invalid'),
parentId: z.string().min(1).nullable(),
})
.partial();
export type PublicUpdateLabelInput = z.infer<typeof publicUpdateLabelInputSchema>;
export const publicSearchQuerySchema = z.object({
q: z.string().trim().min(1, 'validation.required').max(200),
pond: z.string().trim().optional(),
label: z.string().trim().optional(),
});
export type PublicSearchQuery = z.infer<typeof publicSearchQuerySchema>;
export interface PublicSearchResultView {
pondSlug: string;
pageSlug: string;
title: string;
snippet: string;
}
/** Re-exported internal shapes the public surface serves verbatim. */
export type PublicLabelTree = LabelTreeNode[];
export type PublicLabelView = LabelView;
export type PublicCommentView = CommentView;