feat: Reinitialize frontend with SvelteKit and TypeScript

- Delete old Vite+Svelte frontend
- Initialize new SvelteKit project with TypeScript
- Configure Tailwind CSS v4 + DaisyUI
- Implement JWT authentication with auto-refresh
- Create login page with form validation (Zod)
- Add protected route guards
- Update Docker configuration for single-stage build
- Add E2E tests with Playwright (6/11 passing)
- Fix Svelte 5 reactivity with $state() runes

Known issues:
- 5 E2E tests failing (timing/async issues)
- Token refresh implementation needs debugging
- Validation error display timing
This commit is contained in:
2026-02-17 16:19:59 -05:00
parent 54df6018f5
commit de2d83092e
28274 changed files with 3816354 additions and 90 deletions

View File

@@ -0,0 +1,42 @@
import type { JSONSchema } from '../jsonSchema/index.js';
import { type SchemaShape } from '../jsonSchema/schemaShape.js';
import type { InputConstraints } from '../jsonSchema/constraints.js';
import type { Schema, ValidationResult, Infer as InferSchema, InferIn as InferInSchema, Registry } from './typeSchema.js';
export type { Schema, ValidationIssue, ValidationResult } from './typeSchema.js';
export type Infer<T extends Schema, K extends keyof Registry = keyof Registry> = NonNullable<InferSchema<T, K>>;
export type InferIn<T extends Schema, K extends keyof Registry = keyof Registry> = NonNullable<InferInSchema<T, K>>;
export type ValidationLibrary = 'arktype' | 'classvalidator' | 'custom' | 'joi' | 'superform' | 'typebox' | 'valibot' | 'yup' | 'zod' | 'zod4' | 'vine' | 'schemasafe' | 'superstruct' | 'effect';
export type AdapterOptions<T> = {
jsonSchema?: JSONSchema;
defaults?: T;
};
export type RequiredDefaultsOptions<T> = {
defaults: T;
jsonSchema?: JSONSchema;
};
export type ClientValidationAdapter<Out, In = Out> = {
superFormValidationLibrary: ValidationLibrary;
validate: (data: unknown) => Promise<ValidationResult<Out>>;
shape?: SchemaShape;
};
type BaseValidationAdapter<Out, In = Out> = ClientValidationAdapter<Out, In> & {
jsonSchema: JSONSchema;
defaults?: Out;
constraints?: InputConstraints<Out>;
};
export type ValidationAdapter<Out, In = Out> = BaseValidationAdapter<Out, In> & {
defaults: Out;
constraints: InputConstraints<Out>;
shape: SchemaShape;
id: string;
};
/**
* If the adapter options doesn't have a "defaults" or "jsonSchema" fields,
* this is a convenient function for creating a JSON schema.
* If no transformer exist for the adapter, use RequiredDefaultsOptions.
* @see {AdapterOptions}
* @see {RequiredDefaultsOptions}
* @__NO_SIDE_EFFECTS__
*/
export declare function createJsonSchema(options: object, transformer?: () => JSONSchema): {};
export declare function createAdapter<Out, In>(adapter: BaseValidationAdapter<Out, In>, jsonSchema?: JSONSchema): ValidationAdapter<Out, In>;

View File

@@ -0,0 +1,41 @@
import { constraints as schemaConstraints } from '../jsonSchema/constraints.js';
import { defaultValues } from '../jsonSchema/schemaDefaults.js';
import { schemaShape } from '../jsonSchema/schemaShape.js';
import { schemaHash } from '../jsonSchema/schemaHash.js';
import { SuperFormError } from '../errors.js';
import { simpleSchema } from './simple-schema/index.js';
/**
* If the adapter options doesn't have a "defaults" or "jsonSchema" fields,
* this is a convenient function for creating a JSON schema.
* If no transformer exist for the adapter, use RequiredDefaultsOptions.
* @see {AdapterOptions}
* @see {RequiredDefaultsOptions}
* @__NO_SIDE_EFFECTS__
*/
export function createJsonSchema(options, transformer) {
return 'jsonSchema' in options && options.jsonSchema
? options.jsonSchema
: !transformer && 'defaults' in options && options.defaults
? simpleSchema(options.defaults)
: transformer
? /* @__PURE__ */ transformer()
: () => {
throw new SuperFormError('The "defaults" option is required for this adapter.');
};
}
/* @__NO_SIDE_EFFECTS__ */
export function createAdapter(adapter, jsonSchema) {
if (!adapter || !('superFormValidationLibrary' in adapter)) {
throw new SuperFormError('Superforms v2 requires a validation adapter for the schema. ' +
'Import one of your choice from "sveltekit-superforms/adapters" and wrap the schema with it.');
}
if (!jsonSchema)
jsonSchema = adapter.jsonSchema;
return {
...adapter,
constraints: adapter.constraints ?? schemaConstraints(jsonSchema),
defaults: adapter.defaults ?? defaultValues(jsonSchema),
shape: schemaShape(jsonSchema),
id: schemaHash(jsonSchema)
};
}

View File

@@ -0,0 +1,10 @@
import type { type } from 'arktype';
import type { JSONSchema7 } from 'json-schema';
import { type AdapterOptions, type ClientValidationAdapter, type Infer, type ValidationAdapter } from './adapters.js';
type Options = Parameters<type.Any['toJsonSchema']>[0];
export declare const arktypeToJSONSchema: <S extends type.Any>(schema: S, options?: Options) => JSONSchema7;
export declare const arktype: <T extends type.Any>(schema: T, options?: (AdapterOptions<Infer<T, "arktype">> & {
config?: Options;
}) | undefined) => ValidationAdapter<T["infer"], T["inferIn"]>;
export declare const arktypeClient: <T extends type.Any>(schema: T) => ClientValidationAdapter<T["infer"], T["inferIn"]>;
export {};

View File

@@ -0,0 +1,79 @@
import { defaultValues } from '../jsonSchema/schemaDefaults.js';
import { memoize } from '../memoize.js';
import { createAdapter } from './adapters.js';
async function modules() {
const { type } = await import(/* webpackIgnore: true */ 'arktype');
return { type };
}
const fetchModule = /* @__PURE__ */ memoize(modules);
const defaultJSONSchemaOptions = {
fallback: {
default: (ctx) => {
if ('domain' in ctx && ctx.domain === 'bigint') {
return {
...ctx.base,
type: 'string',
format: 'bigint'
};
}
return ctx.base;
},
date: (ctx) => ({
...ctx.base,
type: 'string',
format: 'date-time',
description: ctx.after ? `after ${ctx.after}` : 'anytime'
})
}
};
/* @__NO_SIDE_EFFECTS__ */
export const arktypeToJSONSchema = (schema, options) => {
return schema.toJsonSchema({ ...defaultJSONSchemaOptions, ...options });
};
/**
* Creates defaults for ArkType schemas by filtering out undefined values.
*
* ArkType with exactOptionalPropertyTypes requires optional properties to be
* omitted rather than set to undefined.
*
* @param jsonSchema - JSON schema to generate defaults from
* @returns Default values object without undefined properties
*/
function createArktypeDefaults(jsonSchema) {
return Object.fromEntries(Object.entries(defaultValues(jsonSchema)).filter(([, value]) => value !== undefined));
}
async function _validate(schema, data) {
const { type } = await fetchModule();
const result = schema(data);
if (!(result instanceof type.errors)) {
return {
data: result,
success: true
};
}
const issues = [];
for (const error of result) {
issues.push({ message: error.problem, path: Array.from(error.path) });
}
return {
issues,
success: false
};
}
function _arktype(schema, options) {
const jsonSchema = options?.jsonSchema ?? arktypeToJSONSchema(schema, options?.config);
return createAdapter({
superFormValidationLibrary: 'arktype',
defaults: options?.defaults ?? createArktypeDefaults(jsonSchema),
jsonSchema,
validate: async (data) => _validate(schema, data)
});
}
function _arktypeClient(schema) {
return {
superFormValidationLibrary: 'arktype',
validate: async (data) => _validate(schema, data)
};
}
export const arktype = /* @__PURE__ */ memoize(_arktype);
export const arktypeClient = /* @__PURE__ */ memoize(_arktypeClient);

View File

@@ -0,0 +1,4 @@
import { type ValidationAdapter, type Infer, type InferIn, type ClientValidationAdapter, type RequiredDefaultsOptions } from './adapters.js';
import type { Schema } from '@typeschema/class-validator';
export declare const classvalidator: <T extends Schema>(schema: T, options: RequiredDefaultsOptions<Infer<T, "classvalidator">>) => ValidationAdapter<Infer<T, "classvalidator">, InferIn<T, "classvalidator">>;
export declare const classvalidatorClient: <T extends Schema>(schema: T) => ClientValidationAdapter<Infer<T, "classvalidator">, InferIn<T, "classvalidator">>;

