moved main pkg back to deps to reduce tarball size + removed zod + cleanup old code + fixed tests

This commit is contained in:
dswbx
2024-12-07 10:11:26 +01:00
parent e5924b33e5
commit 71dbfc5469
11 changed files with 28 additions and 578 deletions

View File

@@ -1,54 +0,0 @@
import { describe, expect, it, test } from "bun:test";
import { Endpoint } from "../../src/core";
import { mockFetch2, unmockFetch } from "./helper";
const testC: any = {
json: (res: any) => Response.json(res)
};
const testNext = async () => {};
describe("Endpoint", async () => {
it("behaves as expected", async () => {
const endpoint = new Endpoint("GET", "/test", async () => {
return { hello: "test" };
});
expect(endpoint.method).toBe("GET");
expect(endpoint.path).toBe("/test");
const handler = endpoint.toHandler();
const response = await handler(testC, testNext);
expect(response.ok).toBe(true);
expect(await response.json()).toEqual({ hello: "test" });
});
it("can be $request(ed)", async () => {
const obj = { hello: "test" };
const baseUrl = "https://local.com:123";
const endpoint = Endpoint.get("/test", async () => obj);
mockFetch2(async (input: RequestInfo, init: RequestInit) => {
expect(input).toBe(`${baseUrl}/test`);
return new Response(JSON.stringify(obj), { status: 200 });
});
const response = await endpoint.$request({}, baseUrl);
expect(response).toEqual({
status: 200,
ok: true,
response: obj
});
unmockFetch();
});
it("resolves helper functions", async () => {
const params = ["/test", () => ({ hello: "test" })];
["get", "post", "patch", "put", "delete"].forEach((method) => {
const endpoint = Endpoint[method](...params);
expect(endpoint.method).toBe(method.toUpperCase());
expect(endpoint.path).toBe(params[0]);
});
});
});

View File

