From 4b336dd9030e504c711547d9ccfe52103302a0bc Mon Sep 17 00:00:00 2001 From: Billy Le Date: Thu, 8 Aug 2024 23:05:10 +0800 Subject: [PATCH] * fix: set default values from zod if FormData key is not present * fix: remove duplicate while loop. use correct boolean values on validation * chore: rephrase changeset --- .changeset/nervous-garlics-beam.md | 5 ++ .../src/actions/runtime/virtual/server.ts | 16 +++++- .../units/actions/form-data-to-object.test.js | 57 +++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 .changeset/nervous-garlics-beam.md diff --git a/.changeset/nervous-garlics-beam.md b/.changeset/nervous-garlics-beam.md new file mode 100644 index 000000000000..2e5413d74cd7 --- /dev/null +++ b/.changeset/nervous-garlics-beam.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes Astro Actions `input` validation when using `default` values with a form input. diff --git a/packages/astro/src/actions/runtime/virtual/server.ts b/packages/astro/src/actions/runtime/virtual/server.ts index 7aea22b2fc7f..e334cb632c47 100644 --- a/packages/astro/src/actions/runtime/virtual/server.ts +++ b/packages/astro/src/actions/runtime/virtual/server.ts @@ -134,11 +134,21 @@ export function formDataToObject( const obj: Record = {}; for (const [key, baseValidator] of Object.entries(schema.shape)) { let validator = baseValidator; - while (validator instanceof z.ZodOptional || validator instanceof z.ZodNullable) { + + while (validator instanceof z.ZodOptional || validator instanceof z.ZodNullable || validator instanceof z.ZodDefault) { + // use default value when key is undefined + if(validator instanceof z.ZodDefault && !formData.has(key)) { + obj[key] = validator._def.defaultValue(); + } validator = validator._def.innerType; } - if (validator instanceof z.ZodBoolean) { - obj[key] = formData.has(key); + + if (!formData.has(key) && key in obj) { + // continue loop if form input is not found and default value is set + continue; + } else if (validator instanceof z.ZodBoolean) { + const val = formData.get(key); + obj[key] = val === 'true' ? true : val === 'false' ? false : formData.has(key) } else if (validator instanceof z.ZodArray) { obj[key] = handleFormDataGetAll(key, formData, validator); } else { diff --git a/packages/astro/test/units/actions/form-data-to-object.test.js b/packages/astro/test/units/actions/form-data-to-object.test.js index 6cce8f3d7df4..b27d50c8c3e6 100644 --- a/packages/astro/test/units/actions/form-data-to-object.test.js +++ b/packages/astro/test/units/actions/form-data-to-object.test.js @@ -46,15 +46,24 @@ describe('formDataToObject', () => { it('should handle boolean checks', () => { const formData = new FormData(); formData.set('isCool', 'yes'); + formData.set('isTrue', true) + formData.set("isFalse", false); + formData.set("falseString", 'false') const input = z.object({ isCool: z.boolean(), isNotCool: z.boolean(), + isTrue: z.boolean(), + isFalse: z.boolean(), + falseString: z.boolean(), }); const res = formDataToObject(formData, input); assert.equal(res.isCool, true); assert.equal(res.isNotCool, false); + assert.equal(res.isTrue, true); + assert.equal(res.isFalse, false); + assert.equal(res.falseString, false) }); it('should handle optional values', () => { @@ -91,6 +100,37 @@ describe('formDataToObject', () => { assert.equal(res.age, null); }); + it('should handle zod default values', () => { + const formData = new FormData(); + + const input = z.object({ + name: z.string().default("test"), + email: z.string().default('test@test.test'), + favoriteNumbers: z.array(z.number()).default([1,2]) + }); + + const res = formDataToObject(formData, input); + assert.equal(res.name, 'test'); + assert.equal(res.email, 'test@test.test'); + assert.deepEqual(res.favoriteNumbers, [1, 2]); + }) + + it('should handle zod chaining of optional, default, and nullish values', () => { + const formData = new FormData(); + formData.set('email', 'test@test.test') + + const input = z.object({ + name: z.string().default("test").optional(), + email: z.string().optional().nullish(), + favoriteNumbers: z.array(z.number()).default([1,2]).nullish().optional() + }); + + const res = formDataToObject(formData, input); + assert.equal(res.name, 'test'); + assert.equal(res.email, 'test@test.test'); + assert.deepEqual(res.favoriteNumbers, [1, 2]) + }) + it('should handle File objects', () => { const formData = new FormData(); formData.set('file', new File([''], 'test.txt')); @@ -135,4 +175,21 @@ describe('formDataToObject', () => { assert.ok(Array.isArray(res.age), 'age is not an array'); assert.deepEqual(res.age.sort(), [25, 30, 35]); }); + + it('should handle an array of File objects', () => { + const formData = new FormData(); + const file1 = new File([''], 'test1.txt'); + const file2 = new File([''], 'test2.txt') + formData.append('files', file1); + formData.append('files', file2); + + const input = z.object({ + files: z.array(z.instanceof(File)) + }); + + const res = formDataToObject(formData, input); + + assert.equal(res.files instanceof Array, true); + assert.deepEqual(res.files, [file1, file2]) + }); });