View File

@@ -0,0 +1,40 @@
import { createAdapter, createJsonSchema } from './adapters.js';
import { memoize } from '../memoize.js';
async function modules() {
const { validate } = await import(/* webpackIgnore: true */ '@typeschema/class-validator');
return { validate };
}
const fetchModule = /* @__PURE__ */ memoize(modules);
async function validate(schema, data) {
const { validate } = await fetchModule();
const result = await validate(schema, data);
if (result.success) {
return {
data: result.data,
success: true
};
}
return {
issues: result.issues.map(({ message, path }) => ({
message,
path
})),
success: false
};
}
function _classvalidator(schema, options) {
return createAdapter({
superFormValidationLibrary: 'classvalidator',
validate: async (data) => validate(schema, data),
jsonSchema: createJsonSchema(options),
defaults: options.defaults
});
}
function _classvalidatorClient(schema) {
return {
superFormValidationLibrary: 'classvalidator',
validate: async (data) => validate(schema, data)
};
}
export const classvalidator = /* @__PURE__ */ memoize(_classvalidator);
export const classvalidatorClient = /* @__PURE__ */ memoize(_classvalidatorClient);

View File

@@ -0,0 +1,13 @@
import { Schema } from 'effect';
import type { ParseOptions } from 'effect/SchemaAST';
import type { JSONSchema as TJSONSchema } from '../jsonSchema/index.js';
import { type AdapterOptions, type ClientValidationAdapter, type Infer, type InferIn, type ValidationAdapter } from './adapters.js';
export declare const effectToJSONSchema: <A, I>(schema: Schema.Schema<A, I>) => TJSONSchema;
type AnySchema = Schema.Schema<any, any>;
export declare const effect: <T extends AnySchema>(schema: T, options?: (AdapterOptions<Infer<T, "effect">> & {
parseOptions?: ParseOptions;
}) | undefined) => ValidationAdapter<Infer<T, "effect">, InferIn<T, "effect">>;
export declare const effectClient: <T extends AnySchema>(schema: T, options?: (AdapterOptions<Infer<T, "effect">> & {
parseOptions?: ParseOptions;
}) | undefined) => ClientValidationAdapter<Infer<T, "effect">, InferIn<T, "effect">>;
export {};

View File

@@ -0,0 +1,41 @@
import { Schema, JSONSchema, Either } from 'effect';
import { ArrayFormatter } from 'effect/ParseResult';
import { createAdapter } from './adapters.js';
import { memoize } from '../memoize.js';
export const effectToJSONSchema = (schema) => {
// effect's json schema type is slightly different so we have to cast it
return JSONSchema.make(schema);
};
async function validate(schema, data, options) {
const result = Schema.decodeUnknownEither(schema, { errors: 'all' })(data, options?.parseOptions);
if (Either.isRight(result)) {
return {
data: result.right,
success: true
};
}
return {
// get rid of the _tag property
issues: ArrayFormatter.formatErrorSync(result.left).map(({ message, path }) => ({
message,
path: [...path] // path is readonly array so we have to copy it
})),
success: false
};
}
function _effect(schema, options) {
return createAdapter({
superFormValidationLibrary: 'effect',
validate: async (data) => validate(schema, data, options),
jsonSchema: options?.jsonSchema ?? effectToJSONSchema(schema),
defaults: options?.defaults
});
}
function _effectClient(schema, options) {
return {
superFormValidationLibrary: 'effect',
validate: async (data) => validate(schema, data, options)
};
}
export const effect = /* @__PURE__ */ memoize(_effect);
export const effectClient = /* @__PURE__ */ memoize(_effectClient);

View File

@@ -0,0 +1,14 @@
export type { ValidationAdapter, ClientValidationAdapter, Infer, InferIn } from './adapters.js';
export { arktype, arktypeClient } from './arktype.js';
export { classvalidator, classvalidatorClient } from './classvalidator.js';
export { effect, effectClient } from './effect.js';
export { joi, joiClient } from './joi.js';
export { superformClient } from './superform.js';
export { typebox, typeboxClient } from './typebox.js';
export { valibot, valibotClient } from './valibot.js';
export { yup, yupClient } from './yup.js';
export { zod, zodClient, type ZodValidation, type ZodObjectTypes, type ZodObjectType } from './zod.js';
export { zod as zod4, zodClient as zod4Client, type ZodValidationSchema } from './zod4.js';
export { vine, vineClient } from './vine.js';
export { schemasafe, schemasafeClient } from './schemasafe.js';
export { superstruct, superstructClient } from './superstruct.js';

View File

@@ -0,0 +1,17 @@
export { arktype, arktypeClient } from './arktype.js';
export { classvalidator, classvalidatorClient } from './classvalidator.js';
export { effect, effectClient } from './effect.js';
export { joi, joiClient } from './joi.js';
export { superformClient } from './superform.js';
export { typebox, typeboxClient } from './typebox.js';
export { valibot, valibotClient } from './valibot.js';
export { yup, yupClient } from './yup.js';
export { zod, zodClient } from './zod.js';
export { zod as zod4, zodClient as zod4Client } from './zod4.js';
export { vine, vineClient } from './vine.js';
export { schemasafe, schemasafeClient } from './schemasafe.js';
export { superstruct, superstructClient } from './superstruct.js';
/*
// Cannot use ajv due to not being ESM compatible.
export { ajv } from './ajv.js';
*/

View File

@@ -0,0 +1,28 @@
import type { JSONSchema } from '../../jsonSchema/index.js';
type Joi = Record<string, any>;
type Transformer = (schema: JSONSchema, joi: Joi, transformer?: Transformer) => JSONSchema;
/**
* Converts the supplied joi validation object into a JSON schema object,
* optionally applying a transformation.
*
* @param {JoiValidation} joi
* @param {TransformFunction} [transformer=null]
* @returns {JSONSchema}
*/
declare function convert(joi: Record<string, any>, transformer?: Transformer): JSONSchema;
declare namespace convert {
var TYPES: Record<string, Transformer>;
}
export default convert;
/**
* Joi Validation Object
* @typedef {object} JoiValidation
*/
/**
* Transformation Function - applied just before `convert()` returns and called as `function(object):object`
* @typedef {function} TransformFunction
*/
/**
* JSON Schema Object
* @typedef {object} JSONSchema
*/

View File

