fix: Reject collections.update requests that include both description and data (#12648)

This commit is contained in:
Tom Moor
2026-06-11 21:30:47 -04:00
committed by GitHub
parent 9791ff1170
commit c65b020655
2 changed files with 105 additions and 33 deletions
@@ -1257,6 +1257,23 @@ describe("#collections.create", () => {
expect(res.status).toEqual(400);
});
it("rejects providing both description and data", async () => {
const user = await buildUser();
const res = await server.post("/api/collections.create", user, {
body: {
name: "Test",
description: "Test",
data: {
type: "doc",
content: [
{ type: "paragraph", content: [{ type: "text", text: "Test" }] },
],
},
},
});
expect(res.status).toEqual(400);
});
it("should allow setting sharing to false", async () => {
const user = await buildUser();
const res = await server.post("/api/collections.create", user, {
@@ -1448,6 +1465,50 @@ describe("#collections.update", () => {
expect(collection.content).toBeTruthy();
});
it("replaces rendered content when description is updated post-create", async () => {
const team = await buildTeam();
const admin = await buildAdmin({ teamId: team.id });
const createRes = await server.post("/api/collections.create", admin, {
headers: { "x-api-version": "3" },
body: { name: "Foo", description: "Original" },
});
const { id } = (await createRes.json()).data;
const updateRes = await server.post("/api/collections.update", admin, {
headers: { "x-api-version": "3" },
body: { id, description: "Replaced" },
});
expect(updateRes.status).toEqual(200);
const infoRes = await server.post("/api/collections.info", admin, {
headers: { "x-api-version": "3" },
body: { id },
});
const content = JSON.stringify((await infoRes.json()).data.data);
expect(content).toContain("Replaced");
expect(content).not.toContain("Original");
});
it("rejects providing both description and data", async () => {
const team = await buildTeam();
const admin = await buildAdmin({ teamId: team.id });
const collection = await buildCollection({ teamId: team.id });
const res = await server.post("/api/collections.update", admin, {
body: {
id: collection.id,
description: "Test",
data: {
type: "doc",
content: [
{ type: "paragraph", content: [{ type: "text", text: "Test" }] },
],
},
},
});
expect(res.status).toEqual(400);
});
it("allows editing data", async () => {
const team = await buildTeam();
const admin = await buildAdmin({ teamId: team.id });
+44 -33
View File
@@ -15,39 +15,50 @@ const BaseIdSchema = z.object({
id: zodIdType(),
});
/** The landing page can be set from description (markdown) or data (rich content), but not both. */
const refineBodyContent = <T extends { description?: unknown; data?: unknown }>(
body: T
) => isUndefined(body.description) || isUndefined(body.data);
const bodyContentError = {
error: "Only one of description or data may be provided",
};
export const CollectionsCreateSchema = BaseSchema.extend({
body: z.object({
name: z.string(),
color: z
.string()
.regex(ValidateColor.regex, { message: ValidateColor.message })
.nullish(),
description: z.string().nullish(),
data: ProsemirrorSchema({ allowEmpty: true }).nullish(),
permission: z
.enum(CollectionPermission)
.nullish()
.transform((val) => (isUndefined(val) ? null : val)),
sharing: z.boolean().prefault(true),
icon: zodIconType().optional(),
sort: z
.object({
field: z.union([z.literal("title"), z.literal("index")]),
direction: z.union([z.literal("asc"), z.literal("desc")]),
})
.prefault(Collection.DEFAULT_SORT),
index: z
.string()
.regex(ValidateIndex.regex, { message: ValidateIndex.message })
.max(ValidateIndex.maxLength, {
message: `Must be ${ValidateIndex.maxLength} or fewer characters long`,
})
.optional(),
commenting: z.boolean().nullish(),
templateManagement: z
.enum([CollectionPermission.Admin, CollectionPermission.ReadWrite])
.prefault(CollectionPermission.Admin),
}),
body: z
.object({
name: z.string(),
color: z
.string()
.regex(ValidateColor.regex, { message: ValidateColor.message })
.nullish(),
description: z.string().nullish(),
data: ProsemirrorSchema({ allowEmpty: true }).nullish(),
permission: z
.enum(CollectionPermission)
.nullish()
.transform((val) => (isUndefined(val) ? null : val)),
sharing: z.boolean().prefault(true),
icon: zodIconType().optional(),
sort: z
.object({
field: z.union([z.literal("title"), z.literal("index")]),
direction: z.union([z.literal("asc"), z.literal("desc")]),
})
.prefault(Collection.DEFAULT_SORT),
index: z
.string()
.regex(ValidateIndex.regex, { message: ValidateIndex.message })
.max(ValidateIndex.maxLength, {
message: `Must be ${ValidateIndex.maxLength} or fewer characters long`,
})
.optional(),
commenting: z.boolean().nullish(),
templateManagement: z
.enum([CollectionPermission.Admin, CollectionPermission.ReadWrite])
.prefault(CollectionPermission.Admin),
})
.refine(refineBodyContent, bodyContentError),
});
export type CollectionsCreateReq = z.infer<typeof CollectionsCreateSchema>;
@@ -188,7 +199,7 @@ export const CollectionsUpdateSchema = BaseSchema.extend({
templateManagement: z
.enum([CollectionPermission.Admin, CollectionPermission.ReadWrite])
.optional(),
}),
}).refine(refineBodyContent, bodyContentError),
});
export type CollectionsUpdateReq = z.infer<typeof CollectionsUpdateSchema>;