Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/client/news/129.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Stopped the content and user data schemas from silently stripping fields they do not declare (e.g. `changeNote`, custom dexterity fields, custom member properties). Unknown keys are now passed through to the backend, which is the authority on which fields exist. @jmevissen
97 changes: 51 additions & 46 deletions packages/client/src/validation/content.ts
Comment thread
jmevissen marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -60,57 +60,61 @@ export const RelatedItemPayloadSchema = z
title: true,
});

export const createContentDataSchema = z.object({
'@id': z.string().optional(),
'@static_behaviors': z.unknown().optional(),
'@type': z.string(),
allow_discussion: z.boolean().optional(),
blocks: z.unknown().optional(),
blocks_layout: z.object({ items: z.array(z.string()) }).optional(),
contributors: z.array(z.string()).optional(),
creators: z.array(z.string()).optional(),
description: z.string().optional(),
effective: z.string().nullable().optional(),
exclude_from_nav: z.boolean().optional(),
expires: z.string().nullable().optional(),
file: z
.object({
'content-type': z.string(),
data: z.string(),
encoding: z.string(),
filename: z.string(),
})
.optional(),
id: z.string().optional(),
image: z
.object({
'content-type': z.string(),
data: z.string(),
encoding: z.string(),
filename: z.string(),
})
.optional(),
language: z.string().optional(),
preview_caption: z.string().optional(),
preview_image: z
.object({
'content-type': z.string(),
data: z.string(),
encoding: z.string(),
filename: z.string(),
})
.optional(),
relatedItems: z.array(RelatedItemPayloadSchema).optional(),
rights: z.string().nullable().optional(),
title: z.string(),
versioning_enabled: z.boolean().optional(),
});
export const createContentDataSchema = z
.object({
'@id': z.string().optional(),
'@static_behaviors': z.unknown().optional(),
'@type': z.string(),
allow_discussion: z.boolean().optional(),
blocks: z.unknown().optional(),
blocks_layout: z.object({ items: z.array(z.string()) }).optional(),
changeNote: z.string().optional(),
contributors: z.array(z.string()).optional(),
creators: z.array(z.string()).optional(),
description: z.string().optional(),
effective: z.string().nullable().optional(),
exclude_from_nav: z.boolean().optional(),
expires: z.string().nullable().optional(),
file: z
.object({
'content-type': z.string(),
data: z.string(),
encoding: z.string(),
filename: z.string(),
})
.optional(),
id: z.string().optional(),
image: z
.object({
'content-type': z.string(),
data: z.string(),
encoding: z.string(),
filename: z.string(),
})
.optional(),
language: z.string().optional(),
preview_caption: z.string().optional(),
preview_image: z
.object({
'content-type': z.string(),
data: z.string(),
encoding: z.string(),
filename: z.string(),
})
.optional(),
relatedItems: z.array(RelatedItemPayloadSchema).optional(),
rights: z.string().nullable().optional(),
title: z.string(),
versioning_enabled: z.boolean().optional(),
})
.passthrough();

export const updateContentDataSchema = z
.object({
allow_discussion: z.boolean().optional(),
blocks: z.unknown().optional(),
blocks_layout: z.object({ items: z.array(z.string()) }).optional(),
changeNote: z.string().optional(),
contributors: z.array(z.string()).optional(),
creators: z.array(z.string()).optional(),
description: z.string().optional(),
Expand Down Expand Up @@ -141,7 +145,8 @@ export const updateContentDataSchema = z
title: z.string().optional(),
versioning_enabled: z.boolean().optional(),
})
.partial();
.partial()
.passthrough();

export const copyMoveContentDataSchema = z.object({
path: z.string(),
Expand Down
57 changes: 57 additions & 0 deletions packages/client/src/validation/passthrough.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, test } from 'vitest';
import { createContentDataSchema, updateContentDataSchema } from './content';
import { createUserDataSchema, updateUserDataSchema } from './users';

describe('content data schemas', () => {
test('createContentDataSchema keeps unknown (custom dexterity) fields', () => {
const parsed = createContentDataSchema.parse({
'@type': 'Document',
title: 'My Page',
my_custom_field: 'custom value',
});

expect(parsed.my_custom_field).toBe('custom value');
});

test('updateContentDataSchema keeps unknown (custom dexterity) fields', () => {
const parsed = updateContentDataSchema.parse({
title: 'My Page',
my_custom_field: 'custom value',
});

expect(parsed.my_custom_field).toBe('custom value');
});

test('updateContentDataSchema still rejects mistyped declared fields', () => {
expect(() =>
updateContentDataSchema.parse({ exclude_from_nav: 'yes' }),
).toThrow();
});
});

describe('user data schemas', () => {
test('createUserDataSchema keeps custom member properties', () => {
const parsed = createUserDataSchema.parse({
email: 'jane@example.com',
username: 'jane',
phone: '+1 555 0100',
});

expect(parsed.phone).toBe('+1 555 0100');
});

test('updateUserDataSchema keeps custom member properties', () => {
const parsed = updateUserDataSchema.parse({
fullname: 'Jane Doe',
department: 'Engineering',
});

expect(parsed.department).toBe('Engineering');
});

test('updateUserDataSchema still rejects mistyped declared fields', () => {
expect(() =>
updateUserDataSchema.parse({ email: 'not-an-email' }),
).toThrow();
});
});
44 changes: 24 additions & 20 deletions packages/client/src/validation/users.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { z } from 'zod';

export const createUserDataSchema = z.object({
description: z.string().optional(),
email: z.string().email(),
fullname: z.string().optional(),
home_page: z.string().url().optional(),
location: z.string().optional(),
sendPasswordReset: z.boolean().optional(),
username: z.string(),
roles: z.array(z.string()).optional(),
password: z.string().optional(),
});
export const createUserDataSchema = z
.object({
description: z.string().optional(),
email: z.string().email(),
fullname: z.string().optional(),
home_page: z.string().url().optional(),
location: z.string().optional(),
sendPasswordReset: z.boolean().optional(),
username: z.string(),
roles: z.array(z.string()).optional(),
password: z.string().optional(),
})
.passthrough();

const portraitSchema = z.object({
'content-type': z.string(),
Expand All @@ -20,15 +22,17 @@ const portraitSchema = z.object({
scale: z.boolean().optional(),
});

export const updateUserDataSchema = z.object({
description: z.string().optional(),
email: z.string().email().optional(),
fullname: z.string().optional(),
home_page: z.string().url().optional(),
location: z.string().optional(),
username: z.string().optional(),
portrait: portraitSchema.optional(),
});
export const updateUserDataSchema = z
.object({
description: z.string().optional(),
email: z.string().email().optional(),
fullname: z.string().optional(),
home_page: z.string().url().optional(),
location: z.string().optional(),
username: z.string().optional(),
portrait: portraitSchema.optional(),
})
.passthrough();

export const resetPasswordWithTokenDataSchema = z.object({
reset_token: z.string(),
Expand Down
Loading