@@ -0,0 +1,282 @@
// Taken from https://github.com/lightsofapollo/joi-to-json-schema and converted to ESM
// TODO: Need more tests
function assert(condition, errorMessage) {
if (!condition)
throw new Error(errorMessage);
}
const TYPES = {
alternatives: (schema, joi, transformer) => {
const result = (schema.oneOf = []);
joi.matches.forEach(function (match) {
if (match.schema) {
return result.push(convert(match.schema, transformer));
}
if (!match.is) {
throw new Error('joi.when requires an "is"');
}
if (!(match.then || match.otherwise)) {
throw new Error('joi.when requires one or both of "then" and "otherwise"');
}
if (match.then) {
result.push(convert(match.then, transformer));
}
if (match.otherwise) {
result.push(convert(match.otherwise, transformer));
}
});
return schema;
},
date: (schema) => {
schema.type = 'Date';
/*
if (joi._flags.timestamp) {
schema.type = 'integer';
schema.format = 'unix-time';
return schema;
}
schema.type = 'string';
schema.format = 'date-time';
*/
return schema;
},
any: (schema) => {
delete schema.type;
//schema.type = ['array', 'boolean', 'number', 'object', 'string', 'null'];
return schema;
},
array: (schema, joi, transformer) => {
schema.type = 'array';
joi._rules?.forEach((test) => {
switch (test.name) {
case 'unique':
schema.uniqueItems = true;
break;
case 'length':
schema.minItems = schema.maxItems = test.args.limit;
break;
case 'min':
schema.minItems = test.args.limit;
break;
case 'max':
schema.maxItems = test.args.limit;
break;
}
});
if (joi.$_terms) {
/*
Ordered is not a part of the spec.
if (joi.$_terms.ordered.length) {
schema.ordered = joi.$_terms.ordered.map((item) => convert(item, transformer));
}
*/
let list;
if (joi.$_terms._inclusions.length) {
list = joi.$_terms._inclusions;
}
else if (joi.$_terms._requireds.length) {
list = joi.$_terms._requireds;
}
if (list) {
schema.items = convert(list[0], transformer);
}
}
return schema;
},
binary: (schema, joi) => {
schema.type = 'string';
schema.contentMediaType =
joi._meta.length > 0 && joi._meta[0].contentMediaType
? joi._meta[0].contentMediaType
: 'text/plain';
schema.contentEncoding = joi._flags.encoding ? joi._flags.encoding : 'binary';
return schema;
},
boolean: (schema) => {
schema.type = 'boolean';
return schema;
},
number: (schema, joi) => {
schema.type = 'number';
joi._rules?.forEach((test) => {
switch (test.name) {
case 'integer':
schema.type = 'integer';
break;
case 'less':
//schema.exclusiveMaximum = true;
//schema.maximum = test.args.limit;
schema.exclusiveMaximum = test.args.limit;
break;
case 'greater':
//schema.exclusiveMinimum = true;
//schema.minimum = test.args.limit;
schema.exclusiveMinimum = test.args.limit;
break;
case 'min':
schema.minimum = test.args.limit;
break;
case 'max':
schema.maximum = test.args.limit;
break;
case 'precision': {
let multipleOf;
if (test.args.limit && test.args.limit > 1) {
multipleOf = JSON.parse('0.' + '0'.repeat(test.args.limit - 1) + '1');
}
else {
multipleOf = 1;
}
schema.multipleOf = multipleOf;
break;
}
}
});
return schema;
},
string: (schema, joi) => {
schema.type = 'string';
joi._rules.forEach((test) => {
switch (test.name) {
case 'email':
schema.format = 'email';
break;
case 'pattern':
case 'regex': {
const arg = test.args;
const pattern = arg && arg.regex ? arg.regex : arg;
schema.pattern = String(pattern).replace(/^\//, '').replace(/\/$/, '');
break;
}
case 'min':
schema.minLength = test.args.limit;
break;
case 'max':
schema.maxLength = test.args.limit;
break;
case 'length':
schema.minLength = schema.maxLength = test.args.limit;
break;
case 'uri':
schema.format = 'uri';
break;
}
});
return schema;
},
object: (schema, joi, transformer) => {
schema.type = 'object';
schema.properties = {};
schema.additionalProperties = Boolean(joi._flags.allowUnknown || !joi._inner.children);
schema.pattern =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
joi.patterns?.map((pattern) => {
return { regex: pattern.regex, rule: convert(pattern.rule, transformer) };
}) ?? [];
if (!joi.$_terms.keys?.length) {
return schema;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
joi.$_terms.keys.forEach((property) => {
if (property.schema._flags.presence !== 'forbidden') {
if (!schema.properties)
schema.properties = {};
schema.properties[property.key] = convert(property.schema, transformer);
if (property.schema._flags.presence === 'required' ||
(property.schema._settings &&
property.schema._settings.presence === 'required' &&
property.schema._flags.presence !== 'optional')) {
schema.required = schema.required || [];
schema.required.push(property.key);
}
}
});
return schema;
}
};
/**
* Converts the supplied joi validation object into a JSON schema object,
* optionally applying a transformation.
*
* @param {JoiValidation} joi
* @param {TransformFunction} [transformer=null]
* @returns {JSONSchema}
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function convert(joi, transformer) {
assert('object' === typeof joi && 'type' in joi, 'requires a joi schema object');
if (!TYPES[joi.type]) {
throw new Error(`sorry, do not know how to convert unknown joi type: "${joi.type}"`);
}
if (transformer) {
assert('function' === typeof transformer, 'transformer must be a function');
}
// JSON Schema root for this type.
const schema = {};
// Copy over the details that all schemas may have...
if (joi._description) {
schema.description = joi._description;
}
if (joi._examples && joi._examples.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
schema.examples = joi._examples.map((e) => e.value);
}
if (joi._examples && joi._examples.length === 1) {
schema.examples = joi._examples[0].value;
}
// Add the label as a title if it exists
if (joi._settings && joi._settings.language && joi._settings.language.label) {
schema.title = joi._settings.language.label;
}
else if (joi._flags && joi._flags.label) {
schema.title = joi._flags.label;
}
// Checking for undefined and null explicitly to allow false and 0 values
if (joi._flags && joi._flags.default !== undefined && joi._flags.default !== null) {
schema['default'] = joi._flags.default;
}
if (joi._valids && joi._valids._set && (joi._valids._set.size || joi._valids._set.length)) {
if (Array.isArray(joi.children) || !joi._flags.allowOnly) {
return {
anyOf: [
{
type: joi.type,
enum: [...joi._valids._set]
},
TYPES[joi.type](schema, joi, transformer)
]
};
}
schema['enum'] = [...joi._valids._set];
}
let result = TYPES[joi.type](schema, joi, transformer);
if (transformer) {
result = transformer(result, joi);
}
if (joi._valids?._values && joi._valids._values.size && !joi._flags.allowOnly) {
const constants = Array.from(joi._valids._values).map((v) => ({
const: v
}));
if (result.anyOf) {
result.anyOf = [...constants, ...result.anyOf];
}
else {
result = { anyOf: [...constants, result] };
}
}
return result;
}
//module.exports = convert;
convert.TYPES = TYPES;
/**
* Joi Validation Object
* @typedef {object} JoiValidation
*/
/**
* Transformation Function - applied just before `convert()` returns and called as `function(object):object`
* @typedef {function} TransformFunction
*/
/**
* JSON Schema Object
* @typedef {object} JSONSchema
*/

View File

@@ -0,0 +1,4 @@
import { type ValidationAdapter, type AdapterOptions, type ClientValidationAdapter, type Infer } from './adapters.js';
import type { ObjectSchema } from 'joi';
export declare const joi: <T extends ObjectSchema>(schema: T, options?: AdapterOptions<Infer<T, "joi">> | undefined) => ValidationAdapter<Record<string, unknown>>;
export declare const joiClient: <T extends ObjectSchema>(schema: T) => ClientValidationAdapter<Record<string, unknown>>;

View File

@@ -0,0 +1,36 @@
import { createAdapter } from './adapters.js';
import { memoize } from '../memoize.js';
import convert from './joi-to-json-schema/index.js';
async function validate(schema, data) {
const result = schema.validate(data, { abortEarly: false });
if (result.error == null) {
return {
data: result.value,
success: true
};
}
return {
issues: result.error.details.map(({ message, path }) => ({
message,
path
})),
success: false
};
}
/* @__NO_SIDE_EFFECTS__ */
function _joi(schema, options) {
return createAdapter({
superFormValidationLibrary: 'joi',
jsonSchema: options?.jsonSchema ?? convert(schema),
defaults: options?.defaults,
validate: async (data) => validate(schema, data)
});
}
function _joiClient(schema) {
return {
superFormValidationLibrary: 'joi',
validate: async (data) => validate(schema, data)
};
}
export const joi = /* @__PURE__ */ memoize(_joi);
export const joiClient = /* @__PURE__ */ memoize(_joiClient);

View File

@@ -0,0 +1,10 @@
import { type AdapterOptions, type ClientValidationAdapter, type ValidationAdapter } from './adapters.js';
import type { ValidatorOptions } from '@exodus/schemasafe';
import type { FromSchema, JSONSchema } from 'json-schema-to-ts';
export declare const schemasafe: <T extends JSONSchema | Record<string, unknown>, Data = unknown extends FromSchema<T> ? Record<string, unknown> : FromSchema<T>, Out = [Data] extends [never] ? Record<string, unknown> : Data>(schema: T, options?: (AdapterOptions<Out> & {
descriptionAsErrors?: boolean;
config?: ValidatorOptions;
}) | undefined) => ValidationAdapter<Out>;
export declare const schemasafeClient: <T extends JSONSchema | Record<string, unknown>, Data = unknown extends FromSchema<T> ? Record<string, unknown> : FromSchema<T>, Out = [Data] extends [never] ? Record<string, unknown> : Data>(schema: T, options?: (AdapterOptions<Out> & {
config?: ValidatorOptions;
}) | undefined) => ClientValidationAdapter<Out>;

View File

@@ -0,0 +1,90 @@
import { memoize } from '../memoize.js';
import { createAdapter } from './adapters.js';
import { pathExists } from '../traversal.js';
async function modules() {
const { validator } = await import(/* webpackIgnore: true */ '@exodus/schemasafe');
return { validator };
}
const fetchModule = /* @__PURE__ */ memoize(modules);
/*
* Adapter specificts:
* 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
* Must duplicate validate method, otherwise the above type inference will fail.
*/
const Email = /^[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;
const defaultOptions = {
formats: {
email: (str) => Email.test(str)
},
includeErrors: true,
allErrors: true
};
async function cachedValidator(currentSchema, config) {
const { validator } = await fetchModule();
if (!cache.has(currentSchema)) {
cache.set(currentSchema, validator(currentSchema, {
...defaultOptions,
...config
}));
}
return cache.get(currentSchema);
}
function _schemasafe(schema, options) {
return createAdapter({
superFormValidationLibrary: 'schemasafe',
jsonSchema: schema,
defaults: options?.defaults,
async validate(data) {
const validator = await cachedValidator(schema, options?.config);
const isValid = validator(data);
if (isValid) {
return {
data: data,
success: true
};
}
return {
issues: (validator.errors ?? []).map(({ instanceLocation, keywordLocation }) => ({
message: options?.descriptionAsErrors
? errorDescription(schema, keywordLocation)
: keywordLocation,
path: instanceLocation.split('/').slice(1)
})),
success: false
};
}
});
}
function _schemasafeClient(schema, options) {
return {
superFormValidationLibrary: 'schemasafe',
async validate(data) {
const validator = await cachedValidator(schema, options?.config);
const isValid = validator(data);
if (isValid) {
return {
data: data,
success: true
};
}
return {
issues: (validator.errors ?? []).map(({ instanceLocation, keywordLocation }) => ({
message: keywordLocation,
path: instanceLocation.split('/').slice(1)
})),
success: false
};
}
};
}
export const schemasafe = /* @__PURE__ */ memoize(_schemasafe);
export const schemasafeClient = /* @__PURE__ */ memoize(_schemasafeClient);
const cache = new WeakMap();
function errorDescription(schema, keywordLocation) {
if (!keywordLocation.startsWith('#/'))
return keywordLocation;
const searchPath = keywordLocation.slice(2).split('/');
const path = pathExists(schema, searchPath);
return path?.parent.description ?? keywordLocation;
}

View File

@@ -0,0 +1,5 @@
import type { JSONSchema } from '../../jsonSchema/index.js';
/**
* Simple JSON Schema generator for validation libraries without introspection.
*/
export declare function simpleSchema(value: unknown): JSONSchema;

View File

@@ -0,0 +1,31 @@
/**
* Simple JSON Schema generator for validation libraries without introspection.
*/
export function simpleSchema(value) {
if (value === null || value === undefined) {
return {};
}
switch (typeof value) {
case 'object': {
if (value instanceof Date) {
return { type: 'integer', format: 'unix-time' };
}
if (Array.isArray(value)) {
const output = { type: 'array' };
output.items = value.length ? simpleSchema(value[0]) : {};
return output;
}
else {
const obj = value;
return {
type: 'object',
properties: Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, simpleSchema(value)])),
required: Object.keys(obj).filter((key) => (!obj[key] && obj[key] !== undefined && obj[key] !== null) ||
(Array.isArray(obj[key]) && !obj[key].length)),
additionalProperties: false
};
}
}
}
return { type: typeof value };
}

