Skip to content

Commit 6533f0b

Browse files
committed
message and setError throws if the status option is below 400.
Support for passthrough on a schema.
1 parent 7da39a4 commit 6533f0b

File tree

3 files changed

+152
-67
lines changed

3 files changed

+152
-67
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- For type safety, you cannot send `null` or `undefined` to `superForm` anymore. Use `superValidate`, or pass a complete data object to `superForm`. Default values can be added with the `defaultValues` function.
1313
- The `valid` option is removed from `message`, any status >= 400 will return a fail.
1414

15+
### Changed
16+
17+
- `message/setMessage` and `setError` will now throw an error if the status option is below 400.
18+
1519
### Added
1620

1721
- Arrays and objects in the schema can now have errors! They can be found at `field._errors` in the `$errors` store.
1822
- `validate` will now validate the whole form when it's called with no arguments.
23+
- Support for `passthrough()` on a schema, `superValidate` will allow extra keys in that case.
1924

2025
## [1.0.0-rc.2]
2126

@@ -36,7 +41,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3641

3742
### Added
3843

39-
- Errors can now be added to arrays and objects in the schema.
4044
- Added a `posted` store, a boolean which is false if the form hasn't been posted during its current lifetime.
4145
- `reset` now has an additional `data` option that can be used to re-populate the form with data, and `id` to set a different form id.
4246
- `intProxy`, `numberProxy`, `dateProxy` and `stringProxy` now have an `empty` option, so empty values can be set to `null` or `undefined`.

src/index.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,3 +979,55 @@ describe('Schema errors with arrays and objects', () => {
979979
expect(form.errors.tags?.[0]).toBeUndefined();
980980
});
981981
});
982+
983+
test('Passthrough validation', async () => {
984+
const schema = z.object({
985+
name: z.string().min(2)
986+
});
987+
988+
const data = {
989+
name: 'test',
990+
extra: 'field'
991+
};
992+
993+
const form = await superValidate(data, schema.passthrough());
994+
assert(form.valid === true);
995+
expect(form.data).toStrictEqual({
996+
name: 'test',
997+
extra: 'field'
998+
});
999+
1000+
const failedData = {
1001+
name: '',
1002+
extra2: 'field2'
1003+
};
1004+
1005+
const form2 = await superValidate(failedData, schema.passthrough());
1006+
assert(form2.valid === false);
1007+
expect(form2.data).toStrictEqual({
1008+
name: '',
1009+
extra2: 'field2'
1010+
});
1011+
1012+
const data3 = {
1013+
name: 'test',
1014+
extra: 'field'
1015+
};
1016+
1017+
const form3 = await superValidate(data3, schema);
1018+
assert(form3.valid === true);
1019+
expect(form3.data).toStrictEqual({
1020+
name: 'test'
1021+
});
1022+
1023+
const failedData2 = {
1024+
name: '',
1025+
extra2: 'field2'
1026+
};
1027+
1028+
const form4 = await superValidate(failedData2, schema);
1029+
assert(form4.valid === false);
1030+
expect(form4.data).toStrictEqual({
1031+
name: ''
1032+
});
1033+
});

src/lib/superValidate.ts

Lines changed: 95 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,26 @@ import { mapErrors } from './errors.js';
4343

4444
export { defaultValues } from './schemaEntity.js';
4545

46+
/**
47+
* Sends a message with a form, with an optional HTTP status code that will set
48+
* form.valid to false if status >= 400. A status lower than 400 cannot be sent.
49+
*/
4650
export function message<T extends ZodValidation<AnyZodObject>, M>(
4751
form: SuperValidated<T, M>,
4852
message: M,
4953
options?: {
5054
status?: number;
5155
}
5256
) {
53-
form.message = message;
54-
if (options?.status && options.status >= 400) form.valid = false;
57+
if (options?.status) {
58+
if (options.status >= 400) form.valid = false;
59+
else
60+
throw new SuperFormError(
61+
'Cannot set a form message status lower than 400.'
62+
);
63+
}
5564

65+
form.message = message;
5666
return !form.valid ? fail(options?.status ?? 400, { form }) : { form };
5767
}
5868