@@ -1,6 +1,5 @@
import { describe, expect, test } from "bun:test";
import { type ObjectQuery, convert, validate } from "../../../src/core/object/query/object-query";
import { deprecated__whereRepoSchema } from "../../../src/data";
describe("object-query", () => {
const q: ObjectQuery = { name: "Michael" };
@@ -8,19 +7,6 @@ describe("object-query", () => {
const q3: ObjectQuery = { name: "Michael", age: { $gt: 18 } };
const bag = { q, q2, q3 };
test("translates into legacy", async () => {
for (const [key, value] of Object.entries(bag)) {
const obj = convert(value);
try {
const parsed = deprecated__whereRepoSchema.parse(obj);
expect(parsed).toBeDefined();
} catch (e) {
console.log("errored", { obj, value });
console.error(key, e);
}
}
});
test("validates", async () => {
const converted = convert({
name: { $eq: "ch" }

View File

@@ -3,7 +3,7 @@
"type": "module",
"sideEffects": false,
"bin": "./dist/cli/index.js",
"version": "0.2.3-rc2",
"version": "0.2.3-rc3",
"scripts": {
"build:all": "bun run build && bun run build:cli",
"dev": "vite",
@@ -23,7 +23,17 @@
"license": "FSL-1.1-MIT",
"dependencies": {
"@libsql/client": "^0.14.0",
"@tanstack/react-form": "0.19.2"
"@tanstack/react-form": "0.19.2",
"@sinclair/typebox": "^0.32.34",
"kysely": "^0.27.4",
"liquidjs": "^10.15.0",
"lodash-es": "^4.17.21",
"hono": "^4.6.12",
"fast-xml-parser": "^4.4.0",
"@cfworker/json-schema": "^2.0.1",
"dayjs": "^1.11.13",
"oauth4webapi": "^2.11.1",
"aws4fetch": "^1.0.18"
},
"devDependencies": {
"@libsql/kysely-libsql": "^0.4.1",
@@ -33,28 +43,17 @@
"@mantine/notifications": "^7.13.5",
"@radix-ui/react-scroll-area": "^1.2.0",
"@rjsf/core": "^5.22.2",
"@sinclair/typebox": "^0.32.34",
"@tabler/icons-react": "3.18.0",
"@tanstack/react-query": "^5.59.16",
"@uiw/react-codemirror": "^4.23.6",
"@xyflow/react": "^12.3.2",
"aws4fetch": "^1.0.18",
"dayjs": "^1.11.13",
"fast-xml-parser": "^4.4.0",
"hono": "^4.6.12",
"jotai": "^2.10.1",
"kysely": "^0.27.4",
"liquidjs": "^10.15.0",
"lodash-es": "^4.17.21",
"oauth4webapi": "^2.11.1",
"react-hook-form": "^7.53.1",
"react-icons": "5.2.1",
"react-json-view-lite": "^2.0.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"wouter": "^3.3.5",
"zod": "^3.23.8",
"@cfworker/json-schema": "^2.0.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-liquid": "^6.2.1",

View File

@@ -189,7 +189,11 @@ export class DurableBkndApp extends DurableObject {
const config = options.config;
// change protocol to websocket if libsql
if ("type" in config.connection && config.connection.type === "libsql") {
if (
config?.connection &&
"type" in config.connection &&
config.connection.type === "libsql"
) {
config.connection.config.protocol = "wss";
}

View File

@@ -1,5 +1,5 @@
export { Endpoint, type RequestResponse, type Middleware } from "./server/Endpoint";
export { zValidator } from "./server/lib/zValidator";
import type { Hono, MiddlewareHandler } from "hono";
export { tbValidator } from "./server/lib/tbValidator";
export { Exception, BkndError } from "./errors";
export { isDebug } from "./env";
@@ -11,7 +11,6 @@ export {
type TemplateTypes,
type SimpleRendererOptions
} from "./template/SimpleRenderer";
export { Controller, type ClassController } from "./server/Controller";
export { SchemaObject } from "./object/SchemaObject";
export { DebugLogger } from "./utils/DebugLogger";
export { Permission } from "./security/Permission";
@@ -26,3 +25,10 @@ export {
isBooleanLike
} from "./object/query/query";
export { Registry, type Constructor } from "./registry/Registry";
// compatibility
export type Middleware = MiddlewareHandler<any, any, any>;
export interface ClassController {
getController: () => Hono<any, any, any>;
getMiddleware?: MiddlewareHandler<any, any, any>;
}

View File

@@ -1,155 +0,0 @@
import { Hono, type MiddlewareHandler, type ValidationTargets } from "hono";
import type { H } from "hono/types";
import { safelyParseObjectValues } from "../utils";
import type { Endpoint, Middleware } from "./Endpoint";
import { zValidator } from "./lib/zValidator";
type RouteProxy<Endpoints> = {
[K in keyof Endpoints]: Endpoints[K];
};
export interface ClassController {
getController: () => Hono<any, any, any>;
getMiddleware?: MiddlewareHandler<any, any, any>;
}
/**
* @deprecated
*/
export class Controller<
Endpoints extends Record<string, Endpoint> = Record<string, Endpoint>,
Middlewares extends Record<string, Middleware> = Record<string, Middleware>
> {
protected endpoints: Endpoints = {} as Endpoints;
protected middlewares: Middlewares = {} as Middlewares;
public prefix: string = "/";
public routes: RouteProxy<Endpoints>;
constructor(
prefix: string = "/",
endpoints: Endpoints = {} as Endpoints,
middlewares: Middlewares = {} as Middlewares
) {
this.prefix = prefix;
this.endpoints = endpoints;
this.middlewares = middlewares;
this.routes = new Proxy(
{},
{
get: (_, name: string) => {
return this.endpoints[name];
}
}
) as RouteProxy<Endpoints>;
}
add<Name extends string, E extends Endpoint>(
this: Controller<Endpoints>,
name: Name,
endpoint: E
): Controller<Endpoints & Record<Name, E>> {
const newEndpoints = {
...this.endpoints,
[name]: endpoint
} as Endpoints & Record<Name, E>;
const newController: Controller<Endpoints & Record<Name, E>> = new Controller<
Endpoints & Record<Name, E>
>();
newController.endpoints = newEndpoints;
newController.middlewares = this.middlewares;
return newController;
}
get<Name extends keyof Endpoints>(name: Name): Endpoints[Name] {
return this.endpoints[name];
}
honoify(_hono: Hono = new Hono()) {
const hono = _hono.basePath(this.prefix);
// apply middlewares
for (const m_name in this.middlewares) {
const middleware = this.middlewares[m_name];
if (typeof middleware === "function") {
//if (isDebug()) console.log("+++ appyling middleware", m_name, middleware);
hono.use(middleware);
}
}
// apply endpoints
for (const name in this.endpoints) {
const endpoint = this.endpoints[name];
if (!endpoint) continue;
const handlers: H[] = [];
const supportedValidations: Array<keyof ValidationTargets> = ["param", "query", "json"];
// if validations are present, add them to the handlers
for (const validation of supportedValidations) {
if (endpoint.validation[validation]) {
handlers.push(async (c, next) => {
// @todo: potentially add "strict" to all schemas?
const res = await zValidator(
validation,
endpoint.validation[validation] as any,
(target, value, c) => {
if (["query", "param"].includes(target)) {
return safelyParseObjectValues(value);
}
//console.log("preprocess", target, value, c.req.raw.url);
return value;
}
)(c, next);
if (res instanceof Response && res.status === 400) {
const error = await res.json();
return c.json(
{
error: "Validation error",
target: validation,
message: error
},
400
);
}
return res;
});
}
}
// add actual handler
handlers.push(endpoint.toHandler());
const method = endpoint.method.toLowerCase() as
| "get"
| "post"
| "put"
| "delete"
| "patch";
//if (isDebug()) console.log("--- adding", method, endpoint.path);
hono[method](endpoint.path, ...handlers);
}
return hono;
}
toJSON() {
const endpoints: any = {};
for (const name in this.endpoints) {
const endpoint = this.endpoints[name];
if (!endpoint) continue;
endpoints[name] = {
method: endpoint.method,
path: (this.prefix + endpoint.path).replace("//", "/")
};
}
return endpoints;
}
}

View File

@@ -1,147 +0,0 @@
import type { Context, MiddlewareHandler, Next, ValidationTargets } from "hono";
import type { Handler } from "hono/types";
import { encodeSearch, replaceUrlParam } from "../utils";
import type { Prettify } from "../utils";
type ZodSchema = { [key: string]: any };
type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
type Validation<P, Q, B> = {
[K in keyof ValidationTargets]?: any;
} & {
param?: P extends ZodSchema ? P : undefined;
query?: Q extends ZodSchema ? Q : undefined;
json?: B extends ZodSchema ? B : undefined;
};
type ValidationInput<P, Q, B> = {
param?: P extends ZodSchema ? P["_input"] : undefined;
query?: Q extends ZodSchema ? Q["_input"] : undefined;
json?: B extends ZodSchema ? B["_input"] : undefined;
};
type HonoEnv = any;
export type Middleware = MiddlewareHandler<any, any, any>;
type HandlerFunction<P extends string, R> = (c: Context<HonoEnv, P, any>, next: Next) => R;
export type RequestResponse<R> = {
status: number;
ok: boolean;
response: Awaited<R>;
};
/**
* @deprecated
*/
export class Endpoint<
Path extends string = any,
P extends ZodSchema = any,
Q extends ZodSchema = any,
B extends ZodSchema = any,
R = any
> {
constructor(
readonly method: Method,
readonly path: Path,
readonly handler: HandlerFunction<Path, R>,
readonly validation: Validation<P, Q, B> = {}
) {}
// @todo: typing is not ideal
async $request(
args?: ValidationInput<P, Q, B>,
baseUrl: string = "http://localhost:28623"
): Promise<Prettify<RequestResponse<R>>> {
let path = this.path as string;
if (args?.param) {
path = replaceUrlParam(path, args.param);
}
if (args?.query) {
path += "?" + encodeSearch(args.query);
}
const url = [baseUrl, path].join("").replace(/\/$/, "");
const options: RequestInit = {
method: this.method,
headers: {} as any
};
if (!["GET", "HEAD"].includes(this.method)) {
if (args?.json) {
options.body = JSON.stringify(args.json);
options.headers!["Content-Type"] = "application/json";
}
}
const res = await fetch(url, options);
return {
status: res.status,
ok: res.ok,
response: (await res.json()) as any
};
}
toHandler(): Handler {
return async (c, next) => {
const res = await this.handler(c, next);
//console.log("toHandler:isResponse", res instanceof Response);
//return res;
if (res instanceof Response) {
return res;
}
return c.json(res as any) as unknown as Handler;
};
}
static get<
Path extends string = any,
P extends ZodSchema = any,
Q extends ZodSchema = any,
B extends ZodSchema = any,
R = any
>(path: Path, handler: HandlerFunction<Path, R>, validation?: Validation<P, Q, B>) {
return new Endpoint<Path, P, Q, B, R>("GET", path, handler, validation);
}
static post<
Path extends string = any,
P extends ZodSchema = any,
Q extends ZodSchema = any,
B extends ZodSchema = any,
R = any
>(path: Path, handler: HandlerFunction<Path, R>, validation?: Validation<P, Q, B>) {
return new Endpoint<Path, P, Q, B, R>("POST", path, handler, validation);
}
static patch<
Path extends string = any,
P extends ZodSchema = any,
Q extends ZodSchema = any,
B extends ZodSchema = any,
R = any
>(path: Path, handler: HandlerFunction<Path, R>, validation?: Validation<P, Q, B>) {
return new Endpoint<Path, P, Q, B, R>("PATCH", path, handler, validation);
}
static put<
Path extends string = any,
P extends ZodSchema = any,
Q extends ZodSchema = any,
B extends ZodSchema = any,
R = any
>(path: Path, handler: HandlerFunction<Path, R>, validation?: Validation<P, Q, B>) {
return new Endpoint<Path, P, Q, B, R>("PUT", path, handler, validation);
}
static delete<
Path extends string = any,
P extends ZodSchema = any,
Q extends ZodSchema = any,
B extends ZodSchema = any,
R = any
>(path: Path, handler: HandlerFunction<Path, R>, validation?: Validation<P, Q, B>) {
return new Endpoint<Path, P, Q, B, R>("DELETE", path, handler, validation);
}
}

View File

@@ -1,75 +0,0 @@
import type {
Context,
Env,
Input,
MiddlewareHandler,
TypedResponse,
ValidationTargets,
} from "hono";
import { validator } from "hono/validator";
import type { ZodError, ZodSchema, z } from "zod";
export type Hook<T, E extends Env, P extends string, O = {}> = (
result: { success: true; data: T } | { success: false; error: ZodError; data: T },
c: Context<E, P>,
) => Response | void | TypedResponse<O> | Promise<Response | void | TypedResponse<O>>;
type HasUndefined<T> = undefined extends T ? true : false;
export const zValidator = <
T extends ZodSchema,
Target extends keyof ValidationTargets,
E extends Env,
P extends string,
In = z.input<T>,
Out = z.output<T>,
I extends Input = {
in: HasUndefined<In> extends true
? {
[K in Target]?: K extends "json"
? In
: HasUndefined<keyof ValidationTargets[K]> extends true
? { [K2 in keyof In]?: ValidationTargets[K][K2] }
: { [K2 in keyof In]: ValidationTargets[K][K2] };
}
: {
[K in Target]: K extends "json"
? In
: HasUndefined<keyof ValidationTargets[K]> extends true
? { [K2 in keyof In]?: ValidationTargets[K][K2] }
: { [K2 in keyof In]: ValidationTargets[K][K2] };
};
out: { [K in Target]: Out };
},
V extends I = I,
>(
target: Target,
schema: T,
preprocess?: (target: string, value: In, c: Context<E, P>) => V, // <-- added
hook?: Hook<z.infer<T>, E, P>,
): MiddlewareHandler<E, P, V> =>
// @ts-expect-error not typed well
validator(target, async (value, c) => {
// added: preprocess value first if given
const _value = preprocess ? preprocess(target, value, c) : (value as any);
const result = await schema.safeParseAsync(_value);
if (hook) {
const hookResult = await hook({ data: value, ...result }, c);
if (hookResult) {
if (hookResult instanceof Response) {
return hookResult;
}
if ("response" in hookResult) {
return hookResult.response;
}
}
}
if (!result.success) {
return c.json(result, 400);
}
return result.data as z.infer<T>;
});

View File

@@ -369,9 +369,9 @@ export class DataController implements ClassController {
return c.notFound();
}
const where = c.req.valid("json") as RepoQuery["where"];
console.log("where", where);
//console.log("where", where);
const result = await this.em.mutator(entity).deleteMany(where);
const result = await this.em.mutator(entity).deleteWhere(where);
return c.json(this.mutatorResult(result));
}

View File

@@ -13,8 +13,6 @@ export {
whereSchema
} from "./server/data-query-impl";
export { whereRepoSchema as deprecated__whereRepoSchema } from "./server/query";
export { Connection } from "./connection/Connection";
export { LibsqlConnection, type LibSqlCredentials } from "./connection/LibsqlConnection";
export { SqliteConnection } from "./connection/SqliteConnection";

View File

@@ -1,112 +0,0 @@
import { z } from "zod";
const date = z.union([z.date(), z.string()]);
const numeric = z.union([z.number(), date]);
const boolean = z.union([z.boolean(), z.literal(1), z.literal(0)]);
const value = z.union([z.string(), boolean, numeric]);
const expressionCond = z.union([
z.object({ $eq: value }).strict(),
z.object({ $ne: value }).strict(),
z.object({ $isnull: boolean }).strict(),
z.object({ $notnull: boolean }).strict(),
z.object({ $in: z.array(value) }).strict(),
z.object({ $notin: z.array(value) }).strict(),
z.object({ $gt: numeric }).strict(),
z.object({ $gte: numeric }).strict(),
z.object({ $lt: numeric }).strict(),
z.object({ $lte: numeric }).strict(),
z.object({ $between: z.array(numeric).min(2).max(2) }).strict()
] as const);
// prettier-ignore
const nonOperandString = z
.string()
.regex(/^(?!\$).*/)
.min(1);
// {name: 'Michael'}
const literalCond = z.record(nonOperandString, value);
// { status: { $eq: 1 } }
const literalExpressionCond = z.record(nonOperandString, value.or(expressionCond));
const operandCond = z
.object({
$and: z.array(literalCond.or(expressionCond).or(literalExpressionCond)).optional(),
$or: z.array(literalCond.or(expressionCond).or(literalExpressionCond)).optional()
})
.strict();
const literalSchema = literalCond.or(literalExpressionCond);
export type LiteralSchemaIn = z.input<typeof literalSchema>;
export type LiteralSchema = z.output<typeof literalSchema>;
export const filterSchema = literalSchema.or(operandCond);
export type FilterSchemaIn = z.input<typeof filterSchema>;
export type FilterSchema = z.output<typeof filterSchema>;
const stringArray = z
.union([
z.string().transform((v) => {
if (v.includes(",")) return v.split(",");
return v;
}),
z.array(z.string())
])
.default([])
.transform((v) => (Array.isArray(v) ? v : [v]));
export const whereRepoSchema = z
.preprocess((v: unknown) => {
try {
return JSON.parse(v as string);
} catch {
return v;
}
}, filterSchema)
.default({});
const repoQuerySchema = z.object({
limit: z.coerce.number().default(10),
offset: z.coerce.number().default(0),
sort: z
.preprocess(
(v: unknown) => {
try {
return JSON.parse(v as string);
} catch {
return v;
}
},
z.union([
z.string().transform((v) => {
if (v.includes(":")) {
let [field, dir] = v.split(":") as [string, string];
if (!["asc", "desc"].includes(dir)) dir = "asc";
return { by: field, dir } as { by: string; dir: "asc" | "desc" };
} else {
return { by: v, dir: "asc" } as { by: string; dir: "asc" | "desc" };
}
}),
z.object({
by: z.string(),
dir: z.enum(["asc", "desc"])
})
])
)
.default({ by: "id", dir: "asc" }),
select: stringArray,
with: stringArray,
join: stringArray,
debug: z
.preprocess((v) => {
if (["0", "false"].includes(String(v))) return false;
return Boolean(v);
}, z.boolean())
.default(false), //z.coerce.boolean().catch(false),
where: whereRepoSchema
});
type RepoQueryIn = z.input<typeof repoQuerySchema>;
type RepoQuery = z.output<typeof repoQuerySchema>;