View File

@@ -0,0 +1,10 @@
import type { ClientValidationAdapter } from './adapters.js';
import type { MaybePromise } from '../utils.js';
export type Validators<T extends Record<string, unknown>> = {
[P in keyof T]?: T extends any ? T[P] extends Record<string, unknown> ? Validators<T[P]> : NonNullable<T[P]> extends (infer A)[] ? A extends Record<string, unknown> ? Validators<A> : Validator<T[P]> : Validator<T[P]> : never;
};
export type Validator<V> = (value?: V) => MaybePromise<string | string[] | null | undefined>;
/**
* @deprecated This adapter requires you to do error-prone type checking yourself. If possible, use one of the supported validation libraries instead.
*/
export declare const superformClient: <T extends Record<string, unknown>, T2 extends Partial<T> = Partial<T>>(schema: Validators<T2>) => ClientValidationAdapter<T>;

View File

@@ -0,0 +1,64 @@
import { traversePath, traversePaths } from '../traversal.js';
import { memoize } from '../memoize.js';
function _superform(schema) {
return {
superFormValidationLibrary: 'superform',
async validate(data) {
// Add top-level validator fields to non-existing data fields
// so they will be validated even if the field doesn't exist
if (!data || typeof data !== 'object')
data = {};
else
data = { ...data };
const newData = data;
for (const [key, value] of Object.entries(schema)) {
if (typeof value === 'function' && !(key in newData)) {
// Setting undefined fields so they will be validated based on field existance.
newData[key] = undefined;
}
}
const output = [];
function mapErrors(path, errors) {
if (!errors)
return;
if (typeof errors === 'string')
errors = [errors];
errors.forEach((message) => {
output.push({
path,
message
});
});
}
const queue = [];
traversePaths(newData, async ({ value, path }) => {
// Filter out array indices, the validator structure doesn't contain these.
const validationPath = path.filter((p) => /\D/.test(String(p)));
const maybeValidator = traversePath(schema, validationPath);
if (typeof maybeValidator?.value === 'function') {
const check = maybeValidator.value;
queue.push({ path, errors: check(value) });
}
});
const errors = await Promise.all(queue.map((check) => check.errors));
for (let i = 0; i < errors.length; i++) {
mapErrors(queue[i].path, errors[i]);
}
//console.log('Validating', newData);
//console.log(output);
return output.length
? {
success: false,
issues: output
}
: {
success: true,
data: data
};
}
};
}
/**
* @deprecated This adapter requires you to do error-prone type checking yourself. If possible, use one of the supported validation libraries instead.
*/
export const superformClient = /* @__PURE__ */ memoize(_superform);

View File

@@ -0,0 +1,6 @@
import { type ValidationAdapter, type ClientValidationAdapter, type RequiredDefaultsOptions, type Infer } from './adapters.js';
import type { Struct } from 'superstruct';
type StructObject<T extends Record<string, unknown>> = Struct<T, any>;
export declare const superstruct: <T extends StructObject<Infer<T, "superstruct">>>(schema: T, options: RequiredDefaultsOptions<Infer<T, "superstruct">>) => ValidationAdapter<Infer<T, "superstruct">>;
export declare const superstructClient: <T extends StructObject<Infer<T, "superstruct">>>(schema: T) => ClientValidationAdapter<Infer<T, "superstruct">>;
export {};

View File

@@ -0,0 +1,35 @@
import { createAdapter, createJsonSchema } from './adapters.js';
import { memoize } from '../memoize.js';
async function validate(schema, data) {
const result = schema.validate(data, { coerce: true });
if (!result[0]) {
return {
data: result[1],
success: true
};
}
const errors = result[0];
return {
success: false,
issues: errors.failures().map((error) => ({
message: error.message,
path: error.path
}))
};
}
function _superstruct(schema, options) {
return createAdapter({
superFormValidationLibrary: 'superstruct',
defaults: options.defaults,
jsonSchema: createJsonSchema(options),
validate: async (data) => validate(schema, data)
});
}
function _superstructClient(schema) {
return {
superFormValidationLibrary: 'superstruct',
validate: async (data) => validate(schema, data)
};
}
export const superstruct = /* @__PURE__ */ memoize(_superstruct);
export const superstructClient = /* @__PURE__ */ memoize(_superstructClient);

View File

