From e0033b2288a85b58ce032545870f6e8307539d5c Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 21 Dec 2025 09:46:26 -0500 Subject: [PATCH] stash --- server/routes/api/documents/documents.test.ts | 96 +++++++++++++++++++ server/routes/api/documents/documents.ts | 8 +- server/routes/api/documents/schema.ts | 15 +++ 3 files changed, 118 insertions(+), 1 deletion(-) diff --git a/server/routes/api/documents/documents.test.ts b/server/routes/api/documents/documents.test.ts index 2d5896bb88..2154a9abdb 100644 --- a/server/routes/api/documents/documents.test.ts +++ b/server/routes/api/documents/documents.test.ts @@ -1007,6 +1007,102 @@ describe("#documents.list", () => { expect(body.data.length).toEqual(1); }); + it("should allow advanced filtering", async () => { + const user = await buildUser(); + await buildDocument({ + title: "First document", + userId: user.id, + teamId: user.teamId, + }); + await buildDocument({ + title: "Second document", + userId: user.id, + teamId: user.teamId, + }); + + const res = await server.post("/api/documents.list", { + body: { + token: user.getJwtToken(), + filter: { + field: "title", + operator: "contains", + value: "First", + }, + }, + }); + const body = await res.json(); + expect(res.status).toEqual(200); + expect(body.data.length).toEqual(1); + expect(body.data[0].title).toEqual("First document"); + }); + + it("should allow advanced filtering with logical operators", async () => { + const user = await buildUser(); + await buildDocument({ + title: "First document", + userId: user.id, + teamId: user.teamId, + }); + await buildDocument({ + title: "Second document", + userId: user.id, + teamId: user.teamId, + }); + + const res = await server.post("/api/documents.list", { + body: { + token: user.getJwtToken(), + filter: { + operator: "OR", + filters: [ + { + field: "title", + operator: "eq", + value: "First document", + }, + { + field: "title", + operator: "eq", + value: "Second document", + }, + ], + }, + }, + }); + const body = await res.json(); + expect(res.status).toEqual(200); + expect(body.data.length).toEqual(2); + }); + + it("should allow filtering to include archived", async () => { + const user = await buildUser(); + await buildDocument({ + title: "First document", + userId: user.id, + teamId: user.teamId, + }); + await buildDocument({ + title: "Second document", + userId: user.id, + teamId: user.teamId, + archivedAt: new Date(), + }); + + const res = await server.post("/api/documents.list", { + body: { + token: user.getJwtToken(), + filter: { + field: "archivedAt", + operator: "isNotNull", + value: true, + }, + }, + }); + const body = await res.json(); + expect(res.status).toEqual(200); + expect(body.data.length).toEqual(1); + }); + it("should allow filtering to private collection", async () => { const user = await buildUser(); const collection = await buildCollection({ diff --git a/server/routes/api/documents/documents.ts b/server/routes/api/documents/documents.ts index 2eb045007f..858d4eeab7 100644 --- a/server/routes/api/documents/documents.ts +++ b/server/routes/api/documents/documents.ts @@ -58,6 +58,7 @@ import { DocumentHelper } from "@server/models/helpers/DocumentHelper"; import { ProsemirrorHelper } from "@server/models/helpers/ProsemirrorHelper"; import SearchHelper from "@server/models/helpers/SearchHelper"; import { TextHelper } from "@server/models/helpers/TextHelper"; +import { buildWhere } from "@server/models/filters/Filters"; import { authorize, can, cannot } from "@server/policies"; import { presentDocument, @@ -97,6 +98,7 @@ router.post( parentDocumentId, userId: createdById, statusFilter, + filter, } = ctx.input.body; // always filter by the current team @@ -114,8 +116,12 @@ router.post( ], }; + if (filter) { + where[Op.and].push(buildWhere(filter)); + } + // Exclude archived docs by default - if (!statusFilter) { + if (!statusFilter && !filter) { where[Op.and].push({ archivedAt: { [Op.eq]: null } }); } diff --git a/server/routes/api/documents/schema.ts b/server/routes/api/documents/schema.ts index bf2d94229d..63b772e07e 100644 --- a/server/routes/api/documents/schema.ts +++ b/server/routes/api/documents/schema.ts @@ -5,6 +5,7 @@ import { DocumentPermission, StatusFilter } from "@shared/types"; import { BaseSchema } from "@server/routes/api/schema"; import { zodIconType, zodIdType, zodShareIdType } from "@server/utils/zod"; import { ValidateColor } from "@server/validation"; +import { builderFilterSchema } from "@shared/helpers/FilterHelper"; const DocumentsSortParamsSchema = z.object({ /** Specifies the attributes by which documents will be sorted in the list */ @@ -69,6 +70,17 @@ const BaseIdSchema = z.object({ id: zodIdType(), }); +const filter = builderFilterSchema( + z.enum([ + "createdAt", + "updatedAt", + "archivedAt", + "publishedAt", + "title", + "templateId", + ]) +); + export const DocumentsListSchema = BaseSchema.extend({ body: DocumentsSortParamsSchema.extend({ /** Id of the user who created the doc */ @@ -94,6 +106,9 @@ export const DocumentsListSchema = BaseSchema.extend({ /** Document statuses to include in results */ statusFilter: z.nativeEnum(StatusFilter).array().optional(), + + /** Advanced filters */ + filter: filter.FilterSchema.optional(), }), // Maintains backwards compatibility }).transform((req) => {