diff --git a/app/__test__/app/AppServer.spec.ts b/app/__test__/app/AppServer.spec.ts new file mode 100644 index 00000000..87354d00 --- /dev/null +++ b/app/__test__/app/AppServer.spec.ts @@ -0,0 +1,35 @@ +import { AppServer, serverConfigSchema } from "modules/server/AppServer"; +import { describe, test, expect } from "bun:test"; + +describe("AppServer", () => { + test("config", () => { + { + const server = new AppServer(); + expect(server).toBeDefined(); + expect(server.config).toEqual({ + cors: { + origin: "*", + allow_methods: ["GET", "POST", "PATCH", "PUT", "DELETE"], + allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"], + }, + }); + } + + { + const server = new AppServer({ + cors: { + origin: "https", + allow_methods: ["GET", "POST"], + }, + }); + expect(server).toBeDefined(); + expect(server.config).toEqual({ + cors: { + origin: "https", + allow_methods: ["GET", "POST"], + allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"], + }, + }); + } + }); +}); diff --git a/app/__test__/modules/module-test-suite.ts b/app/__test__/modules/module-test-suite.ts index 610dc289..458f7749 100644 --- a/app/__test__/modules/module-test-suite.ts +++ b/app/__test__/modules/module-test-suite.ts @@ -4,7 +4,6 @@ import { Hono } from "hono"; import { Guard } from "../../src/auth"; import { DebugLogger } from "../../src/core"; import { EventManager } from "../../src/core/events"; -import { Default, stripMark } from "../../src/core/utils"; import { EntityManager } from "../../src/data"; import { Module, type ModuleBuildContext } from "../../src/modules/Module"; import { getDummyConnection } from "../helper"; @@ -45,7 +44,8 @@ export function moduleTestSuite(module: { new (): Module }) { it("uses the default config", async () => { const m = new module(); await m.setContext(ctx).build(); - expect(stripMark(m.toJSON())).toEqual(Default(m.getSchema(), {})); + expect(m.toJSON()).toEqual(m.getSchema().template()); + //expect(stripMark(m.toJSON())).toEqual(Default(m.getSchema(), {})); }); }); } diff --git a/app/build.ts b/app/build.ts index 49bdf95d..13934e06 100644 --- a/app/build.ts +++ b/app/build.ts @@ -226,7 +226,7 @@ function baseConfig(adapter: string, overrides: Partial = {}): tsu }, external: [ /^cloudflare*/, - /^@?(hono|libsql).*?/, + /^@?hono.*?/, /^(bknd|react|next|node).*?/, /.*\.(html)$/, ...external, diff --git a/app/package.json b/app/package.json index 0fc88193..9e6181a7 100644 --- a/app/package.json +++ b/app/package.json @@ -102,7 +102,7 @@ "dotenv": "^16.4.7", "jotai": "^2.12.2", "jsdom": "^26.0.0", - "jsonv-ts": "^0.1.0", + "jsonv-ts": "link:jsonv-ts", "kysely-d1": "^0.3.0", "kysely-generic-sqlite": "^1.2.1", "libsql-stateless-easy": "^1.8.0", diff --git a/app/src/adapter/cloudflare/storage/StorageR2Adapter.ts b/app/src/adapter/cloudflare/storage/StorageR2Adapter.ts index 030855a5..9e11eac3 100644 --- a/app/src/adapter/cloudflare/storage/StorageR2Adapter.ts +++ b/app/src/adapter/cloudflare/storage/StorageR2Adapter.ts @@ -1,15 +1,13 @@ import { registries } from "bknd"; import { isDebug } from "bknd/core"; -import { StringEnum } from "bknd/utils"; import { guessMimeType as guess, StorageAdapter, type FileBody } from "bknd/media"; import { getBindings } from "../bindings"; -import * as tb from "@sinclair/typebox"; -const { Type } = tb; +import { s } from "core/object/schema"; export function makeSchema(bindings: string[] = []) { - return Type.Object( + return s.object( { - binding: bindings.length > 0 ? StringEnum(bindings) : Type.Optional(Type.String()), + binding: bindings.length > 0 ? s.string({ enum: bindings }) : s.string().optional(), }, { title: "R2", description: "Cloudflare R2 storage" }, ); diff --git a/app/src/adapter/node/storage/StorageLocalAdapter.ts b/app/src/adapter/node/storage/StorageLocalAdapter.ts index 88bb395f..9f624e85 100644 --- a/app/src/adapter/node/storage/StorageLocalAdapter.ts +++ b/app/src/adapter/node/storage/StorageLocalAdapter.ts @@ -1,17 +1,16 @@ import { readFile, readdir, stat, unlink, writeFile } from "node:fs/promises"; -import { type Static, isFile, parse } from "bknd/utils"; +import { isFile } from "bknd/utils"; import type { FileBody, FileListObject, FileMeta, FileUploadPayload } from "bknd/media"; import { StorageAdapter, guessMimeType as guess } from "bknd/media"; -import * as tb from "@sinclair/typebox"; -const { Type } = tb; +import { parse, s } from "core/object/schema"; -export const localAdapterConfig = Type.Object( +export const localAdapterConfig = s.object( { - path: Type.String({ default: "./" }), + path: s.string({ default: "./" }), }, { title: "Local", description: "Local file system storage", additionalProperties: false }, ); -export type LocalAdapterConfig = Static; +export type LocalAdapterConfig = s.Static; export class StorageLocalAdapter extends StorageAdapter { private config: LocalAdapterConfig; @@ -62,8 +61,7 @@ export class StorageLocalAdapter extends StorageAdapter { } const filePath = `${this.config.path}/${key}`; - const is_file = isFile(body); - await writeFile(filePath, is_file ? body.stream() : body); + await writeFile(filePath, isFile(body) ? body.stream() : body); return await this.computeEtag(body); } diff --git a/app/src/auth/AppAuth.ts b/app/src/auth/AppAuth.ts index 973332e1..8a493b3e 100644 --- a/app/src/auth/AppAuth.ts +++ b/app/src/auth/AppAuth.ts @@ -20,7 +20,7 @@ declare module "core" { export type CreateUserPayload = { email: string; password: string; [key: string]: any }; -export class AppAuth extends Module { +export class AppAuth extends Module { private _authenticator?: Authenticator; cache: Record = {}; _controller!: AuthController; @@ -197,6 +197,6 @@ export class AppAuth extends Module { enabled: this.isStrategyEnabled(strategy), ...strategy.toJSON(secrets), })), - }; + } as AppAuthSchema; } } diff --git a/app/src/auth/api/AuthController.ts b/app/src/auth/api/AuthController.ts index 1f2b85de..f7dca82f 100644 --- a/app/src/auth/api/AuthController.ts +++ b/app/src/auth/api/AuthController.ts @@ -1,9 +1,9 @@ import { type AppAuth, AuthPermissions, type SafeUser, type Strategy } from "auth"; -import { TypeInvalidError, parse, transformObject } from "core/utils"; +import { transformObject } from "core/utils"; import { DataPermissions } from "data"; import type { Hono } from "hono"; import { Controller, type ServerEnv } from "modules/Controller"; -import { describeRoute, jsc, s } from "core/object/schema"; +import { describeRoute, jsc, s, parse, InvalidSchemaError } from "core/object/schema"; export type AuthActionResponse = { success: boolean; @@ -58,7 +58,7 @@ export class AuthController extends Controller { try { const body = await this.auth.authenticator.getBody(c); const valid = parse(create.schema, body, { - skipMark: true, + //skipMark: true, }); const processed = (await create.preprocess?.(valid)) ?? valid; @@ -78,7 +78,7 @@ export class AuthController extends Controller { data: created as unknown as SafeUser, } as AuthActionResponse); } catch (e) { - if (e instanceof TypeInvalidError) { + if (e instanceof InvalidSchemaError) { return c.json( { success: false, diff --git a/app/src/auth/auth-schema.ts b/app/src/auth/auth-schema.ts index e607d97a..e34a0059 100644 --- a/app/src/auth/auth-schema.ts +++ b/app/src/auth/auth-schema.ts @@ -1,8 +1,7 @@ import { cookieConfig, jwtConfig } from "auth/authenticate/Authenticator"; import { CustomOAuthStrategy, OAuthStrategy, PasswordStrategy } from "auth/authenticate/strategies"; -import { type Static, StringRecord, objectTransform } from "core/utils"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { objectTransform } from "core/utils"; +import { s } from "core/object/schema"; export const Strategies = { password: { @@ -21,64 +20,74 @@ export const Strategies = { export const STRATEGIES = Strategies; const strategiesSchemaObject = objectTransform(STRATEGIES, (strategy, name) => { - return Type.Object( + return s.strictObject( { - enabled: Type.Optional(Type.Boolean({ default: true })), - type: Type.Const(name, { default: name, readOnly: true }), + enabled: s.boolean({ default: true }).optional(), + type: s.literal(name), config: strategy.schema, }, { title: name, - additionalProperties: false, }, ); }); -const strategiesSchema = Type.Union(Object.values(strategiesSchemaObject)); -export type AppAuthStrategies = Static; -export type AppAuthOAuthStrategy = Static; -export type AppAuthCustomOAuthStrategy = Static; -const guardConfigSchema = Type.Object({ - enabled: Type.Optional(Type.Boolean({ default: false })), +const strategiesSchema = s.anyOf(Object.values(strategiesSchemaObject)); +export type AppAuthStrategies = s.Static; +export type AppAuthOAuthStrategy = s.Static; +export type AppAuthCustomOAuthStrategy = s.Static; + +const guardConfigSchema = s.object({ + enabled: s.boolean({ default: false }).optional(), +}); +export const guardRoleSchema = s.strictObject({ + permissions: s.array(s.string()).optional(), + is_default: s.boolean().optional(), + implicit_allow: s.boolean().optional(), }); -export const guardRoleSchema = Type.Object( - { - permissions: Type.Optional(Type.Array(Type.String())), - is_default: Type.Optional(Type.Boolean()), - implicit_allow: Type.Optional(Type.Boolean()), - }, - { additionalProperties: false }, -); -export const authConfigSchema = Type.Object( +const a = s.record(strategiesSchema, { + // ^? + title: "Strategies", + default: { + password: { + type: "password", + enabled: true, + config: { + hashing: "sha256", + }, + }, + }, +}); + +export const authConfigSchema = s.strictObject( { - enabled: Type.Boolean({ default: false }), - basepath: Type.String({ default: "/api/auth" }), - entity_name: Type.String({ default: "users" }), - allow_register: Type.Optional(Type.Boolean({ default: true })), + enabled: s.boolean({ default: false }), + basepath: s.string({ default: "/api/auth" }), + entity_name: s.string({ default: "users" }), + allow_register: s.boolean({ default: true }).optional(), jwt: jwtConfig, cookie: cookieConfig, - strategies: Type.Optional( - StringRecord(strategiesSchema, { - title: "Strategies", - default: { - password: { - type: "password", - enabled: true, - config: { - hashing: "sha256", - }, + strategies: s.record(strategiesSchema, { + title: "Strategies", + default: { + password: { + type: "password", + enabled: true, + config: { + hashing: "sha256", }, }, - }), - ), - guard: Type.Optional(guardConfigSchema), - roles: Type.Optional(StringRecord(guardRoleSchema, { default: {} })), - }, - { - title: "Authentication", - additionalProperties: false, + }, + }), + guard: guardConfigSchema.optional(), + roles: s.record(guardRoleSchema, { default: {} }).optional(), }, + { title: "Authentication" }, ); +const b = authConfigSchema.properties.basepath; +// ^? +const c = authConfigSchema.properties.strategies; +// ^? -export type AppAuthSchema = Static; +export type AppAuthSchema = s.Static; diff --git a/app/src/auth/authenticate/Authenticator.ts b/app/src/auth/authenticate/Authenticator.ts index 4357da05..c383bcf4 100644 --- a/app/src/auth/authenticate/Authenticator.ts +++ b/app/src/auth/authenticate/Authenticator.ts @@ -1,31 +1,23 @@ import { $console, type DB, Exception } from "core"; import { addFlashMessage } from "core/server/flash"; -import { - type Static, - StringEnum, - type TObject, - parse, - runtimeSupports, - truncate, -} from "core/utils"; +import { runtimeSupports, truncate } from "core/utils"; import type { Context, Hono } from "hono"; import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie"; import { sign, verify } from "hono/jwt"; import type { CookieOptions } from "hono/utils/cookie"; import type { ServerEnv } from "modules/Controller"; import { pick } from "lodash-es"; -import * as tbbox from "@sinclair/typebox"; import { InvalidConditionsException } from "auth/errors"; -const { Type } = tbbox; +import { s, parse } from "core/object/schema"; type Input = any; // workaround export type JWTPayload = Parameters[0]; export const strategyActions = ["create", "change"] as const; export type StrategyActionName = (typeof strategyActions)[number]; -export type StrategyAction = { +export type StrategyAction = { schema: S; - preprocess: (input: Static) => Promise>; + preprocess: (input: s.Static) => Promise>; }; export type StrategyActions = Partial>; @@ -59,43 +51,44 @@ export interface UserPool { } const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds -export const cookieConfig = Type.Partial( - Type.Object({ - path: Type.String({ default: "/" }), - sameSite: StringEnum(["strict", "lax", "none"], { default: "lax" }), - secure: Type.Boolean({ default: true }), - httpOnly: Type.Boolean({ default: true }), - expires: Type.Number({ default: defaultCookieExpires }), // seconds - renew: Type.Boolean({ default: true }), - pathSuccess: Type.String({ default: "/" }), - pathLoggedOut: Type.String({ default: "/" }), - }), - { default: {}, additionalProperties: false }, -); +export const cookieConfig = s + .object({ + path: s.string({ default: "/" }), + sameSite: s.string({ enum: ["strict", "lax", "none"], default: "lax" }), + secure: s.boolean({ default: true }), + httpOnly: s.boolean({ default: true }), + expires: s.number({ default: defaultCookieExpires }), // seconds + renew: s.boolean({ default: true }), + pathSuccess: s.string({ default: "/" }), + pathLoggedOut: s.string({ default: "/" }), + }) + .partial() + .strict(); // @todo: maybe add a config to not allow cookie/api tokens to be used interchangably? // see auth.integration test for further details -export const jwtConfig = Type.Object( - { - // @todo: autogenerate a secret if not present. But it must be persisted from AppAuth - secret: Type.String({ default: "" }), - alg: Type.Optional(StringEnum(["HS256", "HS384", "HS512"], { default: "HS256" })), - expires: Type.Optional(Type.Number()), // seconds - issuer: Type.Optional(Type.String()), - fields: Type.Array(Type.String(), { default: ["id", "email", "role"] }), - }, - { - default: {}, - additionalProperties: false, - }, -); -export const authenticatorConfig = Type.Object({ +export const jwtConfig = s + .object( + { + // @todo: autogenerate a secret if not present. But it must be persisted from AppAuth + secret: s.string({ default: "" }), + alg: s.string({ enum: ["HS256", "HS384", "HS512"], default: "HS256" }).optional(), + expires: s.number().optional(), // seconds + issuer: s.string().optional(), + fields: s.array(s.string(), { default: ["id", "email", "role"] }), + }, + { + default: {}, + }, + ) + .strict(); +export const authenticatorConfig = s.object({ jwt: jwtConfig, cookie: cookieConfig, }); -type AuthConfig = Static; +type AuthConfig = s.Static; export type AuthAction = "login" | "register"; export type AuthResolveOptions = { identifier?: "email" | string; diff --git a/app/src/auth/authenticate/strategies/PasswordStrategy.ts b/app/src/auth/authenticate/strategies/PasswordStrategy.ts index 706e14be..8a48b765 100644 --- a/app/src/auth/authenticate/strategies/PasswordStrategy.ts +++ b/app/src/auth/authenticate/strategies/PasswordStrategy.ts @@ -1,19 +1,19 @@ import { type Authenticator, InvalidCredentialsException, type User } from "auth"; -import { $console, tbValidator as tb } from "core"; -import { hash, parse, type Static, StrictObject, StringEnum } from "core/utils"; +import { $console } from "core"; +import { hash } from "core/utils"; import { Hono } from "hono"; import { compare as bcryptCompare, genSalt as bcryptGenSalt, hash as bcryptHash } from "bcryptjs"; -import * as tbbox from "@sinclair/typebox"; import { Strategy } from "./Strategy"; +import { s, parse, jsc } from "core/object/schema"; -const { Type } = tbbox; +const schema = s + .object({ + hashing: s.string({ enum: ["plain", "sha256", "bcrypt"], default: "sha256" }), + rounds: s.number({ minimum: 1, maximum: 10 }).optional(), + }) + .strict(); -const schema = StrictObject({ - hashing: StringEnum(["plain", "sha256", "bcrypt"], { default: "sha256" }), - rounds: Type.Optional(Type.Number({ minimum: 1, maximum: 10 })), -}); - -export type PasswordStrategyOptions = Static; +export type PasswordStrategyOptions = s.Static; export class PasswordStrategy extends Strategy { constructor(config: Partial = {}) { @@ -32,11 +32,11 @@ export class PasswordStrategy extends Strategy { } private getPayloadSchema() { - return Type.Object({ - email: Type.String({ + return s.object({ + email: s.string({ pattern: "^[\\w-\\.\\+_]+@([\\w-]+\\.)+[\\w-]{2,4}$", }), - password: Type.String({ + password: s.string({ minLength: 8, // @todo: this should be configurable }), }); @@ -79,12 +79,12 @@ export class PasswordStrategy extends Strategy { getController(authenticator: Authenticator): Hono { const hono = new Hono(); - const redirectQuerySchema = Type.Object({ - redirect: Type.Optional(Type.String()), + const redirectQuerySchema = s.object({ + redirect: s.string().optional(), }); const payloadSchema = this.getPayloadSchema(); - hono.post("/login", tb("query", redirectQuerySchema), async (c) => { + hono.post("/login", jsc("query", redirectQuerySchema), async (c) => { try { const body = parse(payloadSchema, await authenticator.getBody(c), { onError: (errors) => { @@ -102,7 +102,7 @@ export class PasswordStrategy extends Strategy { } }); - hono.post("/register", tb("query", redirectQuerySchema), async (c) => { + hono.post("/register", jsc("query", redirectQuerySchema), async (c) => { try { const { redirect } = c.req.valid("query"); const { password, email, ...body } = parse( diff --git a/app/src/auth/authenticate/strategies/Strategy.ts b/app/src/auth/authenticate/strategies/Strategy.ts index 28fb95cf..41e0770d 100644 --- a/app/src/auth/authenticate/strategies/Strategy.ts +++ b/app/src/auth/authenticate/strategies/Strategy.ts @@ -5,31 +5,31 @@ import type { StrategyActions, } from "../Authenticator"; import type { Hono } from "hono"; -import type { Static, TSchema } from "@sinclair/typebox"; -import { parse, type TObject } from "core/utils"; +import { type s, parse } from "core/object/schema"; export type StrategyMode = "form" | "external"; -export abstract class Strategy { +export abstract class Strategy { protected actions: StrategyActions = {}; constructor( - protected config: Static, + protected config: s.Static, public type: string, public name: string, public mode: StrategyMode, ) { // don't worry about typing, it'll throw if invalid - this.config = parse(this.getSchema(), (config ?? {}) as any) as Static; + this.config = parse(this.getSchema(), (config ?? {}) as any) as s.Static; } - protected registerAction( + protected registerAction( name: StrategyActionName, schema: S, preprocess: StrategyAction["preprocess"], ): void { this.actions[name] = { schema, + // @ts-expect-error - @todo: fix this preprocess, } as const; } @@ -50,7 +50,7 @@ export abstract class Strategy { return this.name; } - toJSON(secrets?: boolean): { type: string; config: Static | {} | undefined } { + toJSON(secrets?: boolean): { type: string; config: s.Static | {} | undefined } { return { type: this.getType(), config: secrets ? this.config : undefined, diff --git a/app/src/auth/authenticate/strategies/oauth/CustomOAuthStrategy.ts b/app/src/auth/authenticate/strategies/oauth/CustomOAuthStrategy.ts index 9e9c3b81..fcb2f7cf 100644 --- a/app/src/auth/authenticate/strategies/oauth/CustomOAuthStrategy.ts +++ b/app/src/auth/authenticate/strategies/oauth/CustomOAuthStrategy.ts @@ -1,38 +1,36 @@ -import { type Static, StrictObject, StringEnum } from "core/utils"; -import * as tbbox from "@sinclair/typebox"; import type * as oauth from "oauth4webapi"; import { OAuthStrategy } from "./OAuthStrategy"; -const { Type } = tbbox; +import { s } from "core/object/schema"; type SupportedTypes = "oauth2" | "oidc"; type RequireKeys = Required> & Omit; -const UrlString = Type.String({ pattern: "^(https?|wss?)://[^\\s/$.?#].[^\\s]*$" }); -const oauthSchemaCustom = StrictObject( +const UrlString = s.string({ pattern: "^(https?|wss?)://[^\\s/$.?#].[^\\s]*$" }); +const oauthSchemaCustom = s.strictObject( { - type: StringEnum(["oidc", "oauth2"] as const, { default: "oidc" }), - name: Type.String(), - client: StrictObject({ - client_id: Type.String(), - client_secret: Type.String(), - token_endpoint_auth_method: StringEnum(["client_secret_basic"]), + type: s.string({ enum: ["oidc", "oauth2"] as const, default: "oidc" }), + name: s.string(), + client: s.object({ + client_id: s.string(), + client_secret: s.string(), + token_endpoint_auth_method: s.string({ enum: ["client_secret_basic"] }), }), - as: StrictObject({ - issuer: Type.String(), - code_challenge_methods_supported: Type.Optional(StringEnum(["S256"])), - scopes_supported: Type.Optional(Type.Array(Type.String())), - scope_separator: Type.Optional(Type.String({ default: " " })), - authorization_endpoint: Type.Optional(UrlString), - token_endpoint: Type.Optional(UrlString), - userinfo_endpoint: Type.Optional(UrlString), + as: s.strictObject({ + issuer: s.string(), + code_challenge_methods_supported: s.string({ enum: ["S256"] }).optional(), + scopes_supported: s.array(s.string()).optional(), + scope_separator: s.string({ default: " " }).optional(), + authorization_endpoint: UrlString.optional(), + token_endpoint: UrlString.optional(), + userinfo_endpoint: UrlString.optional(), }), // @todo: profile mapping }, { title: "Custom OAuth" }, ); -type OAuthConfigCustom = Static; +type OAuthConfigCustom = s.Static; export type UserProfile = { sub: string; diff --git a/app/src/auth/authenticate/strategies/oauth/OAuthStrategy.ts b/app/src/auth/authenticate/strategies/oauth/OAuthStrategy.ts index 2055d17c..af09e877 100644 --- a/app/src/auth/authenticate/strategies/oauth/OAuthStrategy.ts +++ b/app/src/auth/authenticate/strategies/oauth/OAuthStrategy.ts @@ -1,31 +1,32 @@ import type { AuthAction, Authenticator } from "auth"; import { Exception, isDebug } from "core"; -import { type Static, StringEnum, filterKeys, StrictObject } from "core/utils"; +import { filterKeys } from "core/utils"; import { type Context, Hono } from "hono"; import { getSignedCookie, setSignedCookie } from "hono/cookie"; import * as oauth from "oauth4webapi"; import * as issuers from "./issuers"; -import * as tbbox from "@sinclair/typebox"; import { Strategy } from "auth/authenticate/strategies/Strategy"; -const { Type } = tbbox; +import { s } from "core/object/schema"; type ConfiguredIssuers = keyof typeof issuers; type SupportedTypes = "oauth2" | "oidc"; type RequireKeys = Required> & Omit; -const schemaProvided = Type.Object( +const schemaProvided = s.object( { - name: StringEnum(Object.keys(issuers) as ConfiguredIssuers[]), - type: StringEnum(["oidc", "oauth2"] as const, { default: "oauth2" }), - client: StrictObject({ - client_id: Type.String(), - client_secret: Type.String(), - }), + name: s.string({ enum: Object.keys(issuers) as ConfiguredIssuers[] }), + type: s.string({ enum: ["oidc", "oauth2"] as const, default: "oauth2" }), + client: s + .object({ + client_id: s.string(), + client_secret: s.string(), + }) + .strict(), }, { title: "OAuth" }, ); -type ProvidedOAuthConfig = Static; +type ProvidedOAuthConfig = s.Static; export type CustomOAuthConfig = { type: SupportedTypes; diff --git a/app/src/core/index.ts b/app/src/core/index.ts index a0e96e63..52eefb17 100644 --- a/app/src/core/index.ts +++ b/app/src/core/index.ts @@ -26,7 +26,7 @@ export { } from "./object/query/query"; export { Registry, type Constructor } from "./registry/Registry"; export { getFlashMessage } from "./server/flash"; -export { +/* export { s, parse, jsc, @@ -35,7 +35,7 @@ export { openAPISpecs, type ParseOptions, InvalidSchemaError, -} from "./object/schema"; +} from "./object/schema"; */ export * from "./drivers"; export * from "./console"; diff --git a/app/src/core/object/SchemaObject.ts b/app/src/core/object/SchemaObject.ts index 21199781..a55c0713 100644 --- a/app/src/core/object/SchemaObject.ts +++ b/app/src/core/object/SchemaObject.ts @@ -1,43 +1,39 @@ import { get, has, omit, set } from "lodash-es"; -import { - Default, - type Static, - type TObject, - getFullPathKeys, - mergeObjectWith, - parse, - stripMark, -} from "../utils"; +import { getFullPathKeys, mergeObjectWith } from "../utils"; +import { type s, parse, stripMark } from "core/object/schema"; -export type SchemaObjectOptions = { - onUpdate?: (config: Static) => void | Promise; +export type SchemaObjectOptions = { + onUpdate?: (config: s.Static) => void | Promise; onBeforeUpdate?: ( - from: Static, - to: Static, - ) => Static | Promise>; + from: s.Static, + to: s.Static, + ) => s.Static | Promise>; restrictPaths?: string[]; overwritePaths?: (RegExp | string)[]; forceParse?: boolean; }; -export class SchemaObject { - private readonly _default: Partial>; - private _value: Static; - private _config: Static; +type TSchema = s.ObjectSchema; + +export class SchemaObject { + private readonly _default: Partial>; + private _value: s.Static; + private _config: s.Static; private _restriction_bypass: boolean = false; constructor( private _schema: Schema, - initial?: Partial>, + initial?: Partial>, private options?: SchemaObjectOptions, ) { - this._default = Default(_schema, {} as any) as any; + this._default = _schema.template() as any; this._value = initial ? parse(_schema, structuredClone(initial as any), { + withDefaults: true, forceParse: this.isForceParse(), skipMark: this.isForceParse(), }) - : this._default; + : (this._default as any); this._config = Object.freeze(this._value); } @@ -45,18 +41,21 @@ export class SchemaObject { return this.options?.forceParse ?? true; } - default(): Static { + default() { return this._default; } - private async onBeforeUpdate(from: Static, to: Static): Promise> { + private async onBeforeUpdate( + from: s.Static, + to: s.Static, + ): Promise> { if (this.options?.onBeforeUpdate) { return this.options.onBeforeUpdate(from, to); } return to; } - get(options?: { stripMark?: boolean }): Static { + get(options?: { stripMark?: boolean }): s.Static { if (options?.stripMark) { return stripMark(this._config); } @@ -68,7 +67,7 @@ export class SchemaObject { return structuredClone(this._config); } - async set(config: Static, noEmit?: boolean): Promise> { + async set(config: s.Static, noEmit?: boolean): Promise> { const valid = parse(this._schema, structuredClone(config) as any, { forceParse: true, skipMark: this.isForceParse(), @@ -118,9 +117,9 @@ export class SchemaObject { return; } - async patch(path: string, value: any): Promise<[Partial>, Static]> { + async patch(path: string, value: any): Promise<[Partial>, s.Static]> { const current = this.clone(); - const partial = path.length > 0 ? (set({}, path, value) as Partial>) : value; + const partial = path.length > 0 ? (set({}, path, value) as Partial>) : value; this.throwIfRestricted(partial); @@ -168,9 +167,12 @@ export class SchemaObject { return [partial, newConfig]; } - async overwrite(path: string, value: any): Promise<[Partial>, Static]> { + async overwrite( + path: string, + value: any, + ): Promise<[Partial>, s.Static]> { const current = this.clone(); - const partial = path.length > 0 ? (set({}, path, value) as Partial>) : value; + const partial = path.length > 0 ? (set({}, path, value) as Partial>) : value; this.throwIfRestricted(partial); @@ -194,7 +196,7 @@ export class SchemaObject { return has(this._config, path); } - async remove(path: string): Promise<[Partial>, Static]> { + async remove(path: string): Promise<[Partial>, s.Static]> { this.throwIfRestricted(path); if (!this.has(path)) { @@ -202,9 +204,9 @@ export class SchemaObject { } const current = this.clone(); - const removed = get(current, path) as Partial>; + const removed = get(current, path) as Partial>; const config = omit(current, path); - const newConfig = await this.set(config); + const newConfig = await this.set(config as any); return [removed, newConfig]; } } diff --git a/app/src/core/object/schema/index.ts b/app/src/core/object/schema/index.ts index 5ebb6b6b..6d895da0 100644 --- a/app/src/core/object/schema/index.ts +++ b/app/src/core/object/schema/index.ts @@ -1,6 +1,3 @@ -import { mergeObject } from "core/utils"; - -//export { jsc, type Options, type Hook } from "./validator"; import * as s from "jsonv-ts"; export { validator as jsc, type Options } from "jsonv-ts/hono"; @@ -8,9 +5,18 @@ export { describeRoute, schemaToSpec, openAPISpecs } from "jsonv-ts/hono"; export { s }; +export const stripMark = (o: O): O => o; +export const mark = (o: O): O => o; + +export const stringIdentifier = s.string({ + pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$", + minLength: 2, + maxLength: 150, +}); + export class InvalidSchemaError extends Error { constructor( - public schema: s.TAnySchema, + public schema: s.Schema, public value: unknown, public errors: s.ErrorDetail[] = [], ) { @@ -19,33 +25,52 @@ export class InvalidSchemaError extends Error { `Error: ${JSON.stringify(errors[0], null, 2)}`, ); } + + first() { + return this.errors[0]!; + } + + firstToString() { + const first = this.first(); + return `${first.error} at ${first.instanceLocation}`; + } } export type ParseOptions = { withDefaults?: boolean; - coerse?: boolean; + coerce?: boolean; clone?: boolean; + skipMark?: boolean; // @todo: do something with this + forceParse?: boolean; // @todo: do something with this + onError?: (errors: s.ErrorDetail[]) => void; }; -export const cloneSchema = (schema: S): S => { +export const cloneSchema = (schema: S): S => { const json = schema.toJSON(); return s.fromSchema(json) as S; }; -export function parse( +export function parse( _schema: S, v: unknown, - opts: ParseOptions = {}, -): s.StaticCoerced { - const schema = (opts.clone ? cloneSchema(_schema as any) : _schema) as s.TSchema; - const value = opts.coerse !== false ? schema.coerce(v) : v; + opts?: Options, +): Options extends { coerce: true } ? s.StaticCoerced : s.Static { + const schema = (opts?.clone ? cloneSchema(_schema as any) : _schema) as s.Schema; + let value = opts?.coerce !== false ? schema.coerce(v) : v; + if (opts?.withDefaults) { + value = schema.template(value, { withOptional: true }); + } + const result = schema.validate(value, { shortCircuit: true, ignoreUnsupported: true, }); - if (!result.valid) throw new InvalidSchemaError(schema, v, result.errors); - if (opts.withDefaults) { - return mergeObject(schema.template({ withOptional: true }), value) as any; + if (!result.valid) { + if (opts?.onError) { + opts.onError(result.errors); + } else { + throw new InvalidSchemaError(schema, v, result.errors); + } } return value as any; diff --git a/app/src/core/object/schema/validator.ts b/app/src/core/object/schema/validator.ts deleted file mode 100644 index 7e8c61c4..00000000 --- a/app/src/core/object/schema/validator.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { Context, Env, Input, MiddlewareHandler, ValidationTargets } from "hono"; -import { validator as honoValidator } from "hono/validator"; -import type { Static, StaticCoerced, TAnySchema } from "jsonv-ts"; - -export type Options = { - coerce?: boolean; - includeSchema?: boolean; -}; - -type ValidationResult = { - valid: boolean; - errors: { - keywordLocation: string; - instanceLocation: string; - error: string; - data?: unknown; - }[]; -}; - -export type Hook = ( - result: { result: ValidationResult; data: T }, - c: Context, -) => Response | Promise | void; - -export const validator = < - // @todo: somehow hono prevents the usage of TSchema - Schema extends TAnySchema, - Target extends keyof ValidationTargets, - E extends Env, - P extends string, - Opts extends Options = Options, - Out = Opts extends { coerce: false } ? Static : StaticCoerced, - I extends Input = { - in: { [K in Target]: Static }; - out: { [K in Target]: Out }; - }, ->( - target: Target, - schema: Schema, - options?: Opts, - hook?: Hook, -): MiddlewareHandler => { - // @ts-expect-error not typed well - return honoValidator(target, async (_value, c) => { - const value = options?.coerce !== false ? schema.coerce(_value) : _value; - // @ts-ignore - const result = schema.validate(value); - if (!result.valid) { - return c.json({ ...result, schema }, 400); - } - - if (hook) { - const hookResult = hook({ result, data: value as Out }, c); - if (hookResult) { - return hookResult; - } - } - - return value as Out; - }); -}; - -export const jsc = validator; diff --git a/app/src/core/utils/index.ts b/app/src/core/utils/index.ts index c94c4bb9..00e5c7a4 100644 --- a/app/src/core/utils/index.ts +++ b/app/src/core/utils/index.ts @@ -5,7 +5,7 @@ export * from "./perf"; export * from "./file"; export * from "./reqres"; export * from "./xml"; -export type { Prettify, PrettifyRec } from "./types"; +export type { Prettify, PrettifyRec, RecursivePartial } from "./types"; export * from "./typebox"; export * from "./dates"; export * from "./crypto"; diff --git a/app/src/core/utils/typebox/index.ts b/app/src/core/utils/typebox/index.ts index 1267afc7..c7252cc2 100644 --- a/app/src/core/utils/typebox/index.ts +++ b/app/src/core/utils/typebox/index.ts @@ -14,16 +14,9 @@ import { type ValueErrorIterator, } from "@sinclair/typebox/errors"; import { Check, Default, Value, type ValueError } from "@sinclair/typebox/value"; +import type { RecursivePartial } from "../types"; -export type RecursivePartial = { - [P in keyof T]?: T[P] extends (infer U)[] - ? RecursivePartial[] - : T[P] extends object | undefined - ? RecursivePartial - : T[P]; -}; - -type ParseOptions = { +/* type ParseOptions = { useDefaults?: boolean; decode?: boolean; onError?: (errors: ValueErrorIterator) => void; @@ -198,4 +191,4 @@ SetErrorFunction((error) => { export type { Static, StaticDecode, TSchema, TObject, ValueError, SchemaOptions }; -export { Value, Default, Errors, Check }; +export { Value, Default, Errors, Check }; */ diff --git a/app/src/core/utils/types.d.ts b/app/src/core/utils/types.d.ts index 7afb18f2..b0848d04 100644 --- a/app/src/core/utils/types.d.ts +++ b/app/src/core/utils/types.d.ts @@ -6,3 +6,11 @@ export type Prettify = { export type PrettifyRec = { [K in keyof T]: T[K] extends object ? Prettify : T[K]; } & NonNullable; + +export type RecursivePartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? RecursivePartial[] + : T[P] extends object | undefined + ? RecursivePartial + : T[P]; +}; diff --git a/app/src/data/AppData.ts b/app/src/data/AppData.ts index 10f491b5..1f0b7b34 100644 --- a/app/src/data/AppData.ts +++ b/app/src/data/AppData.ts @@ -11,7 +11,7 @@ import { Module } from "modules/Module"; import { DataController } from "./api/DataController"; import { type AppDataConfig, dataConfigSchema } from "./data-schema"; -export class AppData extends Module { +export class AppData extends Module { override async build() { const { entities: _entities = {}, diff --git a/app/src/data/api/DataController.ts b/app/src/data/api/DataController.ts index f7db2b93..df271c6a 100644 --- a/app/src/data/api/DataController.ts +++ b/app/src/data/api/DataController.ts @@ -74,10 +74,12 @@ export class DataController extends Controller { }), jsc( "query", - s.partialObject({ - force: s.boolean(), - drop: s.boolean(), - }), + s + .object({ + force: s.boolean(), + drop: s.boolean(), + }) + .partial(), ), async (c) => { const { force, drop } = c.req.valid("query"); @@ -258,12 +260,14 @@ export class DataController extends Controller { * Read endpoints */ // read many - const saveRepoQuery = s.partialObject({ - ...omitKeys(repoQuery.properties, ["with"]), - sort: s.string({ default: "id" }), - select: s.array(s.string()), - join: s.array(s.string()), - }); + const saveRepoQuery = s + .object({ + ...omitKeys(repoQuery.properties, ["with"]), + sort: s.string({ default: "id" }), + select: s.array(s.string()), + join: s.array(s.string()), + }) + .partial(); const saveRepoQueryParams = (pick: string[] = Object.keys(repoQuery.properties)) => [ ...(schemaToSpec(saveRepoQuery, "query").parameters?.filter( // @ts-ignore @@ -356,10 +360,12 @@ export class DataController extends Controller { ); // func query - const fnQuery = s.partialObject({ - ...saveRepoQuery.properties, - with: s.object({}), - }); + const fnQuery = s + .object({ + ...saveRepoQuery.properties, + with: s.object({}), + }) + .partial(); hono.post( "/:entity/query", describeRoute({ diff --git a/app/src/data/connection/Connection.ts b/app/src/data/connection/Connection.ts index cd807b74..5b39d6d9 100644 --- a/app/src/data/connection/Connection.ts +++ b/app/src/data/connection/Connection.ts @@ -38,7 +38,7 @@ export interface SelectQueryBuilderExpression extends AliasableExpression export type SchemaResponse = [string, ColumnDataType, ColumnBuilderCallback] | undefined; -const FieldSpecTypes = [ +export const FieldSpecTypes = [ "text", "integer", "real", diff --git a/app/src/data/data-schema.ts b/app/src/data/data-schema.ts index 7137d7d0..7a985b5f 100644 --- a/app/src/data/data-schema.ts +++ b/app/src/data/data-schema.ts @@ -1,10 +1,10 @@ -import { type Static, StringEnum, StringRecord, objectTransform } from "core/utils"; -import * as tb from "@sinclair/typebox"; +import { objectTransform } from "core/utils"; import { MediaField, mediaFieldConfigSchema } from "../media/MediaField"; import { FieldClassMap } from "data/fields"; import { RelationClassMap, RelationFieldClassMap } from "data/relations"; import { entityConfigSchema, entityTypes } from "data/entities"; import { primaryFieldTypes } from "./fields"; +import { s } from "core/object/schema"; export const FIELDS = { ...FieldClassMap, @@ -16,69 +16,55 @@ export type FieldType = keyof typeof FIELDS; export const RELATIONS = RelationClassMap; export const fieldsSchemaObject = objectTransform(FIELDS, (field, name) => { - return tb.Type.Object( + return s.object( { - type: tb.Type.Const(name, { default: name, readOnly: true }), - config: tb.Type.Optional(field.schema), + type: s.literal(name), + config: field.schema.optional(), }, { title: name, }, ); }); -export const fieldsSchema = tb.Type.Union(Object.values(fieldsSchemaObject)); -export const entityFields = StringRecord(fieldsSchema); -export type TAppDataField = Static; -export type TAppDataEntityFields = Static; +export const fieldsSchema = s.anyOf(Object.values(fieldsSchemaObject)); +export const entityFields = s.record(fieldsSchema); +export type TAppDataField = s.Static; +export type TAppDataEntityFields = s.Static; -export const entitiesSchema = tb.Type.Object({ - type: tb.Type.Optional( - tb.Type.String({ enum: entityTypes, default: "regular", readOnly: true }), - ), - config: tb.Type.Optional(entityConfigSchema), - fields: tb.Type.Optional(entityFields), +export const entitiesSchema = s.object({ + type: s.string({ enum: entityTypes, default: "regular", readOnly: true }), + config: entityConfigSchema.optional(), + fields: entityFields.optional(), }); -export type TAppDataEntity = Static; +export type TAppDataEntity = s.Static; export const relationsSchema = Object.entries(RelationClassMap).map(([name, relationClass]) => { - return tb.Type.Object( + return s.object( { - type: tb.Type.Const(name, { default: name, readOnly: true }), - source: tb.Type.String(), - target: tb.Type.String(), - config: tb.Type.Optional(relationClass.schema), + type: s.literal(name), + source: s.string(), + target: s.string(), + config: relationClass.schema.optional(), }, { title: name, }, ); }); -export type TAppDataRelation = Static<(typeof relationsSchema)[number]>; +export type TAppDataRelation = s.Static<(typeof relationsSchema)[number]>; -export const indicesSchema = tb.Type.Object( - { - entity: tb.Type.String(), - fields: tb.Type.Array(tb.Type.String(), { minItems: 1 }), - unique: tb.Type.Optional(tb.Type.Boolean({ default: false })), - }, - { - additionalProperties: false, - }, -); +export const indicesSchema = s.strictObject({ + entity: s.string(), + fields: s.array(s.string(), { minItems: 1 }), + unique: s.boolean({ default: false }).optional(), +}); -export const dataConfigSchema = tb.Type.Object( - { - basepath: tb.Type.Optional(tb.Type.String({ default: "/api/data" })), - default_primary_format: tb.Type.Optional( - StringEnum(primaryFieldTypes, { default: "integer" }), - ), - entities: tb.Type.Optional(StringRecord(entitiesSchema, { default: {} })), - relations: tb.Type.Optional(StringRecord(tb.Type.Union(relationsSchema), { default: {} })), - indices: tb.Type.Optional(StringRecord(indicesSchema, { default: {} })), - }, - { - additionalProperties: false, - }, -); +export const dataConfigSchema = s.strictObject({ + basepath: s.string({ default: "/api/data" }).optional(), + default_primary_format: s.string({ enum: primaryFieldTypes, default: "integer" }).optional(), + entities: s.record(entitiesSchema, { default: {} }).optional(), + relations: s.record(s.anyOf(relationsSchema), { default: {} }).optional(), + indices: s.record(indicesSchema, { default: {} }).optional(), +}); -export type AppDataConfig = Static; +export type AppDataConfig = s.Static; diff --git a/app/src/data/entities/Entity.ts b/app/src/data/entities/Entity.ts index 48f23b97..68246ddf 100644 --- a/app/src/data/entities/Entity.ts +++ b/app/src/data/entities/Entity.ts @@ -1,11 +1,5 @@ import { $console, config } from "core"; -import { - type Static, - StringEnum, - parse, - snakeToPascalWithSpaces, - transformObject, -} from "core/utils"; +import { snakeToPascalWithSpaces, transformObject } from "core/utils"; import { type Field, PrimaryField, @@ -13,25 +7,21 @@ import { type TActionContext, type TRenderContext, } from "../fields"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s, parse } from "core/object/schema"; // @todo: entity must be migrated to typebox -export const entityConfigSchema = Type.Object( - { - name: Type.Optional(Type.String()), - name_singular: Type.Optional(Type.String()), - description: Type.Optional(Type.String()), - sort_field: Type.Optional(Type.String({ default: config.data.default_primary_field })), - sort_dir: Type.Optional(StringEnum(["asc", "desc"], { default: "asc" })), - primary_format: Type.Optional(StringEnum(primaryFieldTypes)), - }, - { - additionalProperties: false, - }, -); +export const entityConfigSchema = s + .strictObject({ + name: s.string(), + name_singular: s.string(), + description: s.string(), + sort_field: s.string({ default: config.data.default_primary_field }), + sort_dir: s.string({ enum: ["asc", "desc"], default: "asc" }), + primary_format: s.string({ enum: primaryFieldTypes }), + }) + .partial(); -export type EntityConfig = Static; +export type EntityConfig = s.Static; export type EntityData = Record; export type EntityJSON = ReturnType; @@ -287,8 +277,10 @@ export class Entity< } const _fields = Object.fromEntries(fields.map((field) => [field.name, field])); - const schema = Type.Object( - transformObject(_fields, (field) => { + const schema = { + type: "object", + additionalProperties: false, + properties: transformObject(_fields, (field) => { const fillable = field.isFillable(options?.context); return { title: field.config.label, @@ -298,8 +290,7 @@ export class Entity< ...field.toJsonSchema(), }; }), - { additionalProperties: false }, - ); + }; return options?.clean ? JSON.parse(JSON.stringify(schema)) : schema; } diff --git a/app/src/data/entities/query/Repository.ts b/app/src/data/entities/query/Repository.ts index f41bd8bc..09aebbea 100644 --- a/app/src/data/entities/query/Repository.ts +++ b/app/src/data/entities/query/Repository.ts @@ -78,8 +78,8 @@ export class Repository, public given: any, - error: TypeInvalidError, + error: InvalidSchemaError, ) { console.error("InvalidFieldConfigException", { given, diff --git a/app/src/data/fields/BooleanField.ts b/app/src/data/fields/BooleanField.ts index 35dfa2d7..fccda529 100644 --- a/app/src/data/fields/BooleanField.ts +++ b/app/src/data/fields/BooleanField.ts @@ -1,18 +1,17 @@ -import type { Static } from "core/utils"; +import { omitKeys } from "core/utils"; import type { EntityManager } from "data"; import { TransformPersistFailedException } from "../errors"; import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field"; -import * as tb from "@sinclair/typebox"; -const { Type } = tb; +import { s } from "core/object/schema"; -export const booleanFieldConfigSchema = Type.Composite([ - Type.Object({ - default_value: Type.Optional(Type.Boolean({ default: false })), - }), - baseFieldConfigSchema, -]); +export const booleanFieldConfigSchema = s + .strictObject({ + default_value: s.boolean({ default: false }), + ...omitKeys(baseFieldConfigSchema.properties, ["default_value"]), + }) + .partial(); -export type BooleanFieldConfig = Static; +export type BooleanFieldConfig = s.Static; export class BooleanField extends Field< BooleanFieldConfig, @@ -86,7 +85,7 @@ export class BooleanField extends Field< } override toJsonSchema() { - return this.toSchemaWrapIfRequired(Type.Boolean({ default: this.getDefault() })); + return this.toSchemaWrapIfRequired(s.boolean({ default: this.getDefault() })); } override toType() { diff --git a/app/src/data/fields/DateField.ts b/app/src/data/fields/DateField.ts index 0fdf91ea..b3a3e0b1 100644 --- a/app/src/data/fields/DateField.ts +++ b/app/src/data/fields/DateField.ts @@ -1,27 +1,21 @@ -import { type Static, StringEnum, dayjs } from "core/utils"; +import { dayjs, omitKeys } from "core/utils"; import type { EntityManager } from "../entities"; import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field"; import { $console } from "core"; -import * as tbbox from "@sinclair/typebox"; import type { TFieldTSType } from "data/entities/EntityTypescript"; -const { Type } = tbbox; +import { s } from "core/object/schema"; -export const dateFieldConfigSchema = Type.Composite( - [ - Type.Object({ - type: StringEnum(["date", "datetime", "week"] as const, { default: "date" }), - timezone: Type.Optional(Type.String()), - min_date: Type.Optional(Type.String()), - max_date: Type.Optional(Type.String()), - }), - baseFieldConfigSchema, - ], - { - additionalProperties: false, - }, -); +export const dateFieldConfigSchema = s + .strictObject({ + type: s.string({ enum: ["date", "datetime", "week"], default: "date" }), + timezone: s.string(), + min_date: s.string(), + max_date: s.string(), + ...omitKeys(baseFieldConfigSchema.properties, ["default_value"]), + }) + .partial(); -export type DateFieldConfig = Static; +export type DateFieldConfig = s.Static; export class DateField extends Field< DateFieldConfig, @@ -143,7 +137,7 @@ export class DateField extends Field< // @todo: check this override toJsonSchema() { - return this.toSchemaWrapIfRequired(Type.String({ default: this.getDefault() })); + return this.toSchemaWrapIfRequired(s.string({ default: this.getDefault() })); } override toType(): TFieldTSType { diff --git a/app/src/data/fields/EnumField.ts b/app/src/data/fields/EnumField.ts index 2bfad088..d9d36534 100644 --- a/app/src/data/fields/EnumField.ts +++ b/app/src/data/fields/EnumField.ts @@ -1,50 +1,33 @@ -import { Const, type Static, StringEnum } from "core/utils"; +import { omitKeys } from "core/utils"; import type { EntityManager } from "data"; import { TransformPersistFailedException } from "../errors"; import { baseFieldConfigSchema, Field, type TActionContext, type TRenderContext } from "./Field"; -import * as tbbox from "@sinclair/typebox"; import type { TFieldTSType } from "data/entities/EntityTypescript"; -const { Type } = tbbox; +import { s } from "core/object/schema"; -export const enumFieldConfigSchema = Type.Composite( - [ - Type.Object({ - default_value: Type.Optional(Type.String()), - options: Type.Optional( - Type.Union([ - Type.Object( - { - type: Const("strings"), - values: Type.Array(Type.String()), - }, - { title: "Strings" }, - ), - Type.Object( - { - type: Const("objects"), - values: Type.Array( - Type.Object({ - label: Type.String(), - value: Type.String(), - }), - ), - }, - { - title: "Objects", - additionalProperties: false, - }, - ), - ]), - ), - }), - baseFieldConfigSchema, - ], - { - additionalProperties: false, - }, -); +export const enumFieldConfigSchema = s + .strictObject({ + default_value: s.string(), + options: s.anyOf([ + s.object({ + type: s.literal("strings"), + values: s.array(s.string()), + }), + s.object({ + type: s.literal("objects"), + values: s.array( + s.object({ + label: s.string(), + value: s.string(), + }), + ), + }), + ]), + ...omitKeys(baseFieldConfigSchema.properties, ["default_value"]), + }) + .partial(); -export type EnumFieldConfig = Static; +export type EnumFieldConfig = s.Static; export class EnumField extends Field< EnumFieldConfig, @@ -136,7 +119,8 @@ export class EnumField (typeof option === "string" ? option : option.value)) ?? []; return this.toSchemaWrapIfRequired( - StringEnum(values, { + s.string({ + enum: values, default: this.getDefault(), }), ); diff --git a/app/src/data/fields/Field.ts b/app/src/data/fields/Field.ts index 5c787ebf..929165d4 100644 --- a/app/src/data/fields/Field.ts +++ b/app/src/data/fields/Field.ts @@ -1,18 +1,10 @@ -import { - parse, - snakeToPascalWithSpaces, - type Static, - StringEnum, - type TSchema, - TypeInvalidError, -} from "core/utils"; +import { snakeToPascalWithSpaces } from "core/utils"; import type { HTMLInputTypeAttribute, InputHTMLAttributes } from "react"; import type { EntityManager } from "../entities"; import { InvalidFieldConfigException, TransformPersistFailedException } from "../errors"; import type { FieldSpec } from "data/connection/Connection"; -import * as tbbox from "@sinclair/typebox"; import type { TFieldTSType } from "data/entities/EntityTypescript"; -const { Type } = tbbox; +import { s, parse, InvalidSchemaError } from "core/object/schema"; // @todo: contexts need to be reworked // e.g. "table" is irrelevant, because if read is not given, it fails @@ -31,43 +23,36 @@ const DEFAULT_FILLABLE = true; const DEFAULT_HIDDEN = false; // @todo: add refine functions (e.g. if required, but not fillable, needs default value) -export const baseFieldConfigSchema = Type.Object( - { - label: Type.Optional(Type.String()), - description: Type.Optional(Type.String()), - required: Type.Optional(Type.Boolean({ default: DEFAULT_REQUIRED })), - fillable: Type.Optional( - Type.Union( - [ - Type.Boolean({ title: "Boolean", default: DEFAULT_FILLABLE }), - Type.Array(StringEnum(ActionContext), { title: "Context", uniqueItems: true }), - ], - { - default: DEFAULT_FILLABLE, - }, - ), +export const baseFieldConfigSchema = s + .strictObject({ + label: s.string(), + description: s.string(), + required: s.boolean({ default: DEFAULT_REQUIRED }), + fillable: s.anyOf( + [ + s.boolean({ title: "Boolean", default: DEFAULT_FILLABLE }), + s.array(s.string({ enum: ActionContext }), { title: "Context", uniqueItems: true }), + ], + { + default: DEFAULT_FILLABLE, + }, ), - hidden: Type.Optional( - Type.Union( - [ - Type.Boolean({ title: "Boolean", default: DEFAULT_HIDDEN }), - // @todo: tmp workaround - Type.Array(StringEnum(TmpContext), { title: "Context", uniqueItems: true }), - ], - { - default: DEFAULT_HIDDEN, - }, - ), + hidden: s.anyOf( + [ + s.boolean({ title: "Boolean", default: DEFAULT_HIDDEN }), + // @todo: tmp workaround + s.array(s.string({ enum: TmpContext }), { title: "Context", uniqueItems: true }), + ], + { + default: DEFAULT_HIDDEN, + }, ), // if field is virtual, it will not call transformPersist & transformRetrieve - virtual: Type.Optional(Type.Boolean()), - default_value: Type.Optional(Type.Any()), - }, - { - additionalProperties: false, - }, -); -export type BaseFieldConfig = Static; + virtual: s.boolean(), + default_value: s.any(), + }) + .partial(); +export type BaseFieldConfig = s.Static; export abstract class Field< Config extends BaseFieldConfig = BaseFieldConfig, @@ -92,7 +77,7 @@ export abstract class Field< try { this.config = parse(this.getSchema(), config || {}) as Config; } catch (e) { - if (e instanceof TypeInvalidError) { + if (e instanceof InvalidSchemaError) { throw new InvalidFieldConfigException(this, config, e); } @@ -104,7 +89,7 @@ export abstract class Field< return this.type; } - protected abstract getSchema(): TSchema; + protected abstract getSchema(): s.ObjectSchema; /** * Used in SchemaManager.ts @@ -224,16 +209,16 @@ export abstract class Field< return value; } - protected toSchemaWrapIfRequired(schema: Schema) { - return this.isRequired() ? schema : Type.Optional(schema); + protected toSchemaWrapIfRequired(schema: Schema): Schema { + return this.isRequired() ? schema : (schema.optional() as any); } protected nullish(value: any) { return value === null || value === undefined; } - toJsonSchema(): TSchema { - return this.toSchemaWrapIfRequired(Type.Any()); + toJsonSchema(): s.Schema { + return this.toSchemaWrapIfRequired(s.any()); } toType(): TFieldTSType { diff --git a/app/src/data/fields/JsonField.ts b/app/src/data/fields/JsonField.ts index db1a6f61..c75124a7 100644 --- a/app/src/data/fields/JsonField.ts +++ b/app/src/data/fields/JsonField.ts @@ -1,14 +1,18 @@ -import type { Static } from "core/utils"; +import { omitKeys } from "core/utils"; import type { EntityManager } from "data"; import { TransformPersistFailedException } from "../errors"; import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field"; -import * as tbbox from "@sinclair/typebox"; import type { TFieldTSType } from "data/entities/EntityTypescript"; -const { Type } = tbbox; +import { s } from "core/object/schema"; -export const jsonFieldConfigSchema = Type.Composite([baseFieldConfigSchema, Type.Object({})]); +export const jsonFieldConfigSchema = s + .strictObject({ + default_value: s.any(), + ...omitKeys(baseFieldConfigSchema.properties, ["default_value"]), + }) + .partial(); -export type JsonFieldConfig = Static; +export type JsonFieldConfig = s.Static; export class JsonField extends Field< JsonFieldConfig, diff --git a/app/src/data/fields/JsonSchemaField.ts b/app/src/data/fields/JsonSchemaField.ts index 1abe2557..e57d014d 100644 --- a/app/src/data/fields/JsonSchemaField.ts +++ b/app/src/data/fields/JsonSchemaField.ts @@ -1,27 +1,21 @@ import { type Schema as JsonSchema, Validator } from "@cfworker/json-schema"; -import { Default, FromSchema, objectToJsLiteral, type Static } from "core/utils"; +import { FromSchema, objectToJsLiteral, omitKeys } from "core/utils"; import type { EntityManager } from "data"; import { TransformPersistFailedException } from "../errors"; import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field"; -import * as tbbox from "@sinclair/typebox"; import type { TFieldTSType } from "data/entities/EntityTypescript"; -const { Type } = tbbox; +import { s } from "core/object/schema"; -export const jsonSchemaFieldConfigSchema = Type.Composite( - [ - Type.Object({ - schema: Type.Object({}, { default: {} }), - ui_schema: Type.Optional(Type.Object({})), - default_from_schema: Type.Optional(Type.Boolean()), - }), - baseFieldConfigSchema, - ], - { - additionalProperties: false, - }, -); +export const jsonSchemaFieldConfigSchema = s + .strictObject({ + schema: s.any({ type: "object", default: {} }), + ui_schema: s.any({ type: "object", default: {} }), + default_from_schema: s.boolean(), + ...omitKeys(baseFieldConfigSchema.properties, ["default_value"]), + }) + .partial(); -export type JsonSchemaFieldConfig = Static; +export type JsonSchemaFieldConfig = s.Static; export class JsonSchemaField< Required extends true | false = false, @@ -84,7 +78,7 @@ export class JsonSchemaField< if (val === null) { if (this.config.default_from_schema) { try { - return Default(FromSchema(this.getJsonSchema()), {}); + return s.fromSchema(this.getJsonSchema()).template(); } catch (e) { return null; } @@ -116,7 +110,7 @@ export class JsonSchemaField< override toJsonSchema() { const schema = this.getJsonSchema() ?? { type: "object" }; return this.toSchemaWrapIfRequired( - FromSchema({ + s.fromSchema({ default: this.getDefault(), ...schema, }), diff --git a/app/src/data/fields/NumberField.ts b/app/src/data/fields/NumberField.ts index 41845607..48c9c80e 100644 --- a/app/src/data/fields/NumberField.ts +++ b/app/src/data/fields/NumberField.ts @@ -1,29 +1,23 @@ -import type { Static } from "core/utils"; import type { EntityManager } from "data"; import { TransformPersistFailedException } from "../errors"; import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field"; -import * as tbbox from "@sinclair/typebox"; import type { TFieldTSType } from "data/entities/EntityTypescript"; -const { Type } = tbbox; +import { s } from "core/object/schema"; +import { omitKeys } from "core/utils"; -export const numberFieldConfigSchema = Type.Composite( - [ - Type.Object({ - default_value: Type.Optional(Type.Number()), - minimum: Type.Optional(Type.Number()), - maximum: Type.Optional(Type.Number()), - exclusiveMinimum: Type.Optional(Type.Number()), - exclusiveMaximum: Type.Optional(Type.Number()), - multipleOf: Type.Optional(Type.Number()), - }), - baseFieldConfigSchema, - ], - { - additionalProperties: false, - }, -); +export const numberFieldConfigSchema = s + .strictObject({ + default_value: s.number(), + minimum: s.number(), + maximum: s.number(), + exclusiveMinimum: s.number(), + exclusiveMaximum: s.number(), + multipleOf: s.number(), + ...omitKeys(baseFieldConfigSchema.properties, ["default_value"]), + }) + .partial(); -export type NumberFieldConfig = Static; +export type NumberFieldConfig = s.Static; export class NumberField extends Field< NumberFieldConfig, @@ -93,7 +87,7 @@ export class NumberField extends Field< override toJsonSchema() { return this.toSchemaWrapIfRequired( - Type.Number({ + s.number({ default: this.getDefault(), minimum: this.config?.minimum, maximum: this.config?.maximum, diff --git a/app/src/data/fields/PrimaryField.ts b/app/src/data/fields/PrimaryField.ts index 2f83c209..d14f12e9 100644 --- a/app/src/data/fields/PrimaryField.ts +++ b/app/src/data/fields/PrimaryField.ts @@ -1,22 +1,22 @@ import { config } from "core"; -import { StringEnum, uuidv7, type Static } from "core/utils"; +import { omitKeys, uuidv7 } from "core/utils"; import { Field, baseFieldConfigSchema } from "./Field"; -import * as tbbox from "@sinclair/typebox"; import type { TFieldTSType } from "data/entities/EntityTypescript"; -const { Type } = tbbox; +import { s } from "core/object/schema"; +import type { FieldSpec } from "data/connection/Connection"; export const primaryFieldTypes = ["integer", "uuid"] as const; export type TPrimaryFieldFormat = (typeof primaryFieldTypes)[number]; -export const primaryFieldConfigSchema = Type.Composite([ - Type.Omit(baseFieldConfigSchema, ["required"]), - Type.Object({ - format: Type.Optional(StringEnum(primaryFieldTypes, { default: "integer" })), - required: Type.Optional(Type.Literal(false)), - }), -]); +export const primaryFieldConfigSchema = s + .strictObject({ + format: s.string({ enum: primaryFieldTypes, default: "integer" }), + required: s.boolean({ default: false }), + ...omitKeys(baseFieldConfigSchema.properties, ["required"]), + }) + .partial(); -export type PrimaryFieldConfig = Static; +export type PrimaryFieldConfig = s.Static; export class PrimaryField extends Field< PrimaryFieldConfig, @@ -41,7 +41,7 @@ export class PrimaryField extends Field< return this.config.format ?? "integer"; } - get fieldType() { + get fieldType(): "integer" | "text" { return this.format === "integer" ? "integer" : "text"; } @@ -67,11 +67,11 @@ export class PrimaryField extends Field< } override toJsonSchema() { - if (this.format === "uuid") { - return this.toSchemaWrapIfRequired(Type.String({ writeOnly: undefined })); - } - - return this.toSchemaWrapIfRequired(Type.Number({ writeOnly: undefined })); + return this.toSchemaWrapIfRequired( + this.format === "integer" + ? s.number({ writeOnly: undefined }) + : s.string({ writeOnly: undefined }), + ); } override toType(): TFieldTSType { diff --git a/app/src/data/fields/TextField.ts b/app/src/data/fields/TextField.ts index 5a439ad9..ec305595 100644 --- a/app/src/data/fields/TextField.ts +++ b/app/src/data/fields/TextField.ts @@ -1,42 +1,24 @@ import type { EntityManager } from "data"; -import type { Static } from "core/utils"; +import { omitKeys } from "core/utils"; import { TransformPersistFailedException } from "../errors"; import { Field, type TActionContext, baseFieldConfigSchema } from "./Field"; -import * as tb from "@sinclair/typebox"; -const { Type } = tb; +import { s } from "core/object/schema"; -export const textFieldConfigSchema = Type.Composite( - [ - Type.Object({ - default_value: Type.Optional(Type.String()), - minLength: Type.Optional(Type.Number()), - maxLength: Type.Optional(Type.Number()), - pattern: Type.Optional(Type.String()), - html_config: Type.Optional( - Type.Object({ - element: Type.Optional(Type.String({ default: "input" })), - props: Type.Optional( - Type.Object( - {}, - { - additionalProperties: Type.Union([ - Type.String({ title: "String" }), - Type.Number({ title: "Number" }), - ]), - }, - ), - ), - }), - ), +export const textFieldConfigSchema = s + .strictObject({ + default_value: s.string(), + minLength: s.number(), + maxLength: s.number(), + pattern: s.string(), + html_config: s.object({ + element: s.string({ default: "input" }), + props: s.record(s.anyOf([s.string({ title: "String" }), s.number({ title: "Number" })])), }), - baseFieldConfigSchema, - ], - { - additionalProperties: false, - }, -); + ...omitKeys(baseFieldConfigSchema.properties, ["default_value"]), + }) + .partial(); -export type TextFieldConfig = Static; +export type TextFieldConfig = s.Static; export class TextField extends Field< TextFieldConfig, @@ -113,7 +95,7 @@ export class TextField extends Field< override toJsonSchema() { return this.toSchemaWrapIfRequired( - Type.String({ + s.string({ default: this.getDefault(), minLength: this.config?.minLength, maxLength: this.config?.maxLength, diff --git a/app/src/data/fields/VirtualField.ts b/app/src/data/fields/VirtualField.ts index ec59ca3e..9d160275 100644 --- a/app/src/data/fields/VirtualField.ts +++ b/app/src/data/fields/VirtualField.ts @@ -1,11 +1,13 @@ -import type { Static } from "core/utils"; import { Field, baseFieldConfigSchema } from "./Field"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; -export const virtualFieldConfigSchema = Type.Composite([baseFieldConfigSchema, Type.Object({})]); +export const virtualFieldConfigSchema = s + .strictObject({ + ...baseFieldConfigSchema.properties, + }) + .partial(); -export type VirtualFieldConfig = Static; +export type VirtualFieldConfig = s.Static; export class VirtualField extends Field { override readonly type = "virtual"; @@ -25,7 +27,7 @@ export class VirtualField extends Field { override toJsonSchema() { return this.toSchemaWrapIfRequired( - Type.Any({ + s.any({ default: this.getDefault(), readOnly: true, }), diff --git a/app/src/data/relations/EntityRelation.ts b/app/src/data/relations/EntityRelation.ts index 35cef59b..f7f01d14 100644 --- a/app/src/data/relations/EntityRelation.ts +++ b/app/src/data/relations/EntityRelation.ts @@ -1,4 +1,4 @@ -import { type Static, parse } from "core/utils"; +import { parse } from "core/object/schema"; import type { ExpressionBuilder, SelectQueryBuilder } from "kysely"; import type { Entity, EntityData, EntityManager } from "../entities"; import { @@ -8,9 +8,8 @@ import { } from "../relations"; import type { RepoQuery } from "../server/query"; import type { RelationType } from "./relation-types"; -import * as tbbox from "@sinclair/typebox"; import type { PrimaryFieldType } from "core"; -const { Type } = tbbox; +import { s } from "core/object/schema"; const directions = ["source", "target"] as const; export type TDirection = (typeof directions)[number]; @@ -18,13 +17,13 @@ export type TDirection = (typeof directions)[number]; export type KyselyJsonFrom = any; export type KyselyQueryBuilder = SelectQueryBuilder; -export type BaseRelationConfig = Static; +export type BaseRelationConfig = s.Static; // @todo: add generic type for relation config export abstract class EntityRelation< Schema extends typeof EntityRelation.schema = typeof EntityRelation.schema, > { - config: Static; + config: s.Static; source: EntityRelationAnchor; target: EntityRelationAnchor; @@ -33,17 +32,17 @@ export abstract class EntityRelation< // allowed directions, used in RelationAccessor for visibility directions: TDirection[] = ["source", "target"]; - static schema = Type.Object({ - mappedBy: Type.Optional(Type.String()), - inversedBy: Type.Optional(Type.String()), - required: Type.Optional(Type.Boolean()), + static schema = s.strictObject({ + mappedBy: s.string().optional(), + inversedBy: s.string().optional(), + required: s.boolean().optional(), }); // don't make protected, App requires it to instantiatable constructor( source: EntityRelationAnchor, target: EntityRelationAnchor, - config: Partial> = {}, + config: Partial> = {}, ) { this.source = source; this.target = target; diff --git a/app/src/data/relations/ManyToManyRelation.ts b/app/src/data/relations/ManyToManyRelation.ts index bd051081..81bc6a35 100644 --- a/app/src/data/relations/ManyToManyRelation.ts +++ b/app/src/data/relations/ManyToManyRelation.ts @@ -1,4 +1,3 @@ -import type { Static } from "core/utils"; import type { ExpressionBuilder } from "kysely"; import { Entity, type EntityManager } from "../entities"; import { type Field, PrimaryField } from "../fields"; @@ -7,10 +6,9 @@ import { EntityRelation, type KyselyQueryBuilder } from "./EntityRelation"; import { EntityRelationAnchor } from "./EntityRelationAnchor"; import { RelationField } from "./RelationField"; import { type RelationType, RelationTypes } from "./relation-types"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; -export type ManyToManyRelationConfig = Static; +export type ManyToManyRelationConfig = s.Static; export class ManyToManyRelation extends EntityRelation { connectionEntity: Entity; @@ -18,18 +16,11 @@ export class ManyToManyRelation extends EntityRelation; - static override schema = Type.Composite( - [ - EntityRelation.schema, - Type.Object({ - connectionTable: Type.Optional(Type.String()), - connectionTableMappedName: Type.Optional(Type.String()), - }), - ], - { - additionalProperties: false, - }, - ); + static override schema = s.strictObject({ + connectionTable: s.string().optional(), + connectionTableMappedName: s.string().optional(), + ...EntityRelation.schema.properties, + }); constructor( source: Entity, diff --git a/app/src/data/relations/ManyToOneRelation.ts b/app/src/data/relations/ManyToOneRelation.ts index c9a1e0b1..77a5a7f4 100644 --- a/app/src/data/relations/ManyToOneRelation.ts +++ b/app/src/data/relations/ManyToOneRelation.ts @@ -1,6 +1,5 @@ import type { PrimaryFieldType } from "core"; import { snakeToPascalWithSpaces } from "core/utils"; -import type { Static } from "core/utils"; import type { ExpressionBuilder } from "kysely"; import type { Entity, EntityManager } from "../entities"; import type { RepoQuery } from "../server/query"; @@ -9,8 +8,7 @@ import { EntityRelationAnchor } from "./EntityRelationAnchor"; import { RelationField, type RelationFieldBaseConfig } from "./RelationField"; import type { MutationInstructionResponse } from "./RelationMutator"; import { type RelationType, RelationTypes } from "./relation-types"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; /** * Source entity receives the mapping field @@ -20,7 +18,7 @@ const { Type } = tbbox; * posts gets a users_id field */ -export type ManyToOneRelationConfig = Static; +export type ManyToOneRelationConfig = s.Static; export class ManyToOneRelation extends EntityRelation { private fieldConfig?: RelationFieldBaseConfig; @@ -28,30 +26,21 @@ export class ManyToOneRelation extends EntityRelation> = {}, + config: Partial> = {}, ) { const mappedBy = config.mappedBy || target.name; const inversedBy = config.inversedBy || source.name; diff --git a/app/src/data/relations/PolymorphicRelation.ts b/app/src/data/relations/PolymorphicRelation.ts index d0e7cddf..3a3d6364 100644 --- a/app/src/data/relations/PolymorphicRelation.ts +++ b/app/src/data/relations/PolymorphicRelation.ts @@ -1,4 +1,3 @@ -import type { Static } from "core/utils"; import type { ExpressionBuilder } from "kysely"; import type { Entity, EntityManager } from "../entities"; import { NumberField, TextField } from "../fields"; @@ -6,24 +5,16 @@ import type { RepoQuery } from "../server/query"; import { EntityRelation, type KyselyJsonFrom, type KyselyQueryBuilder } from "./EntityRelation"; import { EntityRelationAnchor } from "./EntityRelationAnchor"; import { type RelationType, RelationTypes } from "./relation-types"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; -export type PolymorphicRelationConfig = Static; +export type PolymorphicRelationConfig = s.Static; // @todo: what about cascades? export class PolymorphicRelation extends EntityRelation { - static override schema = Type.Composite( - [ - EntityRelation.schema, - Type.Object({ - targetCardinality: Type.Optional(Type.Number()), - }), - ], - { - additionalProperties: false, - }, - ); + static override schema = s.strictObject({ + targetCardinality: s.number().optional(), + ...EntityRelation.schema.properties, + }); constructor(source: Entity, target: Entity, config: Partial = {}) { const mappedBy = config.mappedBy || target.name; diff --git a/app/src/data/relations/RelationField.ts b/app/src/data/relations/RelationField.ts index 67be1876..1f7ad8b3 100644 --- a/app/src/data/relations/RelationField.ts +++ b/app/src/data/relations/RelationField.ts @@ -1,26 +1,22 @@ -import { type Static, StringEnum } from "core/utils"; import type { EntityManager } from "../entities"; -import { Field, baseFieldConfigSchema, primaryFieldTypes } from "../fields"; +import { Field, baseFieldConfigSchema } from "../fields"; import type { EntityRelation } from "./EntityRelation"; import type { EntityRelationAnchor } from "./EntityRelationAnchor"; -import * as tbbox from "@sinclair/typebox"; import type { TFieldTSType } from "data/entities/EntityTypescript"; -const { Type } = tbbox; +import { s } from "core/object/schema"; const CASCADES = ["cascade", "set null", "set default", "restrict", "no action"] as const; -export const relationFieldConfigSchema = Type.Composite([ - baseFieldConfigSchema, - Type.Object({ - reference: Type.String(), - target: Type.String(), // @todo: potentially has to be an instance! - target_field: Type.Optional(Type.String({ default: "id" })), - target_field_type: Type.Optional(StringEnum(["integer", "text"], { default: "integer" })), - on_delete: Type.Optional(StringEnum(CASCADES, { default: "set null" })), - }), -]); +export const relationFieldConfigSchema = s.strictObject({ + reference: s.string(), + target: s.string(), // @todo: potentially has to be an instance! + target_field: s.string({ default: "id" }).optional(), + target_field_type: s.string({ enum: ["text", "integer"], default: "integer" }).optional(), + on_delete: s.string({ enum: CASCADES, default: "set null" }).optional(), + ...baseFieldConfigSchema.properties, +}); -export type RelationFieldConfig = Static; +export type RelationFieldConfig = s.Static; export type RelationFieldBaseConfig = { label?: string }; export class RelationField extends Field { @@ -81,7 +77,7 @@ export class RelationField extends Field { override toJsonSchema() { return this.toSchemaWrapIfRequired( - Type.Number({ + s.number({ $ref: `${this.config?.target}#/properties/${this.config?.target_field}`, }), ); diff --git a/app/src/data/server/query.ts b/app/src/data/server/query.ts index b4ebc9e3..ffdb715b 100644 --- a/app/src/data/server/query.ts +++ b/app/src/data/server/query.ts @@ -2,7 +2,7 @@ import { s } from "core/object/schema"; import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder"; import { $console } from "core"; import { isObject } from "core/utils"; -import type { CoercionOptions, TAnyOf } from "jsonv-ts"; +import type { anyOf, CoercionOptions, Schema } from "jsonv-ts"; // ------- // helpers @@ -36,10 +36,12 @@ const stringArray = s.anyOf( // ------- // sorting const sortDefault = { by: "id", dir: "asc" }; -const sortSchema = s.object({ - by: s.string(), - dir: s.string({ enum: ["asc", "desc"] }).optional(), -}); +const sortSchema = s + .object({ + by: s.string(), + dir: s.string({ enum: ["asc", "desc"] }).optional(), + }) + .strict(); type SortSchema = s.Static; const sort = s.anyOf([s.string(), sortSchema], { default: sortDefault, @@ -88,9 +90,9 @@ export type RepoWithSchema = Record< } >; -const withSchema = (self: s.TSchema): s.TSchemaInOut => +const withSchema = (self: Schema): Schema<{}, Type, Type> => s.anyOf([stringIdentifier, s.array(stringIdentifier), self], { - coerce: function (this: TAnyOf, _value: unknown, opts: CoercionOptions = {}) { + coerce: function (this: typeof anyOf, _value: unknown, opts: CoercionOptions = {}) { let value: any = _value; if (typeof value === "string") { @@ -126,15 +128,17 @@ const withSchema = (self: s.TSchema): s.TSchemaInOut => // ========== // REPO QUERY export const repoQuery = s.recursive((self) => - s.partialObject({ - limit: s.number({ default: 10 }), - offset: s.number({ default: 0 }), - sort, - where, - select: stringArray, - join: stringArray, - with: withSchema(self), - }), + s + .object({ + limit: s.number({ default: 10 }), + offset: s.number({ default: 0 }), + sort, + where, + select: stringArray, + join: stringArray, + with: withSchema(self), + }) + .partial(), ); export const getRepoQueryTemplate = () => repoQuery.template({ @@ -151,3 +155,15 @@ export type RepoQueryIn = { where?: WhereQuery; }; export type RepoQuery = s.StaticCoerced; + +//export type RepoQuery = s.StaticCoerced; +// @todo: CURRENT WORKAROUND +/* export type RepoQuery = { + limit?: number; + offset?: number; + sort?: { by: string; dir: "asc" | "desc" }; + select?: string[]; + with?: Record; + join?: string[]; + where?: WhereQuery; +}; */ diff --git a/app/src/flows/AppFlows.ts b/app/src/flows/AppFlows.ts index 6e9ac364..657ad537 100644 --- a/app/src/flows/AppFlows.ts +++ b/app/src/flows/AppFlows.ts @@ -1,15 +1,16 @@ -import { type Static, transformObject } from "core/utils"; +import { transformObject } from "core/utils"; import { Flow, HttpTrigger } from "flows"; import { Hono } from "hono"; import { Module } from "modules/Module"; import { TASKS, flowsConfigSchema } from "./flows-schema"; +import type { s } from "core/object/schema"; -export type AppFlowsSchema = Static; +export type AppFlowsSchema = s.Static; export type TAppFlowSchema = AppFlowsSchema["flows"][number]; export type TAppFlowTriggerSchema = TAppFlowSchema["trigger"]; export type { TAppFlowTaskSchema } from "./flows-schema"; -export class AppFlows extends Module { +export class AppFlows extends Module { private flows: Record = {}; getSchema() { @@ -80,6 +81,8 @@ export class AppFlows extends Module { this.setBuilt(); } + // @todo: fix this + // @ts-expect-error override toJSON() { return { ...this.config, diff --git a/app/src/flows/flows-schema.ts b/app/src/flows/flows-schema.ts index e5d029b4..46e14450 100644 --- a/app/src/flows/flows-schema.ts +++ b/app/src/flows/flows-schema.ts @@ -1,7 +1,6 @@ -import { Const, type Static, StringRecord, transformObject } from "core/utils"; +import { transformObject } from "core/utils"; import { TaskMap, TriggerMap } from "flows"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; export const TASKS = { ...TaskMap, @@ -10,77 +9,59 @@ export const TASKS = { export const TRIGGERS = TriggerMap; const taskSchemaObject = transformObject(TASKS, (task, name) => { - return Type.Object( + return s.strictObject( { - type: Const(name), + type: s.literal(name), params: task.cls.schema, }, - { title: String(name), additionalProperties: false }, + { title: String(name) }, ); }); -const taskSchema = Type.Union(Object.values(taskSchemaObject)); -export type TAppFlowTaskSchema = Static; +const taskSchema = s.anyOf(Object.values(taskSchemaObject)); +export type TAppFlowTaskSchema = s.Static; const triggerSchemaObject = transformObject(TRIGGERS, (trigger, name) => { - return Type.Object( + return s.strictObject( { - type: Const(name), - config: trigger.cls.schema, + type: s.literal(name), + config: trigger.cls.schema.optional(), }, - { title: String(name), additionalProperties: false }, + { title: String(name) }, ); }); +const triggerSchema = s.anyOf(Object.values(triggerSchemaObject)); +export type TAppFlowTriggerSchema = s.Static; -const connectionSchema = Type.Object({ - source: Type.String(), - target: Type.String(), - config: Type.Object( - { - condition: Type.Optional( - Type.Union([ - Type.Object( - { type: Const("success") }, - { additionalProperties: false, title: "success" }, - ), - Type.Object( - { type: Const("error") }, - { additionalProperties: false, title: "error" }, - ), - Type.Object( - { type: Const("matches"), path: Type.String(), value: Type.String() }, - { additionalProperties: false, title: "matches" }, - ), - ]), - ), - max_retries: Type.Optional(Type.Number()), - }, - { default: {}, additionalProperties: false }, - ), +const connectionSchema = s.strictObject({ + source: s.string(), + target: s.string(), + config: s + .strictObject({ + condition: s.anyOf([ + s.strictObject({ type: s.literal("success") }, { title: "success" }), + s.strictObject({ type: s.literal("error") }, { title: "error" }), + s.strictObject( + { type: s.literal("matches"), path: s.string(), value: s.string() }, + { title: "matches" }, + ), + ]), + max_retries: s.number(), + }) + .partial(), }); // @todo: rework to have fixed ids per task and connections (and preferrably arrays) // causes issues with canvas -export const flowSchema = Type.Object( - { - trigger: Type.Union(Object.values(triggerSchemaObject)), - tasks: Type.Optional(StringRecord(Type.Union(Object.values(taskSchemaObject)))), - connections: Type.Optional(StringRecord(connectionSchema)), - start_task: Type.Optional(Type.String()), - responding_task: Type.Optional(Type.String()), - }, - { - additionalProperties: false, - }, -); -export type TAppFlowSchema = Static; +export const flowSchema = s.strictObject({ + trigger: s.anyOf(Object.values(triggerSchemaObject)), + tasks: s.record(s.anyOf(Object.values(taskSchemaObject))).optional(), + connections: s.record(connectionSchema).optional(), + start_task: s.string().optional(), + responding_task: s.string().optional(), +}); +export type TAppFlowSchema = s.Static; -export const flowsConfigSchema = Type.Object( - { - basepath: Type.String({ default: "/api/flows" }), - flows: StringRecord(flowSchema, { default: {} }), - }, - { - default: {}, - additionalProperties: false, - }, -); +export const flowsConfigSchema = s.strictObject({ + basepath: s.string({ default: "/api/flows" }), + flows: s.record(flowSchema, { default: {} }), +}); diff --git a/app/src/flows/flows/triggers/EventTrigger.ts b/app/src/flows/flows/triggers/EventTrigger.ts index 3924840a..7b4dad73 100644 --- a/app/src/flows/flows/triggers/EventTrigger.ts +++ b/app/src/flows/flows/triggers/EventTrigger.ts @@ -2,19 +2,15 @@ import type { EventManager } from "core/events"; import type { Flow } from "../Flow"; import { Trigger } from "./Trigger"; import { $console } from "core"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; export class EventTrigger extends Trigger { override type = "event"; - static override schema = Type.Composite([ - Trigger.schema, - Type.Object({ - event: Type.String(), - // add match - }), - ]); + static override schema = s.strictObject({ + event: s.string(), + ...Trigger.schema.properties, + }); override async register(flow: Flow, emgr: EventManager) { if (!emgr.eventExists(this.config.event)) { diff --git a/app/src/flows/flows/triggers/HttpTrigger.ts b/app/src/flows/flows/triggers/HttpTrigger.ts index 6dc66d94..19403456 100644 --- a/app/src/flows/flows/triggers/HttpTrigger.ts +++ b/app/src/flows/flows/triggers/HttpTrigger.ts @@ -1,23 +1,19 @@ -import { StringEnum } from "core/utils"; import type { Context, Hono } from "hono"; import type { Flow } from "../Flow"; import { Trigger } from "./Trigger"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; const httpMethods = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const; export class HttpTrigger extends Trigger { override type = "http"; - static override schema = Type.Composite([ - Trigger.schema, - Type.Object({ - path: Type.String({ pattern: "^/.*$" }), - method: StringEnum(httpMethods, { default: "GET" }), - response_type: StringEnum(["json", "text", "html"], { default: "json" }), - }), - ]); + static override schema = s.strictObject({ + path: s.string({ pattern: "^/.*$" }), + method: s.string({ enum: httpMethods, default: "GET" }), + response_type: s.string({ enum: ["json", "text", "html"], default: "json" }), + ...Trigger.schema.properties, + }); override async register(flow: Flow, hono: Hono) { const method = this.config.method.toLowerCase() as any; diff --git a/app/src/flows/flows/triggers/Trigger.ts b/app/src/flows/flows/triggers/Trigger.ts index a8e22157..3414bbc8 100644 --- a/app/src/flows/flows/triggers/Trigger.ts +++ b/app/src/flows/flows/triggers/Trigger.ts @@ -1,20 +1,18 @@ -import { type Static, StringEnum, parse } from "core/utils"; import type { Execution } from "../Execution"; import type { Flow } from "../Flow"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s, parse } from "core/object/schema"; export class Trigger { // @todo: remove this executions: Execution[] = []; type = "manual"; - config: Static; + config: s.Static; - static schema = Type.Object({ - mode: StringEnum(["sync", "async"], { default: "async" }), + static schema = s.strictObject({ + mode: s.string({ enum: ["sync", "async"], default: "async" }), }); - constructor(config?: Partial>) { + constructor(config?: Partial>) { const schema = (this.constructor as typeof Trigger).schema; // @ts-ignore for now this.config = parse(schema, config ?? {}); diff --git a/app/src/flows/tasks/Task.tsx b/app/src/flows/tasks/Task.tsx index e035af92..1bbfcd00 100644 --- a/app/src/flows/tasks/Task.tsx +++ b/app/src/flows/tasks/Task.tsx @@ -1,9 +1,10 @@ -import type { StaticDecode, TSchema } from "@sinclair/typebox"; -import { BkndError, SimpleRenderer } from "core"; -import { type Static, type TObject, Value, parse, ucFirst } from "core/utils"; +//import { BkndError, SimpleRenderer } from "core"; +import { BkndError } from "core/errors"; + +import { s, parse } from "core/object/schema"; import type { InputsMap } from "../flows/Execution"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { SimpleRenderer } from "core/template/SimpleRenderer"; + //type InstanceOf = T extends new (...args: any) => infer R ? R : never; export type TaskResult = { @@ -16,7 +17,10 @@ export type TaskResult = { export type TaskRenderProps = any; -export function dynamic( +// @todo: CURRENT WORKAROUND +export const dynamic = (a: S, b?: any) => null as unknown as S; + +/* export function dynamic( type: Type, parse?: (val: any | string) => Static, ) { @@ -51,23 +55,23 @@ export function dynamic( // @ts-ignore .Encode((val) => val) ); -} +} */ -export abstract class Task { +export abstract class Task { abstract type: string; name: string; /** * The schema of the task's parameters. */ - static schema = Type.Object({}); + static schema = s.any(); /** * The task's parameters. */ - _params: Static; + _params: s.Static; - constructor(name: string, params?: Static) { + constructor(name: string, params?: s.Static) { if (typeof name !== "string") { throw new Error(`Task name must be a string, got ${typeof name}`); } @@ -81,7 +85,7 @@ export abstract class Task { if ( schema === Task.schema && typeof params !== "undefined" && - Object.keys(params).length > 0 + Object.keys(params || {}).length > 0 ) { throw new Error( `Task "${name}" has no schema defined but params passed: ${JSON.stringify(params)}`, @@ -93,18 +97,18 @@ export abstract class Task { } get params() { - return this._params as StaticDecode; + return this._params as s.StaticCoerced; } - protected clone(name: string, params: Static): Task { + protected clone(name: string, params: s.Static): Task { return new (this.constructor as any)(name, params); } - static async resolveParams( + static async resolveParams( schema: S, params: any, inputs: object = {}, - ): Promise> { + ): Promise> { const newParams: any = {}; const renderer = new SimpleRenderer(inputs, { renderKeys: true }); @@ -134,7 +138,8 @@ export abstract class Task { newParams[key] = value; } - return Value.Decode(schema, newParams); + return schema.coerce(newParams); + //return Value.Decode(schema, newParams); } private async cloneWithResolvedParams(_inputs: Map) { diff --git a/app/src/flows/tasks/presets/FetchTask.ts b/app/src/flows/tasks/presets/FetchTask.ts index 17b17a3e..0108b0ce 100644 --- a/app/src/flows/tasks/presets/FetchTask.ts +++ b/app/src/flows/tasks/presets/FetchTask.ts @@ -1,7 +1,5 @@ -import { StringEnum } from "core/utils"; import { Task, dynamic } from "../Task"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; const FetchMethods = ["GET", "POST", "PUT", "PATCH", "DELETE"]; @@ -11,24 +9,22 @@ export class FetchTask> extends Task< > { type = "fetch"; - static override schema = Type.Object({ - url: Type.String({ + static override schema = s.strictObject({ + url: s.string({ pattern: "^(http|https)://", }), - method: Type.Optional(dynamic(StringEnum(FetchMethods, { default: "GET" }))), - headers: Type.Optional( - dynamic( - Type.Array( - Type.Object({ - key: Type.String(), - value: Type.String(), - }), - ), - JSON.parse, + method: dynamic(s.string({ enum: FetchMethods, default: "GET" })).optional(), + headers: dynamic( + s.array( + s.strictObject({ + key: s.string(), + value: s.string(), + }), ), - ), - body: Type.Optional(dynamic(Type.String())), - normal: Type.Optional(dynamic(Type.Number(), Number.parseInt)), + JSON.parse, + ).optional(), + body: dynamic(s.string()).optional(), + normal: dynamic(s.number(), Number.parseInt).optional(), }); protected getBody(): string | undefined { diff --git a/app/src/flows/tasks/presets/LogTask.ts b/app/src/flows/tasks/presets/LogTask.ts index fda6d125..d8e2d0a6 100644 --- a/app/src/flows/tasks/presets/LogTask.ts +++ b/app/src/flows/tasks/presets/LogTask.ts @@ -1,13 +1,12 @@ import { Task } from "../Task"; import { $console } from "core"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; export class LogTask extends Task { type = "log"; - static override schema = Type.Object({ - delay: Type.Number({ default: 10 }), + static override schema = s.strictObject({ + delay: s.number({ default: 10 }), }); async execute() { diff --git a/app/src/flows/tasks/presets/RenderTask.ts b/app/src/flows/tasks/presets/RenderTask.ts index 4ab0a235..e57b01f3 100644 --- a/app/src/flows/tasks/presets/RenderTask.ts +++ b/app/src/flows/tasks/presets/RenderTask.ts @@ -1,6 +1,5 @@ import { Task } from "../Task"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; export class RenderTask> extends Task< typeof RenderTask.schema, @@ -8,8 +7,8 @@ export class RenderTask> extends Task< > { type = "render"; - static override schema = Type.Object({ - render: Type.String(), + static override schema = s.strictObject({ + render: s.string(), }); async execute() { diff --git a/app/src/flows/tasks/presets/SubFlowTask.ts b/app/src/flows/tasks/presets/SubFlowTask.ts index 7832d481..e3a19f3f 100644 --- a/app/src/flows/tasks/presets/SubFlowTask.ts +++ b/app/src/flows/tasks/presets/SubFlowTask.ts @@ -1,7 +1,6 @@ import { Flow } from "../../flows/Flow"; import { Task, dynamic } from "../Task"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; export class SubFlowTask> extends Task< typeof SubFlowTask.schema, @@ -9,10 +8,10 @@ export class SubFlowTask> extends Task< > { type = "subflow"; - static override schema = Type.Object({ - flow: Type.Any(), - input: Type.Optional(dynamic(Type.Any(), JSON.parse)), - loop: Type.Optional(Type.Boolean()), + static override schema = s.strictObject({ + flow: s.any(), + input: dynamic(s.any(), JSON.parse).optional(), + loop: s.boolean().optional(), }); async execute() { diff --git a/app/src/media/AppMedia.ts b/app/src/media/AppMedia.ts index 18b536e8..42cfaa45 100644 --- a/app/src/media/AppMedia.ts +++ b/app/src/media/AppMedia.ts @@ -13,7 +13,7 @@ import { text, } from "../data/prototype"; import { MediaController } from "./api/MediaController"; -import { buildMediaSchema, type mediaConfigSchema, registry } from "./media-schema"; +import { buildMediaSchema, registry, type TAppMediaConfig } from "./media-schema"; export type MediaFieldSchema = FieldSchema; declare module "core" { @@ -23,7 +23,7 @@ declare module "core" { } } -export class AppMedia extends Module { +export class AppMedia extends Module { private _storage?: Storage; override async build() { diff --git a/app/src/media/MediaField.ts b/app/src/media/MediaField.ts index f29a171a..5c2dcc82 100644 --- a/app/src/media/MediaField.ts +++ b/app/src/media/MediaField.ts @@ -1,19 +1,17 @@ -import type { Static } from "core/utils"; import { Field, baseFieldConfigSchema } from "data/fields"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; -export const mediaFieldConfigSchema = Type.Composite([ - Type.Object({ - entity: Type.String(), // @todo: is this really required? - min_items: Type.Optional(Type.Number()), - max_items: Type.Optional(Type.Number()), - mime_types: Type.Optional(Type.Array(Type.String())), - }), - baseFieldConfigSchema, -]); +export const mediaFieldConfigSchema = s + .strictObject({ + entity: s.string(), // @todo: is this really required? + min_items: s.number(), + max_items: s.number(), + mime_types: s.array(s.string()), + ...baseFieldConfigSchema.properties, + }) + .partial(); -export type MediaFieldConfig = Static; +export type MediaFieldConfig = s.Static; export type MediaItem = { id: number; diff --git a/app/src/media/index.ts b/app/src/media/index.ts index d9451ad5..6d2f4f2a 100644 --- a/app/src/media/index.ts +++ b/app/src/media/index.ts @@ -16,6 +16,7 @@ import { StorageCloudinaryAdapter, } from "./storage/adapters/cloudinary/StorageCloudinaryAdapter"; import { type S3AdapterConfig, StorageS3Adapter } from "./storage/adapters/s3/StorageS3Adapter"; +import type { s } from "core/object/schema"; export { StorageAdapter }; export { StorageS3Adapter, type S3AdapterConfig, StorageCloudinaryAdapter, type CloudinaryConfig }; @@ -29,10 +30,10 @@ type ClassThatImplements = Constructor & { prototype: T }; export const MediaAdapterRegistry = new Registry<{ cls: ClassThatImplements; - schema: TObject; + schema: s.Schema; }>((cls: ClassThatImplements) => ({ cls, - schema: cls.prototype.getSchema() as TObject, + schema: cls.prototype.getSchema() as s.Schema, })) .register("s3", StorageS3Adapter) .register("cloudinary", StorageCloudinaryAdapter); diff --git a/app/src/media/media-schema.ts b/app/src/media/media-schema.ts index f02e2a61..5acb898d 100644 --- a/app/src/media/media-schema.ts +++ b/app/src/media/media-schema.ts @@ -1,8 +1,7 @@ -import { Const, type Static, objectTransform } from "core/utils"; +import { objectTransform } from "core/utils"; import { Adapters } from "media"; import { registries } from "modules/registries"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; export const ADAPTERS = { ...Adapters, @@ -12,42 +11,33 @@ export const registry = registries.media; export function buildMediaSchema() { const adapterSchemaObject = objectTransform(registry.all(), (adapter, name) => { - return Type.Object( + return s.strictObject( { - type: Const(name), + type: s.literal(name), config: adapter.schema, }, { title: adapter.schema?.title ?? name, description: adapter.schema?.description, - additionalProperties: false, }, ); }); - const adapterSchema = Type.Union(Object.values(adapterSchemaObject)); - return Type.Object( - { - enabled: Type.Boolean({ default: false }), - basepath: Type.String({ default: "/api/media" }), - entity_name: Type.String({ default: "media" }), - storage: Type.Object( - { - body_max_size: Type.Optional( - Type.Number({ - description: "Max size of the body in bytes. Leave blank for unlimited.", - }), - ), - }, - { default: {} }, - ), - adapter: Type.Optional(adapterSchema), - }, - { - additionalProperties: false, - }, - ); + return s.strictObject({ + enabled: s.boolean({ default: false }), + basepath: s.string({ default: "/api/media" }), + entity_name: s.string({ default: "media" }), + storage: s.strictObject( + { + body_max_size: s.number({ + description: "Max size of the body in bytes. Leave blank for unlimited.", + }), + }, + { default: {} }, + ), + adapter: s.anyOf(Object.values(adapterSchemaObject)).optional(), + }); } export const mediaConfigSchema = buildMediaSchema(); -export type TAppMediaConfig = Static; +export type TAppMediaConfig = s.Static; diff --git a/app/src/media/storage/StorageAdapter.ts b/app/src/media/storage/StorageAdapter.ts index 09aa9571..d6ceaab0 100644 --- a/app/src/media/storage/StorageAdapter.ts +++ b/app/src/media/storage/StorageAdapter.ts @@ -1,6 +1,6 @@ import type { FileListObject, FileMeta } from "media"; import type { FileBody, FileUploadPayload } from "media/storage/Storage"; -import type { TSchema } from "@sinclair/typebox"; +import type { s } from "core/object/schema"; const SYMBOL = Symbol.for("bknd:storage"); @@ -32,6 +32,6 @@ export abstract class StorageAdapter { abstract getObject(key: string, headers: Headers): Promise; abstract getObjectUrl(key: string): string; abstract getObjectMeta(key: string): Promise; - abstract getSchema(): TSchema | undefined; + abstract getSchema(): s.Schema | undefined; abstract toJSON(secrets?: boolean): any; } diff --git a/app/src/media/storage/adapters/cloudinary/StorageCloudinaryAdapter.ts b/app/src/media/storage/adapters/cloudinary/StorageCloudinaryAdapter.ts index 63351508..401f7723 100644 --- a/app/src/media/storage/adapters/cloudinary/StorageCloudinaryAdapter.ts +++ b/app/src/media/storage/adapters/cloudinary/StorageCloudinaryAdapter.ts @@ -1,21 +1,19 @@ import { hash, pickHeaders } from "core/utils"; -import { type Static, parse } from "core/utils"; import type { FileBody, FileListObject, FileMeta } from "../../Storage"; import { StorageAdapter } from "../../StorageAdapter"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s, parse } from "core/object/schema"; -export const cloudinaryAdapterConfig = Type.Object( +export const cloudinaryAdapterConfig = s.object( { - cloud_name: Type.String(), - api_key: Type.String(), - api_secret: Type.String(), - upload_preset: Type.Optional(Type.String()), + cloud_name: s.string(), + api_key: s.string(), + api_secret: s.string(), + upload_preset: s.string().optional(), }, { title: "Cloudinary", description: "Cloudinary media storage" }, ); -export type CloudinaryConfig = Static; +export type CloudinaryConfig = s.Static; type CloudinaryObject = { asset_id: string; diff --git a/app/src/media/storage/adapters/s3/StorageS3Adapter.ts b/app/src/media/storage/adapters/s3/StorageS3Adapter.ts index 6462e839..a88e31f8 100644 --- a/app/src/media/storage/adapters/s3/StorageS3Adapter.ts +++ b/app/src/media/storage/adapters/s3/StorageS3Adapter.ts @@ -7,18 +7,17 @@ import type { PutObjectRequest, } from "@aws-sdk/client-s3"; import { AwsClient, isDebug } from "core"; -import { type Static, isFile, parse, pickHeaders2 } from "core/utils"; +import { isFile, pickHeaders2 } from "core/utils"; import { transform } from "lodash-es"; import type { FileBody, FileListObject } from "../../Storage"; import { StorageAdapter } from "../../StorageAdapter"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { parse, s } from "core/object/schema"; -export const s3AdapterConfig = Type.Object( +export const s3AdapterConfig = s.object( { - access_key: Type.String(), - secret_access_key: Type.String(), - url: Type.String({ + access_key: s.string(), + secret_access_key: s.string(), + url: s.string({ pattern: "^https?://(?:.*)?[^/.]+$", description: "URL to S3 compatible endpoint without trailing slash", examples: [ @@ -33,7 +32,7 @@ export const s3AdapterConfig = Type.Object( }, ); -export type S3AdapterConfig = Static; +export type S3AdapterConfig = s.Static; export class StorageS3Adapter extends StorageAdapter { readonly #config: S3AdapterConfig; diff --git a/app/src/modules/Controller.ts b/app/src/modules/Controller.ts index ee54fedc..b3f29a07 100644 --- a/app/src/modules/Controller.ts +++ b/app/src/modules/Controller.ts @@ -5,7 +5,7 @@ import type { SafeUser } from "auth"; import type { EntityManager } from "data"; import { s } from "core/object/schema"; -export type ServerEnv = Env & { +export interface ServerEnv extends Env { Variables: { app: App; // to prevent resolving auth multiple times @@ -17,7 +17,22 @@ export type ServerEnv = Env & { }; html?: string; }; -}; + [key: string]: any; +} + +/* export type ServerEnv = Env & { + Variables: { + app: App; + // to prevent resolving auth multiple times + auth?: { + resolved: boolean; + registered: boolean; + skip: boolean; + user?: SafeUser; + }; + html?: string; + }; +}; */ export class Controller { protected middlewares = middlewares; diff --git a/app/src/modules/Module.ts b/app/src/modules/Module.ts index d4164973..97a22124 100644 --- a/app/src/modules/Module.ts +++ b/app/src/modules/Module.ts @@ -1,11 +1,13 @@ import type { Guard } from "auth"; import { type DebugLogger, SchemaObject } from "core"; import type { EventManager } from "core/events"; -import type { Static, TSchema } from "core/utils"; import type { Connection, EntityManager } from "data"; import type { Hono } from "hono"; import type { ServerEnv } from "modules/Controller"; import type { ModuleHelper } from "./ModuleHelper"; +import type { s } from "core/object/schema"; + +type PartialRec = { [P in keyof T]?: PartialRec }; export type ModuleBuildContext = { connection: Connection; @@ -18,13 +20,13 @@ export type ModuleBuildContext = { helper: ModuleHelper; }; -export abstract class Module> { +export abstract class Module { private _built = false; private _schema: SchemaObject>; private _listener: any = () => null; constructor( - initial?: Partial>, + initial?: PartialRec, protected _ctx?: ModuleBuildContext, ) { this._schema = new SchemaObject(this.getSchema(), initial, { @@ -47,7 +49,7 @@ export abstract class Module { + onBeforeUpdate(from: Schema, to: Schema): Schema | Promise { return to; } @@ -75,11 +77,13 @@ export abstract class Module> { - return this._schema.default(); + //get configDefault(): s.Static> { + get configDefault(): Schema { + return this._schema.default() as any; } - get config(): Static> { + //get config(): s.Static> { + get config(): Schema { return this._schema.get(); } @@ -130,7 +134,8 @@ export abstract class Module> { + //toJSON(secrets?: boolean): s.Static> { + toJSON(secrets?: boolean): Schema { return this.config; } } diff --git a/app/src/modules/ModuleManager.ts b/app/src/modules/ModuleManager.ts index 825a2209..64debbd6 100644 --- a/app/src/modules/ModuleManager.ts +++ b/app/src/modules/ModuleManager.ts @@ -2,15 +2,7 @@ import { Guard } from "auth"; import { $console, BkndError, DebugLogger, env } from "core"; import { EventManager } from "core/events"; import * as $diff from "core/object/diff"; -import { - Default, - type Static, - StringEnum, - mark, - objectEach, - stripMark, - transformObject, -} from "core/utils"; +import { objectEach, transformObject } from "core/utils"; import type { Connection, Schema } from "data"; import { EntityManager } from "data/entities/EntityManager"; import * as proto from "data/prototype"; @@ -26,18 +18,17 @@ import { AppFlows } from "../flows/AppFlows"; import { AppMedia } from "../media/AppMedia"; import type { ServerEnv } from "./Controller"; import { Module, type ModuleBuildContext } from "./Module"; -import * as tbbox from "@sinclair/typebox"; import { ModuleHelper } from "./ModuleHelper"; -const { Type } = tbbox; +import { s, mark, stripMark } from "core/object/schema"; export type { ModuleBuildContext }; export const MODULES = { server: AppServer, - data: AppData, + data: AppData, // @todo: auth: AppAuth, media: AppMedia, - flows: AppFlows, + flows: AppFlows, // @todo: } as const; // get names of MODULES as an array @@ -53,7 +44,7 @@ export type ModuleSchemas = { }; export type ModuleConfigs = { - [K in keyof ModuleSchemas]: Static; + [K in keyof ModuleSchemas]: s.Static; }; type PartialRec = { [P in keyof T]?: PartialRec }; @@ -101,14 +92,14 @@ export type ConfigTable = { updated_at?: Date; }; -const configJsonSchema = Type.Union([ +const configJsonSchema = s.anyOf([ getDefaultSchema(), - Type.Array( - Type.Object({ - t: StringEnum(["a", "r", "e"]), - p: Type.Array(Type.Union([Type.String(), Type.Number()])), - o: Type.Optional(Type.Any()), - n: Type.Optional(Type.Any()), + s.array( + s.strictObject({ + t: s.string({ enum: ["a", "r", "e"] }), + p: s.array(s.anyOf([s.string(), s.number()])), + o: s.any().optional(), + n: s.any().optional(), }), ), ]); @@ -717,7 +708,8 @@ export function getDefaultSchema() { export function getDefaultConfig(): ModuleConfigs { const config = transformObject(MODULES, (module) => { - return Default(module.prototype.getSchema(), {}); + return module.prototype.getSchema().template(); + //return Default(module.prototype.getSchema(), {}); }); return config as any; diff --git a/app/src/modules/server/AppServer.ts b/app/src/modules/server/AppServer.ts index ba1c5662..427722fd 100644 --- a/app/src/modules/server/AppServer.ts +++ b/app/src/modules/server/AppServer.ts @@ -1,37 +1,27 @@ import { Exception, isDebug, $console } from "core"; -import { type Static, StringEnum } from "core/utils"; import { cors } from "hono/cors"; import { Module } from "modules/Module"; -import * as tbbox from "@sinclair/typebox"; import { AuthException } from "auth/errors"; -const { Type } = tbbox; +import { s } from "core/object/schema"; -const serverMethods = ["GET", "POST", "PATCH", "PUT", "DELETE"]; +const serverMethods = ["GET", "POST", "PATCH", "PUT", "DELETE"] as const; -export const serverConfigSchema = Type.Object( - { - cors: Type.Object( - { - origin: Type.String({ default: "*" }), - allow_methods: Type.Array(StringEnum(serverMethods), { - default: serverMethods, - uniqueItems: true, - }), - allow_headers: Type.Array(Type.String(), { - default: ["Content-Type", "Content-Length", "Authorization", "Accept"], - }), - }, - { default: {}, additionalProperties: false }, - ), - }, - { - additionalProperties: false, - }, -); +export const serverConfigSchema = s.strictObject({ + cors: s.strictObject({ + origin: s.string({ default: "*" }), + allow_methods: s.array(s.string({ enum: serverMethods }), { + default: serverMethods, + uniqueItems: true, + }), + allow_headers: s.array(s.string(), { + default: ["Content-Type", "Content-Length", "Authorization", "Accept"], + }), + }), +}); -export type AppServerConfig = Static; +export type AppServerConfig = s.Static; -export class AppServer extends Module { +export class AppServer extends Module { override getRestrictedPaths() { return []; } diff --git a/app/src/modules/server/SystemController.ts b/app/src/modules/server/SystemController.ts index ec078317..4df29da5 100644 --- a/app/src/modules/server/SystemController.ts +++ b/app/src/modules/server/SystemController.ts @@ -1,15 +1,8 @@ /// import type { App } from "App"; -import { $console, tbValidator as tb } from "core"; -import { - StringEnum, - TypeInvalidError, - datetimeStringLocal, - datetimeStringUTC, - getTimezone, - getTimezoneOffset, -} from "core/utils"; +import { $console } from "core"; +import { datetimeStringLocal, datetimeStringUTC, getTimezone, getTimezoneOffset } from "core/utils"; import { getRuntimeKey } from "core/utils"; import type { Context, Hono } from "hono"; import { Controller } from "modules/Controller"; @@ -20,11 +13,11 @@ import { type ModuleConfigs, type ModuleSchemas, type ModuleKey, - getDefaultConfig, } from "modules/ModuleManager"; import * as SystemPermissions from "modules/permissions"; -import { jsc, s, describeRoute } from "core/object/schema"; +import { jsc, s, describeRoute, InvalidSchemaError } from "core/object/schema"; import { getVersion } from "core/env"; + export type ConfigUpdate = { success: true; module: Key; @@ -104,7 +97,7 @@ export class SystemController extends Controller { } catch (e) { $console.error("config update error", e); - if (e instanceof TypeInvalidError) { + if (e instanceof InvalidSchemaError) { return c.json( { success: false, type: "type-invalid", errors: e.errors }, { status: 400 }, @@ -234,11 +227,13 @@ export class SystemController extends Controller { permission(SystemPermissions.schemaRead), jsc( "query", - s.partialObject({ - config: s.boolean(), - secrets: s.boolean(), - fresh: s.boolean(), - }), + s + .object({ + config: s.boolean(), + secrets: s.boolean(), + fresh: s.boolean(), + }) + .partial(), ), async (c) => { const module = c.req.param("module") as ModuleKey | undefined; diff --git a/app/src/ui/client/schema/data/use-bknd-data.ts b/app/src/ui/client/schema/data/use-bknd-data.ts index d9c4ca0a..723944ef 100644 --- a/app/src/ui/client/schema/data/use-bknd-data.ts +++ b/app/src/ui/client/schema/data/use-bknd-data.ts @@ -1,4 +1,4 @@ -import { TypeInvalidError, parse, transformObject } from "core/utils"; +import { transformObject } from "core/utils"; import { constructEntity } from "data"; import { type TAppDataEntity, @@ -13,8 +13,7 @@ import { import { useBknd } from "ui/client/bknd"; import type { TSchemaActions } from "ui/client/schema/actions"; import { bkndModals } from "ui/modals"; -import * as tb from "@sinclair/typebox"; -const { Type } = tb; +import { s, parse, InvalidSchemaError } from "core/object/schema"; export function useBkndData() { const { config, app, schema, actions: bkndActions } = useBknd(); @@ -27,12 +26,10 @@ export function useBkndData() { const actions = { entity: { add: async (name: string, data: TAppDataEntity) => { - console.log("create entity", { data }); const validated = parse(entitiesSchema, data, { skipMark: true, forceParse: true, }); - console.log("validated", validated); // @todo: check for existing? return await bkndActions.add("data", `entities.${name}`, validated); }, @@ -44,7 +41,6 @@ export function useBkndData() { return { config: async (partial: Partial): Promise => { - console.log("patch config", entityName, partial); return await bkndActions.overwrite( "data", `entities.${entityName}.config`, @@ -57,13 +53,11 @@ export function useBkndData() { }, relations: { add: async (relation: TAppDataRelation) => { - console.log("create relation", { relation }); const name = crypto.randomUUID(); - const validated = parse(Type.Union(relationsSchema), relation, { + const validated = parse(s.anyOf(relationsSchema), relation, { skipMark: true, forceParse: true, }); - console.log("validated", validated); return await bkndActions.add("data", `relations.${name}`, validated); }, }, @@ -120,17 +114,14 @@ const modals = { function entityFieldActions(bkndActions: TSchemaActions, entityName: string) { return { add: async (name: string, field: TAppDataField) => { - console.log("create field", { name, field }); const validated = parse(fieldsSchema, field, { skipMark: true, forceParse: true, }); - console.log("validated", validated); return await bkndActions.add("data", `entities.${entityName}.fields.${name}`, validated); }, patch: () => null, set: async (fields: TAppDataEntityFields) => { - console.log("set fields", entityName, fields); try { const validated = parse(entityFields, fields, { skipMark: true, @@ -141,11 +132,9 @@ function entityFieldActions(bkndActions: TSchemaActions, entityName: string) { `entities.${entityName}.fields`, validated, ); - console.log("res", res); - //bkndActions.set("data", "entities", fields); } catch (e) { console.error("error", e); - if (e instanceof TypeInvalidError) { + if (e instanceof InvalidSchemaError) { alert("Error updating fields: " + e.firstToString()); } else { alert("An error occured, check console. There will be nice error handling soon."); diff --git a/app/src/ui/client/schema/flows/use-flows.ts b/app/src/ui/client/schema/flows/use-flows.ts index 0b8e2882..478112ca 100644 --- a/app/src/ui/client/schema/flows/use-flows.ts +++ b/app/src/ui/client/schema/flows/use-flows.ts @@ -1,4 +1,4 @@ -import { type Static, parse } from "core/utils"; +import { parse } from "core/object/schema"; import { type TAppFlowSchema, flowSchema } from "flows/flows-schema"; import { useBknd } from "../../BkndProvider"; @@ -8,11 +8,8 @@ export function useFlows() { const actions = { flow: { create: async (name: string, data: TAppFlowSchema) => { - console.log("would create", name, data); const parsed = parse(flowSchema, data, { skipMark: true, forceParse: true }); - console.log("parsed", parsed); const res = await bkndActions.add("flows", `flows.${name}`, parsed); - console.log("res", res); }, }, }; diff --git a/app/src/ui/components/form/json-schema/typebox/RJSFTypeboxValidator.ts b/app/src/ui/components/form/json-schema/typebox/RJSFTypeboxValidator.ts index 5e397b61..d76f020b 100644 --- a/app/src/ui/components/form/json-schema/typebox/RJSFTypeboxValidator.ts +++ b/app/src/ui/components/form/json-schema/typebox/RJSFTypeboxValidator.ts @@ -1,4 +1,5 @@ -import { Check, Errors } from "core/utils"; +import { Check } from "@sinclair/typebox/value"; +import { Errors } from "@sinclair/typebox/errors"; import { FromSchema } from "./from-schema"; import type { diff --git a/app/src/ui/elements/auth/AuthForm.tsx b/app/src/ui/elements/auth/AuthForm.tsx index aeae50b0..52728df3 100644 --- a/app/src/ui/elements/auth/AuthForm.tsx +++ b/app/src/ui/elements/auth/AuthForm.tsx @@ -6,15 +6,13 @@ import type { ComponentPropsWithoutRef } from "react"; import { Button } from "ui/components/buttons/Button"; import { Group, Input, Password, Label } from "ui/components/form/Formy/components"; import { SocialLink } from "./SocialLink"; -import type { ValueError } from "@sinclair/typebox/value"; -import { type TSchema, Value } from "core/utils"; import type { Validator } from "json-schema-form-react"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; +import type { ErrorDetail } from "jsonv-ts"; -class TypeboxValidator implements Validator { - async validate(schema: TSchema, data: any) { - return Value.Check(schema, data) ? [] : [...Value.Errors(schema, data)]; +class JsonvTsValidator implements Validator { + async validate(schema: s.Schema, data: any) { + return schema.validate(data).errors; } } @@ -27,12 +25,12 @@ export type LoginFormProps = Omit, "onSubmit" | buttonLabel?: string; }; -const validator = new TypeboxValidator(); -const schema = Type.Object({ - email: Type.String({ +const validator = new JsonvTsValidator(); +const schema = s.strictObject({ + email: s.string({ pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$", }), - password: Type.String({ + password: s.string({ minLength: 8, // @todo: this should be configurable }), }); diff --git a/app/src/ui/hooks/use-search.ts b/app/src/ui/hooks/use-search.ts index 012465d1..64d162a8 100644 --- a/app/src/ui/hooks/use-search.ts +++ b/app/src/ui/hooks/use-search.ts @@ -4,12 +4,12 @@ import { useLocation, useSearch as useWouterSearch } from "wouter"; import { type s, parse } from "core/object/schema"; import { useEffect, useMemo, useState } from "react"; -export type UseSearchOptions = { +export type UseSearchOptions = { defaultValue?: Partial>; beforeEncode?: (search: Partial>) => object; }; -export function useSearch( +export function useSearch( schema: Schema, options?: UseSearchOptions, ) { diff --git a/app/src/ui/modules/data/components/schema/create-modal/CreateModal.tsx b/app/src/ui/modules/data/components/schema/create-modal/CreateModal.tsx index f011f959..d7ca1839 100644 --- a/app/src/ui/modules/data/components/schema/create-modal/CreateModal.tsx +++ b/app/src/ui/modules/data/components/schema/create-modal/CreateModal.tsx @@ -1,6 +1,5 @@ import type { ModalProps } from "@mantine/core"; import type { ContextModalProps } from "@mantine/modals"; -import { type Static, StringEnum, StringIdentifier } from "core/utils"; import { entitiesSchema, fieldsSchema, relationsSchema } from "data/data-schema"; import { useState } from "react"; import { type Modal2Ref, ModalBody, ModalFooter, ModalTitle } from "ui/components/modal/Modal2"; @@ -11,58 +10,51 @@ import { StepEntityFields } from "./step.entity.fields"; import { StepRelation } from "./step.relation"; import { StepSelect } from "./step.select"; import Templates from "./templates/register"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; export type CreateModalRef = Modal2Ref; export const ModalActions = ["entity", "relation", "media"] as const; -export const entitySchema = Type.Composite([ - Type.Object({ - name: StringIdentifier, - }), - entitiesSchema, -]); - -const schemaAction = Type.Union([ - StringEnum(["entity", "relation", "media"]), - Type.String({ pattern: "^template-" }), -]); -export type TSchemaAction = Static; - -const createFieldSchema = Type.Object({ - entity: StringIdentifier, - name: StringIdentifier, - field: Type.Array(fieldsSchema), +export const entitySchema = s.object({ + name: s.string(), + ...entitiesSchema.properties, }); -export type TFieldCreate = Static; -const createModalSchema = Type.Object( - { - action: schemaAction, - initial: Type.Optional(Type.Any()), - entities: Type.Optional( - Type.Object({ - create: Type.Optional(Type.Array(entitySchema)), - }), - ), - relations: Type.Optional( - Type.Object({ - create: Type.Optional(Type.Array(Type.Union(relationsSchema))), - }), - ), - fields: Type.Optional( - Type.Object({ - create: Type.Optional(Type.Array(createFieldSchema)), - }), - ), - }, - { - additionalProperties: false, - }, -); -export type TCreateModalSchema = Static; +// @todo: this union is not fully working, just "string" +const schemaAction = s.anyOf([ + s.string({ enum: ["entity", "relation", "media"] }), + s.string({ pattern: "^template-" }), +]); +export type TSchemaAction = s.Static; + +const createFieldSchema = s.object({ + entity: s.string(), + name: s.string(), + field: s.array(fieldsSchema), +}); +export type TFieldCreate = s.Static; + +const createModalSchema = s.strictObject({ + action: schemaAction, + initial: s.any().optional(), + entities: s + .object({ + create: s.array(entitySchema).optional(), + }) + .optional(), + relations: s + .object({ + create: s.array(s.anyOf(relationsSchema)).optional(), + }) + .optional(), + fields: s + .object({ + create: s.array(createFieldSchema).optional(), + }) + .optional(), +}); +export type TCreateModalSchema = s.Static; export function CreateModal({ context, @@ -70,7 +62,6 @@ export function CreateModal({ innerProps: { initialPath = [], initialState }, }: ContextModalProps<{ initialPath?: string[]; initialState?: TCreateModalSchema }>) { const [path, setPath] = useState(initialPath); - console.log("...", initialPath, initialState); function close() { context.closeModal(id); 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 141c9ec8..874d9d17 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 @@ -1,5 +1,5 @@ -import { typeboxResolver } from "@hookform/resolvers/typebox"; -import { type Static, objectCleanEmpty } from "core/utils"; +//import { typeboxResolver } from "@hookform/resolvers/typebox"; +import { objectCleanEmpty } from "core/utils"; import { type TAppDataEntityFields, entitiesSchema } from "data/data-schema"; import { mergeWith } from "lodash-es"; import { useRef } from "react"; @@ -12,9 +12,10 @@ import { } from "ui/routes/data/forms/entity.fields.form"; 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"; const schema = entitiesSchema; -type Schema = Static; +type Schema = s.Static; export function StepEntityFields() { const { nextStep, stepBack, state, setState } = useStepContext(); @@ -40,7 +41,8 @@ export function StepEntityFields() { setValue, } = useForm({ mode: "onTouched", - resolver: typeboxResolver(schema), + // @todo: add resolver + //resolver: typeboxResolver(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 253fc4f2..782d03ae 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,4 +1,4 @@ -import { typeboxResolver } from "@hookform/resolvers/typebox"; +//import { typeboxResolver } from "@hookform/resolvers/typebox"; import { TextInput, Textarea } from "@mantine/core"; import { useFocusTrap } from "@mantine/hooks"; @@ -10,7 +10,6 @@ import { entitySchema, useStepContext, } from "./CreateModal"; -import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect"; export function StepEntity() { const focusTrapRef = useFocusTrap(); @@ -18,7 +17,8 @@ export function StepEntity() { const { nextStep, stepBack, state, setState } = useStepContext(); const { register, handleSubmit, formState, watch, control } = useForm({ mode: "onTouched", - resolver: typeboxResolver(entitySchema), + // @todo: add resolver + //resolver: typeboxResolver(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 d7868a5c..92fa01ee 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 @@ -1,8 +1,5 @@ -import { typeboxResolver } from "@hookform/resolvers/typebox"; import { Switch, TextInput } from "@mantine/core"; -import { TypeRegistry } from "@sinclair/typebox"; import { IconDatabase } from "@tabler/icons-react"; -import { type Static, StringEnum, StringIdentifier, registerCustomTypeboxKinds } from "core/utils"; import { ManyToOneRelation, type RelationType, RelationTypes } from "data"; import type { ReactNode } from "react"; import { type Control, type FieldValues, type UseFormRegister, useForm } from "react-hook-form"; @@ -14,11 +11,7 @@ import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelec import { useStepContext } from "ui/components/steps/Steps"; import { useEvent } from "ui/hooks/use-event"; import { ModalBody, ModalFooter, type TCreateModalSchema } from "./CreateModal"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; - -// @todo: check if this could become an issue -registerCustomTypeboxKinds(TypeRegistry); +import { s, stringIdentifier } from "core/object/schema"; const Relations: { type: RelationType; @@ -47,11 +40,11 @@ const Relations: { }, ]; -const schema = Type.Object({ - type: StringEnum(Relations.map((r) => r.type)), - source: StringIdentifier, - target: StringIdentifier, - config: Type.Object({}), +const schema = s.strictObject({ + type: s.string({ enum: Relations.map((r) => r.type) }), + source: stringIdentifier, + target: stringIdentifier, + config: s.object({}), }); type ComponentCtx = { @@ -73,8 +66,9 @@ export function StepRelation() { watch, control, } = useForm({ - resolver: typeboxResolver(schema), - defaultValues: (state.relations?.create?.[0] ?? {}) as Static, + // @todo: implement resolver + //resolver: typeboxResolver(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 e1c99dd6..7d18f76c 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 @@ -1,6 +1,5 @@ -import { typeboxResolver } from "@hookform/resolvers/typebox"; import { Radio, TextInput } from "@mantine/core"; -import { Default, type Static, StringEnum, StringIdentifier, transformObject } from "core/utils"; +import { transformObject } from "core/utils"; import type { MediaFieldConfig } from "media/MediaField"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; @@ -15,16 +14,15 @@ import { type TFieldCreate, useStepContext, } from "../../CreateModal"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s, stringIdentifier } from "core/object/schema"; -const schema = Type.Object({ - entity: StringIdentifier, - cardinality_type: StringEnum(["single", "multiple"], { default: "multiple" }), - cardinality: Type.Optional(Type.Number({ minimum: 1 })), - name: StringIdentifier, +const schema = s.object({ + entity: stringIdentifier, + cardinality_type: s.string({ enum: ["single", "multiple"], default: "multiple" }), + cardinality: s.number({ minimum: 1 }).optional(), + name: stringIdentifier, }); -type TCreateModalMediaSchema = Static; +type TCreateModalMediaSchema = s.Static; export function TemplateMediaComponent() { const { stepBack, setState, state, path, nextStep } = useStepContext(); @@ -36,8 +34,10 @@ export function TemplateMediaComponent() { control, } = useForm({ mode: "onChange", - resolver: typeboxResolver(schema), - defaultValues: Default(schema, state.initial ?? {}) as TCreateModalMediaSchema, + // @todo: add resolver + //resolver: typeboxResolver(schema), + defaultValues: schema.template(state.initial ?? {}) as TCreateModalMediaSchema, + //defaultValues: Default(schema, state.initial ?? {}) as TCreateModalMediaSchema, }); const [forbidden, setForbidden] = useState(false); diff --git a/app/src/ui/modules/flows/components/TriggerComponent.tsx b/app/src/ui/modules/flows/components/TriggerComponent.tsx index c0207a65..058b1499 100644 --- a/app/src/ui/modules/flows/components/TriggerComponent.tsx +++ b/app/src/ui/modules/flows/components/TriggerComponent.tsx @@ -1,11 +1,10 @@ import { Handle, type Node, type NodeProps, Position } from "@xyflow/react"; -import { Const, transformObject } from "core/utils"; +import { transformObject } from "core/utils"; import { type Trigger, TriggerMap } from "flows"; import type { IconType } from "react-icons"; import { TbCircleLetterT } from "react-icons/tb"; import { JsonSchemaForm } from "ui/components/form/json-schema"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; export type TaskComponentProps = NodeProps> & { Icon?: IconType; @@ -14,9 +13,9 @@ export type TaskComponentProps = NodeProps> & { const triggerSchemas = Object.values( transformObject(TriggerMap, (trigger, name) => - Type.Object( + s.object( { - type: Const(name), + type: s.literal(name), config: trigger.cls.schema, }, { title: String(name), additionalProperties: false }, @@ -47,7 +46,7 @@ export function TriggerComponent({
; +type TFetchTaskSchema = s.Static; type FetchTaskFormProps = NodeProps> & { params: TFetchTaskSchema; onChange: (params: any) => void; @@ -42,8 +37,9 @@ export function FetchTaskForm({ onChange, params, ...props }: FetchTaskFormProps watch, control, } = useForm({ - resolver: typeboxResolver(schema), - defaultValues: params as Static, + // @todo: add resolver + //resolver: typeboxResolver(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/tasks/TaskNode.tsx b/app/src/ui/modules/flows/components2/nodes/tasks/TaskNode.tsx index e0f0d387..0874203f 100644 --- a/app/src/ui/modules/flows/components2/nodes/tasks/TaskNode.tsx +++ b/app/src/ui/modules/flows/components2/nodes/tasks/TaskNode.tsx @@ -1,14 +1,10 @@ -import { TypeRegistry } from "@sinclair/typebox"; import { type Node, type NodeProps, Position } from "@xyflow/react"; -import { registerCustomTypeboxKinds } from "core/utils"; import type { TAppFlowTaskSchema } from "flows/AppFlows"; import { useFlowCanvas, useFlowSelector } from "../../../hooks/use-flow"; import { Handle } from "../Handle"; import { FetchTaskForm } from "./FetchTaskNode"; import { RenderNode } from "./RenderNode"; -registerCustomTypeboxKinds(TypeRegistry); - const TaskComponents = { fetch: FetchTaskForm, render: RenderNode, 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 718536f2..93cb1c4d 100644 --- a/app/src/ui/modules/flows/components2/nodes/triggers/TriggerNode.tsx +++ b/app/src/ui/modules/flows/components2/nodes/triggers/TriggerNode.tsx @@ -1,7 +1,6 @@ -import { typeboxResolver } from "@hookform/resolvers/typebox"; import { TextInput } from "@mantine/core"; import type { Node, NodeProps } from "@xyflow/react"; -import { Const, type Static, registerCustomTypeboxKinds, transformObject } from "core/utils"; +import { transformObject } from "core/utils"; import { TriggerMap } from "flows"; import type { TAppFlowTriggerSchema } from "flows/AppFlows"; import { useForm } from "react-hook-form"; @@ -11,22 +10,18 @@ import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelec import { useFlowCanvas, useFlowSelector } from "../../../hooks/use-flow"; import { BaseNode } from "../BaseNode"; import { Handle } from "../Handle"; -import * as tb from "@sinclair/typebox"; -const { Type, TypeRegistry } = tb; +import { s } from "core/object/schema"; -// @todo: check if this could become an issue -registerCustomTypeboxKinds(TypeRegistry); - -const schema = Type.Object({ - trigger: Type.Union( +const schema = s.object({ + trigger: s.anyOf( Object.values( transformObject(TriggerMap, (trigger, name) => - Type.Object( + s.strictObject( { - type: Const(name), + type: s.literal(name), config: trigger.cls.schema, }, - { title: String(name), additionalProperties: false }, + { title: String(name) }, ), ), ), @@ -50,13 +45,14 @@ export const TriggerNode = (props: NodeProps, + // @todo: add resolver + //resolver: typeboxResolver(schema), + defaultValues: { trigger: state } as s.Static, mode: "onChange", }); const data = watch("trigger"); - async function onSubmit(data: Static) { + async function onSubmit(data: s.Static) { console.log("submit", data.trigger); // @ts-ignore await actions.trigger.update(data.trigger); diff --git a/app/src/ui/modules/flows/hooks/use-flow/index.tsx b/app/src/ui/modules/flows/hooks/use-flow/index.tsx index 2d311d90..77cc5015 100644 --- a/app/src/ui/modules/flows/hooks/use-flow/index.tsx +++ b/app/src/ui/modules/flows/hooks/use-flow/index.tsx @@ -46,7 +46,7 @@ export const flowStateAtom = atom({ const FlowCanvasContext = createContext(undefined!); -const DEFAULT_FLOW = { trigger: {}, tasks: {}, connections: {} }; +const DEFAULT_FLOW: TAppFlowSchema = { trigger: { type: "manual" }, tasks: {}, connections: {} }; export function FlowCanvasProvider({ children, name }: { children: any; name?: string }) { //const [dirty, setDirty] = useState(false); const setFlowState = useSetAtom(flowStateAtom); @@ -71,7 +71,7 @@ export function FlowCanvasProvider({ children, name }: { children: any; name?: s update: async (trigger: TAppFlowTriggerSchema | any) => { console.log("update trigger", trigger); setFlowState((state) => { - const flow = state.flow || DEFAULT_FLOW; + const flow = state.flow || (DEFAULT_FLOW as any); return { ...state, dirty: true, flow: { ...flow, trigger } }; }); //return s.actions.patch("flows", `flows.flows.${name}`, { trigger }); diff --git a/app/src/ui/routes/auth/auth.roles.tsx b/app/src/ui/routes/auth/auth.roles.tsx index e7b7f4a0..004e76ca 100644 --- a/app/src/ui/routes/auth/auth.roles.tsx +++ b/app/src/ui/routes/auth/auth.roles.tsx @@ -1,4 +1,4 @@ -import { StringIdentifier, transformObject, ucFirstAllSnakeToPascalWithSpaces } from "core/utils"; +import { transformObject, ucFirstAllSnakeToPascalWithSpaces } from "core/utils"; import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth"; import { Alert } from "ui/components/display/Alert"; import { bkndModals } from "ui/modals"; @@ -6,6 +6,7 @@ import { Button } from "../../components/buttons/Button"; import { CellValue, DataTable } from "../../components/table/DataTable"; import * as AppShell from "../../layouts/AppShell/AppShell"; import { routes, useNavigate } from "../../lib/routes"; +import { stringIdentifier } from "core/object/schema"; export function AuthRolesList() { const [navigate] = useNavigate(); @@ -31,7 +32,7 @@ export function AuthRolesList() { schema: { type: "object", properties: { - name: StringIdentifier, + name: stringIdentifier, }, required: ["name"], }, diff --git a/app/src/ui/routes/auth/auth.strategies.tsx b/app/src/ui/routes/auth/auth.strategies.tsx index 8fbc3b7c..2aa0c4f9 100644 --- a/app/src/ui/routes/auth/auth.strategies.tsx +++ b/app/src/ui/routes/auth/auth.strategies.tsx @@ -64,8 +64,7 @@ function AuthStrategiesListInternal() { const config = $auth.config.strategies; const schema = $auth.schema.properties.strategies; const schemas = Object.fromEntries( - // @ts-ignore - $auth.schema.properties.strategies.additionalProperties.anyOf.map((s) => [ + $auth.schema.properties.strategies?.additionalProperties?.anyOf.map((s) => [ s.properties.type.const, s, ]), @@ -76,7 +75,12 @@ function AuthStrategiesListInternal() { } return ( -
+ ({ dirty: state.dirty, diff --git a/app/src/ui/routes/auth/forms/role.form.tsx b/app/src/ui/routes/auth/forms/role.form.tsx index cf5d4105..294c531f 100644 --- a/app/src/ui/routes/auth/forms/role.form.tsx +++ b/app/src/ui/routes/auth/forms/role.form.tsx @@ -1,15 +1,16 @@ -import { typeboxResolver } from "@hookform/resolvers/typebox"; +//import { typeboxResolver } from "@hookform/resolvers/typebox"; import { Input, Switch, Tooltip } from "@mantine/core"; import { guardRoleSchema } from "auth/auth-schema"; -import { type Static, ucFirst } from "core/utils"; +import { ucFirst } from "core/utils"; import { forwardRef, useImperativeHandle } from "react"; import { type UseControllerProps, useController, useForm } from "react-hook-form"; 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"; const schema = guardRoleSchema; -type Role = Static; +type Role = s.Static; export type AuthRoleFormRef = { getData: () => Role; @@ -33,7 +34,8 @@ export const AuthRoleForm = forwardRef< reset, getValues, } = useForm({ - resolver: typeboxResolver(schema), + // @todo: add resolver + //resolver: typeboxResolver(schema), defaultValues: role, }); @@ -87,7 +89,7 @@ const Permissions = ({ const { field: { value, onChange: fieldOnChange, ...field }, fieldState, - } = useController, "permissions">({ + } = useController, "permissions">({ name: "permissions", control, }); 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 a70b787e..e6723b84 100644 --- a/app/src/ui/routes/data/forms/entity.fields.form.tsx +++ b/app/src/ui/routes/data/forms/entity.fields.form.tsx @@ -1,13 +1,5 @@ -import { typeboxResolver } from "@hookform/resolvers/typebox"; import { Tabs, TextInput, Textarea, Tooltip, Switch } from "@mantine/core"; -import { useDisclosure } from "@mantine/hooks"; -import { - Default, - type Static, - StringIdentifier, - objectCleanEmpty, - ucFirstAllSnakeToPascalWithSpaces, -} from "core/utils"; +import { objectCleanEmpty, omitKeys, ucFirstAllSnakeToPascalWithSpaces } from "core/utils"; import { type TAppDataEntityFields, fieldsSchemaObject as originalFieldsSchemaObject, @@ -26,31 +18,25 @@ import { type SortableItemProps, SortableList } from "ui/components/list/Sortabl import { Popover } from "ui/components/overlay/Popover"; import { type TFieldSpec, fieldSpecs } from "ui/modules/data/components/fields-specs"; import { dataFieldsUiSchema } from "../../settings/routes/data.settings"; -import * as tbbox from "@sinclair/typebox"; 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"; -const { Type } = tbbox; +import { s, stringIdentifier } from "core/object/schema"; const fieldsSchemaObject = originalFieldsSchemaObject; -const fieldsSchema = Type.Union(Object.values(fieldsSchemaObject)); +const fieldsSchema = s.anyOf(Object.values(fieldsSchemaObject)); -const fieldSchema = Type.Object( - { - name: StringIdentifier, - new: Type.Optional(Type.Boolean({ const: true })), - field: fieldsSchema, - }, - { - additionalProperties: false, - }, -); -type TFieldSchema = Static; - -const schema = Type.Object({ - fields: Type.Array(fieldSchema), +const fieldSchema = s.strictObject({ + name: stringIdentifier, + new: s.boolean({ const: true }).optional(), + field: fieldsSchema, }); -type TFieldsFormSchema = Static; +type TFieldSchema = s.Static; + +const schema = s.strictObject({ + fields: s.array(fieldSchema), +}); +type TFieldsFormSchema = s.Static; const fieldTypes = Object.keys(fieldsSchemaObject); const defaultType = fieldTypes[0]; @@ -58,7 +44,9 @@ const commonProps = ["label", "description", "required", "fillable", "hidden", " function specificFieldSchema(type: keyof typeof fieldsSchemaObject) { //console.log("specificFieldSchema", type); - return Type.Omit(fieldsSchemaObject[type]?.properties.config, commonProps); + return s.object( + omitKeys(fieldsSchemaObject[type]?.properties.config.properties, commonProps as any), + ); } export type EntityFieldsFormProps = { @@ -100,7 +88,8 @@ export const EntityFieldsForm = forwardRef(function FlowCreateModal(props, ref) { @@ -61,16 +55,17 @@ export function StepCreate() { register, formState: { isValid, errors }, } = useForm({ - resolver: typeboxResolver(schema), + // @todo: implement resolver + //resolver: typeboxResolver(schema), defaultValues: { name: "", trigger: "manual", mode: "async", - } as Static, + } as s.Static, mode: "onSubmit", }); - async function onSubmit(data: Static) { + async function onSubmit(data: s.Static) { console.log(data, isValid); actions.flow.create(data.name, { trigger: { diff --git a/app/src/ui/routes/settings/components/Setting.tsx b/app/src/ui/routes/settings/components/Setting.tsx index 1ad6e6d6..d3078f82 100644 --- a/app/src/ui/routes/settings/components/Setting.tsx +++ b/app/src/ui/routes/settings/components/Setting.tsx @@ -1,5 +1,5 @@ import { useHotkeys } from "@mantine/hooks"; -import { type TObject, ucFirst } from "core/utils"; +import { ucFirst } from "core/utils"; import { omit } from "lodash-es"; import { type ReactNode, useMemo, useRef, useState } from "react"; import { TbSettings } from "react-icons/tb"; @@ -18,10 +18,11 @@ import { Link, Route, useLocation } from "wouter"; import { extractSchema } from "../utils/schema"; import { SettingNewModal, type SettingsNewModalProps } from "./SettingNewModal"; import { SettingSchemaModal, type SettingsSchemaModalRef } from "./SettingSchemaModal"; +import type { s } from "core/object/schema"; export type SettingProps< - Schema extends TObject = TObject, - Props = Schema extends TObject ? TProperties : any, + Schema extends s.ObjectSchema = s.ObjectSchema, + Props = Schema extends s.ObjectSchema ? TProperties : any, > = { schema: Schema; config: any; @@ -44,7 +45,7 @@ export type SettingProps< }; }; -export function Setting({ +export function Setting({ schema, uiSchema, config, diff --git a/app/src/ui/routes/settings/components/SettingNewModal.tsx b/app/src/ui/routes/settings/components/SettingNewModal.tsx index 5e8fff51..2d4e6f83 100644 --- a/app/src/ui/routes/settings/components/SettingNewModal.tsx +++ b/app/src/ui/routes/settings/components/SettingNewModal.tsx @@ -1,8 +1,6 @@ import { useDisclosure, useFocusTrap } from "@mantine/hooks"; -import type { TObject } from "core/utils"; import { omit } from "lodash-es"; import { useRef, useState } from "react"; -import { TbCirclePlus, TbVariable } from "react-icons/tb"; import { useBknd } from "ui/client/BkndProvider"; import { Button } from "ui/components/buttons/Button"; import * as Formy from "ui/components/form/Formy"; @@ -10,9 +8,10 @@ import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json- import { Dropdown } from "ui/components/overlay/Dropdown"; import { Modal } from "ui/components/overlay/Modal"; import { useLocation } from "wouter"; +import type { s } from "core/object/schema"; export type SettingsNewModalProps = { - schema: TObject; + schema: s.ObjectSchema; uiSchema?: object; anyOfValues?: Record; path: string[]; diff --git a/app/src/ui/routes/settings/utils/schema.ts b/app/src/ui/routes/settings/utils/schema.ts index 852f852a..61508de9 100644 --- a/app/src/ui/routes/settings/utils/schema.ts +++ b/app/src/ui/routes/settings/utils/schema.ts @@ -1,11 +1,11 @@ -import type { Static, TObject } from "core/utils"; import type { JSONSchema7 } from "json-schema"; import { cloneDeep, omit, pick } from "lodash-es"; +import type { s } from "core/object/schema"; export function extractSchema< - Schema extends TObject, + Schema extends s.ObjectSchema, Keys extends keyof Schema["properties"], - Config extends Static, + Config extends s.Static, >( schema: Schema, config: Config, @@ -22,12 +22,12 @@ export function extractSchema< }, ] { if (!schema.properties) { - return [{ ...schema }, config, {} as any]; + return [{ ...schema.toJSON() }, config, {} as any]; } const newSchema = cloneDeep(schema); const updated = { - ...newSchema, + ...newSchema.toJSON(), properties: omit(newSchema.properties, keys), }; if (updated.required) { diff --git a/app/src/ui/routes/test/tests/flow-create-schema-test.tsx b/app/src/ui/routes/test/tests/flow-create-schema-test.tsx index 2301240c..a05b8f78 100644 --- a/app/src/ui/routes/test/tests/flow-create-schema-test.tsx +++ b/app/src/ui/routes/test/tests/flow-create-schema-test.tsx @@ -1,9 +1,9 @@ -import { parse } from "core/utils"; import { AppFlows } from "flows/AppFlows"; import { useState } from "react"; import { JsonViewer } from "../../../components/code/JsonViewer"; import { JsonSchemaForm } from "../../../components/form/json-schema"; import { Scrollable } from "../../../layouts/AppShell/AppShell"; +import { parse } from "core/object/schema"; export default function FlowCreateSchemaTest() { //const schema = flowsConfigSchema; diff --git a/app/src/ui/routes/test/tests/json-schema-form3.tsx b/app/src/ui/routes/test/tests/json-schema-form3.tsx index 885c903e..be2bfb03 100644 --- a/app/src/ui/routes/test/tests/json-schema-form3.tsx +++ b/app/src/ui/routes/test/tests/json-schema-form3.tsx @@ -73,7 +73,7 @@ export default function JsonSchemaForm3() { return (
- + {/* console.log("change", data)} diff --git a/app/tsconfig.json b/app/tsconfig.json index 967533f5..a40d88a7 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { - "types": ["bun-types"], "composite": false, "incremental": true, "module": "ESNext", @@ -32,6 +31,7 @@ "paths": { "*": ["./src/*"], "bknd": ["./src/index.ts"], + "bknd/utils": ["./src/core/utils/index.ts"], "bknd/core": ["./src/core/index.ts"], "bknd/adapter": ["./src/adapter/index.ts"], "bknd/client": ["./src/ui/client/index.ts"], diff --git a/bun.lock b/bun.lock index 7a373522..5ebd1606 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,7 @@ }, "app": { "name": "bknd", - "version": "0.15.0-rc.2", + "version": "0.15.0-rc.3", "bin": "./dist/cli/index.js", "dependencies": { "@cfworker/json-schema": "^4.1.1", @@ -43,7 +43,6 @@ "object-path-immutable": "^4.1.2", "radix-ui": "^1.1.3", "swr": "^2.3.3", - "uuid": "^11.1.0", }, "devDependencies": { "@aws-sdk/client-s3": "^3.758.0", @@ -75,7 +74,7 @@ "dotenv": "^16.4.7", "jotai": "^2.12.2", "jsdom": "^26.0.0", - "jsonv-ts": "^0.1.0", + "jsonv-ts": "link:jsonv-ts", "kysely-d1": "^0.3.0", "kysely-generic-sqlite": "^1.2.1", "libsql-stateless-easy": "^1.8.0", @@ -98,6 +97,7 @@ "tsc-alias": "^1.8.11", "tsup": "^8.4.0", "tsx": "^4.19.3", + "uuid": "^11.1.0", "vite": "^6.3.5", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.0.9", @@ -1222,7 +1222,7 @@ "@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="], - "@types/bun": ["@types/bun@1.2.16", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="], + "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], @@ -2504,7 +2504,7 @@ "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], - "jsonv-ts": ["jsonv-ts@0.1.0", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-wJ+79o49MNie2Xk9w1hPN8ozjqemVWXOfWUTdioLui/SeGDC7C+QKXTDxsmUaIay86lorkjb3CCGo6JDKbyTZQ=="], + "jsonv-ts": ["jsonv-ts@link:jsonv-ts", {}], "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=="], @@ -4020,7 +4020,7 @@ "@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], - "@types/bun/bun-types": ["bun-types@1.2.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="], + "@types/bun/bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], "@types/pg/pg-types": ["pg-types@4.0.2", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng=="],