@@ -0,0 +1,144 @@
import type { TSchema, Static as Static$1 } from 'typebox';
import type { type } from 'arktype';
import type { AnySchema } from 'joi';
import type { Infer as ClassValidatorInfer, InferIn as ClassValidatorInferIn, Schema as ClassValidatorSchema } from '@typeschema/class-validator';
import type { GenericSchema, GenericSchemaAsync, InferInput as Input, InferOutput as Output } from 'valibot';
import type { Schema as Schema$2, InferType } from 'yup';
import type { ZodTypeAny, input, output } from 'zod/v3';
import type { $ZodType as $Zod4Type, input as zod4Input, output as zod4Output } from 'zod/v4/core';
import type { SchemaTypes, Infer as VineInfer } from '@vinejs/vine/types';
import type { FromSchema, JSONSchema } from 'json-schema-to-ts';
import type { Struct, Infer as Infer$2 } from 'superstruct';
import type { Schema as Schema$1 } from 'effect';
type Replace<T, From, To> = NonNullable<T> extends From ? To | Exclude<T, From> : NonNullable<T> extends object ? {
[K in keyof T]: Replace<T[K], From, To>;
} : T;
type IfDefined<T> = any extends T ? never : T;
type UnknownIfNever<T> = [T] extends [never] ? unknown : T;
type Schema = {
[K in keyof Registry]: IfDefined<InferSchema<Registry[K]>>;
}[keyof Registry];
interface Resolver<TSchema = unknown> {
schema: TSchema;
input: unknown;
output: unknown;
base: unknown;
}
type InferInput<TResolver extends Resolver, TSchema> = (TResolver & {
schema: TSchema;
})['input'];
type InferOutput<TResolver extends Resolver, TSchema> = (TResolver & {
schema: TSchema;
})['output'];
type InferSchema<TResolver extends Resolver> = TResolver['base'];
type ValidationIssue = {
message: string;
path?: Array<string | number | symbol>;
};
type ValidationResult<TOutput = any> = {
success: true;
data: TOutput;
} | {
success: false;
issues: Array<ValidationIssue>;
};
interface ArkTypeResolver extends Resolver {
base: type.Any;
input: this['schema'] extends type.Any ? this['schema']['inferIn'] : never;
output: this['schema'] extends type.Any ? this['schema']['infer'] : never;
}
interface ClassValidatorResolver extends Resolver {
base: ClassValidatorSchema;
input: this['schema'] extends ClassValidatorSchema ? ClassValidatorInferIn<this['schema']> : never;
output: this['schema'] extends ClassValidatorSchema ? ClassValidatorInfer<this['schema']> : never;
}
type CustomSchema<T = any> = (data: unknown) => Promise<T> | T;
interface CustomResolver extends Resolver {
base: CustomSchema;
input: this['schema'] extends CustomSchema ? this['schema'] extends {
_def: unknown;
} ? never : keyof this['schema'] extends never ? Awaited<ReturnType<this['schema']>> : never : never;
output: this['schema'] extends CustomSchema ? this['schema'] extends {
_def: unknown;
} ? never : keyof this['schema'] extends never ? Awaited<ReturnType<this['schema']>> : never : never;
}
interface JoiResolver extends Resolver {
base: AnySchema;
}
interface TypeBoxResolver extends Resolver {
base: TSchema & {
'~kind': string;
};
input: this['schema'] extends TSchema & {
'~kind': string;
} ? Static$1<this['schema']> : never;
output: this['schema'] extends TSchema & {
'~kind': string;
} ? Static$1<this['schema']> : never;
}
interface ValibotResolver extends Resolver {
base: GenericSchema | GenericSchemaAsync;
input: this['schema'] extends GenericSchema | GenericSchemaAsync ? Input<this['schema']> : never;
output: this['schema'] extends GenericSchema | GenericSchemaAsync ? Output<this['schema']> : never;
}
interface YupResolver extends Resolver {
base: Schema$2;
input: this['schema'] extends Schema$2 ? InferType<this['schema']> : never;
output: this['schema'] extends Schema$2 ? InferType<this['schema']> : never;
}
interface ZodResolver extends Resolver {
base: ZodTypeAny;
input: this['schema'] extends ZodTypeAny ? input<this['schema']> : never;
output: this['schema'] extends ZodTypeAny ? output<this['schema']> : never;
}
interface Zod4Resolver extends Resolver {
base: $Zod4Type;
input: this['schema'] extends $Zod4Type ? zod4Input<this['schema']> : never;
output: this['schema'] extends $Zod4Type ? zod4Output<this['schema']> : never;
}
interface VineResolver extends Resolver {
base: SchemaTypes;
input: this['schema'] extends SchemaTypes ? Replace<VineInfer<this['schema']>, Date, string> : never;
output: this['schema'] extends SchemaTypes ? VineInfer<this['schema']> : never;
}
interface SchemasafeResolver<Schema extends JSONSchema, Data = FromSchema<Schema>> extends Resolver {
base: JSONSchema;
input: this['schema'] extends Schema ? Data : never;
output: this['schema'] extends Schema ? Data : never;
}
interface SuperstructResolver extends Resolver {
base: Struct<any, any>;
input: this['schema'] extends Struct<any, any> ? Infer$2<this['schema']> : never;
output: this['schema'] extends Struct<any, any> ? Infer$2<this['schema']> : never;
}
interface EffectResolver extends Resolver {
base: Schema$1.Schema<any>;
input: this['schema'] extends Schema$1.Schema<any> ? Schema$1.Schema.Encoded<this['schema']> : never;
output: this['schema'] extends Schema$1.Schema<any> ? Schema$1.Schema.Type<this['schema']> : never;
}
export type Registry = {
arktype: ArkTypeResolver;
classvalidator: ClassValidatorResolver;
custom: CustomResolver;
joi: JoiResolver;
typebox: TypeBoxResolver;
valibot: ValibotResolver;
yup: YupResolver;
zod: ZodResolver;
zod4: Zod4Resolver;
vine: VineResolver;
schemasafe: SchemasafeResolver<JSONSchema>;
superstruct: SuperstructResolver;
effect: EffectResolver;
};
type Infer<TSchema extends Schema, Keys extends keyof Registry = keyof Registry> = UnknownIfNever<TSchema extends {
_zod: unknown;
} ? IfDefined<InferOutput<Registry['zod4'], TSchema>> : {
[K in Keys]: IfDefined<InferOutput<Registry[K], TSchema>>;
}[Keys]>;
type InferIn<TSchema extends Schema, Keys extends keyof Registry = keyof Registry> = UnknownIfNever<TSchema extends {
_zod: unknown;
} ? IfDefined<InferInput<Registry['zod4'], TSchema>> : {
[K in Keys]: IfDefined<InferInput<Registry[K], TSchema>>;
}[Keys]>;
export type { Infer, InferIn, Schema, ValidationIssue, ValidationResult };

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,17 @@
import { type ValidationAdapter, type Infer, type InferIn, type ClientValidationAdapter } from './adapters.js';
import type { TSchema } from 'typebox';
import Type from 'typebox';
/**
* Custom TypeBox type for Date for testing purposes.
*/
export declare class TDate extends Type.Base<globalThis.Date> {
Check(value: unknown): value is globalThis.Date;
Errors(value: unknown): object[];
Create(): globalThis.Date;
}
/**
* Custom TypeBox type for Date for testing purposes.
*/
export declare function Date(): TDate;
export declare const typebox: <T extends TSchema>(schema: T) => ValidationAdapter<Infer<T, "typebox">, InferIn<T, "typebox">>;
export declare const typeboxClient: <T extends TSchema>(schema: T) => ClientValidationAdapter<Infer<T, "typebox">, InferIn<T, "typebox">>;

View File

