Skip to content

Commit 776aa5f

Browse files
committed
Type inference finally works for schemasafe
1 parent 28f4c99 commit 776aa5f

File tree

7 files changed

+179
-27
lines changed

7 files changed

+179
-27
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"json-schema",
1414
"arktype",
1515
"joi",
16+
"schemasafe",
1617
"typebox",
1718
"valibot",
1819
"vinejs",

src/lib/adapters/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export { valibot, valibotClient } from './valibot.js';
88
export { yup, yupClient } from './yup.js';
99
export { zod, zodClient } from './zod.js';
1010
export { vine, vineClient } from './vine.js';
11+
export { schemasafe, schemasafeClient } from './schemasafe.js';
1112

1213
/*
1314
// Cannot use due to moduleResolution problem: https://github.com/ianstormtaylor/superstruct/issues/1200

src/lib/adapters/schemasafe.ts

Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { memoize } from '$lib/memoize.js';
22
import {
33
createAdapter,
44
type AdapterOptions,
5-
type ValidationAdapter,
6-
type ValidationResult
5+
type ClientValidationAdapter,
6+
type ValidationAdapter
77
} from './adapters.js';
88
import {
99
validator,
@@ -15,60 +15,103 @@ import {
1515
import type { FromSchema, JSONSchema } from 'json-schema-to-ts';
1616
import type { JSONSchema as JSONSchema7 } from '$lib/jsonSchema/index.js';
1717

18+
/*
19+
* Adapter specificts:
20+
* Type inference problem unless this is applied:
21+
* https://github.com/ThomasAribart/json-schema-to-ts/blob/main/documentation/FAQs/applying-from-schema-on-generics.md
22+
* Must duplicate validate method, otherwise the above type inference will fail.
23+
*/
24+
1825
const Email =
1926
/^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
2027

21-
// Type inference problem unless this is applied: https://github.com/ThomasAribart/json-schema-to-ts/blob/main/documentation/FAQs/applying-from-schema-on-generics.md
28+
const defaultOptions = {
29+
formats: {
30+
email: (str: string) => Email.test(str)
31+
},
32+
includeErrors: true,
33+
allErrors: true
34+
};
35+
36+
function cachedValidator(currentSchema: JSONSchema7, config?: ValidatorOptions) {
37+
if (!cache.has(currentSchema)) {
38+
cache.set(
39+
currentSchema,
40+
validator(currentSchema as Schema, {
41+
...defaultOptions,
42+
...config
43+
})
44+
);
45+
}
46+
47+
return cache.get(currentSchema)!;
48+
}
2249

