From 71dbfc5469d5cb08ec2a6b6c5ed357b21e7e898a Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 7 Dec 2024 10:11:26 +0100 Subject: [PATCH] moved main pkg back to deps to reduce tarball size + removed zod + cleanup old code + fixed tests --- app/__test__/core/Endpoint.spec.ts | 54 ------ app/__test__/core/object/object-query.spec.ts | 14 -- app/package.json | 25 ++- .../cloudflare/cloudflare-workers.adapter.ts | 6 +- app/src/core/index.ts | 12 +- app/src/core/server/Controller.ts | 155 ------------------ app/src/core/server/Endpoint.ts | 147 ----------------- app/src/core/server/lib/zValidator.ts | 75 --------- app/src/data/api/DataController.ts | 4 +- app/src/data/index.ts | 2 - app/src/data/server/query.ts | 112 ------------- 11 files changed, 28 insertions(+), 578 deletions(-) delete mode 100644 app/__test__/core/Endpoint.spec.ts delete mode 100644 app/src/core/server/Controller.ts delete mode 100644 app/src/core/server/Endpoint.ts delete mode 100644 app/src/core/server/lib/zValidator.ts delete mode 100644 app/src/data/server/query.ts diff --git a/app/__test__/core/Endpoint.spec.ts b/app/__test__/core/Endpoint.spec.ts deleted file mode 100644 index 8dd9c10..0000000 --- a/app/__test__/core/Endpoint.spec.ts +++ /dev/null @@ -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]); - }); - }); -}); diff --git a/app/__test__/core/object/object-query.spec.ts b/app/__test__/core/object/object-query.spec.ts index 66e7215..a6940a9 100644 --- a/app/__test__/core/object/object-query.spec.ts +++ b/app/__test__/core/object/object-query.spec.ts @@ -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" } diff --git a/app/package.json b/app/package.json index e29c82d..9653b74 100644 --- a/app/package.json +++ b/app/package.json @@ -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", diff --git a/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts b/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts index 5224c10..0d2126a 100644 --- a/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts +++ b/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts @@ -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"; } diff --git a/app/src/core/index.ts b/app/src/core/index.ts index 5229bd9..e296c1d 100644 --- a/app/src/core/index.ts +++ b/app/src/core/index.ts @@ -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; +export interface ClassController { + getController: () => Hono; + getMiddleware?: MiddlewareHandler; +} diff --git a/app/src/core/server/Controller.ts b/app/src/core/server/Controller.ts deleted file mode 100644 index 9c74571..0000000 --- a/app/src/core/server/Controller.ts +++ /dev/null @@ -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 = { - [K in keyof Endpoints]: Endpoints[K]; -}; - -export interface ClassController { - getController: () => Hono; - getMiddleware?: MiddlewareHandler; -} - -/** - * @deprecated - */ -export class Controller< - Endpoints extends Record = Record, - Middlewares extends Record = Record -> { - protected endpoints: Endpoints = {} as Endpoints; - protected middlewares: Middlewares = {} as Middlewares; - - public prefix: string = "/"; - public routes: RouteProxy; - - 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; - } - - add( - this: Controller, - name: Name, - endpoint: E - ): Controller> { - const newEndpoints = { - ...this.endpoints, - [name]: endpoint - } as Endpoints & Record; - const newController: Controller> = new Controller< - Endpoints & Record - >(); - newController.endpoints = newEndpoints; - newController.middlewares = this.middlewares; - return newController; - } - - get(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 = ["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; - } -} diff --git a/app/src/core/server/Endpoint.ts b/app/src/core/server/Endpoint.ts deleted file mode 100644 index c45b644..0000000 --- a/app/src/core/server/Endpoint.ts +++ /dev/null @@ -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 = { - [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 = { - 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; - -type HandlerFunction

= (c: Context, next: Next) => R; -export type RequestResponse = { - status: number; - ok: boolean; - response: Awaited; -}; - -/** - * @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, - readonly validation: Validation = {} - ) {} - - // @todo: typing is not ideal - async $request( - args?: ValidationInput, - baseUrl: string = "http://localhost:28623" - ): Promise>> { - 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, validation?: Validation) { - return new Endpoint("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, validation?: Validation) { - return new Endpoint("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, validation?: Validation) { - return new Endpoint("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, validation?: Validation) { - return new Endpoint("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, validation?: Validation) { - return new Endpoint("DELETE", path, handler, validation); - } -} diff --git a/app/src/core/server/lib/zValidator.ts b/app/src/core/server/lib/zValidator.ts deleted file mode 100644 index 4d16a59..0000000 --- a/app/src/core/server/lib/zValidator.ts +++ /dev/null @@ -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 = ( - result: { success: true; data: T } | { success: false; error: ZodError; data: T }, - c: Context, -) => Response | void | TypedResponse | Promise>; - -type HasUndefined = undefined extends T ? true : false; - -export const zValidator = < - T extends ZodSchema, - Target extends keyof ValidationTargets, - E extends Env, - P extends string, - In = z.input, - Out = z.output, - I extends Input = { - in: HasUndefined extends true - ? { - [K in Target]?: K extends "json" - ? In - : HasUndefined extends true - ? { [K2 in keyof In]?: ValidationTargets[K][K2] } - : { [K2 in keyof In]: ValidationTargets[K][K2] }; - } - : { - [K in Target]: K extends "json" - ? In - : HasUndefined 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) => V, // <-- added - hook?: Hook, E, P>, -): MiddlewareHandler => - // @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; - }); diff --git a/app/src/data/api/DataController.ts b/app/src/data/api/DataController.ts index 4036b44..3f459dc 100644 --- a/app/src/data/api/DataController.ts +++ b/app/src/data/api/DataController.ts @@ -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)); } diff --git a/app/src/data/index.ts b/app/src/data/index.ts index f4a67bc..284c653 100644 --- a/app/src/data/index.ts +++ b/app/src/data/index.ts @@ -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"; diff --git a/app/src/data/server/query.ts b/app/src/data/server/query.ts deleted file mode 100644 index ddc03db..0000000 --- a/app/src/data/server/query.ts +++ /dev/null @@ -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; -export type LiteralSchema = z.output; - -export const filterSchema = literalSchema.or(operandCond); -export type FilterSchemaIn = z.input; -export type FilterSchema = z.output; - -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; -type RepoQuery = z.output;