diff --git a/app/__test__/core/Registry.spec.ts b/app/__test__/core/Registry.spec.ts index 2a11250b..ee608b0d 100644 --- a/app/__test__/core/Registry.spec.ts +++ b/app/__test__/core/Registry.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { type TObject, type TString, Type } from "@sinclair/typebox"; import { Registry } from "core"; +import { s } from "core/object/schema"; type Constructor = new (...args: any[]) => T; @@ -11,7 +11,7 @@ class What { return null; } getType() { - return Type.Object({ type: Type.String() }); + return s.object({ type: s.string() }); } } class What2 extends What {} @@ -19,7 +19,7 @@ class NotAllowed {} type Test1 = { cls: new (...args: any[]) => What; - schema: TObject<{ type: TString }>; + schema: s.ObjectSchema<{ type: s.StringSchema }>; enabled: boolean; }; @@ -28,7 +28,7 @@ describe("Registry", () => { const registry = new Registry().set({ first: { cls: What, - schema: Type.Object({ type: Type.String(), what: Type.String() }), + schema: s.object({ type: s.string(), what: s.string() }), enabled: true, }, } satisfies Record); @@ -37,7 +37,7 @@ describe("Registry", () => { expect(item).toBeDefined(); expect(item?.cls).toBe(What); - const second = Type.Object({ type: Type.String(), what: Type.String() }); + const second = s.object({ type: s.string(), what: s.string() }); registry.add("second", { cls: What2, schema: second, @@ -46,7 +46,7 @@ describe("Registry", () => { // @ts-ignore expect(registry.get("second").schema).toEqual(second); - const third = Type.Object({ type: Type.String({ default: "1" }), what22: Type.String() }); + const third = s.object({ type: s.string({ default: "1" }), what22: s.string() }); registry.add("third", { // @ts-expect-error cls: NotAllowed, @@ -56,7 +56,7 @@ describe("Registry", () => { // @ts-ignore expect(registry.get("third").schema).toEqual(third); - const fourth = Type.Object({ type: Type.Number(), what22: Type.String() }); + const fourth = s.object({ type: s.number(), what22: s.string() }); registry.add("fourth", { cls: What, // @ts-expect-error @@ -81,6 +81,8 @@ describe("Registry", () => { registry.register("what2", What2); expect(registry.get("what2")).toBeDefined(); expect(registry.get("what2").cls).toBe(What2); - expect(registry.get("what2").schema).toEqual(What2.prototype.getType()); + expect(JSON.stringify(registry.get("what2").schema)).toEqual( + JSON.stringify(What2.prototype.getType()), + ); }); }); diff --git a/app/__test__/core/object/SchemaObject.spec.ts b/app/__test__/core/object/SchemaObject.spec.ts index c0d2fb45..bedcb817 100644 --- a/app/__test__/core/object/SchemaObject.spec.ts +++ b/app/__test__/core/object/SchemaObject.spec.ts @@ -1,6 +1,5 @@ import { describe, expect, test } from "bun:test"; import { SchemaObject } from "../../../src/core"; -import { Type } from "@sinclair/typebox"; import { s } from "core/object/schema"; describe("SchemaObject", async () => { diff --git a/app/__test__/data/specs/fields/FieldIndex.spec.ts b/app/__test__/data/specs/fields/FieldIndex.spec.ts index d9dec20f..ecc5e0c1 100644 --- a/app/__test__/data/specs/fields/FieldIndex.spec.ts +++ b/app/__test__/data/specs/fields/FieldIndex.spec.ts @@ -1,5 +1,4 @@ import { describe, expect, test } from "bun:test"; -import { Type } from "@sinclair/typebox"; import { Entity, EntityIndex, Field } from "../../../../src/data"; import { s } from "core/object/schema"; diff --git a/app/__test__/flows/SubWorkflowTask.spec.ts b/app/__test__/flows/SubWorkflowTask.spec.ts index 0cae057b..7d70ebf6 100644 --- a/app/__test__/flows/SubWorkflowTask.spec.ts +++ b/app/__test__/flows/SubWorkflowTask.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows"; -import { Type } from "@sinclair/typebox"; +import { s } from "core/object/schema"; export class StringifyTask extends Task< typeof StringifyTask.schema, @@ -8,11 +8,9 @@ export class StringifyTask extends Task< > { type = "stringify"; - static override schema = Type.Optional( - Type.Object({ - input: Type.Optional(Type.String()), - }), - ); + static override schema = s.object({ + input: s.string().optional(), + }); async execute() { return JSON.stringify(this.params.input) as Output; diff --git a/app/__test__/flows/Task.spec.ts b/app/__test__/flows/Task.spec.ts index 5ce11e1c..b8e451cf 100644 --- a/app/__test__/flows/Task.spec.ts +++ b/app/__test__/flows/Task.spec.ts @@ -1,12 +1,12 @@ import { describe, expect, test } from "bun:test"; -import { Type } from "@sinclair/typebox"; import { Task } from "../../src/flows"; import { dynamic } from "../../src/flows/tasks/Task"; +import { s } from "core/object/schema"; describe.skip("Task", async () => { test("resolveParams: template with parse", async () => { const result = await Task.resolveParams( - Type.Object({ test: dynamic(Type.Number()) }), + s.object({ test: dynamic(s.number()) }), { test: "{{ some.path }}", }, @@ -22,7 +22,7 @@ describe.skip("Task", async () => { test("resolveParams: with string", async () => { const result = await Task.resolveParams( - Type.Object({ test: Type.String() }), + s.object({ test: s.string() }), { test: "{{ some.path }}", }, @@ -38,7 +38,7 @@ describe.skip("Task", async () => { test("resolveParams: with object", async () => { const result = await Task.resolveParams( - Type.Object({ test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })) }), + s.object({ test: dynamic(s.object({ key: s.string(), value: s.string() })) }), { test: { key: "path", value: "{{ some.path }}" }, }, diff --git a/app/__test__/flows/workflow-basic.test.ts b/app/__test__/flows/workflow-basic.test.ts index f148c136..48c0b9e6 100644 --- a/app/__test__/flows/workflow-basic.test.ts +++ b/app/__test__/flows/workflow-basic.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"; import { isEqual } from "lodash-es"; import { _jsonp, withDisabledConsole } from "../../src/core/utils"; -import { type Static, Type } from "@sinclair/typebox"; +import { s } from "core/object/schema"; import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows"; /*beforeAll(disableConsoleLog); @@ -11,19 +11,19 @@ afterAll(enableConsoleLog);*/ class ExecTask extends Task { type = "exec"; - static override schema = Type.Object({ - delay: Type.Number({ default: 10 }), + static override schema = s.object({ + delay: s.number({ default: 10 }), }); constructor( name: string, - params: Static, + params: s.Static, private func: () => Promise, ) { super(name, params); } - override clone(name: string, params: Static) { + override clone(name: string, params: s.Static) { return new ExecTask(name, params, this.func); } diff --git a/app/__test__/modules/ModuleManager.spec.ts b/app/__test__/modules/ModuleManager.spec.ts index 65972697..876c5827 100644 --- a/app/__test__/modules/ModuleManager.spec.ts +++ b/app/__test__/modules/ModuleManager.spec.ts @@ -1,13 +1,11 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; import { disableConsoleLog, enableConsoleLog } from "core/utils"; -import { Type } from "@sinclair/typebox"; import { Connection, entity, text } from "data"; import { Module } from "modules/Module"; import { type ConfigTable, getDefaultConfig, ModuleManager } from "modules/ModuleManager"; import { CURRENT_VERSION, TABLE_NAME } from "modules/migrations"; import { getDummyConnection } from "../helper"; import { s, stripMark } from "core/object/schema"; -import type { Static } from "@sinclair/typebox"; describe("ModuleManager", async () => { test("s1: no config, no build", async () => { diff --git a/app/package.json b/app/package.json index 4165bdb1..95c6ff4f 100644 --- a/app/package.json +++ b/app/package.json @@ -86,6 +86,7 @@ "@mantine/notifications": "^7.17.1", "@playwright/test": "^1.51.1", "@rjsf/core": "5.22.2", + "@standard-schema/spec": "^1.0.0", "@tabler/icons-react": "3.18.0", "@tailwindcss/postcss": "^4.0.12", "@tailwindcss/vite": "^4.0.12", @@ -101,7 +102,7 @@ "dotenv": "^16.4.7", "jotai": "^2.12.2", "jsdom": "^26.0.0", - "jsonv-ts": "0.2.0-alpha.5", + "jsonv-ts": "^0.2.1", "kysely-d1": "^0.3.0", "kysely-generic-sqlite": "^1.2.1", "libsql-stateless-easy": "^1.8.0", diff --git a/app/src/core/index.ts b/app/src/core/index.ts index 82507936..fae1eb2b 100644 --- a/app/src/core/index.ts +++ b/app/src/core/index.ts @@ -1,6 +1,5 @@ import type { Hono, MiddlewareHandler } from "hono"; -export { tbValidator } from "./server/lib/tbValidator"; export { Exception, BkndError } from "./errors"; export { isDebug, env } from "./env"; export { type PrimaryFieldType, config, type DB, type AppEntity } from "./config"; diff --git a/app/src/core/server/lib/index.ts b/app/src/core/server/lib/index.ts deleted file mode 100644 index d6eea75e..00000000 --- a/app/src/core/server/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { tbValidator } from "./tbValidator"; diff --git a/app/src/core/server/lib/tbValidator.ts b/app/src/core/server/lib/tbValidator.ts deleted file mode 100644 index 6ae4c410..00000000 --- a/app/src/core/server/lib/tbValidator.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { StaticDecode, TSchema } from "@sinclair/typebox"; -import { Value, type ValueError } from "@sinclair/typebox/value"; -import type { Context, Env, MiddlewareHandler, ValidationTargets } from "hono"; -import { validator } from "hono/validator"; - -type Hook = ( - result: { success: true; data: T } | { success: false; errors: ValueError[] }, - c: Context, -) => Response | Promise | void; - -export function tbValidator< - T extends TSchema, - Target extends keyof ValidationTargets, - E extends Env, - P extends string, - V extends { in: { [K in Target]: StaticDecode }; out: { [K in Target]: StaticDecode } }, ->(target: Target, schema: T, hook?: Hook, E, P>): MiddlewareHandler { - // Compile the provided schema once rather than per validation. This could be optimized further using a shared schema - // compilation pool similar to the Fastify implementation. - - // @ts-expect-error not typed well - return validator(target, (data, c) => { - if (Value.Check(schema, data)) { - // always decode - const decoded = Value.Decode(schema, data); - - if (hook) { - const hookResult = hook({ success: true, data: decoded }, c); - if (hookResult instanceof Response || hookResult instanceof Promise) { - return hookResult; - } - } - return decoded; - } - return c.json({ success: false, errors: [...Value.Errors(schema, data)] }, 400); - }); -} diff --git a/app/src/core/utils/index.ts b/app/src/core/utils/index.ts index d4a6ccc7..e60d272e 100644 --- a/app/src/core/utils/index.ts +++ b/app/src/core/utils/index.ts @@ -7,11 +7,9 @@ export * from "./file"; export * from "./reqres"; export * from "./xml"; export type { Prettify, PrettifyRec, RecursivePartial } from "./types"; -export * from "./typebox"; export * from "./dates"; export * from "./crypto"; export * from "./uuid"; -export { FromSchema } from "./typebox/from-schema"; export * from "./test"; export * from "./runtime"; export * from "./numbers"; diff --git a/app/src/core/utils/typebox/from-schema.ts b/app/src/core/utils/typebox/from-schema.ts deleted file mode 100644 index b9399782..00000000 --- a/app/src/core/utils/typebox/from-schema.ts +++ /dev/null @@ -1,270 +0,0 @@ -/*-------------------------------------------------------------------------- - -@sinclair/typebox/prototypes - -The MIT License (MIT) - -Copyright (c) 2017-2024 Haydn Paterson (sinclair) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ----------------------------------------------------------------------------*/ - -import * as Type from "@sinclair/typebox"; - -// ------------------------------------------------------------------ -// Schematics -// ------------------------------------------------------------------ -const IsExact = (value: unknown, expect: unknown) => value === expect; -const IsSValue = (value: unknown): value is SValue => - Type.ValueGuard.IsString(value) || - Type.ValueGuard.IsNumber(value) || - Type.ValueGuard.IsBoolean(value); -const IsSEnum = (value: unknown): value is SEnum => - Type.ValueGuard.IsObject(value) && - Type.ValueGuard.IsArray(value.enum) && - value.enum.every((value) => IsSValue(value)); -const IsSAllOf = (value: unknown): value is SAllOf => - Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.allOf); -const IsSAnyOf = (value: unknown): value is SAnyOf => - Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.anyOf); -const IsSOneOf = (value: unknown): value is SOneOf => - Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.oneOf); -const IsSTuple = (value: unknown): value is STuple => - Type.ValueGuard.IsObject(value) && - IsExact(value.type, "array") && - Type.ValueGuard.IsArray(value.items); -const IsSArray = (value: unknown): value is SArray => - Type.ValueGuard.IsObject(value) && - IsExact(value.type, "array") && - !Type.ValueGuard.IsArray(value.items) && - Type.ValueGuard.IsObject(value.items); -const IsSConst = (value: unknown): value is SConst => - // biome-ignore lint/complexity/useLiteralKeys: - Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsObject(value["const"]); -const IsSString = (value: unknown): value is SString => - Type.ValueGuard.IsObject(value) && IsExact(value.type, "string"); -const IsSNumber = (value: unknown): value is SNumber => - Type.ValueGuard.IsObject(value) && IsExact(value.type, "number"); -const IsSInteger = (value: unknown): value is SInteger => - Type.ValueGuard.IsObject(value) && IsExact(value.type, "integer"); -const IsSBoolean = (value: unknown): value is SBoolean => - Type.ValueGuard.IsObject(value) && IsExact(value.type, "boolean"); -const IsSNull = (value: unknown): value is SBoolean => - Type.ValueGuard.IsObject(value) && IsExact(value.type, "null"); -const IsSProperties = (value: unknown): value is SProperties => Type.ValueGuard.IsObject(value); -// biome-ignore format: keep -const IsSObject = (value: unknown): value is SObject => Type.ValueGuard.IsObject(value) && IsExact(value.type, 'object') && IsSProperties(value.properties) && (value.required === undefined || Type.ValueGuard.IsArray(value.required) && value.required.every((value: unknown) => Type.ValueGuard.IsString(value))) -type SValue = string | number | boolean; -type SEnum = Readonly<{ enum: readonly SValue[] }>; -type SAllOf = Readonly<{ allOf: readonly unknown[] }>; -type SAnyOf = Readonly<{ anyOf: readonly unknown[] }>; -type SOneOf = Readonly<{ oneOf: readonly unknown[] }>; -type SProperties = Record; -type SObject = Readonly<{ type: "object"; properties: SProperties; required?: readonly string[] }>; -type STuple = Readonly<{ type: "array"; items: readonly unknown[] }>; -type SArray = Readonly<{ type: "array"; items: unknown }>; -type SConst = Readonly<{ const: SValue }>; -type SString = Readonly<{ type: "string" }>; -type SNumber = Readonly<{ type: "number" }>; -type SInteger = Readonly<{ type: "integer" }>; -type SBoolean = Readonly<{ type: "boolean" }>; -type SNull = Readonly<{ type: "null" }>; -// ------------------------------------------------------------------ -// FromRest -// ------------------------------------------------------------------ -// biome-ignore format: keep -type TFromRest = ( - // biome-ignore lint/complexity/noUselessTypeConstraint: - T extends readonly [infer L extends unknown, ...infer R extends unknown[]] - ? TFromSchema extends infer S extends Type.TSchema - ? TFromRest - : TFromRest - : Acc -) -function FromRest(T: T): TFromRest { - return T.map((L) => FromSchema(L)) as never; -} -// ------------------------------------------------------------------ -// FromEnumRest -// ------------------------------------------------------------------ -// biome-ignore format: keep -type TFromEnumRest = ( - T extends readonly [infer L extends SValue, ...infer R extends SValue[]] - ? TFromEnumRest]> - : Acc -) -function FromEnumRest(T: T): TFromEnumRest { - return T.map((L) => Type.Literal(L)) as never; -} -// ------------------------------------------------------------------ -// AllOf -// ------------------------------------------------------------------ -// biome-ignore format: keep -type TFromAllOf = ( - TFromRest extends infer Rest extends Type.TSchema[] - ? Type.TIntersectEvaluated - : Type.TNever -) -function FromAllOf(T: T): TFromAllOf { - return Type.IntersectEvaluated(FromRest(T.allOf), T); -} -// ------------------------------------------------------------------ -// AnyOf -// ------------------------------------------------------------------ -// biome-ignore format: keep -type TFromAnyOf = ( - TFromRest extends infer Rest extends Type.TSchema[] - ? Type.TUnionEvaluated - : Type.TNever -) -function FromAnyOf(T: T): TFromAnyOf { - return Type.UnionEvaluated(FromRest(T.anyOf), T); -} -// ------------------------------------------------------------------ -// OneOf -// ------------------------------------------------------------------ -// biome-ignore format: keep -type TFromOneOf = ( - TFromRest extends infer Rest extends Type.TSchema[] - ? Type.TUnionEvaluated - : Type.TNever -) -function FromOneOf(T: T): TFromOneOf { - return Type.UnionEvaluated(FromRest(T.oneOf), T); -} -// ------------------------------------------------------------------ -// Enum -// ------------------------------------------------------------------ -// biome-ignore format: keep -type TFromEnum = ( - TFromEnumRest extends infer Elements extends Type.TSchema[] - ? Type.TUnionEvaluated - : Type.TNever -) -function FromEnum(T: T): TFromEnum { - return Type.UnionEvaluated(FromEnumRest(T.enum)); -} -// ------------------------------------------------------------------ -// Tuple -// ------------------------------------------------------------------ -// biome-ignore format: keep -type TFromTuple = ( - TFromRest extends infer Elements extends Type.TSchema[] - ? Type.TTuple - : Type.TTuple<[]> -) -// biome-ignore format: keep -function FromTuple(T: T): TFromTuple { - return Type.Tuple(FromRest(T.items), T) as never -} -// ------------------------------------------------------------------ -// Array -// ------------------------------------------------------------------ -// biome-ignore format: keep -type TFromArray = ( - TFromSchema extends infer Items extends Type.TSchema - ? Type.TArray - : Type.TArray -) -// biome-ignore format: keep -function FromArray(T: T): TFromArray { - return Type.Array(FromSchema(T.items), T) as never -} -// ------------------------------------------------------------------ -// Const -// ------------------------------------------------------------------ -// biome-ignore format: keep -type TFromConst = ( - Type.Ensure> -) -function FromConst(T: T) { - return Type.Literal(T.const, T); -} -// ------------------------------------------------------------------ -// Object -// ------------------------------------------------------------------ -type TFromPropertiesIsOptional< - K extends PropertyKey, - R extends string | unknown, -> = unknown extends R ? true : K extends R ? false : true; -// biome-ignore format: keep -type TFromProperties = Type.Evaluate<{ - -readonly [K in keyof T]: TFromPropertiesIsOptional extends true - ? Type.TOptional> - : TFromSchema -}> -// biome-ignore format: keep -type TFromObject = ( - TFromProperties[number]> extends infer Properties extends Type.TProperties - ? Type.TObject - : Type.TObject<{}> -) -function FromObject(T: T): TFromObject { - const properties = globalThis.Object.getOwnPropertyNames(T.properties).reduce((Acc, K) => { - return { - // biome-ignore lint/performance/noAccumulatingSpread: - ...Acc, - [K]: T.required?.includes(K) - ? FromSchema(T.properties[K]) - : Type.Optional(FromSchema(T.properties[K])), - }; - }, {} as Type.TProperties); - return Type.Object(properties, T) as never; -} -// ------------------------------------------------------------------ -// FromSchema -// ------------------------------------------------------------------ -// biome-ignore format: keep -export type TFromSchema = ( - T extends SAllOf ? TFromAllOf : - T extends SAnyOf ? TFromAnyOf : - T extends SOneOf ? TFromOneOf : - T extends SEnum ? TFromEnum : - T extends SObject ? TFromObject : - T extends STuple ? TFromTuple : - T extends SArray ? TFromArray : - T extends SConst ? TFromConst : - T extends SString ? Type.TString : - T extends SNumber ? Type.TNumber : - T extends SInteger ? Type.TInteger : - T extends SBoolean ? Type.TBoolean : - T extends SNull ? Type.TNull : - Type.TUnknown -) -/** Parses a TypeBox type from raw JsonSchema */ -export function FromSchema(T: T): TFromSchema { - // biome-ignore format: keep - return ( - IsSAllOf(T) ? FromAllOf(T) : - IsSAnyOf(T) ? FromAnyOf(T) : - IsSOneOf(T) ? FromOneOf(T) : - IsSEnum(T) ? FromEnum(T) : - IsSObject(T) ? FromObject(T) : - IsSTuple(T) ? FromTuple(T) : - IsSArray(T) ? FromArray(T) : - IsSConst(T) ? FromConst(T) : - IsSString(T) ? Type.String(T) : - IsSNumber(T) ? Type.Number(T) : - IsSInteger(T) ? Type.Integer(T) : - IsSBoolean(T) ? Type.Boolean(T) : - IsSNull(T) ? Type.Null(T) : - Type.Unknown(T || {}) - ) as never -} diff --git a/app/src/core/utils/typebox/index.ts b/app/src/core/utils/typebox/index.ts deleted file mode 100644 index c7252cc2..00000000 --- a/app/src/core/utils/typebox/index.ts +++ /dev/null @@ -1,194 +0,0 @@ -import * as tb from "@sinclair/typebox"; -import type { - TypeRegistry, - Static, - StaticDecode, - TSchema, - SchemaOptions, - TObject, -} from "@sinclair/typebox"; -import { - DefaultErrorFunction, - Errors, - SetErrorFunction, - type ValueErrorIterator, -} from "@sinclair/typebox/errors"; -import { Check, Default, Value, type ValueError } from "@sinclair/typebox/value"; -import type { RecursivePartial } from "../types"; - -/* type ParseOptions = { - useDefaults?: boolean; - decode?: boolean; - onError?: (errors: ValueErrorIterator) => void; - forceParse?: boolean; - skipMark?: boolean; -}; - -const validationSymbol = Symbol("tb-parse-validation"); - -export class TypeInvalidError extends Error { - errors: ValueError[]; - constructor( - public schema: tb.TSchema, - public data: unknown, - message?: string, - ) { - //console.warn("errored schema", JSON.stringify(schema, null, 2)); - super(message ?? `Invalid: ${JSON.stringify(data)}`); - this.errors = [...Errors(schema, data)]; - } - - first() { - return this.errors[0]!; - } - - firstToString() { - const first = this.first(); - return `${first.message} at "${first.path}"`; - } - - toJSON() { - return { - message: this.message, - schema: this.schema, - data: this.data, - errors: this.errors, - }; - } -} - -export function stripMark(obj: O) { - const newObj = structuredClone(obj); - mark(newObj, false); - return newObj as O; -} - -export function mark(obj: any, validated = true) { - if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) { - if (validated) { - obj[validationSymbol] = true; - } else { - delete obj[validationSymbol]; - } - for (const key in obj) { - if (typeof obj[key] === "object" && obj[key] !== null) { - mark(obj[key], validated); - } - } - } -} - -export function parse( - schema: Schema, - data: RecursivePartial>, - options?: ParseOptions, -): tb.Static { - if (!options?.forceParse && typeof data === "object" && validationSymbol in data) { - if (options?.useDefaults === false) { - return data as tb.Static; - } - - // this is important as defaults are expected - return Default(schema, data as any) as tb.Static; - } - - const parsed = options?.useDefaults === false ? data : Default(schema, data); - - if (Check(schema, parsed)) { - options?.skipMark !== true && mark(parsed, true); - return parsed as tb.Static; - } else if (options?.onError) { - options.onError(Errors(schema, data)); - } else { - throw new TypeInvalidError(schema, data); - } - - // @todo: check this - return undefined as any; -} - -export function parseDecode( - schema: Schema, - data: RecursivePartial>, -): tb.StaticDecode { - const parsed = Default(schema, data); - - if (Check(schema, parsed)) { - return parsed as tb.StaticDecode; - } - - throw new TypeInvalidError(schema, data); -} - -export function strictParse( - schema: Schema, - data: tb.Static, - options?: ParseOptions, -): tb.Static { - return parse(schema, data as any, options); -} - -export function registerCustomTypeboxKinds(registry: typeof TypeRegistry) { - registry.Set("StringEnum", (schema: any, value: any) => { - return typeof value === "string" && schema.enum.includes(value); - }); -} -registerCustomTypeboxKinds(tb.TypeRegistry); - -export const StringEnum = ( - values: T, - options?: tb.StringOptions, -) => - tb.Type.Unsafe({ - [tb.Kind]: "StringEnum", - type: "string", - enum: values, - ...options, - }); - -// key value record compatible with RJSF and typebox inference -// acting like a Record, but using an Object with additionalProperties -export const StringRecord = (properties: T, options?: tb.ObjectOptions) => - tb.Type.Object({}, { ...options, additionalProperties: properties }) as unknown as tb.TRecord< - tb.TString, - typeof properties - >; - -// fixed value that only be what is given + prefilled -export const Const = ( - value: T, - options?: tb.SchemaOptions, -) => - tb.Type.Literal(value, { - ...options, - default: value, - const: value, - readOnly: true, - }) as tb.TLiteral; - -export const StringIdentifier = tb.Type.String({ - pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$", - minLength: 2, - maxLength: 150, -}); - -export const StrictObject = ( - properties: T, - options?: tb.ObjectOptions, -): tb.TObject => tb.Type.Object(properties, { ...options, additionalProperties: false }); - -SetErrorFunction((error) => { - if (error?.schema?.errorMessage) { - return error.schema.errorMessage; - } - - if (error?.schema?.[tb.Kind] === "StringEnum") { - return `Expected: ${error.schema.enum.map((e) => `"${e}"`).join(", ")}`; - } - - return DefaultErrorFunction(error); -}); - -export type { Static, StaticDecode, TSchema, TObject, ValueError, SchemaOptions }; - -export { Value, Default, Errors, Check }; */ diff --git a/app/src/media/api/MediaController.ts b/app/src/media/api/MediaController.ts index dc53a2c5..2caf74a4 100644 --- a/app/src/media/api/MediaController.ts +++ b/app/src/media/api/MediaController.ts @@ -1,4 +1,4 @@ -import { isDebug, tbValidator as tb } from "core"; +import { isDebug } from "core"; import { HttpStatus, getFileFromContext } from "core/utils"; import type { StorageAdapter } from "media"; import { StorageEvents, getRandomizedFilename, MediaPermissions } from "media"; diff --git a/app/src/media/index.ts b/app/src/media/index.ts index 6d2f4f2a..ce3d9b00 100644 --- a/app/src/media/index.ts +++ b/app/src/media/index.ts @@ -1,4 +1,3 @@ -import type { TObject } from "@sinclair/typebox"; import { type Constructor, Registry } from "core"; export { guess as guessMimeType } from "./storage/mime-types-tiny"; diff --git a/app/src/modules/server/openapi.ts b/app/src/modules/server/openapi.ts deleted file mode 100644 index f5514674..00000000 --- a/app/src/modules/server/openapi.ts +++ /dev/null @@ -1,313 +0,0 @@ -import type { ModuleConfigs } from "modules/ModuleManager"; -import type { OpenAPIV3 as OAS } from "openapi-types"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; - -function prefixPaths(paths: OAS.PathsObject, prefix: string): OAS.PathsObject { - const result: OAS.PathsObject = {}; - for (const [path, pathItem] of Object.entries(paths)) { - result[`${prefix}${path}`] = pathItem; - } - return result; -} - -function systemRoutes(config: ModuleConfigs): { paths: OAS.Document["paths"] } { - const tags = ["system"]; - const paths: OAS.PathsObject = { - "/ping": { - get: { - summary: "Ping", - responses: { - "200": { - description: "Pong", - content: { - "application/json": { - schema: Type.Object({ - pong: Type.Boolean({ default: true }), - }), - }, - }, - }, - }, - tags, - }, - }, - "/config": { - get: { - summary: "Get config", - responses: { - "200": { - description: "Config", - content: { - "application/json": { - schema: Type.Object({ - version: Type.Number() as any, - server: Type.Object({}), - data: Type.Object({}), - auth: Type.Object({}), - flows: Type.Object({}), - media: Type.Object({}), - }), - }, - }, - }, - }, - tags, - }, - }, - "/schema": { - get: { - summary: "Get config", - responses: { - "200": { - description: "Config", - content: { - "application/json": { - schema: Type.Object({ - version: Type.Number() as any, - schema: Type.Object({ - server: Type.Object({}), - data: Type.Object({}), - auth: Type.Object({}), - flows: Type.Object({}), - media: Type.Object({}), - }), - }), - }, - }, - }, - }, - tags, - }, - }, - }; - - return { paths: prefixPaths(paths, "/api/system") }; -} - -function dataRoutes(config: ModuleConfigs): { paths: OAS.Document["paths"] } { - const schemas = { - entityData: Type.Object({ - id: Type.Number() as any, - }), - }; - const repoManyResponses: OAS.ResponsesObject = { - "200": { - description: "List of entities", - content: { - "application/json": { - schema: Type.Array(schemas.entityData), - }, - }, - }, - }; - const repoSingleResponses: OAS.ResponsesObject = { - "200": { - description: "Entity", - content: { - "application/json": { - schema: schemas.entityData, - }, - }, - }, - }; - const params = { - entity: { - name: "entity", - in: "path", - required: true, - schema: Type.String(), - }, - entityId: { - name: "id", - in: "path", - required: true, - schema: Type.Number() as any, - }, - }; - - const tags = ["data"]; - const paths: OAS.PathsObject = { - "/entity/{entity}": { - get: { - summary: "List entities", - parameters: [params.entity], - responses: repoManyResponses, - tags, - }, - post: { - summary: "Create entity", - parameters: [params.entity], - requestBody: { - content: { - "application/json": { - schema: Type.Object({}), - }, - }, - }, - responses: repoSingleResponses, - tags, - }, - }, - "/entity/{entity}/{id}": { - get: { - summary: "Get entity", - parameters: [params.entity, params.entityId], - responses: repoSingleResponses, - tags, - }, - patch: { - summary: "Update entity", - parameters: [params.entity, params.entityId], - requestBody: { - content: { - "application/json": { - schema: Type.Object({}), - }, - }, - }, - responses: repoSingleResponses, - tags, - }, - delete: { - summary: "Delete entity", - parameters: [params.entity, params.entityId], - responses: { - "200": { - description: "Entity deleted", - }, - }, - tags, - }, - }, - }; - - return { paths: prefixPaths(paths, config.data.basepath!) }; -} - -function authRoutes(config: ModuleConfigs): { paths: OAS.Document["paths"] } { - const schemas = { - user: Type.Object({ - id: Type.String(), - email: Type.String(), - name: Type.String(), - }), - }; - - const tags = ["auth"]; - const paths: OAS.PathsObject = { - "/password/login": { - post: { - summary: "Login", - requestBody: { - content: { - "application/json": { - schema: Type.Object({ - email: Type.String(), - password: Type.String(), - }), - }, - }, - }, - responses: { - "200": { - description: "User", - content: { - "application/json": { - schema: Type.Object({ - user: schemas.user, - }), - }, - }, - }, - }, - tags, - }, - }, - "/password/register": { - post: { - summary: "Register", - requestBody: { - content: { - "application/json": { - schema: Type.Object({ - email: Type.String(), - password: Type.String(), - }), - }, - }, - }, - responses: { - "200": { - description: "User", - content: { - "application/json": { - schema: Type.Object({ - user: schemas.user, - }), - }, - }, - }, - }, - tags, - }, - }, - "/me": { - get: { - summary: "Get me", - responses: { - "200": { - description: "User", - content: { - "application/json": { - schema: Type.Object({ - user: schemas.user, - }), - }, - }, - }, - }, - tags, - }, - }, - "/strategies": { - get: { - summary: "Get auth strategies", - responses: { - "200": { - description: "Strategies", - content: { - "application/json": { - schema: Type.Object({ - strategies: Type.Object({}), - }), - }, - }, - }, - }, - tags, - }, - }, - }; - - return { paths: prefixPaths(paths, config.auth.basepath!) }; -} - -export function generateOpenAPI(config: ModuleConfigs): OAS.Document { - const system = systemRoutes(config); - const data = dataRoutes(config); - const auth = authRoutes(config); - - return { - openapi: "3.1.0", - info: { - title: "bknd API", - version: "0.0.0", - }, - paths: { - ...system.paths, - ...data.paths, - ...auth.paths, - }, - }; -} diff --git a/app/src/ui/components/form/json-schema/JsonSchemaForm.tsx b/app/src/ui/components/form/json-schema/JsonSchemaForm.tsx index 588f1001..f0c3a776 100644 --- a/app/src/ui/components/form/json-schema/JsonSchemaForm.tsx +++ b/app/src/ui/components/form/json-schema/JsonSchemaForm.tsx @@ -5,10 +5,10 @@ import { cloneDeep } from "lodash-es"; import { forwardRef, useId, useImperativeHandle, useRef, useState } from "react"; import { fields as Fields } from "./fields"; import { templates as Templates } from "./templates"; -import { RJSFTypeboxValidator } from "./typebox/RJSFTypeboxValidator"; import { widgets as Widgets } from "./widgets"; +import { JsonvTsValidator } from "./JsonvTsValidator"; -const validator = new RJSFTypeboxValidator(); +const validator = new JsonvTsValidator(); // @todo: don't import FormProps, instead, copy it here instead of "any" export type JsonSchemaFormProps = any & { diff --git a/app/src/ui/components/form/json-schema/JsonSchemaValidator.ts b/app/src/ui/components/form/json-schema/JsonSchemaValidator.ts deleted file mode 100644 index df449369..00000000 --- a/app/src/ui/components/form/json-schema/JsonSchemaValidator.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { type OutputUnit, Validator } from "@cfworker/json-schema"; -import type { - CustomValidator, - ErrorSchema, - ErrorTransformer, - FormContextType, - RJSFSchema, - RJSFValidationError, - StrictRJSFSchema, - UiSchema, - ValidationData, - ValidatorType, -} from "@rjsf/utils"; -import { toErrorSchema } from "@rjsf/utils"; -import { get } from "lodash-es"; - -function removeUndefinedKeys(obj: any): any { - if (!obj) return obj; - - if (typeof obj === "object") { - Object.keys(obj).forEach((key) => { - if (obj[key] === undefined) { - delete obj[key]; - } else if (typeof obj[key] === "object") { - removeUndefinedKeys(obj[key]); - } - }); - } - - if (Array.isArray(obj)) { - return obj.filter((item) => item !== undefined); - } - - return obj; -} - -function onlyKeepMostSpecific(errors: OutputUnit[]) { - const mostSpecific = errors.filter((error) => { - return !errors.some((other) => { - return error !== other && other.instanceLocation.startsWith(error.instanceLocation); - }); - }); - return mostSpecific; -} - -const debug = true; -const validate = true; - -export class JsonSchemaValidator< - T = any, - S extends StrictRJSFSchema = RJSFSchema, - F extends FormContextType = any, -> implements ValidatorType -{ - // @ts-ignore - rawValidation(schema: S, formData?: T) { - if (!validate) return { errors: [], validationError: null }; - - debug && console.log("JsonSchemaValidator.rawValidation", schema, formData); - const validator = new Validator(schema as any); - const validation = validator.validate(removeUndefinedKeys(formData)); - const specificErrors = onlyKeepMostSpecific(validation.errors); - - return { errors: specificErrors, validationError: null as any }; - } - - validateFormData( - formData: T | undefined, - schema: S, - customValidate?: CustomValidator, - transformErrors?: ErrorTransformer, - uiSchema?: UiSchema, - ): ValidationData { - if (!validate) return { errors: [], errorSchema: {} as any }; - - debug && - console.log( - "JsonSchemaValidator.validateFormData", - formData, - schema, - customValidate, - transformErrors, - uiSchema, - ); - const { errors } = this.rawValidation(schema, formData); - debug && console.log("errors", { errors }); - - const transformedErrors = errors - //.filter((error) => error.keyword !== "properties") - .map((error) => { - const schemaLocation = error.keywordLocation.replace(/^#\/?/, "").split("/").join("."); - const propertyError = get(schema, schemaLocation); - const errorText = `${error.error.replace(/\.$/, "")}${propertyError ? ` "${propertyError}"` : ""}`; - //console.log(error, schemaLocation, get(schema, schemaLocation)); - return { - name: error.keyword, - message: errorText, - property: "." + error.instanceLocation.replace(/^#\/?/, "").split("/").join("."), - schemaPath: error.keywordLocation, - stack: error.error, - }; - }); - debug && console.log("transformed", transformedErrors); - - return { - errors: transformedErrors, - errorSchema: toErrorSchema(transformedErrors), - } as any; - } - - toErrorList(errorSchema?: ErrorSchema, fieldPath?: string[]): RJSFValidationError[] { - debug && console.log("JsonSchemaValidator.toErrorList", errorSchema, fieldPath); - return []; - } - - isValid(schema: S, formData: T | undefined, rootSchema: S): boolean { - if (!validate) return true; - debug && console.log("JsonSchemaValidator.isValid", schema, formData, rootSchema); - return this.rawValidation(schema, formData).errors.length === 0; - } -} diff --git a/app/src/ui/components/form/json-schema/typebox/RJSFTypeboxValidator.ts b/app/src/ui/components/form/json-schema/JsonvTsValidator.ts similarity index 67% rename from app/src/ui/components/form/json-schema/typebox/RJSFTypeboxValidator.ts rename to app/src/ui/components/form/json-schema/JsonvTsValidator.ts index d76f020b..a3f8b53f 100644 --- a/app/src/ui/components/form/json-schema/typebox/RJSFTypeboxValidator.ts +++ b/app/src/ui/components/form/json-schema/JsonvTsValidator.ts @@ -1,6 +1,4 @@ -import { Check } from "@sinclair/typebox/value"; -import { Errors } from "@sinclair/typebox/errors"; -import { FromSchema } from "./from-schema"; +import * as s from "jsonv-ts"; import type { CustomValidator, @@ -16,7 +14,7 @@ import { toErrorSchema } from "@rjsf/utils"; const validate = true; -export class RJSFTypeboxValidator +export class JsonvTsValidator implements ValidatorType { // @ts-ignore @@ -24,16 +22,16 @@ export class RJSFTypeboxValidator { - const schemaLocation = error.path.substring(1).split("/").join("."); - return { name: "any", - message: error.message, - property: "." + schemaLocation, - schemaPath: error.path, - stack: error.message, + message: error.error, + property: "." + error.instanceLocation.substring(1).split("/").join("."), + schemaPath: error.instanceLocation, + stack: error.error, }; }); diff --git a/app/src/ui/components/form/json-schema/typebox/from-schema.ts b/app/src/ui/components/form/json-schema/typebox/from-schema.ts deleted file mode 100644 index cb11350f..00000000 --- a/app/src/ui/components/form/json-schema/typebox/from-schema.ts +++ /dev/null @@ -1,299 +0,0 @@ -/*-------------------------------------------------------------------------- - -@sinclair/typebox/prototypes - -The MIT License (MIT) - -Copyright (c) 2017-2024 Haydn Paterson (sinclair) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ----------------------------------------------------------------------------*/ - -import * as Type from "@sinclair/typebox"; - -// ------------------------------------------------------------------ -// Schematics -// ------------------------------------------------------------------ -const IsExact = (value: unknown, expect: unknown) => value === expect; -const IsSValue = (value: unknown): value is SValue => - Type.ValueGuard.IsString(value) || - Type.ValueGuard.IsNumber(value) || - Type.ValueGuard.IsBoolean(value); -const IsSEnum = (value: unknown): value is SEnum => - Type.ValueGuard.IsObject(value) && - Type.ValueGuard.IsArray(value.enum) && - value.enum.every((value) => IsSValue(value)); -const IsSAllOf = (value: unknown): value is SAllOf => - Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.allOf); -const IsSAnyOf = (value: unknown): value is SAnyOf => - Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.anyOf); -const IsSOneOf = (value: unknown): value is SOneOf => - Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.oneOf); -const IsSTuple = (value: unknown): value is STuple => - Type.ValueGuard.IsObject(value) && - IsExact(value.type, "array") && - Type.ValueGuard.IsArray(value.items); -const IsSArray = (value: unknown): value is SArray => - Type.ValueGuard.IsObject(value) && - IsExact(value.type, "array") && - !Type.ValueGuard.IsArray(value.items) && - Type.ValueGuard.IsObject(value.items); -const IsSConst = (value: unknown): value is SConst => - // biome-ignore lint: reason - Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsObject(value["const"]); -const IsSString = (value: unknown): value is SString => - Type.ValueGuard.IsObject(value) && IsExact(value.type, "string"); -const IsSNumber = (value: unknown): value is SNumber => - Type.ValueGuard.IsObject(value) && IsExact(value.type, "number"); -const IsSInteger = (value: unknown): value is SInteger => - Type.ValueGuard.IsObject(value) && IsExact(value.type, "integer"); -const IsSBoolean = (value: unknown): value is SBoolean => - Type.ValueGuard.IsObject(value) && IsExact(value.type, "boolean"); -const IsSNull = (value: unknown): value is SBoolean => - Type.ValueGuard.IsObject(value) && IsExact(value.type, "null"); -const IsSProperties = (value: unknown): value is SProperties => Type.ValueGuard.IsObject(value); -// prettier-ignore -// biome-ignore format: -const IsSObject = (value: unknown): value is SObject => Type.ValueGuard.IsObject(value) && IsExact(value.type, 'object') && IsSProperties(value.properties) && (value.required === undefined || Type.ValueGuard.IsArray(value.required) && value.required.every((value: unknown) => Type.ValueGuard.IsString(value))) -type SValue = string | number | boolean; -type SEnum = Readonly<{ enum: readonly SValue[] }>; -type SAllOf = Readonly<{ allOf: readonly unknown[] }>; -type SAnyOf = Readonly<{ anyOf: readonly unknown[] }>; -type SOneOf = Readonly<{ oneOf: readonly unknown[] }>; -type SProperties = Record; -type SObject = Readonly<{ - type: "object"; - properties: SProperties; - required?: readonly string[]; -}>; -type STuple = Readonly<{ type: "array"; items: readonly unknown[] }>; -type SArray = Readonly<{ type: "array"; items: unknown }>; -type SConst = Readonly<{ const: SValue }>; -type SString = Readonly<{ type: "string" }>; -type SNumber = Readonly<{ type: "number" }>; -type SInteger = Readonly<{ type: "integer" }>; -type SBoolean = Readonly<{ type: "boolean" }>; -type SNull = Readonly<{ type: "null" }>; -// ------------------------------------------------------------------ -// FromRest -// ------------------------------------------------------------------ -// prettier-ignore -// biome-ignore format: -type TFromRest = ( - // biome-ignore lint: reason - T extends readonly [infer L extends unknown, ...infer R extends unknown[]] - ? TFromSchema extends infer S extends Type.TSchema - ? TFromRest - : TFromRest - : Acc -) -function FromRest(T: T): TFromRest { - return T.map((L) => FromSchema(L)) as never; -} -// ------------------------------------------------------------------ -// FromEnumRest -// ------------------------------------------------------------------ -// prettier-ignore -// biome-ignore format: -type TFromEnumRest = ( - T extends readonly [infer L extends SValue, ...infer R extends SValue[]] - ? TFromEnumRest]> - : Acc -) -function FromEnumRest(T: T): TFromEnumRest { - return T.map((L) => Type.Literal(L)) as never; -} -// ------------------------------------------------------------------ -// AllOf -// ------------------------------------------------------------------ -// prettier-ignore -// biome-ignore format: -type TFromAllOf = ( - TFromRest extends infer Rest extends Type.TSchema[] - ? Type.TIntersectEvaluated - : Type.TNever -) -function FromAllOf(T: T): TFromAllOf { - return Type.IntersectEvaluated(FromRest(T.allOf), T); -} -// ------------------------------------------------------------------ -// AnyOf -// ------------------------------------------------------------------ -// prettier-ignore -// biome-ignore format: -type TFromAnyOf = ( - TFromRest extends infer Rest extends Type.TSchema[] - ? Type.TUnionEvaluated - : Type.TNever -) -function FromAnyOf(T: T): TFromAnyOf { - return Type.UnionEvaluated(FromRest(T.anyOf), T); -} -// ------------------------------------------------------------------ -// OneOf -// ------------------------------------------------------------------ -// prettier-ignore -// biome-ignore format: -type TFromOneOf = ( - TFromRest extends infer Rest extends Type.TSchema[] - ? Type.TUnionEvaluated - : Type.TNever -) -function FromOneOf(T: T): TFromOneOf { - return Type.UnionEvaluated(FromRest(T.oneOf), T); -} -// ------------------------------------------------------------------ -// Enum -// ------------------------------------------------------------------ -// prettier-ignore -// biome-ignore format: -type TFromEnum = ( - TFromEnumRest extends infer Elements extends Type.TSchema[] - ? Type.TUnionEvaluated - : Type.TNever -) -function FromEnum(T: T): TFromEnum { - return Type.UnionEvaluated(FromEnumRest(T.enum)); -} -// ------------------------------------------------------------------ -// Tuple -// ------------------------------------------------------------------ -// prettier-ignore -// biome-ignore format: -type TFromTuple = ( - TFromRest extends infer Elements extends Type.TSchema[] - ? Type.TTuple - : Type.TTuple<[]> -) -// prettier-ignore -// biome-ignore format: -function FromTuple(T: T): TFromTuple { - return Type.Tuple(FromRest(T.items), T) as never -} -// ------------------------------------------------------------------ -// Array -// ------------------------------------------------------------------ -// prettier-ignore -// biome-ignore format: -type TFromArray = ( - TFromSchema extends infer Items extends Type.TSchema - ? Type.TArray - : Type.TArray -) -// prettier-ignore -// biome-ignore format: -function FromArray(T: T): TFromArray { - return Type.Array(FromSchema(T.items), T) as never -} -// ------------------------------------------------------------------ -// Const -// ------------------------------------------------------------------ -// prettier-ignore -// biome-ignore format: -type TFromConst = ( - Type.Ensure> -) -function FromConst(T: T) { - return Type.Literal(T.const, T); -} -// ------------------------------------------------------------------ -// Object -// ------------------------------------------------------------------ -type TFromPropertiesIsOptional< - K extends PropertyKey, - R extends string | unknown, -> = unknown extends R ? true : K extends R ? false : true; -// prettier-ignore -// biome-ignore format: -type TFromProperties = Type.Evaluate<{ - -readonly [K in keyof T]: TFromPropertiesIsOptional extends true - ? Type.TOptional> - : TFromSchema -}> -// prettier-ignore -// biome-ignore format: -type TFromObject = ( - TFromProperties[number]> extends infer Properties extends Type.TProperties - ? Type.TObject - : Type.TObject<{}> -) -function FromObject(T: T): TFromObject { - const properties = globalThis.Object.getOwnPropertyNames(T.properties).reduce((Acc, K) => { - return { - // biome-ignore lint: - ...Acc, - [K]: - // biome-ignore lint: reason - T.required && T.required.includes(K) - ? FromSchema(T.properties[K]) - : Type.Optional(FromSchema(T.properties[K])), - }; - }, {} as Type.TProperties); - - if ("additionalProperties" in T) { - return Type.Object(properties, { - additionalProperties: FromSchema(T.additionalProperties), - }) as never; - } - - return Type.Object(properties, T) as never; -} -// ------------------------------------------------------------------ -// FromSchema -// ------------------------------------------------------------------ -// prettier-ignore -// biome-ignore format: -export type TFromSchema = ( - T extends SAllOf ? TFromAllOf : - T extends SAnyOf ? TFromAnyOf : - T extends SOneOf ? TFromOneOf : - T extends SEnum ? TFromEnum : - T extends SObject ? TFromObject : - T extends STuple ? TFromTuple : - T extends SArray ? TFromArray : - T extends SConst ? TFromConst : - T extends SString ? Type.TString : - T extends SNumber ? Type.TNumber : - T extends SInteger ? Type.TInteger : - T extends SBoolean ? Type.TBoolean : - T extends SNull ? Type.TNull : - Type.TUnknown -) -/** Parses a TypeBox type from raw JsonSchema */ -export function FromSchema(T: T): TFromSchema { - // prettier-ignore - // biome-ignore format: - return ( - IsSAllOf(T) ? FromAllOf(T) : - IsSAnyOf(T) ? FromAnyOf(T) : - IsSOneOf(T) ? FromOneOf(T) : - IsSEnum(T) ? FromEnum(T) : - IsSObject(T) ? FromObject(T) : - IsSTuple(T) ? FromTuple(T) : - IsSArray(T) ? FromArray(T) : - IsSConst(T) ? FromConst(T) : - IsSString(T) ? Type.String(T) : - IsSNumber(T) ? Type.Number(T) : - IsSInteger(T) ? Type.Integer(T) : - IsSBoolean(T) ? Type.Boolean(T) : - IsSNull(T) ? Type.Null(T) : - Type.Unknown(T || {}) - ) as never -} diff --git a/app/src/ui/modules/data/components/schema/create-modal/step.entity.fields.tsx b/app/src/ui/modules/data/components/schema/create-modal/step.entity.fields.tsx index 874d9d17..c52ceb5b 100644 --- a/app/src/ui/modules/data/components/schema/create-modal/step.entity.fields.tsx +++ b/app/src/ui/modules/data/components/schema/create-modal/step.entity.fields.tsx @@ -13,6 +13,7 @@ import { import { ModalBody, ModalFooter, type TCreateModalSchema, useStepContext } from "./CreateModal"; import { useBkndData } from "ui/client/schema/data/use-bknd-data"; import type { s } from "core/object/schema"; +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; const schema = entitiesSchema; type Schema = s.Static; @@ -41,8 +42,7 @@ export function StepEntityFields() { setValue, } = useForm({ mode: "onTouched", - // @todo: add resolver - //resolver: typeboxResolver(schema), + resolver: standardSchemaResolver(schema), defaultValues: initial as NonNullable, }); diff --git a/app/src/ui/modules/data/components/schema/create-modal/step.entity.tsx b/app/src/ui/modules/data/components/schema/create-modal/step.entity.tsx index 782d03ae..6c105f9a 100644 --- a/app/src/ui/modules/data/components/schema/create-modal/step.entity.tsx +++ b/app/src/ui/modules/data/components/schema/create-modal/step.entity.tsx @@ -1,5 +1,4 @@ -//import { typeboxResolver } from "@hookform/resolvers/typebox"; - +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; import { TextInput, Textarea } from "@mantine/core"; import { useFocusTrap } from "@mantine/hooks"; import { useForm } from "react-hook-form"; @@ -17,8 +16,7 @@ export function StepEntity() { const { nextStep, stepBack, state, setState } = useStepContext(); const { register, handleSubmit, formState, watch, control } = useForm({ mode: "onTouched", - // @todo: add resolver - //resolver: typeboxResolver(entitySchema), + resolver: standardSchemaResolver(entitySchema), defaultValues: state.entities?.create?.[0] ?? {}, }); /*const data = watch(); diff --git a/app/src/ui/modules/data/components/schema/create-modal/step.relation.tsx b/app/src/ui/modules/data/components/schema/create-modal/step.relation.tsx index 92fa01ee..e152508f 100644 --- a/app/src/ui/modules/data/components/schema/create-modal/step.relation.tsx +++ b/app/src/ui/modules/data/components/schema/create-modal/step.relation.tsx @@ -12,6 +12,7 @@ import { useStepContext } from "ui/components/steps/Steps"; import { useEvent } from "ui/hooks/use-event"; import { ModalBody, ModalFooter, type TCreateModalSchema } from "./CreateModal"; import { s, stringIdentifier } from "core/object/schema"; +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; const Relations: { type: RelationType; @@ -66,8 +67,7 @@ export function StepRelation() { watch, control, } = useForm({ - // @todo: implement resolver - //resolver: typeboxResolver(schema), + resolver: standardSchemaResolver(schema), defaultValues: (state.relations?.create?.[0] ?? {}) as s.Static, }); const data = watch(); diff --git a/app/src/ui/modules/data/components/schema/create-modal/templates/media/template.media.component.tsx b/app/src/ui/modules/data/components/schema/create-modal/templates/media/template.media.component.tsx index 7d18f76c..4137ab43 100644 --- a/app/src/ui/modules/data/components/schema/create-modal/templates/media/template.media.component.tsx +++ b/app/src/ui/modules/data/components/schema/create-modal/templates/media/template.media.component.tsx @@ -15,6 +15,7 @@ import { useStepContext, } from "../../CreateModal"; import { s, stringIdentifier } from "core/object/schema"; +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; const schema = s.object({ entity: stringIdentifier, @@ -34,8 +35,7 @@ export function TemplateMediaComponent() { control, } = useForm({ mode: "onChange", - // @todo: add resolver - //resolver: typeboxResolver(schema), + resolver: standardSchemaResolver(schema), defaultValues: schema.template(state.initial ?? {}) as TCreateModalMediaSchema, //defaultValues: Default(schema, state.initial ?? {}) as TCreateModalMediaSchema, }); diff --git a/app/src/ui/modules/flows/components2/nodes/tasks/FetchTaskNode.tsx b/app/src/ui/modules/flows/components2/nodes/tasks/FetchTaskNode.tsx index e7431c49..52339c23 100644 --- a/app/src/ui/modules/flows/components2/nodes/tasks/FetchTaskNode.tsx +++ b/app/src/ui/modules/flows/components2/nodes/tasks/FetchTaskNode.tsx @@ -13,6 +13,7 @@ import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelec import type { TFlowNodeData } from "../../../hooks/use-flow"; import { KeyValueInput } from "../../form/KeyValueInput"; import { BaseNode } from "../BaseNode"; +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; const schema = s.object({ query: s.record(s.string()).optional(), @@ -37,8 +38,7 @@ export function FetchTaskForm({ onChange, params, ...props }: FetchTaskFormProps watch, control, } = useForm({ - // @todo: add resolver - //resolver: typeboxResolver(schema), + resolver: standardSchemaResolver(schema), defaultValues: params as s.Static, mode: "onChange", //defaultValues: (state.relations?.create?.[0] ?? {}) as Static diff --git a/app/src/ui/modules/flows/components2/nodes/triggers/TriggerNode.tsx b/app/src/ui/modules/flows/components2/nodes/triggers/TriggerNode.tsx index 93cb1c4d..ad0979d4 100644 --- a/app/src/ui/modules/flows/components2/nodes/triggers/TriggerNode.tsx +++ b/app/src/ui/modules/flows/components2/nodes/triggers/TriggerNode.tsx @@ -11,6 +11,7 @@ import { useFlowCanvas, useFlowSelector } from "../../../hooks/use-flow"; import { BaseNode } from "../BaseNode"; import { Handle } from "../Handle"; import { s } from "core/object/schema"; +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; const schema = s.object({ trigger: s.anyOf( @@ -45,8 +46,7 @@ export const TriggerNode = (props: NodeProps, mode: "onChange", }); diff --git a/app/src/ui/routes/auth/forms/role.form.tsx b/app/src/ui/routes/auth/forms/role.form.tsx index 294c531f..f811584c 100644 --- a/app/src/ui/routes/auth/forms/role.form.tsx +++ b/app/src/ui/routes/auth/forms/role.form.tsx @@ -1,4 +1,3 @@ -//import { typeboxResolver } from "@hookform/resolvers/typebox"; import { Input, Switch, Tooltip } from "@mantine/core"; import { guardRoleSchema } from "auth/auth-schema"; import { ucFirst } from "core/utils"; @@ -8,6 +7,7 @@ import { useBknd } from "ui/client/bknd"; import { Button } from "ui/components/buttons/Button"; import { MantineSwitch } from "ui/components/form/hook-form-mantine/MantineSwitch"; import type { s } from "core/object/schema"; +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; const schema = guardRoleSchema; type Role = s.Static; @@ -34,8 +34,7 @@ export const AuthRoleForm = forwardRef< reset, getValues, } = useForm({ - // @todo: add resolver - //resolver: typeboxResolver(schema), + resolver: standardSchemaResolver(schema), defaultValues: role, }); diff --git a/app/src/ui/routes/data/forms/entity.fields.form.tsx b/app/src/ui/routes/data/forms/entity.fields.form.tsx index e6723b84..917fe6fc 100644 --- a/app/src/ui/routes/data/forms/entity.fields.form.tsx +++ b/app/src/ui/routes/data/forms/entity.fields.form.tsx @@ -22,6 +22,7 @@ import { useRoutePathState } from "ui/hooks/use-route-path-state"; import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect"; import type { TPrimaryFieldFormat } from "data/fields/PrimaryField"; import { s, stringIdentifier } from "core/object/schema"; +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; const fieldsSchemaObject = originalFieldsSchemaObject; const fieldsSchema = s.anyOf(Object.values(fieldsSchemaObject)); @@ -88,8 +89,7 @@ export const EntityFieldsForm = forwardRef { - async validate(schema: TSchema, data: any) { - return Value.Check(schema, data) ? [] : [...Value.Errors(schema, data)]; - } -} -const validator = new TypeboxValidator(); - -const schema = Type.Object({ - name: Type.String(), - age: Type.Optional(Type.Number()), -}); - -export default function JsonSchemaFormReactTest() { - const [data, setData] = useState(null); - - return ( - <> -
- {({ errors, dirty, reset }) => ( - <> -
- - Form {dirty ? "*" : ""} (valid: {errors.length === 0 ? "valid" : "invalid"}) - -
-
- - -
-
- - -
- - )} -
-
{JSON.stringify(data, null, 2)}
- - ); -} diff --git a/app/src/ui/routes/test/tests/react-hook-errors.tsx b/app/src/ui/routes/test/tests/react-hook-errors.tsx index 036a44db..08212998 100644 --- a/app/src/ui/routes/test/tests/react-hook-errors.tsx +++ b/app/src/ui/routes/test/tests/react-hook-errors.tsx @@ -1,11 +1,11 @@ -import { typeboxResolver } from "@hookform/resolvers/typebox"; +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; import { TextInput } from "@mantine/core"; -import { Type } from "@sinclair/typebox"; import { useForm } from "react-hook-form"; +import { s } from "core/object/schema"; -const schema = Type.Object({ - example: Type.Optional(Type.String()), - exampleRequired: Type.String({ minLength: 2 }), +const schema = s.object({ + example: s.string().optional(), + exampleRequired: s.string({ minLength: 2 }), }); export default function ReactHookErrors() { @@ -15,8 +15,10 @@ export default function ReactHookErrors() { watch, formState: { errors }, } = useForm({ - resolver: typeboxResolver(schema), + resolver: standardSchemaResolver(schema), }); + const data = watch(); + const onSubmit = (data) => console.log(data); console.log(watch("example")); // watch input value by passing the name of it diff --git a/bun.lock b/bun.lock index 8d730fcd..36c9ad81 100644 --- a/bun.lock +++ b/bun.lock @@ -58,6 +58,7 @@ "@mantine/notifications": "^7.17.1", "@playwright/test": "^1.51.1", "@rjsf/core": "5.22.2", + "@standard-schema/spec": "^1.0.0", "@tabler/icons-react": "3.18.0", "@tailwindcss/postcss": "^4.0.12", "@tailwindcss/vite": "^4.0.12", @@ -73,7 +74,7 @@ "dotenv": "^16.4.7", "jotai": "^2.12.2", "jsdom": "^26.0.0", - "jsonv-ts": "0.2.0-alpha.5", + "jsonv-ts": "^0.2.1", "kysely-d1": "^0.3.0", "kysely-generic-sqlite": "^1.2.1", "libsql-stateless-easy": "^1.8.0", @@ -1151,6 +1152,8 @@ "@sqlite.org/sqlite-wasm": ["@sqlite.org/sqlite-wasm@3.48.0-build4", "", { "bin": { "sqlite-wasm": "bin/index.js" } }, "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ=="], + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], @@ -2503,7 +2506,7 @@ "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], - "jsonv-ts": ["jsonv-ts@0.2.0-alpha.5", "", { "optionalDependencies": { "hono": "4.7.11" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-o1rnGgMY0TGiInweOizqak4JNGyvOGIv9UpGqJokq6Nj8RnrXLvdaU+K9v99iqrnbilKwBV8N/wYS8h0iNOQpA=="], + "jsonv-ts": ["jsonv-ts@0.2.1", "", { "optionalDependencies": { "hono": "4.7.11" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-k2uc4DdQgz5wk4eXx6jM0xOUEGbedVMPhP1mi8+NAg2pjo85cgBv1lCyo9Wqc9cmbZHTGfho7PvAsRIH33LfDA=="], "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],