initial refactor

This commit is contained in:
dswbx
2025-06-21 13:35:58 +02:00
parent b2086c4da7
commit 42edce904f
93 changed files with 1021 additions and 1216 deletions
+35
View File
@@ -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"],
},
});
}
});
});
+2 -2
View File
@@ -4,7 +4,6 @@ import { Hono } from "hono";
import { Guard } from "../../src/auth"; import { Guard } from "../../src/auth";
import { DebugLogger } from "../../src/core"; import { DebugLogger } from "../../src/core";
import { EventManager } from "../../src/core/events"; import { EventManager } from "../../src/core/events";
import { Default, stripMark } from "../../src/core/utils";
import { EntityManager } from "../../src/data"; import { EntityManager } from "../../src/data";
import { Module, type ModuleBuildContext } from "../../src/modules/Module"; import { Module, type ModuleBuildContext } from "../../src/modules/Module";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
@@ -45,7 +44,8 @@ export function moduleTestSuite(module: { new (): Module }) {
it("uses the default config", async () => { it("uses the default config", async () => {
const m = new module(); const m = new module();
await m.setContext(ctx).build(); 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(), {}));
}); });
}); });
} }
+1 -1
View File
@@ -226,7 +226,7 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
}, },
external: [ external: [
/^cloudflare*/, /^cloudflare*/,
/^@?(hono|libsql).*?/, /^@?hono.*?/,
/^(bknd|react|next|node).*?/, /^(bknd|react|next|node).*?/,
/.*\.(html)$/, /.*\.(html)$/,
...external, ...external,
+1 -1
View File
@@ -102,7 +102,7 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"jotai": "^2.12.2", "jotai": "^2.12.2",
"jsdom": "^26.0.0", "jsdom": "^26.0.0",
"jsonv-ts": "^0.1.0", "jsonv-ts": "link:jsonv-ts",
"kysely-d1": "^0.3.0", "kysely-d1": "^0.3.0",
"kysely-generic-sqlite": "^1.2.1", "kysely-generic-sqlite": "^1.2.1",
"libsql-stateless-easy": "^1.8.0", "libsql-stateless-easy": "^1.8.0",
@@ -1,15 +1,13 @@
import { registries } from "bknd"; import { registries } from "bknd";
import { isDebug } from "bknd/core"; import { isDebug } from "bknd/core";
import { StringEnum } from "bknd/utils";
import { guessMimeType as guess, StorageAdapter, type FileBody } from "bknd/media"; import { guessMimeType as guess, StorageAdapter, type FileBody } from "bknd/media";
import { getBindings } from "../bindings"; import { getBindings } from "../bindings";
import * as tb from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tb;
export function makeSchema(bindings: string[] = []) { 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" }, { title: "R2", description: "Cloudflare R2 storage" },
); );
@@ -1,17 +1,16 @@
import { readFile, readdir, stat, unlink, writeFile } from "node:fs/promises"; 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 type { FileBody, FileListObject, FileMeta, FileUploadPayload } from "bknd/media";
import { StorageAdapter, guessMimeType as guess } from "bknd/media"; import { StorageAdapter, guessMimeType as guess } from "bknd/media";
import * as tb from "@sinclair/typebox"; import { parse, s } from "core/object/schema";
const { Type } = tb;
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 }, { title: "Local", description: "Local file system storage", additionalProperties: false },
); );
export type LocalAdapterConfig = Static<typeof localAdapterConfig>; export type LocalAdapterConfig = s.Static<typeof localAdapterConfig>;
export class StorageLocalAdapter extends StorageAdapter { export class StorageLocalAdapter extends StorageAdapter {
private config: LocalAdapterConfig; private config: LocalAdapterConfig;
@@ -62,8 +61,7 @@ export class StorageLocalAdapter extends StorageAdapter {
} }
const filePath = `${this.config.path}/${key}`; const filePath = `${this.config.path}/${key}`;
const is_file = isFile(body); await writeFile(filePath, isFile(body) ? body.stream() : body);
await writeFile(filePath, is_file ? body.stream() : body);
return await this.computeEtag(body); return await this.computeEtag(body);
} }
+2 -2
View File
@@ -20,7 +20,7 @@ declare module "core" {
export type CreateUserPayload = { email: string; password: string; [key: string]: any }; export type CreateUserPayload = { email: string; password: string; [key: string]: any };
export class AppAuth extends Module<typeof authConfigSchema> { export class AppAuth extends Module<AppAuthSchema> {
private _authenticator?: Authenticator; private _authenticator?: Authenticator;
cache: Record<string, any> = {}; cache: Record<string, any> = {};
_controller!: AuthController; _controller!: AuthController;
@@ -197,6 +197,6 @@ export class AppAuth extends Module<typeof authConfigSchema> {
enabled: this.isStrategyEnabled(strategy), enabled: this.isStrategyEnabled(strategy),
...strategy.toJSON(secrets), ...strategy.toJSON(secrets),
})), })),
}; } as AppAuthSchema;
} }
} }
+4 -4
View File
@@ -1,9 +1,9 @@
import { type AppAuth, AuthPermissions, type SafeUser, type Strategy } from "auth"; 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 { DataPermissions } from "data";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { Controller, type ServerEnv } from "modules/Controller"; 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 = { export type AuthActionResponse = {
success: boolean; success: boolean;
@@ -58,7 +58,7 @@ export class AuthController extends Controller {
try { try {
const body = await this.auth.authenticator.getBody(c); const body = await this.auth.authenticator.getBody(c);
const valid = parse(create.schema, body, { const valid = parse(create.schema, body, {
skipMark: true, //skipMark: true,
}); });
const processed = (await create.preprocess?.(valid)) ?? valid; const processed = (await create.preprocess?.(valid)) ?? valid;
@@ -78,7 +78,7 @@ export class AuthController extends Controller {
data: created as unknown as SafeUser, data: created as unknown as SafeUser,
} as AuthActionResponse); } as AuthActionResponse);
} catch (e) { } catch (e) {
if (e instanceof TypeInvalidError) { if (e instanceof InvalidSchemaError) {
return c.json( return c.json(
{ {
success: false, success: false,
+45 -36
View File
@@ -1,8 +1,7 @@
import { cookieConfig, jwtConfig } from "auth/authenticate/Authenticator"; import { cookieConfig, jwtConfig } from "auth/authenticate/Authenticator";
import { CustomOAuthStrategy, OAuthStrategy, PasswordStrategy } from "auth/authenticate/strategies"; import { CustomOAuthStrategy, OAuthStrategy, PasswordStrategy } from "auth/authenticate/strategies";
import { type Static, StringRecord, objectTransform } from "core/utils"; import { objectTransform } from "core/utils";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox;
export const Strategies = { export const Strategies = {
password: { password: {
@@ -21,45 +20,55 @@ export const Strategies = {
export const STRATEGIES = Strategies; export const STRATEGIES = Strategies;
const strategiesSchemaObject = objectTransform(STRATEGIES, (strategy, name) => { const strategiesSchemaObject = objectTransform(STRATEGIES, (strategy, name) => {
return Type.Object( return s.strictObject(
{ {
enabled: Type.Optional(Type.Boolean({ default: true })), enabled: s.boolean({ default: true }).optional(),
type: Type.Const(name, { default: name, readOnly: true }), type: s.literal(name),
config: strategy.schema, config: strategy.schema,
}, },
{ {
title: name, title: name,
additionalProperties: false,
}, },
); );
}); });
const strategiesSchema = Type.Union(Object.values(strategiesSchemaObject));
export type AppAuthStrategies = Static<typeof strategiesSchema>;
export type AppAuthOAuthStrategy = Static<typeof STRATEGIES.oauth.schema>;
export type AppAuthCustomOAuthStrategy = Static<typeof STRATEGIES.custom_oauth.schema>;
const guardConfigSchema = Type.Object({ const strategiesSchema = s.anyOf(Object.values(strategiesSchemaObject));
enabled: Type.Optional(Type.Boolean({ default: false })), export type AppAuthStrategies = s.Static<typeof strategiesSchema>;
export type AppAuthOAuthStrategy = s.Static<typeof STRATEGIES.oauth.schema>;
export type AppAuthCustomOAuthStrategy = s.Static<typeof STRATEGIES.custom_oauth.schema>;
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 }), enabled: s.boolean({ default: false }),
basepath: Type.String({ default: "/api/auth" }), basepath: s.string({ default: "/api/auth" }),
entity_name: Type.String({ default: "users" }), entity_name: s.string({ default: "users" }),
allow_register: Type.Optional(Type.Boolean({ default: true })), allow_register: s.boolean({ default: true }).optional(),
jwt: jwtConfig, jwt: jwtConfig,
cookie: cookieConfig, cookie: cookieConfig,
strategies: Type.Optional( strategies: s.record(strategiesSchema, {
StringRecord(strategiesSchema, {
title: "Strategies", title: "Strategies",
default: { default: {
password: { password: {
@@ -71,14 +80,14 @@ export const authConfigSchema = Type.Object(
}, },
}, },
}), }),
), guard: guardConfigSchema.optional(),
guard: Type.Optional(guardConfigSchema), roles: s.record(guardRoleSchema, { default: {} }).optional(),
roles: Type.Optional(StringRecord(guardRoleSchema, { default: {} })),
},
{
title: "Authentication",
additionalProperties: false,
}, },
{ title: "Authentication" },
); );
const b = authConfigSchema.properties.basepath;
// ^?
const c = authConfigSchema.properties.strategies;
// ^?
export type AppAuthSchema = Static<typeof authConfigSchema>; export type AppAuthSchema = s.Static<typeof authConfigSchema>;
+28 -35
View File
@@ -1,31 +1,23 @@
import { $console, type DB, Exception } from "core"; import { $console, type DB, Exception } from "core";
import { addFlashMessage } from "core/server/flash"; import { addFlashMessage } from "core/server/flash";
import { import { runtimeSupports, truncate } from "core/utils";
type Static,
StringEnum,
type TObject,
parse,
runtimeSupports,
truncate,
} from "core/utils";
import type { Context, Hono } from "hono"; import type { Context, Hono } from "hono";
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie"; import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
import { sign, verify } from "hono/jwt"; import { sign, verify } from "hono/jwt";
import type { CookieOptions } from "hono/utils/cookie"; import type { CookieOptions } from "hono/utils/cookie";
import type { ServerEnv } from "modules/Controller"; import type { ServerEnv } from "modules/Controller";
import { pick } from "lodash-es"; import { pick } from "lodash-es";
import * as tbbox from "@sinclair/typebox";
import { InvalidConditionsException } from "auth/errors"; import { InvalidConditionsException } from "auth/errors";
const { Type } = tbbox; import { s, parse } from "core/object/schema";
type Input = any; // workaround type Input = any; // workaround
export type JWTPayload = Parameters<typeof sign>[0]; export type JWTPayload = Parameters<typeof sign>[0];
export const strategyActions = ["create", "change"] as const; export const strategyActions = ["create", "change"] as const;
export type StrategyActionName = (typeof strategyActions)[number]; export type StrategyActionName = (typeof strategyActions)[number];
export type StrategyAction<S extends TObject = TObject> = { export type StrategyAction<S extends s.ObjectSchema = s.ObjectSchema> = {
schema: S; schema: S;
preprocess: (input: Static<S>) => Promise<Omit<DB["users"], "id" | "strategy">>; preprocess: (input: s.Static<S>) => Promise<Omit<DB["users"], "id" | "strategy">>;
}; };
export type StrategyActions = Partial<Record<StrategyActionName, StrategyAction>>; export type StrategyActions = Partial<Record<StrategyActionName, StrategyAction>>;
@@ -59,43 +51,44 @@ export interface UserPool {
} }
const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds
export const cookieConfig = Type.Partial( export const cookieConfig = s
Type.Object({ .object({
path: Type.String({ default: "/" }), path: s.string({ default: "/" }),
sameSite: StringEnum(["strict", "lax", "none"], { default: "lax" }), sameSite: s.string({ enum: ["strict", "lax", "none"], default: "lax" }),
secure: Type.Boolean({ default: true }), secure: s.boolean({ default: true }),
httpOnly: Type.Boolean({ default: true }), httpOnly: s.boolean({ default: true }),
expires: Type.Number({ default: defaultCookieExpires }), // seconds expires: s.number({ default: defaultCookieExpires }), // seconds
renew: Type.Boolean({ default: true }), renew: s.boolean({ default: true }),
pathSuccess: Type.String({ default: "/" }), pathSuccess: s.string({ default: "/" }),
pathLoggedOut: Type.String({ default: "/" }), pathLoggedOut: s.string({ default: "/" }),
}), })
{ default: {}, additionalProperties: false }, .partial()
); .strict();
// @todo: maybe add a config to not allow cookie/api tokens to be used interchangably? // @todo: maybe add a config to not allow cookie/api tokens to be used interchangably?
// see auth.integration test for further details // see auth.integration test for further details
export const jwtConfig = Type.Object( export const jwtConfig = s
.object(
{ {
// @todo: autogenerate a secret if not present. But it must be persisted from AppAuth // @todo: autogenerate a secret if not present. But it must be persisted from AppAuth
secret: Type.String({ default: "" }), secret: s.string({ default: "" }),
alg: Type.Optional(StringEnum(["HS256", "HS384", "HS512"], { default: "HS256" })), alg: s.string({ enum: ["HS256", "HS384", "HS512"], default: "HS256" }).optional(),
expires: Type.Optional(Type.Number()), // seconds expires: s.number().optional(), // seconds
issuer: Type.Optional(Type.String()), issuer: s.string().optional(),
fields: Type.Array(Type.String(), { default: ["id", "email", "role"] }), fields: s.array(s.string(), { default: ["id", "email", "role"] }),
}, },
{ {
default: {}, default: {},
additionalProperties: false,
}, },
); )
export const authenticatorConfig = Type.Object({ .strict();
export const authenticatorConfig = s.object({
jwt: jwtConfig, jwt: jwtConfig,
cookie: cookieConfig, cookie: cookieConfig,
}); });
type AuthConfig = Static<typeof authenticatorConfig>; type AuthConfig = s.Static<typeof authenticatorConfig>;
export type AuthAction = "login" | "register"; export type AuthAction = "login" | "register";
export type AuthResolveOptions = { export type AuthResolveOptions = {
identifier?: "email" | string; identifier?: "email" | string;
@@ -1,19 +1,19 @@
import { type Authenticator, InvalidCredentialsException, type User } from "auth"; import { type Authenticator, InvalidCredentialsException, type User } from "auth";
import { $console, tbValidator as tb } from "core"; import { $console } from "core";
import { hash, parse, type Static, StrictObject, StringEnum } from "core/utils"; import { hash } from "core/utils";
import { Hono } from "hono"; import { Hono } from "hono";
import { compare as bcryptCompare, genSalt as bcryptGenSalt, hash as bcryptHash } from "bcryptjs"; import { compare as bcryptCompare, genSalt as bcryptGenSalt, hash as bcryptHash } from "bcryptjs";
import * as tbbox from "@sinclair/typebox";
import { Strategy } from "./Strategy"; 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({ export type PasswordStrategyOptions = s.Static<typeof schema>;
hashing: StringEnum(["plain", "sha256", "bcrypt"], { default: "sha256" }),
rounds: Type.Optional(Type.Number({ minimum: 1, maximum: 10 })),
});
export type PasswordStrategyOptions = Static<typeof schema>;
export class PasswordStrategy extends Strategy<typeof schema> { export class PasswordStrategy extends Strategy<typeof schema> {
constructor(config: Partial<PasswordStrategyOptions> = {}) { constructor(config: Partial<PasswordStrategyOptions> = {}) {
@@ -32,11 +32,11 @@ export class PasswordStrategy extends Strategy<typeof schema> {
} }
private getPayloadSchema() { private getPayloadSchema() {
return Type.Object({ return s.object({
email: Type.String({ email: s.string({
pattern: "^[\\w-\\.\\+_]+@([\\w-]+\\.)+[\\w-]{2,4}$", pattern: "^[\\w-\\.\\+_]+@([\\w-]+\\.)+[\\w-]{2,4}$",
}), }),
password: Type.String({ password: s.string({
minLength: 8, // @todo: this should be configurable minLength: 8, // @todo: this should be configurable
}), }),
}); });
@@ -79,12 +79,12 @@ export class PasswordStrategy extends Strategy<typeof schema> {
getController(authenticator: Authenticator): Hono<any> { getController(authenticator: Authenticator): Hono<any> {
const hono = new Hono(); const hono = new Hono();
const redirectQuerySchema = Type.Object({ const redirectQuerySchema = s.object({
redirect: Type.Optional(Type.String()), redirect: s.string().optional(),
}); });
const payloadSchema = this.getPayloadSchema(); const payloadSchema = this.getPayloadSchema();
hono.post("/login", tb("query", redirectQuerySchema), async (c) => { hono.post("/login", jsc("query", redirectQuerySchema), async (c) => {
try { try {
const body = parse(payloadSchema, await authenticator.getBody(c), { const body = parse(payloadSchema, await authenticator.getBody(c), {
onError: (errors) => { onError: (errors) => {
@@ -102,7 +102,7 @@ export class PasswordStrategy extends Strategy<typeof schema> {
} }
}); });
hono.post("/register", tb("query", redirectQuerySchema), async (c) => { hono.post("/register", jsc("query", redirectQuerySchema), async (c) => {
try { try {
const { redirect } = c.req.valid("query"); const { redirect } = c.req.valid("query");
const { password, email, ...body } = parse( const { password, email, ...body } = parse(
@@ -5,31 +5,31 @@ import type {
StrategyActions, StrategyActions,
} from "../Authenticator"; } from "../Authenticator";
import type { Hono } from "hono"; import type { Hono } from "hono";
import type { Static, TSchema } from "@sinclair/typebox"; import { type s, parse } from "core/object/schema";
import { parse, type TObject } from "core/utils";
export type StrategyMode = "form" | "external"; export type StrategyMode = "form" | "external";
export abstract class Strategy<Schema extends TSchema = TSchema> { export abstract class Strategy<Schema extends s.Schema = s.Schema> {
protected actions: StrategyActions = {}; protected actions: StrategyActions = {};
constructor( constructor(
protected config: Static<Schema>, protected config: s.Static<Schema>,
public type: string, public type: string,
public name: string, public name: string,
public mode: StrategyMode, public mode: StrategyMode,
) { ) {
// don't worry about typing, it'll throw if invalid // don't worry about typing, it'll throw if invalid
this.config = parse(this.getSchema(), (config ?? {}) as any) as Static<Schema>; this.config = parse(this.getSchema(), (config ?? {}) as any) as s.Static<Schema>;
} }
protected registerAction<S extends TObject = TObject>( protected registerAction<S extends s.ObjectSchema = s.ObjectSchema>(
name: StrategyActionName, name: StrategyActionName,
schema: S, schema: S,
preprocess: StrategyAction<S>["preprocess"], preprocess: StrategyAction<S>["preprocess"],
): void { ): void {
this.actions[name] = { this.actions[name] = {
schema, schema,
// @ts-expect-error - @todo: fix this
preprocess, preprocess,
} as const; } as const;
} }
@@ -50,7 +50,7 @@ export abstract class Strategy<Schema extends TSchema = TSchema> {
return this.name; return this.name;
} }
toJSON(secrets?: boolean): { type: string; config: Static<Schema> | {} | undefined } { toJSON(secrets?: boolean): { type: string; config: s.Static<Schema> | {} | undefined } {
return { return {
type: this.getType(), type: this.getType(),
config: secrets ? this.config : undefined, config: secrets ? this.config : undefined,
@@ -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 type * as oauth from "oauth4webapi";
import { OAuthStrategy } from "./OAuthStrategy"; import { OAuthStrategy } from "./OAuthStrategy";
const { Type } = tbbox; import { s } from "core/object/schema";
type SupportedTypes = "oauth2" | "oidc"; type SupportedTypes = "oauth2" | "oidc";
type RequireKeys<T extends object, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>; type RequireKeys<T extends object, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;
const UrlString = Type.String({ pattern: "^(https?|wss?)://[^\\s/$.?#].[^\\s]*$" }); const UrlString = s.string({ pattern: "^(https?|wss?)://[^\\s/$.?#].[^\\s]*$" });
const oauthSchemaCustom = StrictObject( const oauthSchemaCustom = s.strictObject(
{ {
type: StringEnum(["oidc", "oauth2"] as const, { default: "oidc" }), type: s.string({ enum: ["oidc", "oauth2"] as const, default: "oidc" }),
name: Type.String(), name: s.string(),
client: StrictObject({ client: s.object({
client_id: Type.String(), client_id: s.string(),
client_secret: Type.String(), client_secret: s.string(),
token_endpoint_auth_method: StringEnum(["client_secret_basic"]), token_endpoint_auth_method: s.string({ enum: ["client_secret_basic"] }),
}), }),
as: StrictObject({ as: s.strictObject({
issuer: Type.String(), issuer: s.string(),
code_challenge_methods_supported: Type.Optional(StringEnum(["S256"])), code_challenge_methods_supported: s.string({ enum: ["S256"] }).optional(),
scopes_supported: Type.Optional(Type.Array(Type.String())), scopes_supported: s.array(s.string()).optional(),
scope_separator: Type.Optional(Type.String({ default: " " })), scope_separator: s.string({ default: " " }).optional(),
authorization_endpoint: Type.Optional(UrlString), authorization_endpoint: UrlString.optional(),
token_endpoint: Type.Optional(UrlString), token_endpoint: UrlString.optional(),
userinfo_endpoint: Type.Optional(UrlString), userinfo_endpoint: UrlString.optional(),
}), }),
// @todo: profile mapping // @todo: profile mapping
}, },
{ title: "Custom OAuth" }, { title: "Custom OAuth" },
); );
type OAuthConfigCustom = Static<typeof oauthSchemaCustom>; type OAuthConfigCustom = s.Static<typeof oauthSchemaCustom>;
export type UserProfile = { export type UserProfile = {
sub: string; sub: string;
@@ -1,31 +1,32 @@
import type { AuthAction, Authenticator } from "auth"; import type { AuthAction, Authenticator } from "auth";
import { Exception, isDebug } from "core"; 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 { type Context, Hono } from "hono";
import { getSignedCookie, setSignedCookie } from "hono/cookie"; import { getSignedCookie, setSignedCookie } from "hono/cookie";
import * as oauth from "oauth4webapi"; import * as oauth from "oauth4webapi";
import * as issuers from "./issuers"; import * as issuers from "./issuers";
import * as tbbox from "@sinclair/typebox";
import { Strategy } from "auth/authenticate/strategies/Strategy"; import { Strategy } from "auth/authenticate/strategies/Strategy";
const { Type } = tbbox; import { s } from "core/object/schema";
type ConfiguredIssuers = keyof typeof issuers; type ConfiguredIssuers = keyof typeof issuers;
type SupportedTypes = "oauth2" | "oidc"; type SupportedTypes = "oauth2" | "oidc";
type RequireKeys<T extends object, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>; type RequireKeys<T extends object, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;
const schemaProvided = Type.Object( const schemaProvided = s.object(
{ {
name: StringEnum(Object.keys(issuers) as ConfiguredIssuers[]), name: s.string({ enum: Object.keys(issuers) as ConfiguredIssuers[] }),
type: StringEnum(["oidc", "oauth2"] as const, { default: "oauth2" }), type: s.string({ enum: ["oidc", "oauth2"] as const, default: "oauth2" }),
client: StrictObject({ client: s
client_id: Type.String(), .object({
client_secret: Type.String(), client_id: s.string(),
}), client_secret: s.string(),
})
.strict(),
}, },
{ title: "OAuth" }, { title: "OAuth" },
); );
type ProvidedOAuthConfig = Static<typeof schemaProvided>; type ProvidedOAuthConfig = s.Static<typeof schemaProvided>;
export type CustomOAuthConfig = { export type CustomOAuthConfig = {
type: SupportedTypes; type: SupportedTypes;
+2 -2
View File
@@ -26,7 +26,7 @@ export {
} from "./object/query/query"; } from "./object/query/query";
export { Registry, type Constructor } from "./registry/Registry"; export { Registry, type Constructor } from "./registry/Registry";
export { getFlashMessage } from "./server/flash"; export { getFlashMessage } from "./server/flash";
export { /* export {
s, s,
parse, parse,
jsc, jsc,
@@ -35,7 +35,7 @@ export {
openAPISpecs, openAPISpecs,
type ParseOptions, type ParseOptions,
InvalidSchemaError, InvalidSchemaError,
} from "./object/schema"; } from "./object/schema"; */
export * from "./drivers"; export * from "./drivers";
export * from "./console"; export * from "./console";
+34 -32
View File
@@ -1,43 +1,39 @@
import { get, has, omit, set } from "lodash-es"; import { get, has, omit, set } from "lodash-es";
import { import { getFullPathKeys, mergeObjectWith } from "../utils";
Default, import { type s, parse, stripMark } from "core/object/schema";
type Static,
type TObject,
getFullPathKeys,
mergeObjectWith,
parse,
stripMark,
} from "../utils";
export type SchemaObjectOptions<Schema extends TObject> = { export type SchemaObjectOptions<Schema extends s.Schema> = {
onUpdate?: (config: Static<Schema>) => void | Promise<void>; onUpdate?: (config: s.Static<Schema>) => void | Promise<void>;
onBeforeUpdate?: ( onBeforeUpdate?: (
from: Static<Schema>, from: s.Static<Schema>,
to: Static<Schema>, to: s.Static<Schema>,
) => Static<Schema> | Promise<Static<Schema>>; ) => s.Static<Schema> | Promise<s.Static<Schema>>;
restrictPaths?: string[]; restrictPaths?: string[];
overwritePaths?: (RegExp | string)[]; overwritePaths?: (RegExp | string)[];
forceParse?: boolean; forceParse?: boolean;
}; };
export class SchemaObject<Schema extends TObject> { type TSchema = s.ObjectSchema<any>;
private readonly _default: Partial<Static<Schema>>;
private _value: Static<Schema>; export class SchemaObject<Schema extends TSchema = TSchema> {
private _config: Static<Schema>; private readonly _default: Partial<s.Static<Schema>>;
private _value: s.Static<Schema>;
private _config: s.Static<Schema>;
private _restriction_bypass: boolean = false; private _restriction_bypass: boolean = false;
constructor( constructor(
private _schema: Schema, private _schema: Schema,
initial?: Partial<Static<Schema>>, initial?: Partial<s.Static<Schema>>,
private options?: SchemaObjectOptions<Schema>, private options?: SchemaObjectOptions<Schema>,
) { ) {
this._default = Default(_schema, {} as any) as any; this._default = _schema.template() as any;
this._value = initial this._value = initial
? parse(_schema, structuredClone(initial as any), { ? parse(_schema, structuredClone(initial as any), {
withDefaults: true,
forceParse: this.isForceParse(), forceParse: this.isForceParse(),
skipMark: this.isForceParse(), skipMark: this.isForceParse(),
}) })
: this._default; : (this._default as any);
this._config = Object.freeze(this._value); this._config = Object.freeze(this._value);
} }
@@ -45,18 +41,21 @@ export class SchemaObject<Schema extends TObject> {
return this.options?.forceParse ?? true; return this.options?.forceParse ?? true;
} }
default(): Static<Schema> { default() {
return this._default; return this._default;
} }
private async onBeforeUpdate(from: Static<Schema>, to: Static<Schema>): Promise<Static<Schema>> { private async onBeforeUpdate(
from: s.Static<Schema>,
to: s.Static<Schema>,
): Promise<s.Static<Schema>> {
if (this.options?.onBeforeUpdate) { if (this.options?.onBeforeUpdate) {
return this.options.onBeforeUpdate(from, to); return this.options.onBeforeUpdate(from, to);
} }
return to; return to;
} }
get(options?: { stripMark?: boolean }): Static<Schema> { get(options?: { stripMark?: boolean }): s.Static<Schema> {
if (options?.stripMark) { if (options?.stripMark) {
return stripMark(this._config); return stripMark(this._config);
} }
@@ -68,7 +67,7 @@ export class SchemaObject<Schema extends TObject> {
return structuredClone(this._config); return structuredClone(this._config);
} }
async set(config: Static<Schema>, noEmit?: boolean): Promise<Static<Schema>> { async set(config: s.Static<Schema>, noEmit?: boolean): Promise<s.Static<Schema>> {
const valid = parse(this._schema, structuredClone(config) as any, { const valid = parse(this._schema, structuredClone(config) as any, {
forceParse: true, forceParse: true,
skipMark: this.isForceParse(), skipMark: this.isForceParse(),
@@ -118,9 +117,9 @@ export class SchemaObject<Schema extends TObject> {
return; return;
} }
async patch(path: string, value: any): Promise<[Partial<Static<Schema>>, Static<Schema>]> { async patch(path: string, value: any): Promise<[Partial<s.Static<Schema>>, s.Static<Schema>]> {
const current = this.clone(); const current = this.clone();
const partial = path.length > 0 ? (set({}, path, value) as Partial<Static<Schema>>) : value; const partial = path.length > 0 ? (set({}, path, value) as Partial<s.Static<Schema>>) : value;
this.throwIfRestricted(partial); this.throwIfRestricted(partial);
@@ -168,9 +167,12 @@ export class SchemaObject<Schema extends TObject> {
return [partial, newConfig]; return [partial, newConfig];
} }
async overwrite(path: string, value: any): Promise<[Partial<Static<Schema>>, Static<Schema>]> { async overwrite(
path: string,
value: any,
): Promise<[Partial<s.Static<Schema>>, s.Static<Schema>]> {
const current = this.clone(); const current = this.clone();
const partial = path.length > 0 ? (set({}, path, value) as Partial<Static<Schema>>) : value; const partial = path.length > 0 ? (set({}, path, value) as Partial<s.Static<Schema>>) : value;
this.throwIfRestricted(partial); this.throwIfRestricted(partial);
@@ -194,7 +196,7 @@ export class SchemaObject<Schema extends TObject> {
return has(this._config, path); return has(this._config, path);
} }
async remove(path: string): Promise<[Partial<Static<Schema>>, Static<Schema>]> { async remove(path: string): Promise<[Partial<s.Static<Schema>>, s.Static<Schema>]> {
this.throwIfRestricted(path); this.throwIfRestricted(path);
if (!this.has(path)) { if (!this.has(path)) {
@@ -202,9 +204,9 @@ export class SchemaObject<Schema extends TObject> {
} }
const current = this.clone(); const current = this.clone();
const removed = get(current, path) as Partial<Static<Schema>>; const removed = get(current, path) as Partial<s.Static<Schema>>;
const config = omit(current, path); const config = omit(current, path);
const newConfig = await this.set(config); const newConfig = await this.set(config as any);
return [removed, newConfig]; return [removed, newConfig];
} }
} }
+39 -14
View File
@@ -1,6 +1,3 @@
import { mergeObject } from "core/utils";
//export { jsc, type Options, type Hook } from "./validator";
import * as s from "jsonv-ts"; import * as s from "jsonv-ts";
export { validator as jsc, type Options } from "jsonv-ts/hono"; 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 { s };
export const stripMark = <O extends object>(o: O): O => o;
export const mark = <O extends object>(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 { export class InvalidSchemaError extends Error {
constructor( constructor(
public schema: s.TAnySchema, public schema: s.Schema,
public value: unknown, public value: unknown,
public errors: s.ErrorDetail[] = [], public errors: s.ErrorDetail[] = [],
) { ) {
@@ -19,33 +25,52 @@ export class InvalidSchemaError extends Error {
`Error: ${JSON.stringify(errors[0], null, 2)}`, `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 = { export type ParseOptions = {
withDefaults?: boolean; withDefaults?: boolean;
coerse?: boolean; coerce?: boolean;
clone?: 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 = <S extends s.TSchema>(schema: S): S => { export const cloneSchema = <S extends s.Schema>(schema: S): S => {
const json = schema.toJSON(); const json = schema.toJSON();
return s.fromSchema(json) as S; return s.fromSchema(json) as S;
}; };
export function parse<S extends s.TAnySchema>( export function parse<S extends s.Schema, Options extends ParseOptions = ParseOptions>(
_schema: S, _schema: S,
v: unknown, v: unknown,
opts: ParseOptions = {}, opts?: Options,
): s.StaticCoerced<S> { ): Options extends { coerce: true } ? s.StaticCoerced<S> : s.Static<S> {
const schema = (opts.clone ? cloneSchema(_schema as any) : _schema) as s.TSchema; const schema = (opts?.clone ? cloneSchema(_schema as any) : _schema) as s.Schema;
const value = opts.coerse !== false ? schema.coerce(v) : v; let value = opts?.coerce !== false ? schema.coerce(v) : v;
if (opts?.withDefaults) {
value = schema.template(value, { withOptional: true });
}
const result = schema.validate(value, { const result = schema.validate(value, {
shortCircuit: true, shortCircuit: true,
ignoreUnsupported: true, ignoreUnsupported: true,
}); });
if (!result.valid) throw new InvalidSchemaError(schema, v, result.errors); if (!result.valid) {
if (opts.withDefaults) { if (opts?.onError) {
return mergeObject(schema.template({ withOptional: true }), value) as any; opts.onError(result.errors);
} else {
throw new InvalidSchemaError(schema, v, result.errors);
}
} }
return value as any; return value as any;
-63
View File
@@ -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<T, E extends Env, P extends string> = (
result: { result: ValidationResult; data: T },
c: Context<E, P>,
) => Response | Promise<Response> | 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<Schema> : StaticCoerced<Schema>,
I extends Input = {
in: { [K in Target]: Static<Schema> };
out: { [K in Target]: Out };
},
>(
target: Target,
schema: Schema,
options?: Opts,
hook?: Hook<Out, E, P>,
): MiddlewareHandler<E, P, I> => {
// @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;
+1 -1
View File
@@ -5,7 +5,7 @@ export * from "./perf";
export * from "./file"; export * from "./file";
export * from "./reqres"; export * from "./reqres";
export * from "./xml"; export * from "./xml";
export type { Prettify, PrettifyRec } from "./types"; export type { Prettify, PrettifyRec, RecursivePartial } from "./types";
export * from "./typebox"; export * from "./typebox";
export * from "./dates"; export * from "./dates";
export * from "./crypto"; export * from "./crypto";
+3 -10
View File
@@ -14,16 +14,9 @@ import {
type ValueErrorIterator, type ValueErrorIterator,
} from "@sinclair/typebox/errors"; } from "@sinclair/typebox/errors";
import { Check, Default, Value, type ValueError } from "@sinclair/typebox/value"; import { Check, Default, Value, type ValueError } from "@sinclair/typebox/value";
import type { RecursivePartial } from "../types";
export type RecursivePartial<T> = { /* type ParseOptions = {
[P in keyof T]?: T[P] extends (infer U)[]
? RecursivePartial<U>[]
: T[P] extends object | undefined
? RecursivePartial<T[P]>
: T[P];
};
type ParseOptions = {
useDefaults?: boolean; useDefaults?: boolean;
decode?: boolean; decode?: boolean;
onError?: (errors: ValueErrorIterator) => void; onError?: (errors: ValueErrorIterator) => void;
@@ -198,4 +191,4 @@ SetErrorFunction((error) => {
export type { Static, StaticDecode, TSchema, TObject, ValueError, SchemaOptions }; export type { Static, StaticDecode, TSchema, TObject, ValueError, SchemaOptions };
export { Value, Default, Errors, Check }; export { Value, Default, Errors, Check }; */
+8
View File
@@ -6,3 +6,11 @@ export type Prettify<T> = {
export type PrettifyRec<T> = { export type PrettifyRec<T> = {
[K in keyof T]: T[K] extends object ? Prettify<T[K]> : T[K]; [K in keyof T]: T[K] extends object ? Prettify<T[K]> : T[K];
} & NonNullable<unknown>; } & NonNullable<unknown>;
export type RecursivePartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[]
? RecursivePartial<U>[]
: T[P] extends object | undefined
? RecursivePartial<T[P]>
: T[P];
};
+1 -1
View File
@@ -11,7 +11,7 @@ import { Module } from "modules/Module";
import { DataController } from "./api/DataController"; import { DataController } from "./api/DataController";
import { type AppDataConfig, dataConfigSchema } from "./data-schema"; import { type AppDataConfig, dataConfigSchema } from "./data-schema";
export class AppData extends Module<typeof dataConfigSchema> { export class AppData extends Module<AppDataConfig> {
override async build() { override async build() {
const { const {
entities: _entities = {}, entities: _entities = {},
+12 -6
View File
@@ -74,10 +74,12 @@ export class DataController extends Controller {
}), }),
jsc( jsc(
"query", "query",
s.partialObject({ s
.object({
force: s.boolean(), force: s.boolean(),
drop: s.boolean(), drop: s.boolean(),
}), })
.partial(),
), ),
async (c) => { async (c) => {
const { force, drop } = c.req.valid("query"); const { force, drop } = c.req.valid("query");
@@ -258,12 +260,14 @@ export class DataController extends Controller {
* Read endpoints * Read endpoints
*/ */
// read many // read many
const saveRepoQuery = s.partialObject({ const saveRepoQuery = s
.object({
...omitKeys(repoQuery.properties, ["with"]), ...omitKeys(repoQuery.properties, ["with"]),
sort: s.string({ default: "id" }), sort: s.string({ default: "id" }),
select: s.array(s.string()), select: s.array(s.string()),
join: s.array(s.string()), join: s.array(s.string()),
}); })
.partial();
const saveRepoQueryParams = (pick: string[] = Object.keys(repoQuery.properties)) => [ const saveRepoQueryParams = (pick: string[] = Object.keys(repoQuery.properties)) => [
...(schemaToSpec(saveRepoQuery, "query").parameters?.filter( ...(schemaToSpec(saveRepoQuery, "query").parameters?.filter(
// @ts-ignore // @ts-ignore
@@ -356,10 +360,12 @@ export class DataController extends Controller {
); );
// func query // func query
const fnQuery = s.partialObject({ const fnQuery = s
.object({
...saveRepoQuery.properties, ...saveRepoQuery.properties,
with: s.object({}), with: s.object({}),
}); })
.partial();
hono.post( hono.post(
"/:entity/query", "/:entity/query",
describeRoute({ describeRoute({
+1 -1
View File
@@ -38,7 +38,7 @@ export interface SelectQueryBuilderExpression<O> extends AliasableExpression<O>
export type SchemaResponse = [string, ColumnDataType, ColumnBuilderCallback] | undefined; export type SchemaResponse = [string, ColumnDataType, ColumnBuilderCallback] | undefined;
const FieldSpecTypes = [ export const FieldSpecTypes = [
"text", "text",
"integer", "integer",
"real", "real",
+33 -47
View File
@@ -1,10 +1,10 @@
import { type Static, StringEnum, StringRecord, objectTransform } from "core/utils"; import { objectTransform } from "core/utils";
import * as tb from "@sinclair/typebox";
import { MediaField, mediaFieldConfigSchema } from "../media/MediaField"; import { MediaField, mediaFieldConfigSchema } from "../media/MediaField";
import { FieldClassMap } from "data/fields"; import { FieldClassMap } from "data/fields";
import { RelationClassMap, RelationFieldClassMap } from "data/relations"; import { RelationClassMap, RelationFieldClassMap } from "data/relations";
import { entityConfigSchema, entityTypes } from "data/entities"; import { entityConfigSchema, entityTypes } from "data/entities";
import { primaryFieldTypes } from "./fields"; import { primaryFieldTypes } from "./fields";
import { s } from "core/object/schema";
export const FIELDS = { export const FIELDS = {
...FieldClassMap, ...FieldClassMap,
@@ -16,69 +16,55 @@ export type FieldType = keyof typeof FIELDS;
export const RELATIONS = RelationClassMap; export const RELATIONS = RelationClassMap;
export const fieldsSchemaObject = objectTransform(FIELDS, (field, name) => { export const fieldsSchemaObject = objectTransform(FIELDS, (field, name) => {
return tb.Type.Object( return s.object(
{ {
type: tb.Type.Const(name, { default: name, readOnly: true }), type: s.literal(name),
config: tb.Type.Optional(field.schema), config: field.schema.optional(),
}, },
{ {
title: name, title: name,
}, },
); );
}); });
export const fieldsSchema = tb.Type.Union(Object.values(fieldsSchemaObject)); export const fieldsSchema = s.anyOf(Object.values(fieldsSchemaObject));
export const entityFields = StringRecord(fieldsSchema); export const entityFields = s.record(fieldsSchema);
export type TAppDataField = Static<typeof fieldsSchema>; export type TAppDataField = s.Static<typeof fieldsSchema>;
export type TAppDataEntityFields = Static<typeof entityFields>; export type TAppDataEntityFields = s.Static<typeof entityFields>;
export const entitiesSchema = tb.Type.Object({ export const entitiesSchema = s.object({
type: tb.Type.Optional( type: s.string({ enum: entityTypes, default: "regular", readOnly: true }),
tb.Type.String({ enum: entityTypes, default: "regular", readOnly: true }), config: entityConfigSchema.optional(),
), fields: entityFields.optional(),
config: tb.Type.Optional(entityConfigSchema),
fields: tb.Type.Optional(entityFields),
}); });
export type TAppDataEntity = Static<typeof entitiesSchema>; export type TAppDataEntity = s.Static<typeof entitiesSchema>;
export const relationsSchema = Object.entries(RelationClassMap).map(([name, relationClass]) => { 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 }), type: s.literal(name),
source: tb.Type.String(), source: s.string(),
target: tb.Type.String(), target: s.string(),
config: tb.Type.Optional(relationClass.schema), config: relationClass.schema.optional(),
}, },
{ {
title: name, title: name,
}, },
); );
}); });
export type TAppDataRelation = Static<(typeof relationsSchema)[number]>; export type TAppDataRelation = s.Static<(typeof relationsSchema)[number]>;
export const indicesSchema = tb.Type.Object( export const indicesSchema = s.strictObject({
{ entity: s.string(),
entity: tb.Type.String(), fields: s.array(s.string(), { minItems: 1 }),
fields: tb.Type.Array(tb.Type.String(), { minItems: 1 }), unique: s.boolean({ default: false }).optional(),
unique: tb.Type.Optional(tb.Type.Boolean({ default: false })), });
},
{
additionalProperties: false,
},
);
export const dataConfigSchema = tb.Type.Object( export const dataConfigSchema = s.strictObject({
{ basepath: s.string({ default: "/api/data" }).optional(),
basepath: tb.Type.Optional(tb.Type.String({ default: "/api/data" })), default_primary_format: s.string({ enum: primaryFieldTypes, default: "integer" }).optional(),
default_primary_format: tb.Type.Optional( entities: s.record(entitiesSchema, { default: {} }).optional(),
StringEnum(primaryFieldTypes, { default: "integer" }), relations: s.record(s.anyOf(relationsSchema), { default: {} }).optional(),
), indices: s.record(indicesSchema, { default: {} }).optional(),
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 type AppDataConfig = Static<typeof dataConfigSchema>; export type AppDataConfig = s.Static<typeof dataConfigSchema>;
+18 -27
View File
@@ -1,11 +1,5 @@
import { $console, config } from "core"; import { $console, config } from "core";
import { import { snakeToPascalWithSpaces, transformObject } from "core/utils";
type Static,
StringEnum,
parse,
snakeToPascalWithSpaces,
transformObject,
} from "core/utils";
import { import {
type Field, type Field,
PrimaryField, PrimaryField,
@@ -13,25 +7,21 @@ import {
type TActionContext, type TActionContext,
type TRenderContext, type TRenderContext,
} from "../fields"; } from "../fields";
import * as tbbox from "@sinclair/typebox"; import { s, parse } from "core/object/schema";
const { Type } = tbbox;
// @todo: entity must be migrated to typebox // @todo: entity must be migrated to typebox
export const entityConfigSchema = Type.Object( export const entityConfigSchema = s
{ .strictObject({
name: Type.Optional(Type.String()), name: s.string(),
name_singular: Type.Optional(Type.String()), name_singular: s.string(),
description: Type.Optional(Type.String()), description: s.string(),
sort_field: Type.Optional(Type.String({ default: config.data.default_primary_field })), sort_field: s.string({ default: config.data.default_primary_field }),
sort_dir: Type.Optional(StringEnum(["asc", "desc"], { default: "asc" })), sort_dir: s.string({ enum: ["asc", "desc"], default: "asc" }),
primary_format: Type.Optional(StringEnum(primaryFieldTypes)), primary_format: s.string({ enum: primaryFieldTypes }),
}, })
{ .partial();
additionalProperties: false,
},
);
export type EntityConfig = Static<typeof entityConfigSchema>; export type EntityConfig = s.Static<typeof entityConfigSchema>;
export type EntityData = Record<string, any>; export type EntityData = Record<string, any>;
export type EntityJSON = ReturnType<Entity["toJSON"]>; export type EntityJSON = ReturnType<Entity["toJSON"]>;
@@ -287,8 +277,10 @@ export class Entity<
} }
const _fields = Object.fromEntries(fields.map((field) => [field.name, field])); const _fields = Object.fromEntries(fields.map((field) => [field.name, field]));
const schema = Type.Object( const schema = {
transformObject(_fields, (field) => { type: "object",
additionalProperties: false,
properties: transformObject(_fields, (field) => {
const fillable = field.isFillable(options?.context); const fillable = field.isFillable(options?.context);
return { return {
title: field.config.label, title: field.config.label,
@@ -298,8 +290,7 @@ export class Entity<
...field.toJsonSchema(), ...field.toJsonSchema(),
}; };
}), }),
{ additionalProperties: false }, };
);
return options?.clean ? JSON.parse(JSON.stringify(schema)) : schema; return options?.clean ? JSON.parse(JSON.stringify(schema)) : schema;
} }
+3 -3
View File
@@ -78,8 +78,8 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
this.checkIndex(entity.name, options.sort.by, "sort"); this.checkIndex(entity.name, options.sort.by, "sort");
validated.sort = { validated.sort = {
dir: "asc", dir: options.sort.dir ?? "asc",
...options.sort, by: options.sort.by,
}; };
} }
@@ -345,7 +345,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
...refQueryOptions, ...refQueryOptions,
where: { where: {
...refQueryOptions.where, ...refQueryOptions.where,
..._options?.where, ...(_options?.where ?? {}),
}, },
}; };
+3 -2
View File
@@ -1,5 +1,6 @@
import { Exception } from "core"; import { Exception } from "core";
import { HttpStatus, type TypeInvalidError } from "core/utils"; import { HttpStatus } from "core/utils";
import type { InvalidSchemaError } from "core/object/schema";
import type { Entity } from "./entities"; import type { Entity } from "./entities";
import type { Field } from "./fields"; import type { Field } from "./fields";
@@ -42,7 +43,7 @@ export class InvalidFieldConfigException extends Exception {
constructor( constructor(
field: Field<any, any, any>, field: Field<any, any, any>,
public given: any, public given: any,
error: TypeInvalidError, error: InvalidSchemaError,
) { ) {
console.error("InvalidFieldConfigException", { console.error("InvalidFieldConfigException", {
given, given,
+10 -11
View File
@@ -1,18 +1,17 @@
import type { Static } from "core/utils"; import { omitKeys } from "core/utils";
import type { EntityManager } from "data"; import type { EntityManager } from "data";
import { TransformPersistFailedException } from "../errors"; import { TransformPersistFailedException } from "../errors";
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field"; import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
import * as tb from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tb;
export const booleanFieldConfigSchema = Type.Composite([ export const booleanFieldConfigSchema = s
Type.Object({ .strictObject({
default_value: Type.Optional(Type.Boolean({ default: false })), default_value: s.boolean({ default: false }),
}), ...omitKeys(baseFieldConfigSchema.properties, ["default_value"]),
baseFieldConfigSchema, })
]); .partial();
export type BooleanFieldConfig = Static<typeof booleanFieldConfigSchema>; export type BooleanFieldConfig = s.Static<typeof booleanFieldConfigSchema>;
export class BooleanField<Required extends true | false = false> extends Field< export class BooleanField<Required extends true | false = false> extends Field<
BooleanFieldConfig, BooleanFieldConfig,
@@ -86,7 +85,7 @@ export class BooleanField<Required extends true | false = false> extends Field<
} }
override toJsonSchema() { override toJsonSchema() {
return this.toSchemaWrapIfRequired(Type.Boolean({ default: this.getDefault() })); return this.toSchemaWrapIfRequired(s.boolean({ default: this.getDefault() }));
} }
override toType() { override toType() {
+13 -19
View File
@@ -1,27 +1,21 @@
import { type Static, StringEnum, dayjs } from "core/utils"; import { dayjs, omitKeys } from "core/utils";
import type { EntityManager } from "../entities"; import type { EntityManager } from "../entities";
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field"; import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
import { $console } from "core"; import { $console } from "core";
import * as tbbox from "@sinclair/typebox";
import type { TFieldTSType } from "data/entities/EntityTypescript"; import type { TFieldTSType } from "data/entities/EntityTypescript";
const { Type } = tbbox; import { s } from "core/object/schema";
export const dateFieldConfigSchema = Type.Composite( export const dateFieldConfigSchema = s
[ .strictObject({
Type.Object({ type: s.string({ enum: ["date", "datetime", "week"], default: "date" }),
type: StringEnum(["date", "datetime", "week"] as const, { default: "date" }), timezone: s.string(),
timezone: Type.Optional(Type.String()), min_date: s.string(),
min_date: Type.Optional(Type.String()), max_date: s.string(),
max_date: Type.Optional(Type.String()), ...omitKeys(baseFieldConfigSchema.properties, ["default_value"]),
}), })
baseFieldConfigSchema, .partial();
],
{
additionalProperties: false,
},
);
export type DateFieldConfig = Static<typeof dateFieldConfigSchema>; export type DateFieldConfig = s.Static<typeof dateFieldConfigSchema>;
export class DateField<Required extends true | false = false> extends Field< export class DateField<Required extends true | false = false> extends Field<
DateFieldConfig, DateFieldConfig,
@@ -143,7 +137,7 @@ export class DateField<Required extends true | false = false> extends Field<
// @todo: check this // @todo: check this
override toJsonSchema() { override toJsonSchema() {
return this.toSchemaWrapIfRequired(Type.String({ default: this.getDefault() })); return this.toSchemaWrapIfRequired(s.string({ default: this.getDefault() }));
} }
override toType(): TFieldTSType { override toType(): TFieldTSType {
+23 -39
View File
@@ -1,50 +1,33 @@
import { Const, type Static, StringEnum } from "core/utils"; import { omitKeys } from "core/utils";
import type { EntityManager } from "data"; import type { EntityManager } from "data";
import { TransformPersistFailedException } from "../errors"; import { TransformPersistFailedException } from "../errors";
import { baseFieldConfigSchema, Field, type TActionContext, type TRenderContext } from "./Field"; import { baseFieldConfigSchema, Field, type TActionContext, type TRenderContext } from "./Field";
import * as tbbox from "@sinclair/typebox";
import type { TFieldTSType } from "data/entities/EntityTypescript"; import type { TFieldTSType } from "data/entities/EntityTypescript";
const { Type } = tbbox; import { s } from "core/object/schema";
export const enumFieldConfigSchema = Type.Composite( export const enumFieldConfigSchema = s
[ .strictObject({
Type.Object({ default_value: s.string(),
default_value: Type.Optional(Type.String()), options: s.anyOf([
options: Type.Optional( s.object({
Type.Union([ type: s.literal("strings"),
Type.Object( values: s.array(s.string()),
{ }),
type: Const("strings"), s.object({
values: Type.Array(Type.String()), type: s.literal("objects"),
}, values: s.array(
{ title: "Strings" }, s.object({
), label: s.string(),
Type.Object( value: s.string(),
{
type: Const("objects"),
values: Type.Array(
Type.Object({
label: Type.String(),
value: Type.String(),
}), }),
), ),
}, }),
{
title: "Objects",
additionalProperties: false,
},
),
]), ]),
), ...omitKeys(baseFieldConfigSchema.properties, ["default_value"]),
}), })
baseFieldConfigSchema, .partial();
],
{
additionalProperties: false,
},
);
export type EnumFieldConfig = Static<typeof enumFieldConfigSchema>; export type EnumFieldConfig = s.Static<typeof enumFieldConfigSchema>;
export class EnumField<Required extends true | false = false, TypeOverride = string> extends Field< export class EnumField<Required extends true | false = false, TypeOverride = string> extends Field<
EnumFieldConfig, EnumFieldConfig,
@@ -136,7 +119,8 @@ export class EnumField<Required extends true | false = false, TypeOverride = str
options.values?.map((option) => (typeof option === "string" ? option : option.value)) ?? options.values?.map((option) => (typeof option === "string" ? option : option.value)) ??
[]; [];
return this.toSchemaWrapIfRequired( return this.toSchemaWrapIfRequired(
StringEnum(values, { s.string({
enum: values,
default: this.getDefault(), default: this.getDefault(),
}), }),
); );
+24 -39
View File
@@ -1,18 +1,10 @@
import { import { snakeToPascalWithSpaces } from "core/utils";
parse,
snakeToPascalWithSpaces,
type Static,
StringEnum,
type TSchema,
TypeInvalidError,
} from "core/utils";
import type { HTMLInputTypeAttribute, InputHTMLAttributes } from "react"; import type { HTMLInputTypeAttribute, InputHTMLAttributes } from "react";
import type { EntityManager } from "../entities"; import type { EntityManager } from "../entities";
import { InvalidFieldConfigException, TransformPersistFailedException } from "../errors"; import { InvalidFieldConfigException, TransformPersistFailedException } from "../errors";
import type { FieldSpec } from "data/connection/Connection"; import type { FieldSpec } from "data/connection/Connection";
import * as tbbox from "@sinclair/typebox";
import type { TFieldTSType } from "data/entities/EntityTypescript"; import type { TFieldTSType } from "data/entities/EntityTypescript";
const { Type } = tbbox; import { s, parse, InvalidSchemaError } from "core/object/schema";
// @todo: contexts need to be reworked // @todo: contexts need to be reworked
// e.g. "table" is irrelevant, because if read is not given, it fails // 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; const DEFAULT_HIDDEN = false;
// @todo: add refine functions (e.g. if required, but not fillable, needs default value) // @todo: add refine functions (e.g. if required, but not fillable, needs default value)
export const baseFieldConfigSchema = Type.Object( export const baseFieldConfigSchema = s
{ .strictObject({
label: Type.Optional(Type.String()), label: s.string(),
description: Type.Optional(Type.String()), description: s.string(),
required: Type.Optional(Type.Boolean({ default: DEFAULT_REQUIRED })), required: s.boolean({ default: DEFAULT_REQUIRED }),
fillable: Type.Optional( fillable: s.anyOf(
Type.Union(
[ [
Type.Boolean({ title: "Boolean", default: DEFAULT_FILLABLE }), s.boolean({ title: "Boolean", default: DEFAULT_FILLABLE }),
Type.Array(StringEnum(ActionContext), { title: "Context", uniqueItems: true }), s.array(s.string({ enum: ActionContext }), { title: "Context", uniqueItems: true }),
], ],
{ {
default: DEFAULT_FILLABLE, default: DEFAULT_FILLABLE,
}, },
), ),
), hidden: s.anyOf(
hidden: Type.Optional(
Type.Union(
[ [
Type.Boolean({ title: "Boolean", default: DEFAULT_HIDDEN }), s.boolean({ title: "Boolean", default: DEFAULT_HIDDEN }),
// @todo: tmp workaround // @todo: tmp workaround
Type.Array(StringEnum(TmpContext), { title: "Context", uniqueItems: true }), s.array(s.string({ enum: TmpContext }), { title: "Context", uniqueItems: true }),
], ],
{ {
default: DEFAULT_HIDDEN, default: DEFAULT_HIDDEN,
}, },
), ),
),
// if field is virtual, it will not call transformPersist & transformRetrieve // if field is virtual, it will not call transformPersist & transformRetrieve
virtual: Type.Optional(Type.Boolean()), virtual: s.boolean(),
default_value: Type.Optional(Type.Any()), default_value: s.any(),
}, })
{ .partial();
additionalProperties: false, export type BaseFieldConfig = s.Static<typeof baseFieldConfigSchema>;
},
);
export type BaseFieldConfig = Static<typeof baseFieldConfigSchema>;
export abstract class Field< export abstract class Field<
Config extends BaseFieldConfig = BaseFieldConfig, Config extends BaseFieldConfig = BaseFieldConfig,
@@ -92,7 +77,7 @@ export abstract class Field<
try { try {
this.config = parse(this.getSchema(), config || {}) as Config; this.config = parse(this.getSchema(), config || {}) as Config;
} catch (e) { } catch (e) {
if (e instanceof TypeInvalidError) { if (e instanceof InvalidSchemaError) {
throw new InvalidFieldConfigException(this, config, e); throw new InvalidFieldConfigException(this, config, e);
} }
@@ -104,7 +89,7 @@ export abstract class Field<
return this.type; return this.type;
} }
protected abstract getSchema(): TSchema; protected abstract getSchema(): s.ObjectSchema;
/** /**
* Used in SchemaManager.ts * Used in SchemaManager.ts
@@ -224,16 +209,16 @@ export abstract class Field<
return value; return value;
} }
protected toSchemaWrapIfRequired<Schema extends TSchema>(schema: Schema) { protected toSchemaWrapIfRequired<Schema extends s.Schema>(schema: Schema): Schema {
return this.isRequired() ? schema : Type.Optional(schema); return this.isRequired() ? schema : (schema.optional() as any);
} }
protected nullish(value: any) { protected nullish(value: any) {
return value === null || value === undefined; return value === null || value === undefined;
} }
toJsonSchema(): TSchema { toJsonSchema(): s.Schema {
return this.toSchemaWrapIfRequired(Type.Any()); return this.toSchemaWrapIfRequired(s.any());
} }
toType(): TFieldTSType { toType(): TFieldTSType {
+9 -5
View File
@@ -1,14 +1,18 @@
import type { Static } from "core/utils"; import { omitKeys } from "core/utils";
import type { EntityManager } from "data"; import type { EntityManager } from "data";
import { TransformPersistFailedException } from "../errors"; import { TransformPersistFailedException } from "../errors";
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field"; import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
import * as tbbox from "@sinclair/typebox";
import type { TFieldTSType } from "data/entities/EntityTypescript"; 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<typeof jsonFieldConfigSchema>; export type JsonFieldConfig = s.Static<typeof jsonFieldConfigSchema>;
export class JsonField<Required extends true | false = false, TypeOverride = object> extends Field< export class JsonField<Required extends true | false = false, TypeOverride = object> extends Field<
JsonFieldConfig, JsonFieldConfig,
+13 -19
View File
@@ -1,27 +1,21 @@
import { type Schema as JsonSchema, Validator } from "@cfworker/json-schema"; 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 type { EntityManager } from "data";
import { TransformPersistFailedException } from "../errors"; import { TransformPersistFailedException } from "../errors";
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field"; import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
import * as tbbox from "@sinclair/typebox";
import type { TFieldTSType } from "data/entities/EntityTypescript"; import type { TFieldTSType } from "data/entities/EntityTypescript";
const { Type } = tbbox; import { s } from "core/object/schema";
export const jsonSchemaFieldConfigSchema = Type.Composite( export const jsonSchemaFieldConfigSchema = s
[ .strictObject({
Type.Object({ schema: s.any({ type: "object", default: {} }),
schema: Type.Object({}, { default: {} }), ui_schema: s.any({ type: "object", default: {} }),
ui_schema: Type.Optional(Type.Object({})), default_from_schema: s.boolean(),
default_from_schema: Type.Optional(Type.Boolean()), ...omitKeys(baseFieldConfigSchema.properties, ["default_value"]),
}), })
baseFieldConfigSchema, .partial();
],
{
additionalProperties: false,
},
);
export type JsonSchemaFieldConfig = Static<typeof jsonSchemaFieldConfigSchema>; export type JsonSchemaFieldConfig = s.Static<typeof jsonSchemaFieldConfigSchema>;
export class JsonSchemaField< export class JsonSchemaField<
Required extends true | false = false, Required extends true | false = false,
@@ -84,7 +78,7 @@ export class JsonSchemaField<
if (val === null) { if (val === null) {
if (this.config.default_from_schema) { if (this.config.default_from_schema) {
try { try {
return Default(FromSchema(this.getJsonSchema()), {}); return s.fromSchema(this.getJsonSchema()).template();
} catch (e) { } catch (e) {
return null; return null;
} }
@@ -116,7 +110,7 @@ export class JsonSchemaField<
override toJsonSchema() { override toJsonSchema() {
const schema = this.getJsonSchema() ?? { type: "object" }; const schema = this.getJsonSchema() ?? { type: "object" };
return this.toSchemaWrapIfRequired( return this.toSchemaWrapIfRequired(
FromSchema({ s.fromSchema({
default: this.getDefault(), default: this.getDefault(),
...schema, ...schema,
}), }),
+15 -21
View File
@@ -1,29 +1,23 @@
import type { Static } from "core/utils";
import type { EntityManager } from "data"; import type { EntityManager } from "data";
import { TransformPersistFailedException } from "../errors"; import { TransformPersistFailedException } from "../errors";
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field"; import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
import * as tbbox from "@sinclair/typebox";
import type { TFieldTSType } from "data/entities/EntityTypescript"; 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( export const numberFieldConfigSchema = s
[ .strictObject({
Type.Object({ default_value: s.number(),
default_value: Type.Optional(Type.Number()), minimum: s.number(),
minimum: Type.Optional(Type.Number()), maximum: s.number(),
maximum: Type.Optional(Type.Number()), exclusiveMinimum: s.number(),
exclusiveMinimum: Type.Optional(Type.Number()), exclusiveMaximum: s.number(),
exclusiveMaximum: Type.Optional(Type.Number()), multipleOf: s.number(),
multipleOf: Type.Optional(Type.Number()), ...omitKeys(baseFieldConfigSchema.properties, ["default_value"]),
}), })
baseFieldConfigSchema, .partial();
],
{
additionalProperties: false,
},
);
export type NumberFieldConfig = Static<typeof numberFieldConfigSchema>; export type NumberFieldConfig = s.Static<typeof numberFieldConfigSchema>;
export class NumberField<Required extends true | false = false> extends Field< export class NumberField<Required extends true | false = false> extends Field<
NumberFieldConfig, NumberFieldConfig,
@@ -93,7 +87,7 @@ export class NumberField<Required extends true | false = false> extends Field<
override toJsonSchema() { override toJsonSchema() {
return this.toSchemaWrapIfRequired( return this.toSchemaWrapIfRequired(
Type.Number({ s.number({
default: this.getDefault(), default: this.getDefault(),
minimum: this.config?.minimum, minimum: this.config?.minimum,
maximum: this.config?.maximum, maximum: this.config?.maximum,
+17 -17
View File
@@ -1,22 +1,22 @@
import { config } from "core"; import { config } from "core";
import { StringEnum, uuidv7, type Static } from "core/utils"; import { omitKeys, uuidv7 } from "core/utils";
import { Field, baseFieldConfigSchema } from "./Field"; import { Field, baseFieldConfigSchema } from "./Field";
import * as tbbox from "@sinclair/typebox";
import type { TFieldTSType } from "data/entities/EntityTypescript"; 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 const primaryFieldTypes = ["integer", "uuid"] as const;
export type TPrimaryFieldFormat = (typeof primaryFieldTypes)[number]; export type TPrimaryFieldFormat = (typeof primaryFieldTypes)[number];
export const primaryFieldConfigSchema = Type.Composite([ export const primaryFieldConfigSchema = s
Type.Omit(baseFieldConfigSchema, ["required"]), .strictObject({
Type.Object({ format: s.string({ enum: primaryFieldTypes, default: "integer" }),
format: Type.Optional(StringEnum(primaryFieldTypes, { default: "integer" })), required: s.boolean({ default: false }),
required: Type.Optional(Type.Literal(false)), ...omitKeys(baseFieldConfigSchema.properties, ["required"]),
}), })
]); .partial();
export type PrimaryFieldConfig = Static<typeof primaryFieldConfigSchema>; export type PrimaryFieldConfig = s.Static<typeof primaryFieldConfigSchema>;
export class PrimaryField<Required extends true | false = false> extends Field< export class PrimaryField<Required extends true | false = false> extends Field<
PrimaryFieldConfig, PrimaryFieldConfig,
@@ -41,7 +41,7 @@ export class PrimaryField<Required extends true | false = false> extends Field<
return this.config.format ?? "integer"; return this.config.format ?? "integer";
} }
get fieldType() { get fieldType(): "integer" | "text" {
return this.format === "integer" ? "integer" : "text"; return this.format === "integer" ? "integer" : "text";
} }
@@ -67,11 +67,11 @@ export class PrimaryField<Required extends true | false = false> extends Field<
} }
override toJsonSchema() { override toJsonSchema() {
if (this.format === "uuid") { return this.toSchemaWrapIfRequired(
return this.toSchemaWrapIfRequired(Type.String({ writeOnly: undefined })); this.format === "integer"
} ? s.number({ writeOnly: undefined })
: s.string({ writeOnly: undefined }),
return this.toSchemaWrapIfRequired(Type.Number({ writeOnly: undefined })); );
} }
override toType(): TFieldTSType { override toType(): TFieldTSType {
+16 -34
View File
@@ -1,42 +1,24 @@
import type { EntityManager } from "data"; import type { EntityManager } from "data";
import type { Static } from "core/utils"; import { omitKeys } from "core/utils";
import { TransformPersistFailedException } from "../errors"; import { TransformPersistFailedException } from "../errors";
import { Field, type TActionContext, baseFieldConfigSchema } from "./Field"; import { Field, type TActionContext, baseFieldConfigSchema } from "./Field";
import * as tb from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tb;
export const textFieldConfigSchema = Type.Composite( export const textFieldConfigSchema = s
[ .strictObject({
Type.Object({ default_value: s.string(),
default_value: Type.Optional(Type.String()), minLength: s.number(),
minLength: Type.Optional(Type.Number()), maxLength: s.number(),
maxLength: Type.Optional(Type.Number()), pattern: s.string(),
pattern: Type.Optional(Type.String()), html_config: s.object({
html_config: Type.Optional( element: s.string({ default: "input" }),
Type.Object({ props: s.record(s.anyOf([s.string({ title: "String" }), s.number({ title: "Number" })])),
element: Type.Optional(Type.String({ default: "input" })),
props: Type.Optional(
Type.Object(
{},
{
additionalProperties: Type.Union([
Type.String({ title: "String" }),
Type.Number({ title: "Number" }),
]),
},
),
),
}), }),
), ...omitKeys(baseFieldConfigSchema.properties, ["default_value"]),
}), })
baseFieldConfigSchema, .partial();
],
{
additionalProperties: false,
},
);
export type TextFieldConfig = Static<typeof textFieldConfigSchema>; export type TextFieldConfig = s.Static<typeof textFieldConfigSchema>;
export class TextField<Required extends true | false = false> extends Field< export class TextField<Required extends true | false = false> extends Field<
TextFieldConfig, TextFieldConfig,
@@ -113,7 +95,7 @@ export class TextField<Required extends true | false = false> extends Field<
override toJsonSchema() { override toJsonSchema() {
return this.toSchemaWrapIfRequired( return this.toSchemaWrapIfRequired(
Type.String({ s.string({
default: this.getDefault(), default: this.getDefault(),
minLength: this.config?.minLength, minLength: this.config?.minLength,
maxLength: this.config?.maxLength, maxLength: this.config?.maxLength,
+8 -6
View File
@@ -1,11 +1,13 @@
import type { Static } from "core/utils";
import { Field, baseFieldConfigSchema } from "./Field"; import { Field, baseFieldConfigSchema } from "./Field";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox;
export const virtualFieldConfigSchema = Type.Composite([baseFieldConfigSchema, Type.Object({})]); export const virtualFieldConfigSchema = s
.strictObject({
...baseFieldConfigSchema.properties,
})
.partial();
export type VirtualFieldConfig = Static<typeof virtualFieldConfigSchema>; export type VirtualFieldConfig = s.Static<typeof virtualFieldConfigSchema>;
export class VirtualField extends Field<VirtualFieldConfig> { export class VirtualField extends Field<VirtualFieldConfig> {
override readonly type = "virtual"; override readonly type = "virtual";
@@ -25,7 +27,7 @@ export class VirtualField extends Field<VirtualFieldConfig> {
override toJsonSchema() { override toJsonSchema() {
return this.toSchemaWrapIfRequired( return this.toSchemaWrapIfRequired(
Type.Any({ s.any({
default: this.getDefault(), default: this.getDefault(),
readOnly: true, readOnly: true,
}), }),
+9 -10
View File
@@ -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 { ExpressionBuilder, SelectQueryBuilder } from "kysely";
import type { Entity, EntityData, EntityManager } from "../entities"; import type { Entity, EntityData, EntityManager } from "../entities";
import { import {
@@ -8,9 +8,8 @@ import {
} from "../relations"; } from "../relations";
import type { RepoQuery } from "../server/query"; import type { RepoQuery } from "../server/query";
import type { RelationType } from "./relation-types"; import type { RelationType } from "./relation-types";
import * as tbbox from "@sinclair/typebox";
import type { PrimaryFieldType } from "core"; import type { PrimaryFieldType } from "core";
const { Type } = tbbox; import { s } from "core/object/schema";
const directions = ["source", "target"] as const; const directions = ["source", "target"] as const;
export type TDirection = (typeof directions)[number]; export type TDirection = (typeof directions)[number];
@@ -18,13 +17,13 @@ export type TDirection = (typeof directions)[number];
export type KyselyJsonFrom = any; export type KyselyJsonFrom = any;
export type KyselyQueryBuilder = SelectQueryBuilder<any, any, any>; export type KyselyQueryBuilder = SelectQueryBuilder<any, any, any>;
export type BaseRelationConfig = Static<typeof EntityRelation.schema>; export type BaseRelationConfig = s.Static<typeof EntityRelation.schema>;
// @todo: add generic type for relation config // @todo: add generic type for relation config
export abstract class EntityRelation< export abstract class EntityRelation<
Schema extends typeof EntityRelation.schema = typeof EntityRelation.schema, Schema extends typeof EntityRelation.schema = typeof EntityRelation.schema,
> { > {
config: Static<Schema>; config: s.Static<Schema>;
source: EntityRelationAnchor; source: EntityRelationAnchor;
target: EntityRelationAnchor; target: EntityRelationAnchor;
@@ -33,17 +32,17 @@ export abstract class EntityRelation<
// allowed directions, used in RelationAccessor for visibility // allowed directions, used in RelationAccessor for visibility
directions: TDirection[] = ["source", "target"]; directions: TDirection[] = ["source", "target"];
static schema = Type.Object({ static schema = s.strictObject({
mappedBy: Type.Optional(Type.String()), mappedBy: s.string().optional(),
inversedBy: Type.Optional(Type.String()), inversedBy: s.string().optional(),
required: Type.Optional(Type.Boolean()), required: s.boolean().optional(),
}); });
// don't make protected, App requires it to instantiatable // don't make protected, App requires it to instantiatable
constructor( constructor(
source: EntityRelationAnchor, source: EntityRelationAnchor,
target: EntityRelationAnchor, target: EntityRelationAnchor,
config: Partial<Static<Schema>> = {}, config: Partial<s.Static<Schema>> = {},
) { ) {
this.source = source; this.source = source;
this.target = target; this.target = target;
+7 -16
View File
@@ -1,4 +1,3 @@
import type { Static } from "core/utils";
import type { ExpressionBuilder } from "kysely"; import type { ExpressionBuilder } from "kysely";
import { Entity, type EntityManager } from "../entities"; import { Entity, type EntityManager } from "../entities";
import { type Field, PrimaryField } from "../fields"; import { type Field, PrimaryField } from "../fields";
@@ -7,10 +6,9 @@ import { EntityRelation, type KyselyQueryBuilder } from "./EntityRelation";
import { EntityRelationAnchor } from "./EntityRelationAnchor"; import { EntityRelationAnchor } from "./EntityRelationAnchor";
import { RelationField } from "./RelationField"; import { RelationField } from "./RelationField";
import { type RelationType, RelationTypes } from "./relation-types"; import { type RelationType, RelationTypes } from "./relation-types";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox;
export type ManyToManyRelationConfig = Static<typeof ManyToManyRelation.schema>; export type ManyToManyRelationConfig = s.Static<typeof ManyToManyRelation.schema>;
export class ManyToManyRelation extends EntityRelation<typeof ManyToManyRelation.schema> { export class ManyToManyRelation extends EntityRelation<typeof ManyToManyRelation.schema> {
connectionEntity: Entity; connectionEntity: Entity;
@@ -18,18 +16,11 @@ export class ManyToManyRelation extends EntityRelation<typeof ManyToManyRelation
connectionTableMappedName: string; connectionTableMappedName: string;
private em?: EntityManager<any>; private em?: EntityManager<any>;
static override schema = Type.Composite( static override schema = s.strictObject({
[ connectionTable: s.string().optional(),
EntityRelation.schema, connectionTableMappedName: s.string().optional(),
Type.Object({ ...EntityRelation.schema.properties,
connectionTable: Type.Optional(Type.String()), });
connectionTableMappedName: Type.Optional(Type.String()),
}),
],
{
additionalProperties: false,
},
);
constructor( constructor(
source: Entity, source: Entity,
+13 -24
View File
@@ -1,6 +1,5 @@
import type { PrimaryFieldType } from "core"; import type { PrimaryFieldType } from "core";
import { snakeToPascalWithSpaces } from "core/utils"; import { snakeToPascalWithSpaces } from "core/utils";
import type { Static } from "core/utils";
import type { ExpressionBuilder } from "kysely"; import type { ExpressionBuilder } from "kysely";
import type { Entity, EntityManager } from "../entities"; import type { Entity, EntityManager } from "../entities";
import type { RepoQuery } from "../server/query"; import type { RepoQuery } from "../server/query";
@@ -9,8 +8,7 @@ import { EntityRelationAnchor } from "./EntityRelationAnchor";
import { RelationField, type RelationFieldBaseConfig } from "./RelationField"; import { RelationField, type RelationFieldBaseConfig } from "./RelationField";
import type { MutationInstructionResponse } from "./RelationMutator"; import type { MutationInstructionResponse } from "./RelationMutator";
import { type RelationType, RelationTypes } from "./relation-types"; import { type RelationType, RelationTypes } from "./relation-types";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox;
/** /**
* Source entity receives the mapping field * Source entity receives the mapping field
@@ -20,7 +18,7 @@ const { Type } = tbbox;
* posts gets a users_id field * posts gets a users_id field
*/ */
export type ManyToOneRelationConfig = Static<typeof ManyToOneRelation.schema>; export type ManyToOneRelationConfig = s.Static<typeof ManyToOneRelation.schema>;
export class ManyToOneRelation extends EntityRelation<typeof ManyToOneRelation.schema> { export class ManyToOneRelation extends EntityRelation<typeof ManyToOneRelation.schema> {
private fieldConfig?: RelationFieldBaseConfig; private fieldConfig?: RelationFieldBaseConfig;
@@ -28,30 +26,21 @@ export class ManyToOneRelation extends EntityRelation<typeof ManyToOneRelation.s
with_limit: 5, with_limit: 5,
}; };
static override schema = Type.Composite( static override schema = s.strictObject({
[ sourceCardinality: s.number().optional(),
EntityRelation.schema, with_limit: s.number({ default: ManyToOneRelation.DEFAULTS.with_limit }).optional(),
Type.Object({ fieldConfig: s
sourceCardinality: Type.Optional(Type.Number()), .object({
with_limit: Type.Optional( label: s.string(),
Type.Number({ default: ManyToOneRelation.DEFAULTS.with_limit }), })
), .optional(),
fieldConfig: Type.Optional( ...EntityRelation.schema.properties,
Type.Object({ });
label: Type.String(),
}),
),
}),
],
{
additionalProperties: false,
},
);
constructor( constructor(
source: Entity, source: Entity,
target: Entity, target: Entity,
config: Partial<Static<typeof ManyToOneRelation.schema>> = {}, config: Partial<s.Static<typeof ManyToOneRelation.schema>> = {},
) { ) {
const mappedBy = config.mappedBy || target.name; const mappedBy = config.mappedBy || target.name;
const inversedBy = config.inversedBy || source.name; const inversedBy = config.inversedBy || source.name;
+6 -15
View File
@@ -1,4 +1,3 @@
import type { Static } from "core/utils";
import type { ExpressionBuilder } from "kysely"; import type { ExpressionBuilder } from "kysely";
import type { Entity, EntityManager } from "../entities"; import type { Entity, EntityManager } from "../entities";
import { NumberField, TextField } from "../fields"; import { NumberField, TextField } from "../fields";
@@ -6,24 +5,16 @@ import type { RepoQuery } from "../server/query";
import { EntityRelation, type KyselyJsonFrom, type KyselyQueryBuilder } from "./EntityRelation"; import { EntityRelation, type KyselyJsonFrom, type KyselyQueryBuilder } from "./EntityRelation";
import { EntityRelationAnchor } from "./EntityRelationAnchor"; import { EntityRelationAnchor } from "./EntityRelationAnchor";
import { type RelationType, RelationTypes } from "./relation-types"; import { type RelationType, RelationTypes } from "./relation-types";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox;
export type PolymorphicRelationConfig = Static<typeof PolymorphicRelation.schema>; export type PolymorphicRelationConfig = s.Static<typeof PolymorphicRelation.schema>;
// @todo: what about cascades? // @todo: what about cascades?
export class PolymorphicRelation extends EntityRelation<typeof PolymorphicRelation.schema> { export class PolymorphicRelation extends EntityRelation<typeof PolymorphicRelation.schema> {
static override schema = Type.Composite( static override schema = s.strictObject({
[ targetCardinality: s.number().optional(),
EntityRelation.schema, ...EntityRelation.schema.properties,
Type.Object({ });
targetCardinality: Type.Optional(Type.Number()),
}),
],
{
additionalProperties: false,
},
);
constructor(source: Entity, target: Entity, config: Partial<PolymorphicRelationConfig> = {}) { constructor(source: Entity, target: Entity, config: Partial<PolymorphicRelationConfig> = {}) {
const mappedBy = config.mappedBy || target.name; const mappedBy = config.mappedBy || target.name;
+12 -16
View File
@@ -1,26 +1,22 @@
import { type Static, StringEnum } from "core/utils";
import type { EntityManager } from "../entities"; import type { EntityManager } from "../entities";
import { Field, baseFieldConfigSchema, primaryFieldTypes } from "../fields"; import { Field, baseFieldConfigSchema } from "../fields";
import type { EntityRelation } from "./EntityRelation"; import type { EntityRelation } from "./EntityRelation";
import type { EntityRelationAnchor } from "./EntityRelationAnchor"; import type { EntityRelationAnchor } from "./EntityRelationAnchor";
import * as tbbox from "@sinclair/typebox";
import type { TFieldTSType } from "data/entities/EntityTypescript"; 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; const CASCADES = ["cascade", "set null", "set default", "restrict", "no action"] as const;
export const relationFieldConfigSchema = Type.Composite([ export const relationFieldConfigSchema = s.strictObject({
baseFieldConfigSchema, reference: s.string(),
Type.Object({ target: s.string(), // @todo: potentially has to be an instance!
reference: Type.String(), target_field: s.string({ default: "id" }).optional(),
target: Type.String(), // @todo: potentially has to be an instance! target_field_type: s.string({ enum: ["text", "integer"], default: "integer" }).optional(),
target_field: Type.Optional(Type.String({ default: "id" })), on_delete: s.string({ enum: CASCADES, default: "set null" }).optional(),
target_field_type: Type.Optional(StringEnum(["integer", "text"], { default: "integer" })), ...baseFieldConfigSchema.properties,
on_delete: Type.Optional(StringEnum(CASCADES, { default: "set null" })), });
}),
]);
export type RelationFieldConfig = Static<typeof relationFieldConfigSchema>; export type RelationFieldConfig = s.Static<typeof relationFieldConfigSchema>;
export type RelationFieldBaseConfig = { label?: string }; export type RelationFieldBaseConfig = { label?: string };
export class RelationField extends Field<RelationFieldConfig> { export class RelationField extends Field<RelationFieldConfig> {
@@ -81,7 +77,7 @@ export class RelationField extends Field<RelationFieldConfig> {
override toJsonSchema() { override toJsonSchema() {
return this.toSchemaWrapIfRequired( return this.toSchemaWrapIfRequired(
Type.Number({ s.number({
$ref: `${this.config?.target}#/properties/${this.config?.target_field}`, $ref: `${this.config?.target}#/properties/${this.config?.target_field}`,
}), }),
); );
+23 -7
View File
@@ -2,7 +2,7 @@ import { s } from "core/object/schema";
import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder"; import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder";
import { $console } from "core"; import { $console } from "core";
import { isObject } from "core/utils"; import { isObject } from "core/utils";
import type { CoercionOptions, TAnyOf } from "jsonv-ts"; import type { anyOf, CoercionOptions, Schema } from "jsonv-ts";
// ------- // -------
// helpers // helpers
@@ -36,10 +36,12 @@ const stringArray = s.anyOf(
// ------- // -------
// sorting // sorting
const sortDefault = { by: "id", dir: "asc" }; const sortDefault = { by: "id", dir: "asc" };
const sortSchema = s.object({ const sortSchema = s
.object({
by: s.string(), by: s.string(),
dir: s.string({ enum: ["asc", "desc"] }).optional(), dir: s.string({ enum: ["asc", "desc"] }).optional(),
}); })
.strict();
type SortSchema = s.Static<typeof sortSchema>; type SortSchema = s.Static<typeof sortSchema>;
const sort = s.anyOf([s.string(), sortSchema], { const sort = s.anyOf([s.string(), sortSchema], {
default: sortDefault, default: sortDefault,
@@ -88,9 +90,9 @@ export type RepoWithSchema = Record<
} }
>; >;
const withSchema = <In, Out = In>(self: s.TSchema): s.TSchemaInOut<In, Out> => const withSchema = <Type = unknown>(self: Schema): Schema<{}, Type, Type> =>
s.anyOf([stringIdentifier, s.array(stringIdentifier), self], { s.anyOf([stringIdentifier, s.array(stringIdentifier), self], {
coerce: function (this: TAnyOf<any>, _value: unknown, opts: CoercionOptions = {}) { coerce: function (this: typeof anyOf, _value: unknown, opts: CoercionOptions = {}) {
let value: any = _value; let value: any = _value;
if (typeof value === "string") { if (typeof value === "string") {
@@ -126,7 +128,8 @@ const withSchema = <In, Out = In>(self: s.TSchema): s.TSchemaInOut<In, Out> =>
// ========== // ==========
// REPO QUERY // REPO QUERY
export const repoQuery = s.recursive((self) => export const repoQuery = s.recursive((self) =>
s.partialObject({ s
.object({
limit: s.number({ default: 10 }), limit: s.number({ default: 10 }),
offset: s.number({ default: 0 }), offset: s.number({ default: 0 }),
sort, sort,
@@ -134,7 +137,8 @@ export const repoQuery = s.recursive((self) =>
select: stringArray, select: stringArray,
join: stringArray, join: stringArray,
with: withSchema<RepoWithSchema>(self), with: withSchema<RepoWithSchema>(self),
}), })
.partial(),
); );
export const getRepoQueryTemplate = () => export const getRepoQueryTemplate = () =>
repoQuery.template({ repoQuery.template({
@@ -151,3 +155,15 @@ export type RepoQueryIn = {
where?: WhereQuery; where?: WhereQuery;
}; };
export type RepoQuery = s.StaticCoerced<typeof repoQuery>; export type RepoQuery = s.StaticCoerced<typeof repoQuery>;
//export type RepoQuery = s.StaticCoerced<typeof repoQuery>;
// @todo: CURRENT WORKAROUND
/* export type RepoQuery = {
limit?: number;
offset?: number;
sort?: { by: string; dir: "asc" | "desc" };
select?: string[];
with?: Record<string, RepoQuery>;
join?: string[];
where?: WhereQuery;
}; */
+6 -3
View File
@@ -1,15 +1,16 @@
import { type Static, transformObject } from "core/utils"; import { transformObject } from "core/utils";
import { Flow, HttpTrigger } from "flows"; import { Flow, HttpTrigger } from "flows";
import { Hono } from "hono"; import { Hono } from "hono";
import { Module } from "modules/Module"; import { Module } from "modules/Module";
import { TASKS, flowsConfigSchema } from "./flows-schema"; import { TASKS, flowsConfigSchema } from "./flows-schema";
import type { s } from "core/object/schema";
export type AppFlowsSchema = Static<typeof flowsConfigSchema>; export type AppFlowsSchema = s.Static<typeof flowsConfigSchema>;
export type TAppFlowSchema = AppFlowsSchema["flows"][number]; export type TAppFlowSchema = AppFlowsSchema["flows"][number];
export type TAppFlowTriggerSchema = TAppFlowSchema["trigger"]; export type TAppFlowTriggerSchema = TAppFlowSchema["trigger"];
export type { TAppFlowTaskSchema } from "./flows-schema"; export type { TAppFlowTaskSchema } from "./flows-schema";
export class AppFlows extends Module<typeof flowsConfigSchema> { export class AppFlows extends Module<AppFlowsSchema> {
private flows: Record<string, Flow> = {}; private flows: Record<string, Flow> = {};
getSchema() { getSchema() {
@@ -80,6 +81,8 @@ export class AppFlows extends Module<typeof flowsConfigSchema> {
this.setBuilt(); this.setBuilt();
} }
// @todo: fix this
// @ts-expect-error
override toJSON() { override toJSON() {
return { return {
...this.config, ...this.config,
+39 -58
View File
@@ -1,7 +1,6 @@
import { Const, type Static, StringRecord, transformObject } from "core/utils"; import { transformObject } from "core/utils";
import { TaskMap, TriggerMap } from "flows"; import { TaskMap, TriggerMap } from "flows";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox;
export const TASKS = { export const TASKS = {
...TaskMap, ...TaskMap,
@@ -10,77 +9,59 @@ export const TASKS = {
export const TRIGGERS = TriggerMap; export const TRIGGERS = TriggerMap;
const taskSchemaObject = transformObject(TASKS, (task, name) => { const taskSchemaObject = transformObject(TASKS, (task, name) => {
return Type.Object( return s.strictObject(
{ {
type: Const(name), type: s.literal(name),
params: task.cls.schema, params: task.cls.schema,
}, },
{ title: String(name), additionalProperties: false }, { title: String(name) },
); );
}); });
const taskSchema = Type.Union(Object.values(taskSchemaObject)); const taskSchema = s.anyOf(Object.values(taskSchemaObject));
export type TAppFlowTaskSchema = Static<typeof taskSchema>; export type TAppFlowTaskSchema = s.Static<typeof taskSchema>;
const triggerSchemaObject = transformObject(TRIGGERS, (trigger, name) => { const triggerSchemaObject = transformObject(TRIGGERS, (trigger, name) => {
return Type.Object( return s.strictObject(
{ {
type: Const(name), type: s.literal(name),
config: trigger.cls.schema, 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<typeof triggerSchema>;
const connectionSchema = Type.Object({ const connectionSchema = s.strictObject({
source: Type.String(), source: s.string(),
target: Type.String(), target: s.string(),
config: Type.Object( config: s
{ .strictObject({
condition: Type.Optional( condition: s.anyOf([
Type.Union([ s.strictObject({ type: s.literal("success") }, { title: "success" }),
Type.Object( s.strictObject({ type: s.literal("error") }, { title: "error" }),
{ type: Const("success") }, s.strictObject(
{ additionalProperties: false, title: "success" }, { type: s.literal("matches"), path: s.string(), value: s.string() },
), { title: "matches" },
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: s.number(),
max_retries: Type.Optional(Type.Number()), })
}, .partial(),
{ default: {}, additionalProperties: false },
),
}); });
// @todo: rework to have fixed ids per task and connections (and preferrably arrays) // @todo: rework to have fixed ids per task and connections (and preferrably arrays)
// causes issues with canvas // causes issues with canvas
export const flowSchema = Type.Object( export const flowSchema = s.strictObject({
{ trigger: s.anyOf(Object.values(triggerSchemaObject)),
trigger: Type.Union(Object.values(triggerSchemaObject)), tasks: s.record(s.anyOf(Object.values(taskSchemaObject))).optional(),
tasks: Type.Optional(StringRecord(Type.Union(Object.values(taskSchemaObject)))), connections: s.record(connectionSchema).optional(),
connections: Type.Optional(StringRecord(connectionSchema)), start_task: s.string().optional(),
start_task: Type.Optional(Type.String()), responding_task: s.string().optional(),
responding_task: Type.Optional(Type.String()), });
}, export type TAppFlowSchema = s.Static<typeof flowSchema>;
{
additionalProperties: false,
},
);
export type TAppFlowSchema = Static<typeof flowSchema>;
export const flowsConfigSchema = Type.Object( export const flowsConfigSchema = s.strictObject({
{ basepath: s.string({ default: "/api/flows" }),
basepath: Type.String({ default: "/api/flows" }), flows: s.record(flowSchema, { default: {} }),
flows: StringRecord(flowSchema, { default: {} }), });
},
{
default: {},
additionalProperties: false,
},
);
+5 -9
View File
@@ -2,19 +2,15 @@ import type { EventManager } from "core/events";
import type { Flow } from "../Flow"; import type { Flow } from "../Flow";
import { Trigger } from "./Trigger"; import { Trigger } from "./Trigger";
import { $console } from "core"; import { $console } from "core";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox;
export class EventTrigger extends Trigger<typeof EventTrigger.schema> { export class EventTrigger extends Trigger<typeof EventTrigger.schema> {
override type = "event"; override type = "event";
static override schema = Type.Composite([ static override schema = s.strictObject({
Trigger.schema, event: s.string(),
Type.Object({ ...Trigger.schema.properties,
event: Type.String(), });
// add match
}),
]);
override async register(flow: Flow, emgr: EventManager<any>) { override async register(flow: Flow, emgr: EventManager<any>) {
if (!emgr.eventExists(this.config.event)) { if (!emgr.eventExists(this.config.event)) {
+7 -11
View File
@@ -1,23 +1,19 @@
import { StringEnum } from "core/utils";
import type { Context, Hono } from "hono"; import type { Context, Hono } from "hono";
import type { Flow } from "../Flow"; import type { Flow } from "../Flow";
import { Trigger } from "./Trigger"; import { Trigger } from "./Trigger";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox;
const httpMethods = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const; const httpMethods = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const;
export class HttpTrigger extends Trigger<typeof HttpTrigger.schema> { export class HttpTrigger extends Trigger<typeof HttpTrigger.schema> {
override type = "http"; override type = "http";
static override schema = Type.Composite([ static override schema = s.strictObject({
Trigger.schema, path: s.string({ pattern: "^/.*$" }),
Type.Object({ method: s.string({ enum: httpMethods, default: "GET" }),
path: Type.String({ pattern: "^/.*$" }), response_type: s.string({ enum: ["json", "text", "html"], default: "json" }),
method: StringEnum(httpMethods, { default: "GET" }), ...Trigger.schema.properties,
response_type: StringEnum(["json", "text", "html"], { default: "json" }), });
}),
]);
override async register(flow: Flow, hono: Hono<any>) { override async register(flow: Flow, hono: Hono<any>) {
const method = this.config.method.toLowerCase() as any; const method = this.config.method.toLowerCase() as any;
+5 -7
View File
@@ -1,20 +1,18 @@
import { type Static, StringEnum, parse } from "core/utils";
import type { Execution } from "../Execution"; import type { Execution } from "../Execution";
import type { Flow } from "../Flow"; import type { Flow } from "../Flow";
import * as tbbox from "@sinclair/typebox"; import { s, parse } from "core/object/schema";
const { Type } = tbbox;
export class Trigger<Schema extends typeof Trigger.schema = typeof Trigger.schema> { export class Trigger<Schema extends typeof Trigger.schema = typeof Trigger.schema> {
// @todo: remove this // @todo: remove this
executions: Execution[] = []; executions: Execution[] = [];
type = "manual"; type = "manual";
config: Static<Schema>; config: s.Static<Schema>;
static schema = Type.Object({ static schema = s.strictObject({
mode: StringEnum(["sync", "async"], { default: "async" }), mode: s.string({ enum: ["sync", "async"], default: "async" }),
}); });
constructor(config?: Partial<Static<Schema>>) { constructor(config?: Partial<s.Static<Schema>>) {
const schema = (this.constructor as typeof Trigger).schema; const schema = (this.constructor as typeof Trigger).schema;
// @ts-ignore for now // @ts-ignore for now
this.config = parse(schema, config ?? {}); this.config = parse(schema, config ?? {});
+22 -17
View File
@@ -1,9 +1,10 @@
import type { StaticDecode, TSchema } from "@sinclair/typebox"; //import { BkndError, SimpleRenderer } from "core";
import { BkndError, SimpleRenderer } from "core"; import { BkndError } from "core/errors";
import { type Static, type TObject, Value, parse, ucFirst } from "core/utils";
import { s, parse } from "core/object/schema";
import type { InputsMap } from "../flows/Execution"; import type { InputsMap } from "../flows/Execution";
import * as tbbox from "@sinclair/typebox"; import { SimpleRenderer } from "core/template/SimpleRenderer";
const { Type } = tbbox;
//type InstanceOf<T> = T extends new (...args: any) => infer R ? R : never; //type InstanceOf<T> = T extends new (...args: any) => infer R ? R : never;
export type TaskResult<Output = any> = { export type TaskResult<Output = any> = {
@@ -16,7 +17,10 @@ export type TaskResult<Output = any> = {
export type TaskRenderProps<T extends Task = Task> = any; export type TaskRenderProps<T extends Task = Task> = any;
export function dynamic<Type extends TSchema>( // @todo: CURRENT WORKAROUND
export const dynamic = <S extends s.Schema>(a: S, b?: any) => null as unknown as S;
/* export function dynamic<Type extends TSchema>(
type: Type, type: Type,
parse?: (val: any | string) => Static<Type>, parse?: (val: any | string) => Static<Type>,
) { ) {
@@ -51,23 +55,23 @@ export function dynamic<Type extends TSchema>(
// @ts-ignore // @ts-ignore
.Encode((val) => val) .Encode((val) => val)
); );
} } */
export abstract class Task<Params extends TObject = TObject, Output = unknown> { export abstract class Task<Params extends s.Schema = s.Schema, Output = unknown> {
abstract type: string; abstract type: string;
name: string; name: string;
/** /**
* The schema of the task's parameters. * The schema of the task's parameters.
*/ */
static schema = Type.Object({}); static schema = s.any();
/** /**
* The task's parameters. * The task's parameters.
*/ */
_params: Static<Params>; _params: s.Static<Params>;
constructor(name: string, params?: Static<Params>) { constructor(name: string, params?: s.Static<Params>) {
if (typeof name !== "string") { if (typeof name !== "string") {
throw new Error(`Task name must be a string, got ${typeof name}`); throw new Error(`Task name must be a string, got ${typeof name}`);
} }
@@ -81,7 +85,7 @@ export abstract class Task<Params extends TObject = TObject, Output = unknown> {
if ( if (
schema === Task.schema && schema === Task.schema &&
typeof params !== "undefined" && typeof params !== "undefined" &&
Object.keys(params).length > 0 Object.keys(params || {}).length > 0
) { ) {
throw new Error( throw new Error(
`Task "${name}" has no schema defined but params passed: ${JSON.stringify(params)}`, `Task "${name}" has no schema defined but params passed: ${JSON.stringify(params)}`,
@@ -93,18 +97,18 @@ export abstract class Task<Params extends TObject = TObject, Output = unknown> {
} }
get params() { get params() {
return this._params as StaticDecode<Params>; return this._params as s.StaticCoerced<Params>;
} }
protected clone(name: string, params: Static<Params>): Task { protected clone(name: string, params: s.Static<Params>): Task {
return new (this.constructor as any)(name, params); return new (this.constructor as any)(name, params);
} }
static async resolveParams<S extends TSchema>( static async resolveParams<S extends s.Schema>(
schema: S, schema: S,
params: any, params: any,
inputs: object = {}, inputs: object = {},
): Promise<StaticDecode<S>> { ): Promise<s.StaticCoerced<S>> {
const newParams: any = {}; const newParams: any = {};
const renderer = new SimpleRenderer(inputs, { renderKeys: true }); const renderer = new SimpleRenderer(inputs, { renderKeys: true });
@@ -134,7 +138,8 @@ export abstract class Task<Params extends TObject = TObject, Output = unknown> {
newParams[key] = value; newParams[key] = value;
} }
return Value.Decode(schema, newParams); return schema.coerce(newParams);
//return Value.Decode(schema, newParams);
} }
private async cloneWithResolvedParams(_inputs: Map<string, any>) { private async cloneWithResolvedParams(_inputs: Map<string, any>) {
+12 -16
View File
@@ -1,7 +1,5 @@
import { StringEnum } from "core/utils";
import { Task, dynamic } from "../Task"; import { Task, dynamic } from "../Task";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox;
const FetchMethods = ["GET", "POST", "PUT", "PATCH", "DELETE"]; const FetchMethods = ["GET", "POST", "PUT", "PATCH", "DELETE"];
@@ -11,24 +9,22 @@ export class FetchTask<Output extends Record<string, any>> extends Task<
> { > {
type = "fetch"; type = "fetch";
static override schema = Type.Object({ static override schema = s.strictObject({
url: Type.String({ url: s.string({
pattern: "^(http|https)://", pattern: "^(http|https)://",
}), }),
method: Type.Optional(dynamic(StringEnum(FetchMethods, { default: "GET" }))), method: dynamic(s.string({ enum: FetchMethods, default: "GET" })).optional(),
headers: Type.Optional( headers: dynamic(
dynamic( s.array(
Type.Array( s.strictObject({
Type.Object({ key: s.string(),
key: Type.String(), value: s.string(),
value: Type.String(),
}), }),
), ),
JSON.parse, JSON.parse,
), ).optional(),
), body: dynamic(s.string()).optional(),
body: Type.Optional(dynamic(Type.String())), normal: dynamic(s.number(), Number.parseInt).optional(),
normal: Type.Optional(dynamic(Type.Number(), Number.parseInt)),
}); });
protected getBody(): string | undefined { protected getBody(): string | undefined {
+3 -4
View File
@@ -1,13 +1,12 @@
import { Task } from "../Task"; import { Task } from "../Task";
import { $console } from "core"; import { $console } from "core";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox;
export class LogTask extends Task<typeof LogTask.schema> { export class LogTask extends Task<typeof LogTask.schema> {
type = "log"; type = "log";
static override schema = Type.Object({ static override schema = s.strictObject({
delay: Type.Number({ default: 10 }), delay: s.number({ default: 10 }),
}); });
async execute() { async execute() {
+3 -4
View File
@@ -1,6 +1,5 @@
import { Task } from "../Task"; import { Task } from "../Task";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox;
export class RenderTask<Output extends Record<string, any>> extends Task< export class RenderTask<Output extends Record<string, any>> extends Task<
typeof RenderTask.schema, typeof RenderTask.schema,
@@ -8,8 +7,8 @@ export class RenderTask<Output extends Record<string, any>> extends Task<
> { > {
type = "render"; type = "render";
static override schema = Type.Object({ static override schema = s.strictObject({
render: Type.String(), render: s.string(),
}); });
async execute() { async execute() {
+5 -6
View File
@@ -1,7 +1,6 @@
import { Flow } from "../../flows/Flow"; import { Flow } from "../../flows/Flow";
import { Task, dynamic } from "../Task"; import { Task, dynamic } from "../Task";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox;
export class SubFlowTask<Output extends Record<string, any>> extends Task< export class SubFlowTask<Output extends Record<string, any>> extends Task<
typeof SubFlowTask.schema, typeof SubFlowTask.schema,
@@ -9,10 +8,10 @@ export class SubFlowTask<Output extends Record<string, any>> extends Task<
> { > {
type = "subflow"; type = "subflow";
static override schema = Type.Object({ static override schema = s.strictObject({
flow: Type.Any(), flow: s.any(),
input: Type.Optional(dynamic(Type.Any(), JSON.parse)), input: dynamic(s.any(), JSON.parse).optional(),
loop: Type.Optional(Type.Boolean()), loop: s.boolean().optional(),
}); });
async execute() { async execute() {
+2 -2
View File
@@ -13,7 +13,7 @@ import {
text, text,
} from "../data/prototype"; } from "../data/prototype";
import { MediaController } from "./api/MediaController"; 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<typeof AppMedia.mediaFields>; export type MediaFieldSchema = FieldSchema<typeof AppMedia.mediaFields>;
declare module "core" { declare module "core" {
@@ -23,7 +23,7 @@ declare module "core" {
} }
} }
export class AppMedia extends Module<typeof mediaConfigSchema> { export class AppMedia extends Module<TAppMediaConfig> {
private _storage?: Storage; private _storage?: Storage;
override async build() { override async build() {
+11 -13
View File
@@ -1,19 +1,17 @@
import type { Static } from "core/utils";
import { Field, baseFieldConfigSchema } from "data/fields"; import { Field, baseFieldConfigSchema } from "data/fields";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox;
export const mediaFieldConfigSchema = Type.Composite([ export const mediaFieldConfigSchema = s
Type.Object({ .strictObject({
entity: Type.String(), // @todo: is this really required? entity: s.string(), // @todo: is this really required?
min_items: Type.Optional(Type.Number()), min_items: s.number(),
max_items: Type.Optional(Type.Number()), max_items: s.number(),
mime_types: Type.Optional(Type.Array(Type.String())), mime_types: s.array(s.string()),
}), ...baseFieldConfigSchema.properties,
baseFieldConfigSchema, })
]); .partial();
export type MediaFieldConfig = Static<typeof mediaFieldConfigSchema>; export type MediaFieldConfig = s.Static<typeof mediaFieldConfigSchema>;
export type MediaItem = { export type MediaItem = {
id: number; id: number;
+3 -2
View File
@@ -16,6 +16,7 @@ import {
StorageCloudinaryAdapter, StorageCloudinaryAdapter,
} from "./storage/adapters/cloudinary/StorageCloudinaryAdapter"; } from "./storage/adapters/cloudinary/StorageCloudinaryAdapter";
import { type S3AdapterConfig, StorageS3Adapter } from "./storage/adapters/s3/StorageS3Adapter"; import { type S3AdapterConfig, StorageS3Adapter } from "./storage/adapters/s3/StorageS3Adapter";
import type { s } from "core/object/schema";
export { StorageAdapter }; export { StorageAdapter };
export { StorageS3Adapter, type S3AdapterConfig, StorageCloudinaryAdapter, type CloudinaryConfig }; export { StorageS3Adapter, type S3AdapterConfig, StorageCloudinaryAdapter, type CloudinaryConfig };
@@ -29,10 +30,10 @@ type ClassThatImplements<T> = Constructor<T> & { prototype: T };
export const MediaAdapterRegistry = new Registry<{ export const MediaAdapterRegistry = new Registry<{
cls: ClassThatImplements<StorageAdapter>; cls: ClassThatImplements<StorageAdapter>;
schema: TObject; schema: s.Schema;
}>((cls: ClassThatImplements<StorageAdapter>) => ({ }>((cls: ClassThatImplements<StorageAdapter>) => ({
cls, cls,
schema: cls.prototype.getSchema() as TObject, schema: cls.prototype.getSchema() as s.Schema,
})) }))
.register("s3", StorageS3Adapter) .register("s3", StorageS3Adapter)
.register("cloudinary", StorageCloudinaryAdapter); .register("cloudinary", StorageCloudinaryAdapter);
+13 -23
View File
@@ -1,8 +1,7 @@
import { Const, type Static, objectTransform } from "core/utils"; import { objectTransform } from "core/utils";
import { Adapters } from "media"; import { Adapters } from "media";
import { registries } from "modules/registries"; import { registries } from "modules/registries";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox;
export const ADAPTERS = { export const ADAPTERS = {
...Adapters, ...Adapters,
@@ -12,42 +11,33 @@ export const registry = registries.media;
export function buildMediaSchema() { export function buildMediaSchema() {
const adapterSchemaObject = objectTransform(registry.all(), (adapter, name) => { const adapterSchemaObject = objectTransform(registry.all(), (adapter, name) => {
return Type.Object( return s.strictObject(
{ {
type: Const(name), type: s.literal(name),
config: adapter.schema, config: adapter.schema,
}, },
{ {
title: adapter.schema?.title ?? name, title: adapter.schema?.title ?? name,
description: adapter.schema?.description, description: adapter.schema?.description,
additionalProperties: false,
}, },
); );
}); });
const adapterSchema = Type.Union(Object.values(adapterSchemaObject));
return Type.Object( return s.strictObject({
enabled: s.boolean({ default: false }),
basepath: s.string({ default: "/api/media" }),
entity_name: s.string({ default: "media" }),
storage: s.strictObject(
{ {
enabled: Type.Boolean({ default: false }), body_max_size: s.number({
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.", description: "Max size of the body in bytes. Leave blank for unlimited.",
}), }),
),
}, },
{ default: {} }, { default: {} },
), ),
adapter: Type.Optional(adapterSchema), adapter: s.anyOf(Object.values(adapterSchemaObject)).optional(),
}, });
{
additionalProperties: false,
},
);
} }
export const mediaConfigSchema = buildMediaSchema(); export const mediaConfigSchema = buildMediaSchema();
export type TAppMediaConfig = Static<typeof mediaConfigSchema>; export type TAppMediaConfig = s.Static<typeof mediaConfigSchema>;
+2 -2
View File
@@ -1,6 +1,6 @@
import type { FileListObject, FileMeta } from "media"; import type { FileListObject, FileMeta } from "media";
import type { FileBody, FileUploadPayload } from "media/storage/Storage"; 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"); const SYMBOL = Symbol.for("bknd:storage");
@@ -32,6 +32,6 @@ export abstract class StorageAdapter {
abstract getObject(key: string, headers: Headers): Promise<Response>; abstract getObject(key: string, headers: Headers): Promise<Response>;
abstract getObjectUrl(key: string): string; abstract getObjectUrl(key: string): string;
abstract getObjectMeta(key: string): Promise<FileMeta>; abstract getObjectMeta(key: string): Promise<FileMeta>;
abstract getSchema(): TSchema | undefined; abstract getSchema(): s.Schema | undefined;
abstract toJSON(secrets?: boolean): any; abstract toJSON(secrets?: boolean): any;
} }
@@ -1,21 +1,19 @@
import { hash, pickHeaders } from "core/utils"; import { hash, pickHeaders } from "core/utils";
import { type Static, parse } from "core/utils";
import type { FileBody, FileListObject, FileMeta } from "../../Storage"; import type { FileBody, FileListObject, FileMeta } from "../../Storage";
import { StorageAdapter } from "../../StorageAdapter"; import { StorageAdapter } from "../../StorageAdapter";
import * as tbbox from "@sinclair/typebox"; import { s, parse } from "core/object/schema";
const { Type } = tbbox;
export const cloudinaryAdapterConfig = Type.Object( export const cloudinaryAdapterConfig = s.object(
{ {
cloud_name: Type.String(), cloud_name: s.string(),
api_key: Type.String(), api_key: s.string(),
api_secret: Type.String(), api_secret: s.string(),
upload_preset: Type.Optional(Type.String()), upload_preset: s.string().optional(),
}, },
{ title: "Cloudinary", description: "Cloudinary media storage" }, { title: "Cloudinary", description: "Cloudinary media storage" },
); );
export type CloudinaryConfig = Static<typeof cloudinaryAdapterConfig>; export type CloudinaryConfig = s.Static<typeof cloudinaryAdapterConfig>;
type CloudinaryObject = { type CloudinaryObject = {
asset_id: string; asset_id: string;
@@ -7,18 +7,17 @@ import type {
PutObjectRequest, PutObjectRequest,
} from "@aws-sdk/client-s3"; } from "@aws-sdk/client-s3";
import { AwsClient, isDebug } from "core"; 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 { transform } from "lodash-es";
import type { FileBody, FileListObject } from "../../Storage"; import type { FileBody, FileListObject } from "../../Storage";
import { StorageAdapter } from "../../StorageAdapter"; import { StorageAdapter } from "../../StorageAdapter";
import * as tbbox from "@sinclair/typebox"; import { parse, s } from "core/object/schema";
const { Type } = tbbox;
export const s3AdapterConfig = Type.Object( export const s3AdapterConfig = s.object(
{ {
access_key: Type.String(), access_key: s.string(),
secret_access_key: Type.String(), secret_access_key: s.string(),
url: Type.String({ url: s.string({
pattern: "^https?://(?:.*)?[^/.]+$", pattern: "^https?://(?:.*)?[^/.]+$",
description: "URL to S3 compatible endpoint without trailing slash", description: "URL to S3 compatible endpoint without trailing slash",
examples: [ examples: [
@@ -33,7 +32,7 @@ export const s3AdapterConfig = Type.Object(
}, },
); );
export type S3AdapterConfig = Static<typeof s3AdapterConfig>; export type S3AdapterConfig = s.Static<typeof s3AdapterConfig>;
export class StorageS3Adapter extends StorageAdapter { export class StorageS3Adapter extends StorageAdapter {
readonly #config: S3AdapterConfig; readonly #config: S3AdapterConfig;
+17 -2
View File
@@ -5,7 +5,7 @@ import type { SafeUser } from "auth";
import type { EntityManager } from "data"; import type { EntityManager } from "data";
import { s } from "core/object/schema"; import { s } from "core/object/schema";
export type ServerEnv = Env & { export interface ServerEnv extends Env {
Variables: { Variables: {
app: App; app: App;
// to prevent resolving auth multiple times // to prevent resolving auth multiple times
@@ -17,7 +17,22 @@ export type ServerEnv = Env & {
}; };
html?: string; 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 { export class Controller {
protected middlewares = middlewares; protected middlewares = middlewares;
+13 -8
View File
@@ -1,11 +1,13 @@
import type { Guard } from "auth"; import type { Guard } from "auth";
import { type DebugLogger, SchemaObject } from "core"; import { type DebugLogger, SchemaObject } from "core";
import type { EventManager } from "core/events"; import type { EventManager } from "core/events";
import type { Static, TSchema } from "core/utils";
import type { Connection, EntityManager } from "data"; import type { Connection, EntityManager } from "data";
import type { Hono } from "hono"; import type { Hono } from "hono";
import type { ServerEnv } from "modules/Controller"; import type { ServerEnv } from "modules/Controller";
import type { ModuleHelper } from "./ModuleHelper"; import type { ModuleHelper } from "./ModuleHelper";
import type { s } from "core/object/schema";
type PartialRec<T> = { [P in keyof T]?: PartialRec<T[P]> };
export type ModuleBuildContext = { export type ModuleBuildContext = {
connection: Connection; connection: Connection;
@@ -18,13 +20,13 @@ export type ModuleBuildContext = {
helper: ModuleHelper; helper: ModuleHelper;
}; };
export abstract class Module<Schema extends TSchema = TSchema, ConfigSchema = Static<Schema>> { export abstract class Module<Schema extends object = object> {
private _built = false; private _built = false;
private _schema: SchemaObject<ReturnType<(typeof this)["getSchema"]>>; private _schema: SchemaObject<ReturnType<(typeof this)["getSchema"]>>;
private _listener: any = () => null; private _listener: any = () => null;
constructor( constructor(
initial?: Partial<Static<Schema>>, initial?: PartialRec<Schema>,
protected _ctx?: ModuleBuildContext, protected _ctx?: ModuleBuildContext,
) { ) {
this._schema = new SchemaObject(this.getSchema(), initial, { this._schema = new SchemaObject(this.getSchema(), initial, {
@@ -47,7 +49,7 @@ export abstract class Module<Schema extends TSchema = TSchema, ConfigSchema = St
ctx_reload_required: boolean; ctx_reload_required: boolean;
}; };
onBeforeUpdate(from: ConfigSchema, to: ConfigSchema): ConfigSchema | Promise<ConfigSchema> { onBeforeUpdate(from: Schema, to: Schema): Schema | Promise<Schema> {
return to; return to;
} }
@@ -75,11 +77,13 @@ export abstract class Module<Schema extends TSchema = TSchema, ConfigSchema = St
return undefined; return undefined;
} }
get configDefault(): Static<ReturnType<(typeof this)["getSchema"]>> { //get configDefault(): s.Static<ReturnType<(typeof this)["getSchema"]>> {
return this._schema.default(); get configDefault(): Schema {
return this._schema.default() as any;
} }
get config(): Static<ReturnType<(typeof this)["getSchema"]>> { //get config(): s.Static<ReturnType<(typeof this)["getSchema"]>> {
get config(): Schema {
return this._schema.get(); return this._schema.get();
} }
@@ -130,7 +134,8 @@ export abstract class Module<Schema extends TSchema = TSchema, ConfigSchema = St
} }
} }
toJSON(secrets?: boolean): Static<ReturnType<(typeof this)["getSchema"]>> { //toJSON(secrets?: boolean): s.Static<ReturnType<(typeof this)["getSchema"]>> {
toJSON(secrets?: boolean): Schema {
return this.config; return this.config;
} }
} }
+14 -22
View File
@@ -2,15 +2,7 @@ import { Guard } from "auth";
import { $console, BkndError, DebugLogger, env } from "core"; import { $console, BkndError, DebugLogger, env } from "core";
import { EventManager } from "core/events"; import { EventManager } from "core/events";
import * as $diff from "core/object/diff"; import * as $diff from "core/object/diff";
import { import { objectEach, transformObject } from "core/utils";
Default,
type Static,
StringEnum,
mark,
objectEach,
stripMark,
transformObject,
} from "core/utils";
import type { Connection, Schema } from "data"; import type { Connection, Schema } from "data";
import { EntityManager } from "data/entities/EntityManager"; import { EntityManager } from "data/entities/EntityManager";
import * as proto from "data/prototype"; import * as proto from "data/prototype";
@@ -26,18 +18,17 @@ import { AppFlows } from "../flows/AppFlows";
import { AppMedia } from "../media/AppMedia"; import { AppMedia } from "../media/AppMedia";
import type { ServerEnv } from "./Controller"; import type { ServerEnv } from "./Controller";
import { Module, type ModuleBuildContext } from "./Module"; import { Module, type ModuleBuildContext } from "./Module";
import * as tbbox from "@sinclair/typebox";
import { ModuleHelper } from "./ModuleHelper"; import { ModuleHelper } from "./ModuleHelper";
const { Type } = tbbox; import { s, mark, stripMark } from "core/object/schema";
export type { ModuleBuildContext }; export type { ModuleBuildContext };
export const MODULES = { export const MODULES = {
server: AppServer, server: AppServer,
data: AppData, data: AppData, // @todo:
auth: AppAuth, auth: AppAuth,
media: AppMedia, media: AppMedia,
flows: AppFlows, flows: AppFlows, // @todo:
} as const; } as const;
// get names of MODULES as an array // get names of MODULES as an array
@@ -53,7 +44,7 @@ export type ModuleSchemas = {
}; };
export type ModuleConfigs = { export type ModuleConfigs = {
[K in keyof ModuleSchemas]: Static<ModuleSchemas[K]>; [K in keyof ModuleSchemas]: s.Static<ModuleSchemas[K]>;
}; };
type PartialRec<T> = { [P in keyof T]?: PartialRec<T[P]> }; type PartialRec<T> = { [P in keyof T]?: PartialRec<T[P]> };
@@ -101,14 +92,14 @@ export type ConfigTable<Json = ModuleConfigs> = {
updated_at?: Date; updated_at?: Date;
}; };
const configJsonSchema = Type.Union([ const configJsonSchema = s.anyOf([
getDefaultSchema(), getDefaultSchema(),
Type.Array( s.array(
Type.Object({ s.strictObject({
t: StringEnum(["a", "r", "e"]), t: s.string({ enum: ["a", "r", "e"] }),
p: Type.Array(Type.Union([Type.String(), Type.Number()])), p: s.array(s.anyOf([s.string(), s.number()])),
o: Type.Optional(Type.Any()), o: s.any().optional(),
n: Type.Optional(Type.Any()), n: s.any().optional(),
}), }),
), ),
]); ]);
@@ -717,7 +708,8 @@ export function getDefaultSchema() {
export function getDefaultConfig(): ModuleConfigs { export function getDefaultConfig(): ModuleConfigs {
const config = transformObject(MODULES, (module) => { const config = transformObject(MODULES, (module) => {
return Default(module.prototype.getSchema(), {}); return module.prototype.getSchema().template();
//return Default(module.prototype.getSchema(), {});
}); });
return config as any; return config as any;
+11 -21
View File
@@ -1,37 +1,27 @@
import { Exception, isDebug, $console } from "core"; import { Exception, isDebug, $console } from "core";
import { type Static, StringEnum } from "core/utils";
import { cors } from "hono/cors"; import { cors } from "hono/cors";
import { Module } from "modules/Module"; import { Module } from "modules/Module";
import * as tbbox from "@sinclair/typebox";
import { AuthException } from "auth/errors"; 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( export const serverConfigSchema = s.strictObject({
{ cors: s.strictObject({
cors: Type.Object( origin: s.string({ default: "*" }),
{ allow_methods: s.array(s.string({ enum: serverMethods }), {
origin: Type.String({ default: "*" }),
allow_methods: Type.Array(StringEnum(serverMethods), {
default: serverMethods, default: serverMethods,
uniqueItems: true, uniqueItems: true,
}), }),
allow_headers: Type.Array(Type.String(), { allow_headers: s.array(s.string(), {
default: ["Content-Type", "Content-Length", "Authorization", "Accept"], default: ["Content-Type", "Content-Length", "Authorization", "Accept"],
}), }),
}, }),
{ default: {}, additionalProperties: false }, });
),
},
{
additionalProperties: false,
},
);
export type AppServerConfig = Static<typeof serverConfigSchema>; export type AppServerConfig = s.Static<typeof serverConfigSchema>;
export class AppServer extends Module<typeof serverConfigSchema> { export class AppServer extends Module<AppServerConfig> {
override getRestrictedPaths() { override getRestrictedPaths() {
return []; return [];
} }
+9 -14
View File
@@ -1,15 +1,8 @@
/// <reference types="@cloudflare/workers-types" /> /// <reference types="@cloudflare/workers-types" />
import type { App } from "App"; import type { App } from "App";
import { $console, tbValidator as tb } from "core"; import { $console } from "core";
import { import { datetimeStringLocal, datetimeStringUTC, getTimezone, getTimezoneOffset } from "core/utils";
StringEnum,
TypeInvalidError,
datetimeStringLocal,
datetimeStringUTC,
getTimezone,
getTimezoneOffset,
} from "core/utils";
import { getRuntimeKey } from "core/utils"; import { getRuntimeKey } from "core/utils";
import type { Context, Hono } from "hono"; import type { Context, Hono } from "hono";
import { Controller } from "modules/Controller"; import { Controller } from "modules/Controller";
@@ -20,11 +13,11 @@ import {
type ModuleConfigs, type ModuleConfigs,
type ModuleSchemas, type ModuleSchemas,
type ModuleKey, type ModuleKey,
getDefaultConfig,
} from "modules/ModuleManager"; } from "modules/ModuleManager";
import * as SystemPermissions from "modules/permissions"; 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"; import { getVersion } from "core/env";
export type ConfigUpdate<Key extends ModuleKey = ModuleKey> = { export type ConfigUpdate<Key extends ModuleKey = ModuleKey> = {
success: true; success: true;
module: Key; module: Key;
@@ -104,7 +97,7 @@ export class SystemController extends Controller {
} catch (e) { } catch (e) {
$console.error("config update error", e); $console.error("config update error", e);
if (e instanceof TypeInvalidError) { if (e instanceof InvalidSchemaError) {
return c.json( return c.json(
{ success: false, type: "type-invalid", errors: e.errors }, { success: false, type: "type-invalid", errors: e.errors },
{ status: 400 }, { status: 400 },
@@ -234,11 +227,13 @@ export class SystemController extends Controller {
permission(SystemPermissions.schemaRead), permission(SystemPermissions.schemaRead),
jsc( jsc(
"query", "query",
s.partialObject({ s
.object({
config: s.boolean(), config: s.boolean(),
secrets: s.boolean(), secrets: s.boolean(),
fresh: s.boolean(), fresh: s.boolean(),
}), })
.partial(),
), ),
async (c) => { async (c) => {
const module = c.req.param("module") as ModuleKey | undefined; const module = c.req.param("module") as ModuleKey | undefined;
+4 -15
View File
@@ -1,4 +1,4 @@
import { TypeInvalidError, parse, transformObject } from "core/utils"; import { transformObject } from "core/utils";
import { constructEntity } from "data"; import { constructEntity } from "data";
import { import {
type TAppDataEntity, type TAppDataEntity,
@@ -13,8 +13,7 @@ import {
import { useBknd } from "ui/client/bknd"; import { useBknd } from "ui/client/bknd";
import type { TSchemaActions } from "ui/client/schema/actions"; import type { TSchemaActions } from "ui/client/schema/actions";
import { bkndModals } from "ui/modals"; import { bkndModals } from "ui/modals";
import * as tb from "@sinclair/typebox"; import { s, parse, InvalidSchemaError } from "core/object/schema";
const { Type } = tb;
export function useBkndData() { export function useBkndData() {
const { config, app, schema, actions: bkndActions } = useBknd(); const { config, app, schema, actions: bkndActions } = useBknd();
@@ -27,12 +26,10 @@ export function useBkndData() {
const actions = { const actions = {
entity: { entity: {
add: async (name: string, data: TAppDataEntity) => { add: async (name: string, data: TAppDataEntity) => {
console.log("create entity", { data });
const validated = parse(entitiesSchema, data, { const validated = parse(entitiesSchema, data, {
skipMark: true, skipMark: true,
forceParse: true, forceParse: true,
}); });
console.log("validated", validated);
// @todo: check for existing? // @todo: check for existing?
return await bkndActions.add("data", `entities.${name}`, validated); return await bkndActions.add("data", `entities.${name}`, validated);
}, },
@@ -44,7 +41,6 @@ export function useBkndData() {
return { return {
config: async (partial: Partial<TAppDataEntity["config"]>): Promise<boolean> => { config: async (partial: Partial<TAppDataEntity["config"]>): Promise<boolean> => {
console.log("patch config", entityName, partial);
return await bkndActions.overwrite( return await bkndActions.overwrite(
"data", "data",
`entities.${entityName}.config`, `entities.${entityName}.config`,
@@ -57,13 +53,11 @@ export function useBkndData() {
}, },
relations: { relations: {
add: async (relation: TAppDataRelation) => { add: async (relation: TAppDataRelation) => {
console.log("create relation", { relation });
const name = crypto.randomUUID(); const name = crypto.randomUUID();
const validated = parse(Type.Union(relationsSchema), relation, { const validated = parse(s.anyOf(relationsSchema), relation, {
skipMark: true, skipMark: true,
forceParse: true, forceParse: true,
}); });
console.log("validated", validated);
return await bkndActions.add("data", `relations.${name}`, validated); return await bkndActions.add("data", `relations.${name}`, validated);
}, },
}, },
@@ -120,17 +114,14 @@ const modals = {
function entityFieldActions(bkndActions: TSchemaActions, entityName: string) { function entityFieldActions(bkndActions: TSchemaActions, entityName: string) {
return { return {
add: async (name: string, field: TAppDataField) => { add: async (name: string, field: TAppDataField) => {
console.log("create field", { name, field });
const validated = parse(fieldsSchema, field, { const validated = parse(fieldsSchema, field, {
skipMark: true, skipMark: true,
forceParse: true, forceParse: true,
}); });
console.log("validated", validated);
return await bkndActions.add("data", `entities.${entityName}.fields.${name}`, validated); return await bkndActions.add("data", `entities.${entityName}.fields.${name}`, validated);
}, },
patch: () => null, patch: () => null,
set: async (fields: TAppDataEntityFields) => { set: async (fields: TAppDataEntityFields) => {
console.log("set fields", entityName, fields);
try { try {
const validated = parse(entityFields, fields, { const validated = parse(entityFields, fields, {
skipMark: true, skipMark: true,
@@ -141,11 +132,9 @@ function entityFieldActions(bkndActions: TSchemaActions, entityName: string) {
`entities.${entityName}.fields`, `entities.${entityName}.fields`,
validated, validated,
); );
console.log("res", res);
//bkndActions.set("data", "entities", fields);
} catch (e) { } catch (e) {
console.error("error", e); console.error("error", e);
if (e instanceof TypeInvalidError) { if (e instanceof InvalidSchemaError) {
alert("Error updating fields: " + e.firstToString()); alert("Error updating fields: " + e.firstToString());
} else { } else {
alert("An error occured, check console. There will be nice error handling soon."); alert("An error occured, check console. There will be nice error handling soon.");
+1 -4
View File
@@ -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 { type TAppFlowSchema, flowSchema } from "flows/flows-schema";
import { useBknd } from "../../BkndProvider"; import { useBknd } from "../../BkndProvider";
@@ -8,11 +8,8 @@ export function useFlows() {
const actions = { const actions = {
flow: { flow: {
create: async (name: string, data: TAppFlowSchema) => { create: async (name: string, data: TAppFlowSchema) => {
console.log("would create", name, data);
const parsed = parse(flowSchema, data, { skipMark: true, forceParse: true }); const parsed = parse(flowSchema, data, { skipMark: true, forceParse: true });
console.log("parsed", parsed);
const res = await bkndActions.add("flows", `flows.${name}`, parsed); const res = await bkndActions.add("flows", `flows.${name}`, parsed);
console.log("res", res);
}, },
}, },
}; };
@@ -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 { FromSchema } from "./from-schema";
import type { import type {
+9 -11
View File
@@ -6,15 +6,13 @@ import type { ComponentPropsWithoutRef } from "react";
import { Button } from "ui/components/buttons/Button"; import { Button } from "ui/components/buttons/Button";
import { Group, Input, Password, Label } from "ui/components/form/Formy/components"; import { Group, Input, Password, Label } from "ui/components/form/Formy/components";
import { SocialLink } from "./SocialLink"; 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 type { Validator } from "json-schema-form-react";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox; import type { ErrorDetail } from "jsonv-ts";
class TypeboxValidator implements Validator<ValueError> { class JsonvTsValidator implements Validator<ErrorDetail> {
async validate(schema: TSchema, data: any) { async validate(schema: s.Schema, data: any) {
return Value.Check(schema, data) ? [] : [...Value.Errors(schema, data)]; return schema.validate(data).errors;
} }
} }
@@ -27,12 +25,12 @@ export type LoginFormProps = Omit<ComponentPropsWithoutRef<"form">, "onSubmit" |
buttonLabel?: string; buttonLabel?: string;
}; };
const validator = new TypeboxValidator(); const validator = new JsonvTsValidator();
const schema = Type.Object({ const schema = s.strictObject({
email: Type.String({ email: s.string({
pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$", pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
}), }),
password: Type.String({ password: s.string({
minLength: 8, // @todo: this should be configurable minLength: 8, // @todo: this should be configurable
}), }),
}); });
+2 -2
View File
@@ -4,12 +4,12 @@ import { useLocation, useSearch as useWouterSearch } from "wouter";
import { type s, parse } from "core/object/schema"; import { type s, parse } from "core/object/schema";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
export type UseSearchOptions<Schema extends s.TAnySchema = s.TAnySchema> = { export type UseSearchOptions<Schema extends s.Schema = s.Schema> = {
defaultValue?: Partial<s.StaticCoerced<Schema>>; defaultValue?: Partial<s.StaticCoerced<Schema>>;
beforeEncode?: (search: Partial<s.StaticCoerced<Schema>>) => object; beforeEncode?: (search: Partial<s.StaticCoerced<Schema>>) => object;
}; };
export function useSearch<Schema extends s.TAnySchema = s.TAnySchema>( export function useSearch<Schema extends s.Schema = s.Schema>(
schema: Schema, schema: Schema,
options?: UseSearchOptions<Schema>, options?: UseSearchOptions<Schema>,
) { ) {
@@ -1,6 +1,5 @@
import type { ModalProps } from "@mantine/core"; import type { ModalProps } from "@mantine/core";
import type { ContextModalProps } from "@mantine/modals"; import type { ContextModalProps } from "@mantine/modals";
import { type Static, StringEnum, StringIdentifier } from "core/utils";
import { entitiesSchema, fieldsSchema, relationsSchema } from "data/data-schema"; import { entitiesSchema, fieldsSchema, relationsSchema } from "data/data-schema";
import { useState } from "react"; import { useState } from "react";
import { type Modal2Ref, ModalBody, ModalFooter, ModalTitle } from "ui/components/modal/Modal2"; 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 { StepRelation } from "./step.relation";
import { StepSelect } from "./step.select"; import { StepSelect } from "./step.select";
import Templates from "./templates/register"; import Templates from "./templates/register";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox;
export type CreateModalRef = Modal2Ref; export type CreateModalRef = Modal2Ref;
export const ModalActions = ["entity", "relation", "media"] as const; export const ModalActions = ["entity", "relation", "media"] as const;
export const entitySchema = Type.Composite([ export const entitySchema = s.object({
Type.Object({ name: s.string(),
name: StringIdentifier, ...entitiesSchema.properties,
}),
entitiesSchema,
]);
const schemaAction = Type.Union([
StringEnum(["entity", "relation", "media"]),
Type.String({ pattern: "^template-" }),
]);
export type TSchemaAction = Static<typeof schemaAction>;
const createFieldSchema = Type.Object({
entity: StringIdentifier,
name: StringIdentifier,
field: Type.Array(fieldsSchema),
}); });
export type TFieldCreate = Static<typeof createFieldSchema>;
const createModalSchema = Type.Object( // @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<typeof schemaAction>;
const createFieldSchema = s.object({
entity: s.string(),
name: s.string(),
field: s.array(fieldsSchema),
});
export type TFieldCreate = s.Static<typeof createFieldSchema>;
const createModalSchema = s.strictObject({
action: schemaAction, action: schemaAction,
initial: Type.Optional(Type.Any()), initial: s.any().optional(),
entities: Type.Optional( entities: s
Type.Object({ .object({
create: Type.Optional(Type.Array(entitySchema)), create: s.array(entitySchema).optional(),
}), })
), .optional(),
relations: Type.Optional( relations: s
Type.Object({ .object({
create: Type.Optional(Type.Array(Type.Union(relationsSchema))), create: s.array(s.anyOf(relationsSchema)).optional(),
}), })
), .optional(),
fields: Type.Optional( fields: s
Type.Object({ .object({
create: Type.Optional(Type.Array(createFieldSchema)), create: s.array(createFieldSchema).optional(),
}), })
), .optional(),
}, });
{ export type TCreateModalSchema = s.Static<typeof createModalSchema>;
additionalProperties: false,
},
);
export type TCreateModalSchema = Static<typeof createModalSchema>;
export function CreateModal({ export function CreateModal({
context, context,
@@ -70,7 +62,6 @@ export function CreateModal({
innerProps: { initialPath = [], initialState }, innerProps: { initialPath = [], initialState },
}: ContextModalProps<{ initialPath?: string[]; initialState?: TCreateModalSchema }>) { }: ContextModalProps<{ initialPath?: string[]; initialState?: TCreateModalSchema }>) {
const [path, setPath] = useState<string[]>(initialPath); const [path, setPath] = useState<string[]>(initialPath);
console.log("...", initialPath, initialState);
function close() { function close() {
context.closeModal(id); context.closeModal(id);
@@ -1,5 +1,5 @@
import { typeboxResolver } from "@hookform/resolvers/typebox"; //import { typeboxResolver } from "@hookform/resolvers/typebox";
import { type Static, objectCleanEmpty } from "core/utils"; import { objectCleanEmpty } from "core/utils";
import { type TAppDataEntityFields, entitiesSchema } from "data/data-schema"; import { type TAppDataEntityFields, entitiesSchema } from "data/data-schema";
import { mergeWith } from "lodash-es"; import { mergeWith } from "lodash-es";
import { useRef } from "react"; import { useRef } from "react";
@@ -12,9 +12,10 @@ import {
} from "ui/routes/data/forms/entity.fields.form"; } from "ui/routes/data/forms/entity.fields.form";
import { ModalBody, ModalFooter, type TCreateModalSchema, useStepContext } from "./CreateModal"; import { ModalBody, ModalFooter, type TCreateModalSchema, useStepContext } from "./CreateModal";
import { useBkndData } from "ui/client/schema/data/use-bknd-data"; import { useBkndData } from "ui/client/schema/data/use-bknd-data";
import type { s } from "core/object/schema";
const schema = entitiesSchema; const schema = entitiesSchema;
type Schema = Static<typeof schema>; type Schema = s.Static<typeof schema>;
export function StepEntityFields() { export function StepEntityFields() {
const { nextStep, stepBack, state, setState } = useStepContext<TCreateModalSchema>(); const { nextStep, stepBack, state, setState } = useStepContext<TCreateModalSchema>();
@@ -40,7 +41,8 @@ export function StepEntityFields() {
setValue, setValue,
} = useForm({ } = useForm({
mode: "onTouched", mode: "onTouched",
resolver: typeboxResolver(schema), // @todo: add resolver
//resolver: typeboxResolver(schema),
defaultValues: initial as NonNullable<Schema>, defaultValues: initial as NonNullable<Schema>,
}); });
@@ -1,4 +1,4 @@
import { typeboxResolver } from "@hookform/resolvers/typebox"; //import { typeboxResolver } from "@hookform/resolvers/typebox";
import { TextInput, Textarea } from "@mantine/core"; import { TextInput, Textarea } from "@mantine/core";
import { useFocusTrap } from "@mantine/hooks"; import { useFocusTrap } from "@mantine/hooks";
@@ -10,7 +10,6 @@ import {
entitySchema, entitySchema,
useStepContext, useStepContext,
} from "./CreateModal"; } from "./CreateModal";
import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect";
export function StepEntity() { export function StepEntity() {
const focusTrapRef = useFocusTrap(); const focusTrapRef = useFocusTrap();
@@ -18,7 +17,8 @@ export function StepEntity() {
const { nextStep, stepBack, state, setState } = useStepContext<TCreateModalSchema>(); const { nextStep, stepBack, state, setState } = useStepContext<TCreateModalSchema>();
const { register, handleSubmit, formState, watch, control } = useForm({ const { register, handleSubmit, formState, watch, control } = useForm({
mode: "onTouched", mode: "onTouched",
resolver: typeboxResolver(entitySchema), // @todo: add resolver
//resolver: typeboxResolver(entitySchema),
defaultValues: state.entities?.create?.[0] ?? {}, defaultValues: state.entities?.create?.[0] ?? {},
}); });
/*const data = watch(); /*const data = watch();
@@ -1,8 +1,5 @@
import { typeboxResolver } from "@hookform/resolvers/typebox";
import { Switch, TextInput } from "@mantine/core"; import { Switch, TextInput } from "@mantine/core";
import { TypeRegistry } from "@sinclair/typebox";
import { IconDatabase } from "@tabler/icons-react"; import { IconDatabase } from "@tabler/icons-react";
import { type Static, StringEnum, StringIdentifier, registerCustomTypeboxKinds } from "core/utils";
import { ManyToOneRelation, type RelationType, RelationTypes } from "data"; import { ManyToOneRelation, type RelationType, RelationTypes } from "data";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { type Control, type FieldValues, type UseFormRegister, useForm } from "react-hook-form"; 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 { useStepContext } from "ui/components/steps/Steps";
import { useEvent } from "ui/hooks/use-event"; import { useEvent } from "ui/hooks/use-event";
import { ModalBody, ModalFooter, type TCreateModalSchema } from "./CreateModal"; import { ModalBody, ModalFooter, type TCreateModalSchema } from "./CreateModal";
import * as tbbox from "@sinclair/typebox"; import { s, stringIdentifier } from "core/object/schema";
const { Type } = tbbox;
// @todo: check if this could become an issue
registerCustomTypeboxKinds(TypeRegistry);
const Relations: { const Relations: {
type: RelationType; type: RelationType;
@@ -47,11 +40,11 @@ const Relations: {
}, },
]; ];
const schema = Type.Object({ const schema = s.strictObject({
type: StringEnum(Relations.map((r) => r.type)), type: s.string({ enum: Relations.map((r) => r.type) }),
source: StringIdentifier, source: stringIdentifier,
target: StringIdentifier, target: stringIdentifier,
config: Type.Object({}), config: s.object({}),
}); });
type ComponentCtx<T extends FieldValues = FieldValues> = { type ComponentCtx<T extends FieldValues = FieldValues> = {
@@ -73,8 +66,9 @@ export function StepRelation() {
watch, watch,
control, control,
} = useForm({ } = useForm({
resolver: typeboxResolver(schema), // @todo: implement resolver
defaultValues: (state.relations?.create?.[0] ?? {}) as Static<typeof schema>, //resolver: typeboxResolver(schema),
defaultValues: (state.relations?.create?.[0] ?? {}) as s.Static<typeof schema>,
}); });
const data = watch(); const data = watch();
@@ -1,6 +1,5 @@
import { typeboxResolver } from "@hookform/resolvers/typebox";
import { Radio, TextInput } from "@mantine/core"; 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 type { MediaFieldConfig } from "media/MediaField";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -15,16 +14,15 @@ import {
type TFieldCreate, type TFieldCreate,
useStepContext, useStepContext,
} from "../../CreateModal"; } from "../../CreateModal";
import * as tbbox from "@sinclair/typebox"; import { s, stringIdentifier } from "core/object/schema";
const { Type } = tbbox;
const schema = Type.Object({ const schema = s.object({
entity: StringIdentifier, entity: stringIdentifier,
cardinality_type: StringEnum(["single", "multiple"], { default: "multiple" }), cardinality_type: s.string({ enum: ["single", "multiple"], default: "multiple" }),
cardinality: Type.Optional(Type.Number({ minimum: 1 })), cardinality: s.number({ minimum: 1 }).optional(),
name: StringIdentifier, name: stringIdentifier,
}); });
type TCreateModalMediaSchema = Static<typeof schema>; type TCreateModalMediaSchema = s.Static<typeof schema>;
export function TemplateMediaComponent() { export function TemplateMediaComponent() {
const { stepBack, setState, state, path, nextStep } = useStepContext<TCreateModalSchema>(); const { stepBack, setState, state, path, nextStep } = useStepContext<TCreateModalSchema>();
@@ -36,8 +34,10 @@ export function TemplateMediaComponent() {
control, control,
} = useForm({ } = useForm({
mode: "onChange", mode: "onChange",
resolver: typeboxResolver(schema), // @todo: add resolver
defaultValues: Default(schema, state.initial ?? {}) as TCreateModalMediaSchema, //resolver: typeboxResolver(schema),
defaultValues: schema.template(state.initial ?? {}) as TCreateModalMediaSchema,
//defaultValues: Default(schema, state.initial ?? {}) as TCreateModalMediaSchema,
}); });
const [forbidden, setForbidden] = useState<boolean>(false); const [forbidden, setForbidden] = useState<boolean>(false);
@@ -1,11 +1,10 @@
import { Handle, type Node, type NodeProps, Position } from "@xyflow/react"; 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 Trigger, TriggerMap } from "flows";
import type { IconType } from "react-icons"; import type { IconType } from "react-icons";
import { TbCircleLetterT } from "react-icons/tb"; import { TbCircleLetterT } from "react-icons/tb";
import { JsonSchemaForm } from "ui/components/form/json-schema"; import { JsonSchemaForm } from "ui/components/form/json-schema";
import * as tbbox from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type } = tbbox;
export type TaskComponentProps = NodeProps<Node<{ trigger: Trigger }>> & { export type TaskComponentProps = NodeProps<Node<{ trigger: Trigger }>> & {
Icon?: IconType; Icon?: IconType;
@@ -14,9 +13,9 @@ export type TaskComponentProps = NodeProps<Node<{ trigger: Trigger }>> & {
const triggerSchemas = Object.values( const triggerSchemas = Object.values(
transformObject(TriggerMap, (trigger, name) => transformObject(TriggerMap, (trigger, name) =>
Type.Object( s.object(
{ {
type: Const(name), type: s.literal(name),
config: trigger.cls.schema, config: trigger.cls.schema,
}, },
{ title: String(name), additionalProperties: false }, { title: String(name), additionalProperties: false },
@@ -47,7 +46,7 @@ export function TriggerComponent({
<div className="flex flex-col gap-2 px-3 py-2"> <div className="flex flex-col gap-2 px-3 py-2">
<JsonSchemaForm <JsonSchemaForm
className="legacy" className="legacy"
schema={Type.Union(triggerSchemas)} schema={s.anyOf(triggerSchemas)}
onChange={console.log} onChange={console.log}
formData={trigger} formData={trigger}
{...props} {...props}
@@ -1,30 +1,25 @@
import { typeboxResolver } from "@hookform/resolvers/typebox"; import { Input, TextInput } from "@mantine/core";
import { Input, NativeSelect, Select, TextInput } from "@mantine/core";
import { useToggle } from "@mantine/hooks"; import { useToggle } from "@mantine/hooks";
import { IconMinus, IconPlus, IconWorld } from "@tabler/icons-react"; import { IconMinus, IconPlus, IconWorld } from "@tabler/icons-react";
import type { Node, NodeProps } from "@xyflow/react"; import type { Node, NodeProps } from "@xyflow/react";
import type { Static } from "core/utils"; import { s } from "core/object/schema";
import { FetchTask } from "flows"; import { FetchTask } from "flows";
import { useRef, useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { Button } from "ui/components/buttons/Button"; import { Button } from "ui/components/buttons/Button";
import { JsonViewer } from "ui/components/code/JsonViewer"; import { JsonViewer } from "ui/components/code/JsonViewer";
import { SegmentedControl } from "ui/components/form/SegmentedControl"; import { SegmentedControl } from "ui/components/form/SegmentedControl";
import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect"; import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect";
import { type TFlowNodeData, useFlowSelector } from "../../../hooks/use-flow"; import type { TFlowNodeData } from "../../../hooks/use-flow";
import { KeyValueInput } from "../../form/KeyValueInput"; import { KeyValueInput } from "../../form/KeyValueInput";
import { BaseNode } from "../BaseNode"; import { BaseNode } from "../BaseNode";
import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox;
const schema = Type.Composite([ const schema = s.object({
FetchTask.schema, query: s.record(s.string()).optional(),
Type.Object({ ...FetchTask.schema.properties,
query: Type.Optional(Type.Record(Type.String(), Type.String())), });
}),
]);
type TFetchTaskSchema = Static<typeof FetchTask.schema>; type TFetchTaskSchema = s.Static<typeof FetchTask.schema>;
type FetchTaskFormProps = NodeProps<Node<TFlowNodeData>> & { type FetchTaskFormProps = NodeProps<Node<TFlowNodeData>> & {
params: TFetchTaskSchema; params: TFetchTaskSchema;
onChange: (params: any) => void; onChange: (params: any) => void;
@@ -42,8 +37,9 @@ export function FetchTaskForm({ onChange, params, ...props }: FetchTaskFormProps
watch, watch,
control, control,
} = useForm({ } = useForm({
resolver: typeboxResolver(schema), // @todo: add resolver
defaultValues: params as Static<typeof schema>, //resolver: typeboxResolver(schema),
defaultValues: params as s.Static<typeof schema>,
mode: "onChange", mode: "onChange",
//defaultValues: (state.relations?.create?.[0] ?? {}) as Static<typeof schema> //defaultValues: (state.relations?.create?.[0] ?? {}) as Static<typeof schema>
}); });
@@ -1,14 +1,10 @@
import { TypeRegistry } from "@sinclair/typebox";
import { type Node, type NodeProps, Position } from "@xyflow/react"; import { type Node, type NodeProps, Position } from "@xyflow/react";
import { registerCustomTypeboxKinds } from "core/utils";
import type { TAppFlowTaskSchema } from "flows/AppFlows"; import type { TAppFlowTaskSchema } from "flows/AppFlows";
import { useFlowCanvas, useFlowSelector } from "../../../hooks/use-flow"; import { useFlowCanvas, useFlowSelector } from "../../../hooks/use-flow";
import { Handle } from "../Handle"; import { Handle } from "../Handle";
import { FetchTaskForm } from "./FetchTaskNode"; import { FetchTaskForm } from "./FetchTaskNode";
import { RenderNode } from "./RenderNode"; import { RenderNode } from "./RenderNode";
registerCustomTypeboxKinds(TypeRegistry);
const TaskComponents = { const TaskComponents = {
fetch: FetchTaskForm, fetch: FetchTaskForm,
render: RenderNode, render: RenderNode,
@@ -1,7 +1,6 @@
import { typeboxResolver } from "@hookform/resolvers/typebox";
import { TextInput } from "@mantine/core"; import { TextInput } from "@mantine/core";
import type { Node, NodeProps } from "@xyflow/react"; 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 { TriggerMap } from "flows";
import type { TAppFlowTriggerSchema } from "flows/AppFlows"; import type { TAppFlowTriggerSchema } from "flows/AppFlows";
import { useForm } from "react-hook-form"; 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 { useFlowCanvas, useFlowSelector } from "../../../hooks/use-flow";
import { BaseNode } from "../BaseNode"; import { BaseNode } from "../BaseNode";
import { Handle } from "../Handle"; import { Handle } from "../Handle";
import * as tb from "@sinclair/typebox"; import { s } from "core/object/schema";
const { Type, TypeRegistry } = tb;
// @todo: check if this could become an issue const schema = s.object({
registerCustomTypeboxKinds(TypeRegistry); trigger: s.anyOf(
const schema = Type.Object({
trigger: Type.Union(
Object.values( Object.values(
transformObject(TriggerMap, (trigger, name) => transformObject(TriggerMap, (trigger, name) =>
Type.Object( s.strictObject(
{ {
type: Const(name), type: s.literal(name),
config: trigger.cls.schema, config: trigger.cls.schema,
}, },
{ title: String(name), additionalProperties: false }, { title: String(name) },
), ),
), ),
), ),
@@ -50,13 +45,14 @@ export const TriggerNode = (props: NodeProps<Node<TAppFlowTriggerSchema & { labe
watch, watch,
control, control,
} = useForm({ } = useForm({
resolver: typeboxResolver(schema), // @todo: add resolver
defaultValues: { trigger: state } as Static<typeof schema>, //resolver: typeboxResolver(schema),
defaultValues: { trigger: state } as s.Static<typeof schema>,
mode: "onChange", mode: "onChange",
}); });
const data = watch("trigger"); const data = watch("trigger");
async function onSubmit(data: Static<typeof schema>) { async function onSubmit(data: s.Static<typeof schema>) {
console.log("submit", data.trigger); console.log("submit", data.trigger);
// @ts-ignore // @ts-ignore
await actions.trigger.update(data.trigger); await actions.trigger.update(data.trigger);
@@ -46,7 +46,7 @@ export const flowStateAtom = atom<TFlowState>({
const FlowCanvasContext = createContext<FlowContextType>(undefined!); const FlowCanvasContext = createContext<FlowContextType>(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 }) { export function FlowCanvasProvider({ children, name }: { children: any; name?: string }) {
//const [dirty, setDirty] = useState(false); //const [dirty, setDirty] = useState(false);
const setFlowState = useSetAtom(flowStateAtom); const setFlowState = useSetAtom(flowStateAtom);
@@ -71,7 +71,7 @@ export function FlowCanvasProvider({ children, name }: { children: any; name?: s
update: async (trigger: TAppFlowTriggerSchema | any) => { update: async (trigger: TAppFlowTriggerSchema | any) => {
console.log("update trigger", trigger); console.log("update trigger", trigger);
setFlowState((state) => { setFlowState((state) => {
const flow = state.flow || DEFAULT_FLOW; const flow = state.flow || (DEFAULT_FLOW as any);
return { ...state, dirty: true, flow: { ...flow, trigger } }; return { ...state, dirty: true, flow: { ...flow, trigger } };
}); });
//return s.actions.patch("flows", `flows.flows.${name}`, { trigger }); //return s.actions.patch("flows", `flows.flows.${name}`, { trigger });
+3 -2
View File
@@ -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 { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
import { Alert } from "ui/components/display/Alert"; import { Alert } from "ui/components/display/Alert";
import { bkndModals } from "ui/modals"; import { bkndModals } from "ui/modals";
@@ -6,6 +6,7 @@ import { Button } from "../../components/buttons/Button";
import { CellValue, DataTable } from "../../components/table/DataTable"; import { CellValue, DataTable } from "../../components/table/DataTable";
import * as AppShell from "../../layouts/AppShell/AppShell"; import * as AppShell from "../../layouts/AppShell/AppShell";
import { routes, useNavigate } from "../../lib/routes"; import { routes, useNavigate } from "../../lib/routes";
import { stringIdentifier } from "core/object/schema";
export function AuthRolesList() { export function AuthRolesList() {
const [navigate] = useNavigate(); const [navigate] = useNavigate();
@@ -31,7 +32,7 @@ export function AuthRolesList() {
schema: { schema: {
type: "object", type: "object",
properties: { properties: {
name: StringIdentifier, name: stringIdentifier,
}, },
required: ["name"], required: ["name"],
}, },
+7 -3
View File
@@ -64,8 +64,7 @@ function AuthStrategiesListInternal() {
const config = $auth.config.strategies; const config = $auth.config.strategies;
const schema = $auth.schema.properties.strategies; const schema = $auth.schema.properties.strategies;
const schemas = Object.fromEntries( 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.properties.type.const,
s, s,
]), ]),
@@ -76,7 +75,12 @@ function AuthStrategiesListInternal() {
} }
return ( return (
<Form schema={schema} initialValues={config} onSubmit={handleSubmit} options={formOptions}> <Form
schema={schema.toJSON()}
initialValues={config}
onSubmit={handleSubmit}
options={formOptions}
>
<Subscribe <Subscribe
selector={(state) => ({ selector={(state) => ({
dirty: state.dirty, dirty: state.dirty,
+7 -5
View File
@@ -1,15 +1,16 @@
import { typeboxResolver } from "@hookform/resolvers/typebox"; //import { typeboxResolver } from "@hookform/resolvers/typebox";
import { Input, Switch, Tooltip } from "@mantine/core"; import { Input, Switch, Tooltip } from "@mantine/core";
import { guardRoleSchema } from "auth/auth-schema"; import { guardRoleSchema } from "auth/auth-schema";
import { type Static, ucFirst } from "core/utils"; import { ucFirst } from "core/utils";
import { forwardRef, useImperativeHandle } from "react"; import { forwardRef, useImperativeHandle } from "react";
import { type UseControllerProps, useController, useForm } from "react-hook-form"; import { type UseControllerProps, useController, useForm } from "react-hook-form";
import { useBknd } from "ui/client/bknd"; import { useBknd } from "ui/client/bknd";
import { Button } from "ui/components/buttons/Button"; import { Button } from "ui/components/buttons/Button";
import { MantineSwitch } from "ui/components/form/hook-form-mantine/MantineSwitch"; import { MantineSwitch } from "ui/components/form/hook-form-mantine/MantineSwitch";
import type { s } from "core/object/schema";
const schema = guardRoleSchema; const schema = guardRoleSchema;
type Role = Static<typeof guardRoleSchema>; type Role = s.Static<typeof guardRoleSchema>;
export type AuthRoleFormRef = { export type AuthRoleFormRef = {
getData: () => Role; getData: () => Role;
@@ -33,7 +34,8 @@ export const AuthRoleForm = forwardRef<
reset, reset,
getValues, getValues,
} = useForm({ } = useForm({
resolver: typeboxResolver(schema), // @todo: add resolver
//resolver: typeboxResolver(schema),
defaultValues: role, defaultValues: role,
}); });
@@ -87,7 +89,7 @@ const Permissions = ({
const { const {
field: { value, onChange: fieldOnChange, ...field }, field: { value, onChange: fieldOnChange, ...field },
fieldState, fieldState,
} = useController<Static<typeof schema>, "permissions">({ } = useController<s.Static<typeof schema>, "permissions">({
name: "permissions", name: "permissions",
control, control,
}); });
@@ -1,13 +1,5 @@
import { typeboxResolver } from "@hookform/resolvers/typebox";
import { Tabs, TextInput, Textarea, Tooltip, Switch } from "@mantine/core"; import { Tabs, TextInput, Textarea, Tooltip, Switch } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks"; import { objectCleanEmpty, omitKeys, ucFirstAllSnakeToPascalWithSpaces } from "core/utils";
import {
Default,
type Static,
StringIdentifier,
objectCleanEmpty,
ucFirstAllSnakeToPascalWithSpaces,
} from "core/utils";
import { import {
type TAppDataEntityFields, type TAppDataEntityFields,
fieldsSchemaObject as originalFieldsSchemaObject, fieldsSchemaObject as originalFieldsSchemaObject,
@@ -26,31 +18,25 @@ import { type SortableItemProps, SortableList } from "ui/components/list/Sortabl
import { Popover } from "ui/components/overlay/Popover"; import { Popover } from "ui/components/overlay/Popover";
import { type TFieldSpec, fieldSpecs } from "ui/modules/data/components/fields-specs"; import { type TFieldSpec, fieldSpecs } from "ui/modules/data/components/fields-specs";
import { dataFieldsUiSchema } from "../../settings/routes/data.settings"; import { dataFieldsUiSchema } from "../../settings/routes/data.settings";
import * as tbbox from "@sinclair/typebox";
import { useRoutePathState } from "ui/hooks/use-route-path-state"; import { useRoutePathState } from "ui/hooks/use-route-path-state";
import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect"; import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect";
import type { TPrimaryFieldFormat } from "data/fields/PrimaryField"; import type { TPrimaryFieldFormat } from "data/fields/PrimaryField";
const { Type } = tbbox; import { s, stringIdentifier } from "core/object/schema";
const fieldsSchemaObject = originalFieldsSchemaObject; const fieldsSchemaObject = originalFieldsSchemaObject;
const fieldsSchema = Type.Union(Object.values(fieldsSchemaObject)); const fieldsSchema = s.anyOf(Object.values(fieldsSchemaObject));
const fieldSchema = Type.Object( const fieldSchema = s.strictObject({
{ name: stringIdentifier,
name: StringIdentifier, new: s.boolean({ const: true }).optional(),
new: Type.Optional(Type.Boolean({ const: true })),
field: fieldsSchema, field: fieldsSchema,
},
{
additionalProperties: false,
},
);
type TFieldSchema = Static<typeof fieldSchema>;
const schema = Type.Object({
fields: Type.Array(fieldSchema),
}); });
type TFieldsFormSchema = Static<typeof schema>; type TFieldSchema = s.Static<typeof fieldSchema>;
const schema = s.strictObject({
fields: s.array(fieldSchema),
});
type TFieldsFormSchema = s.Static<typeof schema>;
const fieldTypes = Object.keys(fieldsSchemaObject); const fieldTypes = Object.keys(fieldsSchemaObject);
const defaultType = fieldTypes[0]; const defaultType = fieldTypes[0];
@@ -58,7 +44,9 @@ const commonProps = ["label", "description", "required", "fillable", "hidden", "
function specificFieldSchema(type: keyof typeof fieldsSchemaObject) { function specificFieldSchema(type: keyof typeof fieldsSchemaObject) {
//console.log("specificFieldSchema", type); //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 = { export type EntityFieldsFormProps = {
@@ -100,7 +88,8 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
reset, reset,
} = useForm({ } = useForm({
mode: "all", mode: "all",
resolver: typeboxResolver(schema), // @todo: add resolver
//resolver: typeboxResolver(schema),
defaultValues: { defaultValues: {
fields: entityFields, fields: entityFields,
} as TFieldsFormSchema, } as TFieldsFormSchema,
@@ -135,15 +124,14 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
})); }));
function handleAppend(_type: keyof typeof fieldsSchemaObject) { function handleAppend(_type: keyof typeof fieldsSchemaObject) {
const newField = { append({
name: "", name: "",
new: true, new: true,
field: { field: {
type: _type, type: _type,
config: Default(fieldsSchemaObject[_type]?.properties.config, {}) as any, config: fieldsSchemaObject[_type]?.properties.config.template() as any,
}, },
}; });
append(newField);
} }
const formProps = { const formProps = {
@@ -1,8 +1,5 @@
import { typeboxResolver } from "@hookform/resolvers/typebox";
import { TextInput } from "@mantine/core"; import { TextInput } from "@mantine/core";
import { useFocusTrap } from "@mantine/hooks"; import { useFocusTrap } from "@mantine/hooks";
import { TypeRegistry } from "@sinclair/typebox";
import { type Static, StringEnum, StringIdentifier, registerCustomTypeboxKinds } from "core/utils";
import { TRIGGERS } from "flows/flows-schema"; import { TRIGGERS } from "flows/flows-schema";
import { forwardRef, useState } from "react"; import { forwardRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -16,18 +13,15 @@ import {
ModalTitle, ModalTitle,
} from "../../../components/modal/Modal2"; } from "../../../components/modal/Modal2";
import { Step, Steps, useStepContext } from "../../../components/steps/Steps"; import { Step, Steps, useStepContext } from "../../../components/steps/Steps";
import * as tbbox from "@sinclair/typebox"; import { s, stringIdentifier } from "core/object/schema";
const { Type } = tbbox;
registerCustomTypeboxKinds(TypeRegistry);
export type TCreateFlowModalSchema = any; export type TCreateFlowModalSchema = any;
const triggerNames = Object.keys(TRIGGERS) as unknown as (keyof typeof TRIGGERS)[]; const triggerNames = Object.keys(TRIGGERS) as unknown as (keyof typeof TRIGGERS)[];
const schema = Type.Object({ const schema = s.strictObject({
name: StringIdentifier, name: stringIdentifier,
trigger: StringEnum(triggerNames), trigger: s.string({ enum: triggerNames }),
mode: StringEnum(["async", "sync"]), mode: s.string({ enum: ["async", "sync"] }),
}); });
export const FlowCreateModal = forwardRef<Modal2Ref>(function FlowCreateModal(props, ref) { export const FlowCreateModal = forwardRef<Modal2Ref>(function FlowCreateModal(props, ref) {
@@ -61,16 +55,17 @@ export function StepCreate() {
register, register,
formState: { isValid, errors }, formState: { isValid, errors },
} = useForm({ } = useForm({
resolver: typeboxResolver(schema), // @todo: implement resolver
//resolver: typeboxResolver(schema),
defaultValues: { defaultValues: {
name: "", name: "",
trigger: "manual", trigger: "manual",
mode: "async", mode: "async",
} as Static<typeof schema>, } as s.Static<typeof schema>,
mode: "onSubmit", mode: "onSubmit",
}); });
async function onSubmit(data: Static<typeof schema>) { async function onSubmit(data: s.Static<typeof schema>) {
console.log(data, isValid); console.log(data, isValid);
actions.flow.create(data.name, { actions.flow.create(data.name, {
trigger: { trigger: {
@@ -1,5 +1,5 @@
import { useHotkeys } from "@mantine/hooks"; import { useHotkeys } from "@mantine/hooks";
import { type TObject, ucFirst } from "core/utils"; import { ucFirst } from "core/utils";
import { omit } from "lodash-es"; import { omit } from "lodash-es";
import { type ReactNode, useMemo, useRef, useState } from "react"; import { type ReactNode, useMemo, useRef, useState } from "react";
import { TbSettings } from "react-icons/tb"; import { TbSettings } from "react-icons/tb";
@@ -18,10 +18,11 @@ import { Link, Route, useLocation } from "wouter";
import { extractSchema } from "../utils/schema"; import { extractSchema } from "../utils/schema";
import { SettingNewModal, type SettingsNewModalProps } from "./SettingNewModal"; import { SettingNewModal, type SettingsNewModalProps } from "./SettingNewModal";
import { SettingSchemaModal, type SettingsSchemaModalRef } from "./SettingSchemaModal"; import { SettingSchemaModal, type SettingsSchemaModalRef } from "./SettingSchemaModal";
import type { s } from "core/object/schema";
export type SettingProps< export type SettingProps<
Schema extends TObject = TObject, Schema extends s.ObjectSchema = s.ObjectSchema,
Props = Schema extends TObject<infer TProperties> ? TProperties : any, Props = Schema extends s.ObjectSchema<infer TProperties> ? TProperties : any,
> = { > = {
schema: Schema; schema: Schema;
config: any; config: any;
@@ -44,7 +45,7 @@ export type SettingProps<
}; };
}; };
export function Setting<Schema extends TObject = any>({ export function Setting<Schema extends s.ObjectSchema = s.ObjectSchema>({
schema, schema,
uiSchema, uiSchema,
config, config,
@@ -1,8 +1,6 @@
import { useDisclosure, useFocusTrap } from "@mantine/hooks"; import { useDisclosure, useFocusTrap } from "@mantine/hooks";
import type { TObject } from "core/utils";
import { omit } from "lodash-es"; import { omit } from "lodash-es";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { TbCirclePlus, TbVariable } from "react-icons/tb";
import { useBknd } from "ui/client/BkndProvider"; import { useBknd } from "ui/client/BkndProvider";
import { Button } from "ui/components/buttons/Button"; import { Button } from "ui/components/buttons/Button";
import * as Formy from "ui/components/form/Formy"; 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 { Dropdown } from "ui/components/overlay/Dropdown";
import { Modal } from "ui/components/overlay/Modal"; import { Modal } from "ui/components/overlay/Modal";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import type { s } from "core/object/schema";
export type SettingsNewModalProps = { export type SettingsNewModalProps = {
schema: TObject; schema: s.ObjectSchema;
uiSchema?: object; uiSchema?: object;
anyOfValues?: Record<string, { label: string; icon?: any }>; anyOfValues?: Record<string, { label: string; icon?: any }>;
path: string[]; path: string[];
+5 -5
View File
@@ -1,11 +1,11 @@
import type { Static, TObject } from "core/utils";
import type { JSONSchema7 } from "json-schema"; import type { JSONSchema7 } from "json-schema";
import { cloneDeep, omit, pick } from "lodash-es"; import { cloneDeep, omit, pick } from "lodash-es";
import type { s } from "core/object/schema";
export function extractSchema< export function extractSchema<
Schema extends TObject, Schema extends s.ObjectSchema,
Keys extends keyof Schema["properties"], Keys extends keyof Schema["properties"],
Config extends Static<Schema>, Config extends s.Static<Schema>,
>( >(
schema: Schema, schema: Schema,
config: Config, config: Config,
@@ -22,12 +22,12 @@ export function extractSchema<
}, },
] { ] {
if (!schema.properties) { if (!schema.properties) {
return [{ ...schema }, config, {} as any]; return [{ ...schema.toJSON() }, config, {} as any];
} }
const newSchema = cloneDeep(schema); const newSchema = cloneDeep(schema);
const updated = { const updated = {
...newSchema, ...newSchema.toJSON(),
properties: omit(newSchema.properties, keys), properties: omit(newSchema.properties, keys),
}; };
if (updated.required) { if (updated.required) {
@@ -1,9 +1,9 @@
import { parse } from "core/utils";
import { AppFlows } from "flows/AppFlows"; import { AppFlows } from "flows/AppFlows";
import { useState } from "react"; import { useState } from "react";
import { JsonViewer } from "../../../components/code/JsonViewer"; import { JsonViewer } from "../../../components/code/JsonViewer";
import { JsonSchemaForm } from "../../../components/form/json-schema"; import { JsonSchemaForm } from "../../../components/form/json-schema";
import { Scrollable } from "../../../layouts/AppShell/AppShell"; import { Scrollable } from "../../../layouts/AppShell/AppShell";
import { parse } from "core/object/schema";
export default function FlowCreateSchemaTest() { export default function FlowCreateSchemaTest() {
//const schema = flowsConfigSchema; //const schema = flowsConfigSchema;
@@ -73,7 +73,7 @@ export default function JsonSchemaForm3() {
return ( return (
<Scrollable> <Scrollable>
<div className="flex flex-col p-3"> <div className="flex flex-col p-3">
<Form schema={_schema.auth} options={formOptions} /> <Form schema={_schema.auth.toJSON()} options={formOptions} />
{/*<Form {/*<Form
onChange={(data) => console.log("change", data)} onChange={(data) => console.log("change", data)}
+1 -1
View File
@@ -1,6 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"types": ["bun-types"],
"composite": false, "composite": false,
"incremental": true, "incremental": true,
"module": "ESNext", "module": "ESNext",
@@ -32,6 +31,7 @@
"paths": { "paths": {
"*": ["./src/*"], "*": ["./src/*"],
"bknd": ["./src/index.ts"], "bknd": ["./src/index.ts"],
"bknd/utils": ["./src/core/utils/index.ts"],
"bknd/core": ["./src/core/index.ts"], "bknd/core": ["./src/core/index.ts"],
"bknd/adapter": ["./src/adapter/index.ts"], "bknd/adapter": ["./src/adapter/index.ts"],
"bknd/client": ["./src/ui/client/index.ts"], "bknd/client": ["./src/ui/client/index.ts"],
+6 -6
View File
@@ -15,7 +15,7 @@
}, },
"app": { "app": {
"name": "bknd", "name": "bknd",
"version": "0.15.0-rc.2", "version": "0.15.0-rc.3",
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"dependencies": { "dependencies": {
"@cfworker/json-schema": "^4.1.1", "@cfworker/json-schema": "^4.1.1",
@@ -43,7 +43,6 @@
"object-path-immutable": "^4.1.2", "object-path-immutable": "^4.1.2",
"radix-ui": "^1.1.3", "radix-ui": "^1.1.3",
"swr": "^2.3.3", "swr": "^2.3.3",
"uuid": "^11.1.0",
}, },
"devDependencies": { "devDependencies": {
"@aws-sdk/client-s3": "^3.758.0", "@aws-sdk/client-s3": "^3.758.0",
@@ -75,7 +74,7 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"jotai": "^2.12.2", "jotai": "^2.12.2",
"jsdom": "^26.0.0", "jsdom": "^26.0.0",
"jsonv-ts": "^0.1.0", "jsonv-ts": "link:jsonv-ts",
"kysely-d1": "^0.3.0", "kysely-d1": "^0.3.0",
"kysely-generic-sqlite": "^1.2.1", "kysely-generic-sqlite": "^1.2.1",
"libsql-stateless-easy": "^1.8.0", "libsql-stateless-easy": "^1.8.0",
@@ -98,6 +97,7 @@
"tsc-alias": "^1.8.11", "tsc-alias": "^1.8.11",
"tsup": "^8.4.0", "tsup": "^8.4.0",
"tsx": "^4.19.3", "tsx": "^4.19.3",
"uuid": "^11.1.0",
"vite": "^6.3.5", "vite": "^6.3.5",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.9", "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/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=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
@@ -2504,7 +2504,7 @@
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], "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=="], "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=="], "@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=="], "@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=="],