@@ -0,0 +1,69 @@
import { createAdapter } from './adapters.js';
import { memoize } from '../memoize.js';
import Type from 'typebox';
/**
* Custom TypeBox type for Date for testing purposes.
*/
export class TDate extends Type.Base {
Check(value) {
return value instanceof globalThis.Date;
}
Errors(value) {
return this.Check(value) ? [] : [{ message: 'must be Date' }];
}
Create() {
return new globalThis.Date(0);
}
}
/**
* Custom TypeBox type for Date for testing purposes.
*/
export function Date() {
return new TDate();
}
// From https://github.com/sinclairzx81/typebox/tree/ca4d771b87ee1f8e953036c95a21da7150786d3e/example/formats
const Email = /^[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;
async function modules() {
const { Compile } = await import(/* webpackIgnore: true */ 'typebox/compile');
const Format = await import(/* webpackIgnore: true */ 'typebox/format');
return { Compile, Format };
}
const fetchModule = /* @__PURE__ */ memoize(modules);
async function validate(schema, data) {
const { Compile, Format } = await fetchModule();
if (!compiled.has(schema)) {
compiled.set(schema, Compile(schema));
}
if (!Format.Has('email')) {
Format.Set('email', (value) => Email.test(value));
}
const validator = compiled.get(schema);
const errors = [...(validator?.Errors(data) ?? [])];
if (!errors.length) {
return { success: true, data: data };
}
return {
success: false,
issues: errors.map((issue) => ({
path: issue.instancePath ? issue.instancePath.substring(1).split('/') : [],
message: issue.message
}))
};
}
function _typebox(schema) {
return createAdapter({
superFormValidationLibrary: 'typebox',
validate: async (data) => validate(schema, data),
jsonSchema: schema
});
}
function _typeboxClient(schema) {
return {
superFormValidationLibrary: 'typebox',
validate: async (data) => validate(schema, data)
};
}
export const typebox = /* @__PURE__ */ memoize(_typebox);
export const typeboxClient = /* @__PURE__ */ memoize(_typeboxClient);
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
const compiled = new WeakMap();

View File

@@ -0,0 +1,15 @@
import { type ValidationAdapter, type AdapterOptions, type Infer, type InferIn, type ClientValidationAdapter } from './adapters.js';
import { type GenericSchema, type GenericSchemaAsync, type Config, type GenericIssue } from 'valibot';
import { type ConversionConfig } from '@valibot/to-json-schema';
import type { JSONSchema } from '../jsonSchema/index.js';
type SupportedSchemas = GenericSchema | GenericSchemaAsync;
export declare const valibotToJSONSchema: (options: ConversionConfig & {
schema: SupportedSchemas;
}) => JSONSchema;
export declare const valibot: <T extends SupportedSchemas>(schema: T, options?: (Omit<ConversionConfig, "schema"> & AdapterOptions<Infer<T, "valibot">> & {
config?: Config<GenericIssue<unknown>>;
}) | undefined) => ValidationAdapter<Infer<T, "valibot">, InferIn<T, "valibot">>;
export declare const valibotClient: <T extends SupportedSchemas>(schema: T, options?: (Omit<ConversionConfig, "schema"> & AdapterOptions<Infer<T, "valibot">> & {
config?: Config<GenericIssue<unknown>>;
}) | undefined) => ClientValidationAdapter<Infer<T, "valibot">, InferIn<T, "valibot">>;
export {};

View File

@@ -0,0 +1,59 @@
import { createAdapter } from './adapters.js';
import { safeParseAsync } from 'valibot';
import { memoize } from '../memoize.js';
import { toJsonSchema } from '@valibot/to-json-schema';
const defaultOptions = {
ignoreActions: ['transform', 'mime_type', 'max_size', 'min_size', 'starts_with'],
overrideSchema: (context) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const type = context.valibotSchema.type;
if (type === 'date') {
return { type: 'integer', format: 'unix-time' };
}
if (type === 'bigint') {
return { type: 'string', format: 'bigint' };
}
if (type === 'file' || type === 'blob' || type === 'instance' || type === 'custom') {
return {};
}
}
};
/* @__NO_SIDE_EFFECTS__ */
export const valibotToJSONSchema = (options) => {
const { schema, ...rest } = options;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return toJsonSchema(schema, { ...defaultOptions, ...rest });
};
async function _validate(schema, data, config) {
const result = await safeParseAsync(schema, data, config);
if (result.success) {
return {
data: result.output,
success: true
};
}
return {
issues: result.issues.map(({ message, path }) => ({
message,
path: path?.map(({ key }) => key)
})),
success: false
};
}
function _valibot(schema, options = {}) {
return createAdapter({
superFormValidationLibrary: 'valibot',
validate: async (data) => _validate(schema, data, options?.config),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
jsonSchema: options?.jsonSchema ?? valibotToJSONSchema({ schema: schema, ...options }),
defaults: 'defaults' in options ? options.defaults : undefined
});
}
function _valibotClient(schema, options = {}) {
return {
superFormValidationLibrary: 'valibot',
validate: async (data) => _validate(schema, data, options?.config)
};
}
export const valibot = /* @__PURE__ */ memoize(_valibot);
export const valibotClient = /* @__PURE__ */ memoize(_valibotClient);

View File

@@ -0,0 +1,4 @@
import { type ValidationAdapter, type ClientValidationAdapter, type RequiredDefaultsOptions, type Infer, type InferIn } from './adapters.js';
import type { SchemaTypes } from '@vinejs/vine/types';
export declare const vine: <T extends SchemaTypes>(schema: T, options: RequiredDefaultsOptions<Infer<T, "vine">>) => ValidationAdapter<Infer<T, "vine">, InferIn<T, "vine">>;
export declare const vineClient: <T extends SchemaTypes>(schema: T) => ClientValidationAdapter<Infer<T, "vine">, InferIn<T, "vine">>;

View File

@@ -0,0 +1,47 @@
import { createAdapter, createJsonSchema } from './adapters.js';
import { memoize } from '../memoize.js';
async function modules() {
const { Vine, errors } = await import(/* webpackIgnore: true */ '@vinejs/vine');
return { Vine, errors };
}
const fetchModule = /* @__PURE__ */ memoize(modules);
async function validate(schema, data) {
const { Vine, errors } = await fetchModule();
try {
const output = await new Vine().validate({ schema, data });
return {
success: true,
data: output
};
}
catch (e) {
if (e instanceof errors.E_VALIDATION_ERROR) {
return {
success: false,
issues: e.messages.map((m) => ({
path: m.field.split('.'),
message: m.message
}))
};
}
else {
return { success: false, issues: [] };
}
}
}
function _vine(schema, options) {
return createAdapter({
superFormValidationLibrary: 'vine',
validate: async (data) => validate(schema, data),
jsonSchema: createJsonSchema(options),
defaults: options.defaults
});
}
function _vineClient(schema) {
return {
superFormValidationLibrary: 'vine',
validate: async (data) => validate(schema, data)
};
}
export const vine = /* @__PURE__ */ memoize(_vine);
export const vineClient = /* @__PURE__ */ memoize(_vineClient);

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const arrayConverter: Converter;
export default arrayConverter;

View File

