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