@@ -68,6 +78,10 @@ export function setError<T extends ZodValidation<AnyZodObject>>(
6878
status: 400
6979
}
7080
) {
81+
if (options.status && options.status < 400) {
82+
throw new SuperFormError('Cannot set an error status lower than 400.');
83+
}
84+
7185
const errArr = Array.isArray(error) ? error : [error];
7286

7387
if (!form.errors) form.errors = {};
@@ -328,7 +342,12 @@ function validateResult<T extends AnyZodObject, M>(
328342
constraints: entityInfo.constraints
329343
};
330344
} else {
331-
const { opts: options, schemaKeys, entityInfo } = schemaData;
345+
const {
346+
opts: options,
347+
schemaKeys,
348+
entityInfo,
349+
unwrappedSchema
350+
} = schemaData;
332351

333352
if (!result) {
334353
throw new SuperFormError(
@@ -343,19 +362,27 @@ function validateResult<T extends AnyZodObject, M>(
343362
? mapErrors<T>(result.error.format(), entityInfo.errorShape)
344363
: {};
345364

365+
// passthrough, strip, strict
366+
const zodKeyStatus = unwrappedSchema._def.unknownKeys;
367+
368+
const data =
369+
zodKeyStatus == 'passthrough'
370+
? { ...clone(entityInfo.defaultEntity), ...partialData }
371+
: Object.fromEntries(
372+
schemaKeys.map((key) => [
373+
key,
374+
key in partialData
375+
? partialData[key]
376+
: clone(entityInfo.defaultEntity[key])
377+
])
378+
);
379+
346380
return {
347381
id,
348382
valid: false,
349383
posted,
350384
errors,
351-
data: Object.fromEntries(
352-
schemaKeys.map((key) => [
353-
key,
354-
key in partialData
355-
? partialData[key]
356-
: clone(entityInfo.defaultEntity[key])
357-
])
358-
),
385+
data,
359386
constraints: entityInfo.constraints
360387
};
361388
} else {
@@ -522,61 +549,6 @@ export async function superValidate<
522549
return validateResult<UnwrapEffects<T>, M>(parsed, schemaData, result);
523550
}
524551

525-
export function actionResult<
526-
T extends Record<string, unknown> | App.Error | string,
527-
Type extends T extends string
528-
? 'redirect' | 'error'
529-
: 'success' | 'failure' | 'error'
530-
>(
531-
type: Type,
532-
data?: T,
533-
options?:
534-
| number
535-
| {
536-
status?: number;
537-
message?: Type extends 'redirect' ? App.PageData['flash'] : never;
538-
}
539-
) {
540-
const status =
541-
options && typeof options !== 'number' ? options.status : options;
542-
543-
const result = <T extends { status: number }>(struct: T) => {
544-
return json(
545-
{ type, ...struct },
546-
{
547-
status: struct.status,
548-
headers:
549-
typeof options === 'object' && options.message
550-
? {
551-
'Set-Cookie': `flash=${encodeURIComponent(
552-
JSON.stringify(options.message)
553-
)}; Path=/; Max-Age=120`
554-
}
555-
: undefined
556-
}
557-
);
558-
};
559-
560-
if (type == 'error') {
561-
return result({
562-
status: status || 500,
563-
error: typeof data === 'string' ? { message: data } : data
564-
});
565-
} else if (type == 'redirect') {
566-
return result({
567-
status: status || 303,
568-
location: data
569-
});
570-
} else if (type == 'failure') {
571-
return result({
572-
status: status || 400,
573-
data: stringify(data)
574-
});
575-
} else {
576-
return result({ status: status || 200, data: stringify(data) });
577-
}
578-
}
579-
580552
////////////////////////////////////////////////////////////////////
581553

582554
export function superValidateSync<
@@ -647,3 +619,60 @@ export function superValidateSync<
647619

648620
return validateResult<UnwrapEffects<T>, M>(parsed, schemaData, result);
649621
}
622+
623+
///////////////////////////////////////////////////////////////////////////////
624+
625+
export function actionResult<
626+
T extends Record<string, unknown> | App.Error | string,
627+
Type extends T extends string
628+
? 'redirect' | 'error'
629+
: 'success' | 'failure' | 'error'
630+
>(
631+
type: Type,
632+
data?: T,
633+
options?:
634+
| number
635+
| {
636+
status?: number;
637+
message?: Type extends 'redirect' ? App.PageData['flash'] : never;
638+
}
639+
) {
640+
const status =
641+
options && typeof options !== 'number' ? options.status : options;
642+
643+
const result = <T extends { status: number }>(struct: T) => {
644+
return json(
645+
{ type, ...struct },
646+
{
647+
status: struct.status,
648+
headers:
649+
typeof options === 'object' && options.message
650+
? {
651+
'Set-Cookie': `flash=${encodeURIComponent(
652+
JSON.stringify(options.message)
653+
)}; Path=/; Max-Age=120`
654+
}
655+
: undefined
656+
}
657+
);
658+
};
659+
660+
if (type == 'error') {
661+
return result({
662+
status: status || 500,
663+
error: typeof data === 'string' ? { message: data } : data
664+
});
665+
} else if (type == 'redirect') {
666+
return result({
667+
status: status || 303,
668+
location: data
669+
});
670+
} else if (type == 'failure') {
671+
return result({
672+
status: status || 400,
673+
data: stringify(data)
674+
});
675+
} else {
676+
return result({ status: status || 200, data: stringify(data) });
677+
}
678+
}

0 commit comments

Comments
 (0)