@@ -0,0 +1,31 @@
import commonConverter from './common.js';
const arrayConverter = (description, converters) => {
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
const { innerType } = description;
if (innerType) {
const converter = converters[innerType.type];
jsonSchema.items = converter(innerType, converters);
}
description.tests.forEach((test) => {
switch (test.name) {
case 'length':
if (test.params?.length !== undefined) {
jsonSchema.minItems = jsonSchema.maxItems = Number(test.params.length);
}
break;
case 'min':
if (test.params?.min !== undefined) {
jsonSchema.minItems = Number(test.params.min);
}
break;
case 'max':
if (test.params?.max !== undefined) {
jsonSchema.maxItems = Number(test.params.max);
}
break;
}
});
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default arrayConverter;

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const booleanConverter: Converter;
export default booleanConverter;

View File

@@ -0,0 +1,7 @@
import commonConverter from './common.js';
const booleanConverter = (description, converters) => {
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default booleanConverter;

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const commonConverter: Converter;
export default commonConverter;

View File

@@ -0,0 +1,25 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const commonConverter = (description, converters) => {
const jsonSchema = {};
jsonSchema.type = description.type;
if (description.nullable) {
jsonSchema.type = [jsonSchema.type, 'null'];
}
if (description.oneOf?.length > 0) {
jsonSchema.enum = description.oneOf;
}
if (description.notOneOf?.length > 0) {
jsonSchema.not = {
enum: description.notOneOf
};
}
if (description.label) {
jsonSchema.title = description.label;
}
if (description.default !== undefined) {
// @ts-expect-error default is unknown
jsonSchema.default = description.default;
}
return jsonSchema;
};
export default commonConverter;

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const dateConverter: Converter;
export default dateConverter;

View File

@@ -0,0 +1,9 @@
import commonConverter from './common.js';
const dateConverter = (description, converters) => {
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
jsonSchema.type = 'string';
jsonSchema.format = 'date-time';
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default dateConverter;

View File

@@ -0,0 +1,4 @@
import type { AnySchema } from 'yup';
import type { JSONSchema7 } from 'json-schema';
import type { ResolveOptions } from '../types.js';
export declare function convertSchema(yupSchema: AnySchema, options?: ResolveOptions): JSONSchema7;

View File

@@ -0,0 +1,27 @@
import stringConverter from './string.js';
import numberConverter from './number.js';
import booleanConverter from './boolean.js';
import dateConverter from './date.js';
import arrayConverter from './array.js';
import objectConverter from './object.js';
import tupleConverter from './tuple.js';
import mixedConverter from './mixed.js';
import lazyConverter from './lazy.js';
export function convertSchema(yupSchema, options) {
const { converters, ...resolveOptions } = options || {};
const allConverters = {
string: stringConverter,
number: numberConverter,
boolean: booleanConverter,
date: dateConverter,
array: arrayConverter,
object: objectConverter,
tuple: tupleConverter,
mixed: mixedConverter,
lazy: lazyConverter,
...converters
};
const description = yupSchema.describe(resolveOptions);
const converter = allConverters[description.type];
return converter(description, allConverters);
}

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const lazyConverter: Converter;
export default lazyConverter;

View File

@@ -0,0 +1,8 @@
import commonConverter from './common.js';
/* lazy is kind on an intermediate type. If you call schema.describe() with any argument, even schema.describe({}) which this library does by default, then the lazy functions always try to resolve to their return types. Because we always call schema.describe({}) or schema.describe(ResolveOptions) this is mostly unused but should still be here and return an empty type if it does exist in the schema description for some reason */
const lazyConverter = (description, converters) => {
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default lazyConverter;

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const mixedConverter: Converter;
export default mixedConverter;

View File

@@ -0,0 +1,45 @@
import commonConverter from './common.js';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getType = (item) => {
switch (typeof item) {
case 'string':
return 'string';
case 'number':
return 'number';
case 'boolean':
return 'boolean';
case 'object':
if (Array.isArray(item)) {
return 'array';
}
else if (item === null) {
return 'null';
}
else if (item instanceof Date) {
return 'string';
}
else {
return 'object';
}
default:
return 'null';
}
};
const mixedConverter = (description, converters) => {
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
let types = Array.isArray(description.type) ? description.type : [description.type];
types = types.filter((type) => type !== 'mixed');
if (description.oneOf?.length > 0) {
description.oneOf.forEach((item) => {
types.push(getType(item));
});
}
if (description.default !== undefined) {
types.push(getType(description.default));
}
types = types.filter((type, index, self) => self.indexOf(type) === index);
jsonSchema.type = types;
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default mixedConverter;

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const numberConverter: Converter;
export default numberConverter;

View File

@@ -0,0 +1,35 @@
import commonConverter from './common.js';
const numberConverter = (description, converters) => {
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
description.tests.forEach((test) => {
switch (test.name) {
case 'min':
if (test.params?.min !== undefined) {
jsonSchema.minimum = Number(test.params.min);
}
if (test.params?.more !== undefined) {
jsonSchema.exclusiveMinimum = Number(test.params.more);
}
break;
case 'max':
if (test.params?.max !== undefined) {
jsonSchema.maximum = Number(test.params.max);
}
if (test.params?.less !== undefined) {
jsonSchema.exclusiveMaximum = Number(test.params.less);
}
break;
case 'integer':
if (jsonSchema.type === 'number') {
jsonSchema.type = 'integer';
}
else {
// @ts-expect-error type is known
jsonSchema.type = [...jsonSchema.type, 'integer'].filter((type) => type !== 'number');
}
}
});
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default numberConverter;

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const objectConverter: Converter;
export default objectConverter;

View File

@@ -0,0 +1,25 @@
import commonConverter from './common.js';
// @ts-expect-error description is known
const objectConverter = (description, converters) => {
/* Yup automatically adds an object where each key is undefined as the deafault in its description. So objects automatically get a default :(. The developer should use jsonSchema({ default: undefined }) to remedy this */
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
const properties = {};
const required = [];
Object.keys(description.fields).forEach((fieldName) => {
const fieldDescription = description.fields[fieldName];
const converter = converters[fieldDescription.type];
properties[fieldName] = converter(fieldDescription, converters);
if (!fieldDescription.optional) {
required.push(fieldName);
}
});
if (Object.keys(properties).length > 0) {
jsonSchema.properties = properties;
}
if (Object.keys(required).length > 0) {
jsonSchema.required = required;
}
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default objectConverter;

View File

@@ -0,0 +1,4 @@
import type { Converter } from '../types.js';
export declare const uuidRegExPattern = "^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$";
declare const stringConverter: Converter;
export default stringConverter;

View File

@@ -0,0 +1,45 @@
import commonConverter from './common.js';
export const uuidRegExPattern = '^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$';
const stringConverter = (description, converters) => {
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
description.tests.forEach((test) => {
switch (test.name) {
case 'length':
if (test.params?.length !== undefined) {
jsonSchema.minLength = Number(test.params.length);
jsonSchema.maxLength = Number(test.params.length);
}
break;
case 'min':
if (test.params?.min !== undefined) {
jsonSchema.minLength = Number(test.params.min);
}
break;
case 'max':
if (test.params?.max !== undefined) {
jsonSchema.maxLength = Number(test.params.max);
}
break;
case 'matches':
if (test.params?.regex) {
jsonSchema.pattern = test.params.regex
.toString()
.replace(/^\/(.*)\/[gimusy]*$/, '$1');
}
break;
case 'email':
jsonSchema.format = 'email';
break;
case 'url':
jsonSchema.format = 'uri';
break;
case 'uuid':
jsonSchema.format = 'uuid';
jsonSchema.pattern = uuidRegExPattern;
break;
}
});
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default stringConverter;

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const tupleConverter: Converter;
export default tupleConverter;

View File

@@ -0,0 +1,15 @@
import commonConverter from './common.js';
// @ts-expect-error description is known
const tupleConverter = (description, converters) => {
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
jsonSchema.type = 'array';
jsonSchema.items = description.innerType.map((description) => {
const converter = converters[description.type];
return converter(description, converters);
});
jsonSchema.minItems = jsonSchema.items.length;
jsonSchema.maxItems = jsonSchema.items.length;
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default tupleConverter;

View File

@@ -0,0 +1,2 @@
export { convertSchema } from './converters/index.js';
export { extendSchema } from './methods/index.js';

View File

@@ -0,0 +1,2 @@
export { convertSchema } from './converters/index.js';
export { extendSchema } from './methods/index.js';

View File

@@ -0,0 +1,6 @@
type YupParams = {
addMethod: any;
Schema: any;
};
export declare function extendSchema(yup: YupParams): void;
export {};

View File

@@ -0,0 +1,24 @@
function addMethod(yup, name) {
yup.addMethod(yup.Schema, name, function (value) {
const meta = this.describe().meta || {};
return this.meta({
...meta,
jsonSchema: {
...meta.jsonSchema,
[name]: value
}
});
});
}
export function extendSchema(yup) {
addMethod(yup, 'example');
addMethod(yup, 'examples');
addMethod(yup, 'description');
yup.addMethod(yup.Schema, 'jsonSchema', function (callback) {
const meta = this.describe().meta || {};
return this.meta({
...meta,
jsonSchema: callback(meta.jsonSchema || {})
});
});
}

View File

@@ -0,0 +1,16 @@
import type { JSONSchema7 } from 'json-schema';
import type { SchemaDescription } from 'yup';
export type YupType = 'array' | 'boolean' | 'date' | 'lazy' | 'mixed' | 'number' | 'object' | 'string' | 'tuple';
export type Converters = Record<YupType, Converter>;
export type Converter = (description: SchemaDescription, converters: Converters) => JSONSchema7;
export type Meta = {
jsonSchema?: JSONSchema7;
[key: string]: any;
};
export type ResolveOptions = {
value?: unknown;
parent?: unknown;
context?: unknown;
converters?: Partial<Converters>;
};
export type JsonSchemaCallback = (jsonSchema: JSONSchema7) => JSONSchema7;

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,5 @@
import { type AdapterOptions, type ValidationAdapter, type Infer, type InferIn, type ClientValidationAdapter } from './adapters.js';
import type { AnySchema, Schema } from 'yup';
export declare function yupToJSONSchema(schema: AnySchema): import("json-schema").JSONSchema7;
export declare const yup: <T extends Schema>(schema: T, options?: AdapterOptions<Infer<T, "yup">> | undefined) => ValidationAdapter<Infer<T, "yup">, InferIn<T, "yup">>;
export declare const yupClient: <T extends Schema>(schema: T) => ClientValidationAdapter<Infer<T, "yup">, InferIn<T, "yup">>;

View File

@@ -0,0 +1,56 @@
import { createAdapter } from './adapters.js';
import { splitPath } from '../stringPath.js';
import { memoize } from '../memoize.js';
import { convertSchema } from './yup-to-json-schema/index.js';
const modules = async () => {
const { ValidationError } = await import(/* webpackIgnore: true */ 'yup');
return { ValidationError };
};
const fetchModule = /* @__PURE__ */ memoize(modules);
/* @__NO_SIDE_EFFECTS__ */
export function yupToJSONSchema(schema) {
return convertSchema(schema, {
converters: {
date: (desc, options) => {
return options.string(desc, options);
}
}
});
}
async function validate(schema, data) {
const { ValidationError } = await fetchModule();
try {
return {
success: true,
data: await schema.validate(data, { strict: true, abortEarly: false })
};
}
catch (error) {
if (!(error instanceof ValidationError))
throw error;
return {
success: false,
issues: error.inner.map((error) => ({
message: error.message,
path: error.path !== null && error.path !== undefined ? splitPath(error.path) : undefined
}))
};
}
}
/* @__NO_SIDE_EFFECTS__ */
function _yup(schema, options) {
return createAdapter({
superFormValidationLibrary: 'yup',
validate: async (data) => validate(schema, data),
jsonSchema: options?.jsonSchema ?? yupToJSONSchema(schema),
defaults: options?.defaults
});
}
function _yupClient(schema) {
return {
superFormValidationLibrary: 'yup',
validate: async (data) => validate(schema, data)
};
}
export const yup = /* @__PURE__ */ memoize(_yup);
export const yupClient = /* @__PURE__ */ memoize(_yupClient);

View File

@@ -0,0 +1,15 @@
import { type ZodErrorMap, type ZodType, type ZodTypeDef } from 'zod/v3';
import type { JSONSchema7 } from 'json-schema';
import { type AdapterOptions, type ValidationAdapter, type Infer, type InferIn, type ClientValidationAdapter } from './adapters.js';
import { zodToJsonSchema as zodToJson, type Options } from 'zod-v3-to-json-schema';
export declare const zodToJSONSchema: (...params: Parameters<typeof zodToJson>) => JSONSchema7;
export type ZodObjectType = ZodType<Record<string, unknown>, ZodTypeDef, Record<string, unknown> | undefined>;
export type ZodObjectTypes = ZodObjectType;
export type ZodValidation<T extends ZodObjectTypes = ZodObjectTypes> = T;
export declare const zod: <T extends ZodValidation>(schema: T, options?: (AdapterOptions<Infer<T, "zod">> & {
errorMap?: ZodErrorMap;
config?: Partial<Options>;
}) | undefined) => ValidationAdapter<Infer<T, "zod">, InferIn<T, "zod">>;
export declare const zodClient: <T extends ZodValidation>(schema: T, options?: {
errorMap?: ZodErrorMap;
} | undefined) => ClientValidationAdapter<Infer<T, "zod">, InferIn<T, "zod">>;

View File

@@ -0,0 +1,45 @@
import {} from 'zod/v3';
import { createAdapter } from './adapters.js';
import { zodToJsonSchema as zodToJson } from 'zod-v3-to-json-schema';
import { memoize } from '../memoize.js';
const defaultOptions = {
dateStrategy: 'integer',
pipeStrategy: 'output',
$refStrategy: 'none'
};
/* @__NO_SIDE_EFFECTS__ */
export const zodToJSONSchema = (...params) => {
params[1] = typeof params[1] == 'object' ? { ...defaultOptions, ...params[1] } : defaultOptions;
return zodToJson(...params);
};
async function validate(schema, data, errorMap) {
const result = await schema.safeParseAsync(data, { errorMap });
if (result.success) {
return {
data: result.data,
success: true
};
}
return {
issues: result.error.issues.map(({ message, path }) => ({ message, path })),
success: false
};
}
function _zod(schema, options) {
return createAdapter({
superFormValidationLibrary: 'zod',
validate: async (data) => {
return validate(schema, data, options?.errorMap);
},
jsonSchema: options?.jsonSchema ?? zodToJSONSchema(schema, options?.config),
defaults: options?.defaults
});
}
function _zodClient(schema, options) {
return {
superFormValidationLibrary: 'zod',
validate: async (data) => validate(schema, data, options?.errorMap)
};
}
export const zod = /* @__PURE__ */ memoize(_zod);
export const zodClient = /* @__PURE__ */ memoize(_zodClient);

View File

@@ -0,0 +1,14 @@
import { type $ZodErrorMap, type $ZodType, toJSONSchema } from 'zod/v4/core';
import type { JSONSchema7 } from 'json-schema';
import { type AdapterOptions, type ValidationAdapter, type Infer, type InferIn, type ClientValidationAdapter } from './adapters.js';
type Options = NonNullable<Parameters<typeof toJSONSchema>[1]>;
export type ZodValidationSchema = $ZodType<Record<string, unknown>>;
export declare const zodToJSONSchema: <S extends ZodValidationSchema>(schema: S, options?: Options) => JSONSchema7;
export declare const zod: <T extends ZodValidationSchema>(schema: T, options?: (AdapterOptions<Infer<T, "zod4">> & {
error?: $ZodErrorMap;
config?: Options;
}) | undefined) => ValidationAdapter<Infer<T, "zod4">, InferIn<T, "zod4">>;
export declare const zodClient: <T extends ZodValidationSchema>(schema: T, options?: {
error?: $ZodErrorMap;
} | undefined) => ClientValidationAdapter<Infer<T, "zod4">, InferIn<T, "zod4">>;
export {};

View File

@@ -0,0 +1,144 @@
import { safeParseAsync, toJSONSchema, config } from 'zod/v4/core';
import { createAdapter } from './adapters.js';
import { memoize } from '../memoize.js';
const defaultJSONSchemaOptions = {
unrepresentable: 'any',
override: (ctx) => {
const def = ctx.zodSchema._zod.def;
if (def.type === 'date') {
ctx.jsonSchema.type = 'integer';
ctx.jsonSchema.format = 'unix-time';
}
else if (def.type === 'bigint') {
ctx.jsonSchema.type = 'string';
ctx.jsonSchema.format = 'bigint';
}
else if (def.type === 'pipe') {
// Handle z.stringbool() - it's a pipe from string->transform->boolean
// Colin Hacks explained: stringbool is just string -> transform -> boolean
// When io:'input', we see the string schema; when io:'output', we see boolean
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pipeDef = def;
const inSchema = pipeDef.in;
const outSchema = pipeDef.out;
// Check if it's: string -> (transform or pipe) -> boolean
if (inSchema?._zod?.def.type === 'string') {
// Traverse through the output side (right) to find if it ends in boolean
let currentSchema = outSchema;
let isStringBool = false;
// Traverse through transforms and pipes to find boolean
while (currentSchema?._zod?.def) {
const currentDef = currentSchema._zod.def;
if (currentDef.type === 'boolean') {
isStringBool = true;
break;
}
else if (currentDef.type === 'transform') {
// Transform doesn't have a nested schema, but we can't traverse further
// Check if the transform is inside another pipe
break;
}
else if (currentDef.type === 'pipe') {
// Continue traversing the pipe
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nestedPipeDef = currentDef;
currentSchema = nestedPipeDef.out;
}
else {
break;
}
}
// Also check if outSchema directly is boolean
if (!isStringBool && outSchema?._zod?.def.type === 'boolean') {
isStringBool = true;
}
if (isStringBool) {
// Mark as stringbool so FormData parser knows to handle it as string
ctx.jsonSchema.type = 'string';
ctx.jsonSchema.format = 'stringbool';
}
}
}
else if (def.type === 'set') {
// Handle z.set() - convert to array with uniqueItems
ctx.jsonSchema.type = 'array';
ctx.jsonSchema.uniqueItems = true;
// If there's a default value, convert Set to Array
if ('default' in ctx.jsonSchema && ctx.jsonSchema.default instanceof Set) {
ctx.jsonSchema.default = Array.from(ctx.jsonSchema.default);
}
}
else if (def.type === 'map') {
// Handle z.map() - convert to array of [key, value] tuples
ctx.jsonSchema.type = 'array';
ctx.jsonSchema.format = 'map';
// If there's a default value, convert Map to Array
if ('default' in ctx.jsonSchema && ctx.jsonSchema.default instanceof Map) {
ctx.jsonSchema.default = Array.from(ctx.jsonSchema.default);
}
}
else if (def.type === 'default') {
// Handle z.default() wrapping unrepresentable types
// The default value was already serialized by Zod, which converts Set/Map to {}
// We need to get the original value and convert it properly
const innerDef = def.innerType._zod.def;
if (innerDef.type === 'set' && def.defaultValue instanceof Set) {
// Set the proper schema type for sets
ctx.jsonSchema.type = 'array';
ctx.jsonSchema.uniqueItems = true;
// Convert the default value from Set to Array
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ctx.jsonSchema.default = Array.from(def.defaultValue);
}
else if (innerDef.type === 'map' && def.defaultValue instanceof Map) {
// Set the proper schema type for maps
ctx.jsonSchema.type = 'array';
ctx.jsonSchema.format = 'map';
// Convert the default value from Map to Array of tuples
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ctx.jsonSchema.default = Array.from(def.defaultValue);
}
}
}
};
/* @__NO_SIDE_EFFECTS__ */
export const zodToJSONSchema = (schema, options) => {
return toJSONSchema(schema, { ...defaultJSONSchemaOptions, ...options });
};
async function validate(schema, data, error) {
// Use Zod's global config error map if none provided to preserve custom messages.
// Prioritize customError over localeError when both are set.
if (error === undefined) {
const zConfig = config();
error = zConfig.customError ?? zConfig.localeError;
}
const result = await safeParseAsync(schema, data, { error });
if (result.success) {
return {
data: result.data,
success: true
};
}
return {
issues: result.error.issues.map(({ message, path }) => ({ message, path })),
success: false
};
}
function _zod4(schema, options) {
return createAdapter({
superFormValidationLibrary: 'zod4',
validate: async (data) => {
return validate(schema, data, options?.error);
},
jsonSchema: options?.jsonSchema ?? zodToJSONSchema(schema, options?.config),
defaults: options?.defaults
});
}
function _zod4Client(schema, options) {
return {
superFormValidationLibrary: 'zod4',
validate: async (data) => validate(schema, data, options?.error)
};
}
export const zod = /* @__PURE__ */ memoize(_zod4);
export const zodClient = /* @__PURE__ */ memoize(_zod4Client);