2350
function _schemasafe<
2451
T extends JSONSchema | Record<string, unknown>,
25-
Data = unknown extends FromSchema<T> ? Record<string, unknown> : FromSchema<T>
52+
Data = unknown extends FromSchema<T> ? Record<string, unknown> : FromSchema<T>,
53+
Out = [Data] extends [never] ? Record<string, unknown> : Data
2654
>(
2755
schema: T,
28-
options?: AdapterOptions<Data> & { config?: ValidatorOptions }
29-
): ValidationAdapter<Data> {
56+
options?: AdapterOptions<Out> & { config?: ValidatorOptions }
57+
): ValidationAdapter<Out> {
3058
return createAdapter({
3159
superFormValidationLibrary: 'schemasafe',
3260
jsonSchema: schema as JSONSchema7,
3361
defaults: options?.defaults,
34-
async validate(data: unknown): Promise<ValidationResult<Data>> {
35-
const currentSchema = schema as JSONSchema7;
62+
async validate(data: unknown) {
63+
const validator = cachedValidator(schema as JSONSchema7, options?.config);
64+
const isValid = validator(data as Json);
3665

37-
if (!cache.has(currentSchema)) {
38-
cache.set(
39-
currentSchema,
40-
validator(currentSchema as Schema, {
41-
formats: {
42-
email: (str) => Email.test(str)
43-
},
44-
includeErrors: true,
45-
allErrors: true,
46-
...options?.config
47-
})
48-
);
66+
if (isValid) {
67+
return {
68+
data: data as Out,
69+
success: true
70+
};
4971
}
72+
return {
73+
issues: (validator.errors ?? []).map(({ instanceLocation, keywordLocation }) => ({
74+
message: keywordLocation,
75+
path: instanceLocation.split('/').slice(1)
76+
})),
77+
success: false
78+
};
79+
}
80+
});
81+
}
5082

51-
const _validate = cache.get(currentSchema)!;
52-
53-
const isValid = _validate(data as Json);
83+
function _schemasafeClient<
84+
T extends JSONSchema | Record<string, unknown>,
85+
Data = unknown extends FromSchema<T> ? Record<string, unknown> : FromSchema<T>,
86+
Out = [Data] extends [never] ? Record<string, unknown> : Data
87+
>(
88+
schema: T,
89+
options?: AdapterOptions<Out> & { config?: ValidatorOptions }
90+
): ClientValidationAdapter<Out> {
91+
return {
92+
superFormValidationLibrary: 'schemasafe',
93+
async validate(data: unknown) {
94+
const validator = cachedValidator(schema as JSONSchema7, options?.config);
95+
const isValid = validator(data as Json);
5496

5597
if (isValid) {
5698
return {
57-
data: data as Data,
99+
data: data as Out,
58100
success: true
59101
};
60102
}
61103
return {
62-
issues: (_validate.errors ?? []).map(({ instanceLocation, keywordLocation }) => ({
104+
issues: (validator.errors ?? []).map(({ instanceLocation, keywordLocation }) => ({
63105
message: keywordLocation,
64106
path: instanceLocation.split('/').slice(1)
65107
})),
66108
success: false
67109
};
68110
}
69-
});
111+
};
70112
}
71113

72114
export const schemasafe = /* @__PURE__ */ memoize(_schemasafe);
115+
export const schemasafeClient = /* @__PURE__ */ memoize(_schemasafeClient);
73116

74117
const cache = new WeakMap<JSONSchema7, Validate>();
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { schemasafe } from '$lib/adapters/schemasafe.js';
2+
import { message, superValidate } from '$lib/server/index.js';
3+
import { schema, constSchema } from './schema.js';
4+
import { fail } from '@sveltejs/kit';
5+
6+
export const load = async () => {
7+
const form = await superValidate(schemasafe(schema));
8+
const constForm = await superValidate(schemasafe(constSchema));
9+
10+
return { form, constForm };
11+
};
12+
13+
export const actions = {
14+
default: async ({ request }) => {
15+
const adapter = schemasafe(schema);
16+
const adapter2 = schemasafe(constSchema);
17+
const form = await superValidate(request, adapter);
18+
const form2 = await superValidate(request, adapter2);
19+
20+
if (!form.valid || !form2.valid) {
21+
return fail(400, { form });
22+
}
23+
24+
return message(form, 'Form posted successfully!');
25+
}
26+
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script lang="ts">
2+
import { schemasafeClient } from '$lib/adapters/schemasafe.js';
3+
import { superForm } from '$lib/client/index.js';
4+
import { schema } from './schema.js';
5+
6+
export let data;
7+
8+
const { form } = superForm(data.form);
9+
const { form: form2 } = superForm(data.constForm, {
10+
validators: schemasafeClient(schema)
11+
});
12+
const name: unknown = $form.name;
13+
const name2: string = $form2.name;
14+
</script>
15+
16+
<form method="POST">
17+
<label for="name">Name ({name} {name2})</label>
18+
<input type="text" name="name" bind:value={$form.name} />
19+
20+
<label for="email">E-mail</label>
21+
<input type="email" name="email" bind:value={$form.email} />
22+
23+
<div><button>Submit</button></div>
24+
</form>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { JSONSchema } from 'sveltekit-superforms';
2+
3+
// Define outside the load function so the adapter can be cached
4+
export const schema = {
5+
type: 'object',
6+
properties: {
7+
name: { type: 'string', default: 'Hello world!' },
8+
email: { type: 'string', format: 'email' }
9+
},
10+
required: ['email'],
11+
additionalProperties: false,
12+
$schema: 'http://json-schema.org/draft-07/schema#'
13+
} satisfies JSONSchema;
14+
15+
export const constSchema = {
16+
type: 'object',
17+
properties: {
18+
name: { type: 'string', default: 'Hello world!' },
19+
email: { type: 'string', format: 'email' }
20+
},
21+
required: ['email'],
22+
additionalProperties: false,
23+
$schema: 'http://json-schema.org/draft-07/schema#'
24+
} as const satisfies JSONSchema;

src/tests/superValidate.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import { schemasafe } from '$lib/adapters/schemasafe.js';
5151

5252
import { traversePath } from '$lib/traversal.js';
5353
import { splitPath } from '$lib/stringPath.js';
54-
import { SchemaError } from '$lib/index.js';
54+
import { SchemaError, type JSONSchema } from '$lib/index.js';
5555

5656
///// Test data /////////////////////////////////////////////////////
5757

@@ -274,6 +274,39 @@ describe('Schemasafe', () => {
274274
schemaTest(constAdapter, undefined, 'full', 'string');
275275
schemaTest(adapter, undefined, 'full', 'string');
276276
schemaTest(dynamicAdapter, undefined, 'full', 'string');
277+
278+
it('should work with type inference for superValidate with a request', async () => {
279+
const schema = {
280+
type: 'object',
281+
properties: {
282+
name: { type: 'string', default: 'Hello world!' },
283+
email: { type: 'string', format: 'email' }
284+
},
285+
required: ['email'],
286+
additionalProperties: false,
287+
$schema: 'http://json-schema.org/draft-07/schema#'
288+
} as const satisfies JSONSchema;
289+
290+
const formData = new FormData();
291+
formData.set('name', 'Test');
292+
formData.set('email', 'test');
293+
294+
const request = new Request('https://example.com', {
295+
method: 'POST',
296+
body: formData
297+
});
298+
299+
//const data = await request.formData();
300+
//console.log('🚀 ~ it ~ data:', data);
301+
302+
const adapter = schemasafe(schema);
303+
const form = await superValidate(request, adapter);
304+
console.log('🚀 ~ it ~ form:', form);
305+
const email: string = form.data.email;
306+
307+
assert(!form.valid);
308+
expect(email).toBe('test');
309+
});
277310
});
278311

279312
/////////////////////////////////////////////////////////////////////

0 commit comments

Comments
 (0)