@ -0,0 +1,5 @@
|
|||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pages_pond_id_created_at_idx" ON "pages"("pond_id", "created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pages_pond_id_updated_at_idx" ON "pages"("pond_id", "updated_at");
|
||||||
@ -291,6 +291,10 @@ model Page {
|
|||||||
@@unique([pondId, slug])
|
@@unique([pondId, slug])
|
||||||
@@index([pondId])
|
@@index([pondId])
|
||||||
@@index([parentId])
|
@@index([parentId])
|
||||||
|
// Time-filtered listings (issue #148): "pages of this pond created/updated
|
||||||
|
// since X" hit these instead of scanning the pond.
|
||||||
|
@@index([pondId, createdAt])
|
||||||
|
@@index([pondId, updatedAt])
|
||||||
@@map("pages")
|
@@map("pages")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,9 +12,21 @@ import { z } from 'zod';
|
|||||||
const pond = z.string().min(1);
|
const pond = z.string().min(1);
|
||||||
const page = z.string().min(1);
|
const page = z.string().min(1);
|
||||||
|
|
||||||
|
/** ISO 8601 instant for `…_since` filters (issue #148). */
|
||||||
|
const sinceInstant = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.regex(/^\d{4}-\d{2}-\d{2}([T ].+)?$/)
|
||||||
|
.refine((value) => !Number.isNaN(Date.parse(value)))
|
||||||
|
.transform((value) => new Date(value));
|
||||||
|
|
||||||
export const MCP_TOOL_INPUTS = {
|
export const MCP_TOOL_INPUTS = {
|
||||||
list_ponds: z.object({}),
|
list_ponds: z.object({}),
|
||||||
list_pages: z.object({ pond }),
|
list_pages: z.object({
|
||||||
|
pond,
|
||||||
|
created_since: sinceInstant.optional(),
|
||||||
|
updated_since: sinceInstant.optional(),
|
||||||
|
}),
|
||||||
read_page: z.object({ pond, page }),
|
read_page: z.object({ pond, page }),
|
||||||
search: z.object({
|
search: z.object({
|
||||||
query: z.string().min(1),
|
query: z.string().min(1),
|
||||||
@ -68,8 +80,23 @@ export const MCP_TOOL_DEFINITIONS: {
|
|||||||
name: 'list_pages',
|
name: 'list_pages',
|
||||||
description:
|
description:
|
||||||
'List the readable pages of a pond: slug, title, parent (the page tree), labels, ' +
|
'List the readable pages of a pond: slug, title, parent (the page tree), labels, ' +
|
||||||
'timestamps.',
|
'timestamps. Optional created_since/updated_since (ISO 8601) narrow to pages ' +
|
||||||
inputSchema: { type: 'object', properties: { pond: pondProp }, required: ['pond'] },
|
'created/changed at or after that instant.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
pond: pondProp,
|
||||||
|
created_since: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ISO 8601 instant — only pages created at/after it',
|
||||||
|
},
|
||||||
|
updated_since: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ISO 8601 instant — only pages updated at/after it',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['pond'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'read_page',
|
name: 'read_page',
|
||||||
|
|||||||
@ -97,9 +97,17 @@ export class McpService {
|
|||||||
case 'list_ponds':
|
case 'list_ponds':
|
||||||
return asJson(await this.publicApi.listPonds(user, token, 'mcp'));
|
return asJson(await this.publicApi.listPonds(user, token, 'mcp'));
|
||||||
|
|
||||||
case 'list_pages':
|
case 'list_pages': {
|
||||||
await this.assertPondExposed(input.pond!, token);
|
await this.assertPondExposed(input.pond!, token);
|
||||||
return asJson(await this.publicApi.listPages(user, input.pond!));
|
// The zod input already turned the `…_since` strings into Dates (#148).
|
||||||
|
const since = args as { created_since?: Date; updated_since?: Date };
|
||||||
|
return asJson(
|
||||||
|
await this.publicApi.listPages(user, input.pond!, {
|
||||||
|
createdSince: since.created_since,
|
||||||
|
updatedSince: since.updated_since,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
case 'read_page':
|
case 'read_page':
|
||||||
await this.assertPondExposed(input.pond!, token);
|
await this.assertPondExposed(input.pond!, token);
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import {
|
|||||||
UpdatePageInput,
|
UpdatePageInput,
|
||||||
createPageInputSchema,
|
createPageInputSchema,
|
||||||
pageDeleteQuerySchema,
|
pageDeleteQuerySchema,
|
||||||
|
pageListQuerySchema,
|
||||||
repositionPageInputSchema,
|
repositionPageInputSchema,
|
||||||
updatePageInputSchema,
|
updatePageInputSchema,
|
||||||
} from '@dorfteich/shared';
|
} from '@dorfteich/shared';
|
||||||
@ -57,9 +58,16 @@ export class PagesController {
|
|||||||
@RequiresPondRole('reader', { idParam: 'pondId' }) // the service filters per page
|
@RequiresPondRole('reader', { idParam: 'pondId' }) // the service filters per page
|
||||||
async list(
|
async list(
|
||||||
@Param('pondId') pondId: string,
|
@Param('pondId') pondId: string,
|
||||||
|
@Query('createdSince') createdSince: string | undefined,
|
||||||
|
@Query('updatedSince') updatedSince: string | undefined,
|
||||||
@Req() request: AuthedRequest,
|
@Req() request: AuthedRequest,
|
||||||
): Promise<PageListItemView[]> {
|
): Promise<PageListItemView[]> {
|
||||||
return this.pages.list(request.user!, pondId);
|
// Optional time filters (issue #148); an invalid instant → 400.
|
||||||
|
const query = new ZodValidationPipe(pageListQuerySchema).transform({
|
||||||
|
createdSince: createdSince || undefined,
|
||||||
|
updatedSince: updatedSince || undefined,
|
||||||
|
});
|
||||||
|
return this.pages.list(request.user!, pondId, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('pages/:id')
|
@Get('pages/:id')
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
OutlineEntry,
|
OutlineEntry,
|
||||||
PageDeleteMode,
|
PageDeleteMode,
|
||||||
PageListItemView,
|
PageListItemView,
|
||||||
|
PageListQuery,
|
||||||
PageStateView,
|
PageStateView,
|
||||||
PageView,
|
PageView,
|
||||||
PluginPageSummary,
|
PluginPageSummary,
|
||||||
@ -151,13 +152,24 @@ export class PagesService {
|
|||||||
/** Sidebar page list, ordered per the pond's persisted sort mode (issue #26),
|
/** Sidebar page list, ordered per the pond's persisted sort mode (issue #26),
|
||||||
* each with its assigned label ids for chips and filtering (issue #44).
|
* each with its assigned label ids for chips and filtering (issue #44).
|
||||||
* Filtered to the pages the user may read (issue #52) — a label- or
|
* Filtered to the pages the user may read (issue #52) — a label- or
|
||||||
* page-scoped reader sees only their slice of the pond. */
|
* page-scoped reader sees only their slice of the pond. Optional time
|
||||||
async list(user: User, pondId: string): Promise<PageListItemView[]> {
|
* filters (issue #148) narrow to pages created/updated at or after an
|
||||||
|
* instant. */
|
||||||
|
async list(
|
||||||
|
user: User | null,
|
||||||
|
pondId: string,
|
||||||
|
query?: PageListQuery,
|
||||||
|
): Promise<PageListItemView[]> {
|
||||||
const pond = await this.prisma.pond.findFirst({ where: { id: pondId, deletedAt: null } });
|
const pond = await this.prisma.pond.findFirst({ where: { id: pondId, deletedAt: null } });
|
||||||
if (!pond) throw new NotFoundException();
|
if (!pond) throw new NotFoundException();
|
||||||
const settings = pondSettingsSchema.parse(pond.settings ?? {});
|
const settings = pondSettingsSchema.parse(pond.settings ?? {});
|
||||||
const pages = await this.prisma.page.findMany({
|
const pages = await this.prisma.page.findMany({
|
||||||
where: { pondId, deletedAt: null },
|
where: {
|
||||||
|
pondId,
|
||||||
|
deletedAt: null,
|
||||||
|
...(query?.createdSince ? { createdAt: { gte: query.createdSince } } : {}),
|
||||||
|
...(query?.updatedSince ? { updatedAt: { gte: query.updatedSince } } : {}),
|
||||||
|
},
|
||||||
orderBy: PagesService.SORT_ORDER[settings.sidebarSort],
|
orderBy: PagesService.SORT_ORDER[settings.sidebarSort],
|
||||||
include: { labels: { select: { labelId: true } } },
|
include: { labels: { select: { labelId: true } } },
|
||||||
});
|
});
|
||||||
|
|||||||
@ -273,7 +273,23 @@ export function buildOpenApiDocument(): object {
|
|||||||
'/ponds/{pondSlug}/pages': {
|
'/ponds/{pondSlug}/pages': {
|
||||||
get: {
|
get: {
|
||||||
summary: 'Readable pages of the pond.',
|
summary: 'Readable pages of the pond.',
|
||||||
parameters: [pondParam],
|
parameters: [
|
||||||
|
pondParam,
|
||||||
|
{
|
||||||
|
name: 'createdSince',
|
||||||
|
in: 'query',
|
||||||
|
required: false,
|
||||||
|
schema: { type: 'string', format: 'date-time' },
|
||||||
|
description: 'Only pages created at or after this ISO 8601 instant (issue #148).',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'updatedSince',
|
||||||
|
in: 'query',
|
||||||
|
required: false,
|
||||||
|
schema: { type: 'string', format: 'date-time' },
|
||||||
|
description: 'Only pages updated at or after this ISO 8601 instant (issue #148).',
|
||||||
|
},
|
||||||
|
],
|
||||||
responses: {
|
responses: {
|
||||||
'200': jsonResponse('Pages', { type: 'array', items: ref('PageListItem') }),
|
'200': jsonResponse('Pages', { type: 'array', items: ref('PageListItem') }),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {
|
|||||||
commentListQuerySchema,
|
commentListQuerySchema,
|
||||||
createCommentInputSchema,
|
createCommentInputSchema,
|
||||||
createLabelInputSchema,
|
createLabelInputSchema,
|
||||||
|
pageListQuerySchema,
|
||||||
publicCreatePageInputSchema,
|
publicCreatePageInputSchema,
|
||||||
publicSearchQuerySchema,
|
publicSearchQuerySchema,
|
||||||
publicUpdateLabelInputSchema,
|
publicUpdateLabelInputSchema,
|
||||||
@ -88,9 +89,16 @@ export class PublicApiController {
|
|||||||
@RequiresPondRole('reader', POND)
|
@RequiresPondRole('reader', POND)
|
||||||
listPages(
|
listPages(
|
||||||
@Param('pondSlug') pondSlug: string,
|
@Param('pondSlug') pondSlug: string,
|
||||||
|
@Query('createdSince') createdSince: string | undefined,
|
||||||
|
@Query('updatedSince') updatedSince: string | undefined,
|
||||||
@Req() request: PublicApiRequest,
|
@Req() request: PublicApiRequest,
|
||||||
): Promise<PublicPageListItemView[]> {
|
): Promise<PublicPageListItemView[]> {
|
||||||
return this.publicApi.listPages(request.user!, pondSlug);
|
// Optional time filters (issue #148); an invalid instant → 400.
|
||||||
|
const query = new ZodValidationPipe(pageListQuerySchema).transform({
|
||||||
|
createdSince: createdSince || undefined,
|
||||||
|
updatedSince: updatedSince || undefined,
|
||||||
|
});
|
||||||
|
return this.publicApi.listPages(request.user!, pondSlug, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('ponds/:pondSlug/pages')
|
@Post('ponds/:pondSlug/pages')
|
||||||
|
|||||||
@ -460,6 +460,50 @@ describe.skipIf(!hasTestDb)('public api v1 (e2e, issue #104)', () => {
|
|||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('filters the page list by createdSince/updatedSince (issue #148)', async () => {
|
||||||
|
const old = await pub()
|
||||||
|
.post(`/api/public/v1/ponds/${pondSlug}/pages`)
|
||||||
|
.set('Authorization', bearer('editor'))
|
||||||
|
.send({ title: `Since Old ${suffix}` })
|
||||||
|
.expect(201);
|
||||||
|
const fresh = await pub()
|
||||||
|
.post(`/api/public/v1/ponds/${pondSlug}/pages`)
|
||||||
|
.set('Authorization', bearer('editor'))
|
||||||
|
.send({ title: `Since Fresh ${suffix}` })
|
||||||
|
.expect(201);
|
||||||
|
const oldSlug = (old.body as PublicPageView).slug;
|
||||||
|
const freshSlug = (fresh.body as PublicPageView).slug;
|
||||||
|
// Backdate the old page below the cutoff (raw row: timestamps only).
|
||||||
|
await prisma.page.updateMany({
|
||||||
|
where: { pondId, slug: oldSlug },
|
||||||
|
data: {
|
||||||
|
createdAt: new Date('2000-01-01T00:00:00Z'),
|
||||||
|
updatedAt: new Date('2000-01-02T00:00:00Z'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const param of ['createdSince', 'updatedSince'] as const) {
|
||||||
|
const filtered = await pub()
|
||||||
|
.get(`/api/public/v1/ponds/${pondSlug}/pages?${param}=2020-01-01T00:00:00Z`)
|
||||||
|
.set('Authorization', bearer('reader'))
|
||||||
|
.expect(200);
|
||||||
|
const slugs = (filtered.body as PublicPageListItemView[]).map((p) => p.slug);
|
||||||
|
expect(slugs).toContain(freshSlug);
|
||||||
|
expect(slugs).not.toContain(oldSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unfiltered, both are there; an invalid instant is a 400.
|
||||||
|
const all = await pub()
|
||||||
|
.get(`/api/public/v1/ponds/${pondSlug}/pages`)
|
||||||
|
.set('Authorization', bearer('reader'))
|
||||||
|
.expect(200);
|
||||||
|
expect((all.body as PublicPageListItemView[]).map((p) => p.slug)).toContain(oldSlug);
|
||||||
|
await pub()
|
||||||
|
.get(`/api/public/v1/ponds/${pondSlug}/pages?updatedSince=not-a-date`)
|
||||||
|
.set('Authorization', bearer('reader'))
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
it('manages labels with pond-admin rights and assigns them to pages', async () => {
|
it('manages labels with pond-admin rights and assigns them to pages', async () => {
|
||||||
const label = await pub()
|
const label = await pub()
|
||||||
.post(`/api/public/v1/ponds/${pondSlug}/labels`)
|
.post(`/api/public/v1/ponds/${pondSlug}/labels`)
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
pondFeatureEnabled,
|
pondFeatureEnabled,
|
||||||
pondSettingsSchema,
|
pondSettingsSchema,
|
||||||
type CommentListFilter,
|
type CommentListFilter,
|
||||||
|
type PageListQuery,
|
||||||
type CreateCommentInput,
|
type CreateCommentInput,
|
||||||
type CreateLabelInput,
|
type CreateLabelInput,
|
||||||
type LabelTreeNode,
|
type LabelTreeNode,
|
||||||
@ -93,10 +94,14 @@ export class PublicApiService {
|
|||||||
return this.pondView(pond);
|
return this.pondView(pond);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listPages(user: User, pondSlug: string): Promise<PublicPageListItemView[]> {
|
async listPages(
|
||||||
|
user: User,
|
||||||
|
pondSlug: string,
|
||||||
|
query?: PageListQuery,
|
||||||
|
): Promise<PublicPageListItemView[]> {
|
||||||
const pond = await this.requirePond(pondSlug);
|
const pond = await this.requirePond(pondSlug);
|
||||||
const [items, labelNames] = await Promise.all([
|
const [items, labelNames] = await Promise.all([
|
||||||
this.pages.list(user, pond.id),
|
this.pages.list(user, pond.id, query),
|
||||||
this.labelNames(pond.id),
|
this.labelNames(pond.id),
|
||||||
]);
|
]);
|
||||||
// parentId is already permission-nulled by the list (#106); mapping it
|
// parentId is already permission-nulled by the list (#106); mapping it
|
||||||
|
|||||||
@ -52,6 +52,9 @@ curl -H "$AUTH" https://wiki.example.com/api/public/v1/ponds
|
|||||||
# Seiten eines Teichs: Slug, Titel, Parent (Seitenbaum-Slug), Labels, Zeitstempel
|
# Seiten eines Teichs: Slug, Titel, Parent (Seitenbaum-Slug), Labels, Zeitstempel
|
||||||
curl -H "$AUTH" https://wiki.example.com/api/public/v1/ponds/team/pages
|
curl -H "$AUTH" https://wiki.example.com/api/public/v1/ponds/team/pages
|
||||||
|
|
||||||
|
# Nur Seiten, die seit einem ISO-8601-Zeitpunkt erstellt/geändert wurden (#148)
|
||||||
|
curl -H "$AUTH" "https://wiki.example.com/api/public/v1/ponds/team/pages?updatedSince=2026-07-01T00:00:00Z"
|
||||||
|
|
||||||
# Eine Seite — Markdown-Quelle UND gerendertes, bereinigtes HTML
|
# Eine Seite — Markdown-Quelle UND gerendertes, bereinigtes HTML
|
||||||
curl -H "$AUTH" https://wiki.example.com/api/public/v1/ponds/team/pages/meeting-notes
|
curl -H "$AUTH" https://wiki.example.com/api/public/v1/ponds/team/pages/meeting-notes
|
||||||
|
|
||||||
|
|||||||
@ -65,7 +65,7 @@ Stdio-Clients überbrücken mit `mcp-remote`:
|
|||||||
| Tool | Tut |
|
| Tool | Tut |
|
||||||
| ----------------------------------------------------- | -------------------------------------------------- |
|
| ----------------------------------------------------- | -------------------------------------------------- |
|
||||||
| `list_ponds` | die Teiche, die dieses Token erreicht |
|
| `list_ponds` | die Teiche, die dieses Token erreicht |
|
||||||
| `list_pages(pond)` | Seiten mit Slug, Titel, Parent, Labels |
|
| `list_pages(pond, created_since?, updated_since?)` | Seiten mit Slug, Titel, Parent, Labels |
|
||||||
| `read_page(pond, page)` | eine Seite als Markdown plus Metadaten |
|
| `read_page(pond, page)` | eine Seite als Markdown plus Metadaten |
|
||||||
| `search(query, pond?, label?)` | Volltextsuche mit Snippets |
|
| `search(query, pond?, label?)` | Volltextsuche mit Snippets |
|
||||||
| `create_page(pond, title, markdown, parent?)` | neue Seite aus Markdown _(write)_ |
|
| `create_page(pond, title, markdown, parent?)` | neue Seite aus Markdown _(write)_ |
|
||||||
|
|||||||
@ -49,6 +49,9 @@ curl -H "$AUTH" https://wiki.example.com/api/public/v1/ponds
|
|||||||
# Pages of a pond: slug, title, parent (page-tree slug), labels, timestamps
|
# Pages of a pond: slug, title, parent (page-tree slug), labels, timestamps
|
||||||
curl -H "$AUTH" https://wiki.example.com/api/public/v1/ponds/team/pages
|
curl -H "$AUTH" https://wiki.example.com/api/public/v1/ponds/team/pages
|
||||||
|
|
||||||
|
# Only pages created/updated at or after an ISO 8601 instant (issue #148)
|
||||||
|
curl -H "$AUTH" "https://wiki.example.com/api/public/v1/ponds/team/pages?updatedSince=2026-07-01T00:00:00Z"
|
||||||
|
|
||||||
# One page — Markdown source AND rendered, sanitized HTML
|
# One page — Markdown source AND rendered, sanitized HTML
|
||||||
curl -H "$AUTH" https://wiki.example.com/api/public/v1/ponds/team/pages/meeting-notes
|
curl -H "$AUTH" https://wiki.example.com/api/public/v1/ponds/team/pages/meeting-notes
|
||||||
|
|
||||||
|
|||||||
@ -64,7 +64,7 @@ clients bridge with `mcp-remote`:
|
|||||||
| Tool | Does |
|
| Tool | Does |
|
||||||
| ----------------------------------------------------- | ------------------------------------------- |
|
| ----------------------------------------------------- | ------------------------------------------- |
|
||||||
| `list_ponds` | the ponds this token can reach |
|
| `list_ponds` | the ponds this token can reach |
|
||||||
| `list_pages(pond)` | pages with slug, title, parent, labels |
|
| `list_pages(pond, created_since?, updated_since?)` | pages with slug, title, parent, labels |
|
||||||
| `read_page(pond, page)` | a page as Markdown plus metadata |
|
| `read_page(pond, page)` | a page as Markdown plus metadata |
|
||||||
| `search(query, pond?, label?)` | full-text search with snippets |
|
| `search(query, pond?, label?)` | full-text search with snippets |
|
||||||
| `create_page(pond, title, markdown, parent?)` | new page from Markdown _(write)_ |
|
| `create_page(pond, title, markdown, parent?)` | new page from Markdown _(write)_ |
|
||||||
|
|||||||
@ -43,7 +43,7 @@ curl -H "Authorization: Bearer dt_pat_..." \
|
|||||||
| -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
| -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| Identity | `GET /me` |
|
| Identity | `GET /me` |
|
||||||
| Ponds | `GET /ponds`, `GET /ponds/{slug}` |
|
| Ponds | `GET /ponds`, `GET /ponds/{slug}` |
|
||||||
| Pages | `GET/POST /ponds/{slug}/pages`, `GET/PATCH/DELETE /ponds/{slug}/pages/{pageSlug}` |
|
| Pages | `GET/POST /ponds/{slug}/pages` (list filters `?createdSince=`/`?updatedSince=`), `GET/PATCH/DELETE /ponds/{slug}/pages/{pageSlug}` |
|
||||||
| Search | `GET /search?q=&pond=&label=` |
|
| Search | `GET /search?q=&pond=&label=` |
|
||||||
| Export | `GET /ponds/{slug}/export/markdown` (ZIP) |
|
| Export | `GET /ponds/{slug}/export/markdown` (ZIP) |
|
||||||
| Labels | `GET/POST /ponds/{slug}/labels`, `PATCH/DELETE /ponds/{slug}/labels/{id}`, `PUT/DELETE /ponds/{slug}/pages/{pageSlug}/labels/{id}` |
|
| Labels | `GET/POST /ponds/{slug}/labels`, `PATCH/DELETE /ponds/{slug}/labels/{id}`, `PUT/DELETE /ponds/{slug}/pages/{pageSlug}/labels/{id}` |
|
||||||
|
|||||||
@ -92,6 +92,21 @@ export const publicUpdateLabelInputSchema = z
|
|||||||
.partial();
|
.partial();
|
||||||
export type PublicUpdateLabelInput = z.infer<typeof publicUpdateLabelInputSchema>;
|
export type PublicUpdateLabelInput = z.infer<typeof publicUpdateLabelInputSchema>;
|
||||||
|
|
||||||
|
/** An ISO 8601 instant (date or date-time) for `…Since` filters (issue #148). */
|
||||||
|
const sinceInstant = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.regex(/^\d{4}-\d{2}-\d{2}([T ].+)?$/, 'validation.invalid')
|
||||||
|
.refine((value) => !Number.isNaN(Date.parse(value)), 'validation.invalid')
|
||||||
|
.transform((value) => new Date(value));
|
||||||
|
|
||||||
|
/** Optional time filters for page listings (issue #148), applied as `>=`. */
|
||||||
|
export const pageListQuerySchema = z.object({
|
||||||
|
createdSince: sinceInstant.optional(),
|
||||||
|
updatedSince: sinceInstant.optional(),
|
||||||
|
});
|
||||||
|
export type PageListQuery = z.infer<typeof pageListQuerySchema>;
|
||||||
|
|
||||||
export const publicSearchQuerySchema = z.object({
|
export const publicSearchQuerySchema = z.object({
|
||||||
q: z.string().trim().min(1, 'validation.required').max(200),
|
q: z.string().trim().min(1, 'validation.required').max(200),
|
||||||
pond: z.string().trim().optional(),
|
pond: z.string().trim().optional(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user