Skip to content

Commit 13a28ec

Browse files
committed
feat: implement standard schema support
1 parent fd975d4 commit 13a28ec

32 files changed

+752
-2676
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@
6868
"vite-tsconfig-paths": "^4.3.2",
6969
"vitest": "^1.6.0",
7070
"vue": "^3.4.26",
71-
"yup": "^1.4.0"
71+
"yup": "^1.4.0",
72+
"zod": "^3.22.4"
7273
},
7374
"peerDependencies": {
7475
"vue": "^3.4.26"

packages/joi/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"dist/*.mjs"
4040
],
4141
"dependencies": {
42+
"@standard-schema/spec": "^1.0.0",
4243
"joi": "17.11.0",
4344
"type-fest": "^4.8.3",
4445
"vee-validate": "workspace:*"

packages/joi/src/index.ts

Lines changed: 29 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { TypedSchema, TypedSchemaError } from 'vee-validate';
21
import { AsyncValidationOptions, Schema, ValidationError } from 'joi';
3-
import { merge, normalizeFormPath } from '../../shared';
2+
import { merge } from '../../shared';
43
import { PartialDeep } from 'type-fest';
4+
import { StandardSchemaV1 } from '@standard-schema/spec';
55

66
/**
77
* Gets the type of data from the schema
@@ -19,64 +19,42 @@ export function toTypedSchema<
1919
TSchema extends Schema,
2020
TOutput = DataTypeOf<TSchema>,
2121
TInput = PartialDeep<DataTypeOf<TSchema>>,
22-
>(joiSchema: TSchema, opts?: AsyncValidationOptions): TypedSchema<TInput, TOutput> {
22+
>(joiSchema: TSchema, opts?: AsyncValidationOptions): StandardSchemaV1<TInput, TOutput> {
2323
const validationOptions: AsyncValidationOptions = merge({ abortEarly: false }, opts || {});
2424

25-
const schema: TypedSchema = {
26-
__type: 'VVTypedSchema',
27-
async parse(value) {
28-
try {
29-
const result = await joiSchema.validateAsync(value, validationOptions);
30-
31-
return {
32-
value: result,
33-
errors: [],
34-
};
35-
} catch (err) {
36-
if (!(err instanceof ValidationError)) {
37-
throw err;
25+
const schema: StandardSchemaV1<TInput, TOutput> = {
26+
'~standard': {
27+
vendor: 'vee-validate/joi',
28+
version: 1,
29+
async validate(value) {
30+
try {
31+
const result = await joiSchema.validateAsync(value, validationOptions);
32+
33+
return {
34+
value: result,
35+
issues: undefined,
36+
};
37+
} catch (err) {
38+
if (!(err instanceof ValidationError)) {
39+
throw err;
40+
}
41+
42+
return {
43+
issues: processIssues(err),
44+
};
3845
}
39-
40-
const error = err as ValidationError;
41-
42-
return {
43-
errors: processIssues(error),
44-
rawError: err,
45-
};
46-
}
47-
},
48-
cast(values) {
49-
// Joi doesn't allow us to cast without validating
50-
const result = joiSchema.validate(values);
51-
52-
if (result.error) {
53-
return values;
54-
}
55-
56-
return result.value;
46+
},
5747
},
5848
};
5949

6050
return schema;
6151
}
6252

63-
function processIssues(error: ValidationError): TypedSchemaError[] {
64-
const errors: Record<string, TypedSchemaError> = {};
65-
66-
error.details.forEach(detail => {
67-
const path = normalizeFormPath(detail.path.join('.'));
68-
69-
if (errors[path]) {
70-
errors[path].errors.push(detail.message);
71-
72-
return;
73-
}
74-
75-
errors[path] = {
76-
path,
77-
errors: [detail.message],
53+
function processIssues(error: ValidationError): StandardSchemaV1.Issue[] {
54+
return error.details.map<StandardSchemaV1.Issue>(detail => {
55+
return {
56+
path: detail.path,
57+
message: detail.message,
7858
};
7959
});
80-
81-
return Object.values(errors);
8260
}

packages/joi/tests/joi.spec.ts

Lines changed: 0 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -209,118 +209,6 @@ test('validates typed schema form with joi', async () => {
209209
expect(passwordError.textContent).toBe('');
210210
});
211211

212-
test('uses joi default values for submission', async () => {
213-
const submitSpy = vi.fn();
214-
215-
mountWithHoc({
216-
setup() {
217-
const schema = toTypedSchema(
218-
Joi.object({
219-
age: Joi.number().default(11),
220-
}),
221-
);
222-
223-
const { handleSubmit } = useForm({
224-
validationSchema: schema,
225-
});
226-
227-
// submit now
228-
handleSubmit(submitSpy)();
229-
230-
return {
231-
schema,
232-
};
233-
},
234-
template: `<div></div>`,
235-
});
236-
237-
await flushPromises();
238-
expect(submitSpy).toHaveBeenCalledTimes(1);
239-
expect(submitSpy).toHaveBeenLastCalledWith(
240-
expect.objectContaining({
241-
age: 11,
242-
}),
243-
expect.anything(),
244-
);
245-
});
246-
247-
test('uses joi default values for initial values', async () => {
248-
const initialSpy = vi.fn();
249-
mountWithHoc({
250-
setup() {
251-
const schema = toTypedSchema(
252-
Joi.object({
253-
name: Joi.string().default('test'),
254-
age: Joi.number().default(11),
255-
unknownKey: Joi.string(),
256-
object: Joi.object({
257-
nestedKey: Joi.string(),
258-
nestedDefault: Joi.string().default('nested'),
259-
}).default(),
260-
}),
261-
);
262-
263-
const { values } = useForm({
264-
validationSchema: schema,
265-
});
266-
267-
// submit now
268-
initialSpy(values);
269-
270-
return {
271-
schema,
272-
};
273-
},
274-
template: `<div></div>`,
275-
});
276-
277-
await flushPromises();
278-
expect(initialSpy).toHaveBeenCalledTimes(1);
279-
expect(initialSpy).toHaveBeenLastCalledWith(
280-
expect.objectContaining({
281-
age: 11,
282-
name: 'test',
283-
object: {
284-
nestedDefault: 'nested',
285-
},
286-
}),
287-
);
288-
});
289-
290-
test('reset form should cast the values', async () => {
291-
const valueSpy = vi.fn();
292-
mountWithHoc({
293-
setup() {
294-
const schema = toTypedSchema(
295-
Joi.object({
296-
age: Joi.number(),
297-
}),
298-
);
299-
300-
const { values, resetForm } = useForm({
301-
validationSchema: schema,
302-
});
303-
304-
resetForm({ values: { age: '12' } });
305-
// submit now
306-
valueSpy(values);
307-
308-
return {
309-
schema,
310-
};
311-
},
312-
template: `<div></div>`,
313-
});
314-
315-
await flushPromises();
316-
expect(valueSpy).toHaveBeenCalledTimes(1);
317-
expect(valueSpy).toHaveBeenLastCalledWith(
318-
expect.objectContaining({
319-
age: 12,
320-
}),
321-
);
322-
});
323-
324212
// #4186
325213
test('default values should not be undefined', async () => {
326214
const initialSpy = vi.fn();

packages/nuxt/src/module.ts

Lines changed: 1 addition & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { defineNuxtModule, addComponent, addImports, logger, resolveModule } from '@nuxt/kit';
1+
import { defineNuxtModule, addComponent, addImports, resolveModule } from '@nuxt/kit';
22
import type { Nuxt, NuxtModule } from '@nuxt/schema';
3-
import { isPackageExists } from 'local-pkg';
43

54
type ComponentName = 'Field' | 'Form' | 'ErrorMessage' | 'FieldArray';
65
type TypedSchemaPackage = 'yup' | 'zod' | 'valibot' | 'none';
@@ -37,13 +36,6 @@ const composables = [
3736
'useValidateForm',
3837
];
3938

40-
const schemaProviders = ['zod', 'yup', 'valibot'] as const;
41-
const schemaProviderResolvers: Record<(typeof schemaProviders)[number], string> = {
42-
zod: '@vee-validate/zod',
43-
yup: '@vee-validate/yup',
44-
valibot: '@vee-validate/valibot',
45-
};
46-
4739
export default defineNuxtModule<VeeValidateNuxtOptions>({
4840
meta: {
4941
name: 'vee-validate',
@@ -78,106 +70,9 @@ export default defineNuxtModule<VeeValidateNuxtOptions>({
7870
if (options.typedSchemaPackage === 'none') {
7971
return;
8072
}
81-
82-
if (options.typedSchemaPackage === 'yup') {
83-
checkForYup(options, nuxt);
84-
return;
85-
}
86-
87-
if (options.typedSchemaPackage === 'zod') {
88-
checkForZod(options, nuxt);
89-
return;
90-
}
91-
92-
if (options.typedSchemaPackage === 'valibot') {
93-
checkForValibot(options, nuxt);
94-
return;
95-
}
96-
97-
if (!checkForYup(options, nuxt)) {
98-
if (!checkForZod(options, nuxt)) {
99-
checkForValibot(options, nuxt);
100-
}
101-
}
10273
},
10374
}) as NuxtModule<VeeValidateNuxtOptions>;
10475

105-
function checkSchemaResolverDependencies(pkgName: (typeof schemaProviders)[number]) {
106-
const makeMsg = (installed: string, uninstalled: string) =>
107-
`You seem to be using ${installed}, but you have not installed ${uninstalled}. Please install it to use ${pkgName} with vee-validate.`;
108-
109-
const resolverPkg = schemaProviderResolvers[pkgName];
110-
if (isPackageExists(pkgName) && !isPackageExists(resolverPkg)) {
111-
logger.warn(makeMsg(pkgName, resolverPkg));
112-
return true;
113-
}
114-
115-
if (isPackageExists(resolverPkg) && !isPackageExists(pkgName)) {
116-
logger.warn(makeMsg(resolverPkg, pkgName));
117-
return true;
118-
}
119-
}
120-
121-
function checkForValibot(options: VeeValidateNuxtOptions, nuxt: Nuxt) {
122-
checkSchemaResolverDependencies('valibot');
123-
if (isPackageExists('@vee-validate/valibot') && isPackageExists('valibot')) {
124-
logger.info('Using valibot with vee-validate');
125-
if (options.autoImports) {
126-
addImports({
127-
name: 'toTypedSchema',
128-
as: 'toTypedSchema',
129-
from: '@vee-validate/valibot',
130-
});
131-
}
132-
133-
addMjsAlias('@vee-validate/valibot', 'vee-validate-valibot', nuxt);
134-
135-
return true;
136-
}
137-
138-
return false;
139-
}
140-
141-
function checkForZod(options: VeeValidateNuxtOptions, nuxt: Nuxt) {
142-
checkSchemaResolverDependencies('zod');
143-
if (isPackageExists('@vee-validate/zod') && isPackageExists('zod')) {
144-
logger.info('Using zod with vee-validate');
145-
if (options.autoImports) {
146-
addImports({
147-
name: 'toTypedSchema',
148-
as: 'toTypedSchema',
149-
from: '@vee-validate/zod',
150-
});
151-
}
152-
153-
addMjsAlias('@vee-validate/zod', 'vee-validate-zod', nuxt);
154-
155-
return true;
156-
}
157-
158-
return false;
159-
}
160-
161-
function checkForYup(options: VeeValidateNuxtOptions, nuxt: Nuxt) {
162-
checkSchemaResolverDependencies('yup');
163-
if (isPackageExists('@vee-validate/yup') && isPackageExists('yup')) {
164-
logger.info('Using yup with vee-validate');
165-
if (options.autoImports) {
166-
addImports({
167-
name: 'toTypedSchema',
168-
as: 'toTypedSchema',
169-
from: '@vee-validate/yup',
170-
});
171-
}
172-
173-
addMjsAlias('@vee-validate/yup', 'vee-validate-yup', nuxt);
174-
175-
return true;
176-
}
177-
178-
return false;
179-
}
180-
18176
function addMjsAlias(pkgName: string, fileName: string, nuxt: Nuxt) {
18277
// FIXME: Deprecated, idk why since it duplicate imports
18378
nuxt.options.alias[pkgName] =

packages/rules/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"dist/*.mjs"
3939
],
4040
"dependencies": {
41+
"@standard-schema/spec": "^1.0.0",
4142
"vee-validate": "workspace:*"
4243
}
4344
}

0 commit comments

Comments
 (0)