mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
Controllers: New validation + auto OpenAPI (#173)
* updated controllers to use custom json schema and added auto openapi specs * fix data routes parsing body * added schema exports to core * added swagger link to Admin, switched use-search
This commit is contained in:
18
app/build.ts
18
app/build.ts
@@ -1,5 +1,6 @@
|
||||
import { $ } from "bun";
|
||||
import * as tsup from "tsup";
|
||||
import pkg from "./package.json" with { type: "json" };
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const watch = args.includes("--watch");
|
||||
@@ -9,7 +10,7 @@ const sourcemap = args.includes("--sourcemap");
|
||||
const clean = args.includes("--clean");
|
||||
|
||||
if (clean) {
|
||||
console.log("Cleaning dist (w/o static)");
|
||||
console.info("Cleaning dist (w/o static)");
|
||||
await $`find dist -mindepth 1 ! -path "dist/static/*" ! -path "dist/static" -exec rm -rf {} +`;
|
||||
}
|
||||
|
||||
@@ -21,11 +22,11 @@ function buildTypes() {
|
||||
Bun.spawn(["bun", "build:types"], {
|
||||
stdout: "inherit",
|
||||
onExit: () => {
|
||||
console.log("Types built");
|
||||
console.info("Types built");
|
||||
Bun.spawn(["bun", "tsc-alias"], {
|
||||
stdout: "inherit",
|
||||
onExit: () => {
|
||||
console.log("Types aliased");
|
||||
console.info("Types aliased");
|
||||
types_running = false;
|
||||
},
|
||||
});
|
||||
@@ -47,10 +48,10 @@ if (types && !watch) {
|
||||
}
|
||||
|
||||
function banner(title: string) {
|
||||
console.log("");
|
||||
console.log("=".repeat(40));
|
||||
console.log(title.toUpperCase());
|
||||
console.log("-".repeat(40));
|
||||
console.info("");
|
||||
console.info("=".repeat(40));
|
||||
console.info(title.toUpperCase());
|
||||
console.info("-".repeat(40));
|
||||
}
|
||||
|
||||
// collection of always-external packages
|
||||
@@ -65,6 +66,9 @@ async function buildApi() {
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
define: {
|
||||
__version: JSON.stringify(pkg.version),
|
||||
},
|
||||
entry: [
|
||||
"src/index.ts",
|
||||
"src/core/index.ts",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"@codemirror/lang-html": "^6.4.9",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@hono/swagger-ui": "^0.5.1",
|
||||
"@libsql/client": "^0.15.2",
|
||||
"@mantine/core": "^7.17.1",
|
||||
"@mantine/hooks": "^7.17.1",
|
||||
@@ -64,7 +65,6 @@
|
||||
"json-schema-form-react": "^0.0.2",
|
||||
"json-schema-library": "10.0.0-rc7",
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"jsonv-ts": "^0.0.11",
|
||||
"kysely": "^0.27.6",
|
||||
"lodash-es": "^4.17.21",
|
||||
"oauth4webapi": "^2.11.1",
|
||||
@@ -99,6 +99,7 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"jotai": "^2.12.2",
|
||||
"jsdom": "^26.0.0",
|
||||
"jsonv-ts": "^0.0.14-alpha.6",
|
||||
"kysely-d1": "^0.3.0",
|
||||
"open": "^10.1.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { type AppAuth, AuthPermissions, type SafeUser, type Strategy } from "auth";
|
||||
import { tbValidator as tb } from "core";
|
||||
import { TypeInvalidError, parse, transformObject } from "core/utils";
|
||||
import { DataPermissions } from "data";
|
||||
import type { Hono } from "hono";
|
||||
import { Controller, type ServerEnv } from "modules/Controller";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
const { Type } = tbbox;
|
||||
import { describeRoute, jsc, s } from "core/object/schema";
|
||||
|
||||
export type AuthActionResponse = {
|
||||
success: boolean;
|
||||
@@ -14,10 +12,6 @@ export type AuthActionResponse = {
|
||||
errors?: any;
|
||||
};
|
||||
|
||||
const booleanLike = Type.Transform(Type.String())
|
||||
.Decode((v) => v === "1")
|
||||
.Encode((v) => (v ? "1" : "0"));
|
||||
|
||||
export class AuthController extends Controller {
|
||||
constructor(private auth: AppAuth) {
|
||||
super();
|
||||
@@ -56,6 +50,10 @@ export class AuthController extends Controller {
|
||||
hono.post(
|
||||
"/create",
|
||||
permission([AuthPermissions.createUser, DataPermissions.entityCreate]),
|
||||
describeRoute({
|
||||
summary: "Create a new user",
|
||||
tags: ["auth"],
|
||||
}),
|
||||
async (c) => {
|
||||
try {
|
||||
const body = await this.auth.authenticator.getBody(c);
|
||||
@@ -93,9 +91,16 @@ export class AuthController extends Controller {
|
||||
}
|
||||
},
|
||||
);
|
||||
hono.get("create/schema.json", async (c) => {
|
||||
hono.get(
|
||||
"create/schema.json",
|
||||
describeRoute({
|
||||
summary: "Get the schema for creating a user",
|
||||
tags: ["auth"],
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(create.schema);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
mainHono.route(`/${name}/actions`, hono);
|
||||
@@ -104,16 +109,15 @@ export class AuthController extends Controller {
|
||||
override getController() {
|
||||
const { auth } = this.middlewares;
|
||||
const hono = this.create();
|
||||
const strategies = this.auth.authenticator.getStrategies();
|
||||
|
||||
for (const [name, strategy] of Object.entries(strategies)) {
|
||||
if (!this.auth.isStrategyEnabled(strategy)) continue;
|
||||
|
||||
hono.route(`/${name}`, strategy.getController(this.auth.authenticator));
|
||||
this.registerStrategyActions(strategy, hono);
|
||||
}
|
||||
|
||||
hono.get("/me", auth(), async (c) => {
|
||||
hono.get(
|
||||
"/me",
|
||||
describeRoute({
|
||||
summary: "Get the current user",
|
||||
tags: ["auth"],
|
||||
}),
|
||||
auth(),
|
||||
async (c) => {
|
||||
const claims = c.get("auth")?.user;
|
||||
if (claims) {
|
||||
const { data: user } = await this.userRepo.findId(claims.id);
|
||||
@@ -121,9 +125,17 @@ export class AuthController extends Controller {
|
||||
}
|
||||
|
||||
return c.json({ user: null }, 403);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
hono.get("/logout", auth(), async (c) => {
|
||||
hono.get(
|
||||
"/logout",
|
||||
describeRoute({
|
||||
summary: "Logout the current user",
|
||||
tags: ["auth"],
|
||||
}),
|
||||
auth(),
|
||||
async (c) => {
|
||||
await this.auth.authenticator.logout(c);
|
||||
if (this.auth.authenticator.isJsonRequest(c)) {
|
||||
return c.json({ ok: true });
|
||||
@@ -135,11 +147,16 @@ export class AuthController extends Controller {
|
||||
}
|
||||
|
||||
return c.redirect("/");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
hono.get(
|
||||
"/strategies",
|
||||
tb("query", Type.Object({ include_disabled: Type.Optional(booleanLike) })),
|
||||
describeRoute({
|
||||
summary: "Get the available authentication strategies",
|
||||
tags: ["auth"],
|
||||
}),
|
||||
jsc("query", s.object({ include_disabled: s.boolean().optional() })),
|
||||
async (c) => {
|
||||
const { include_disabled } = c.req.valid("query");
|
||||
const { strategies, basepath } = this.auth.toJSON(false);
|
||||
@@ -157,6 +174,15 @@ export class AuthController extends Controller {
|
||||
},
|
||||
);
|
||||
|
||||
const strategies = this.auth.authenticator.getStrategies();
|
||||
|
||||
for (const [name, strategy] of Object.entries(strategies)) {
|
||||
if (!this.auth.isStrategyEnabled(strategy)) continue;
|
||||
|
||||
hono.route(`/${name}`, strategy.getController(this.auth.authenticator));
|
||||
this.registerStrategyActions(strategy, hono);
|
||||
}
|
||||
|
||||
return hono.all("*", (c) => c.notFound());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,15 @@ export function isDebug(): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export function getVersion(): string {
|
||||
try {
|
||||
// @ts-expect-error - this is a global variable in dev
|
||||
return __version;
|
||||
} catch (e) {
|
||||
return "0.0.0";
|
||||
}
|
||||
}
|
||||
|
||||
const envs = {
|
||||
// used in $console to determine the log level
|
||||
cli_log_level: {
|
||||
|
||||
@@ -26,6 +26,7 @@ export {
|
||||
} from "./object/query/query";
|
||||
export { Registry, type Constructor } from "./registry/Registry";
|
||||
export { getFlashMessage } from "./server/flash";
|
||||
export { s, jsc, describeRoute } from "./object/schema";
|
||||
|
||||
export * from "./console";
|
||||
export * from "./events";
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { mergeObject } from "core/utils";
|
||||
|
||||
export { jsc, type Options, type Hook } from "./validator";
|
||||
//export { jsc, type Options, type Hook } from "./validator";
|
||||
import * as s from "jsonv-ts";
|
||||
|
||||
export { validator as jsc, type Options } from "jsonv-ts/hono";
|
||||
export { describeRoute, schemaToSpec, openAPISpecs } from "jsonv-ts/hono";
|
||||
|
||||
export { s };
|
||||
|
||||
export class InvalidSchemaError extends Error {
|
||||
@@ -21,6 +24,12 @@ export class InvalidSchemaError extends Error {
|
||||
export type ParseOptions = {
|
||||
withDefaults?: boolean;
|
||||
coerse?: boolean;
|
||||
clone?: boolean;
|
||||
};
|
||||
|
||||
const cloneSchema = <S extends s.TSchema>(schema: S): S => {
|
||||
const json = schema.toJSON();
|
||||
return s.fromSchema(json) as S;
|
||||
};
|
||||
|
||||
export function parse<S extends s.TAnySchema>(
|
||||
@@ -28,7 +37,7 @@ export function parse<S extends s.TAnySchema>(
|
||||
v: unknown,
|
||||
opts: ParseOptions = {},
|
||||
): s.StaticCoerced<S> {
|
||||
const schema = _schema as unknown as s.TSchema;
|
||||
const schema = (opts.clone ? cloneSchema(_schema as any) : _schema) as s.TSchema;
|
||||
const value = opts.coerse !== false ? schema.coerce(v) : v;
|
||||
const result = schema.validate(value, {
|
||||
shortCircuit: true,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { $console, isDebug, tbValidator as tb } from "core";
|
||||
import { StringEnum } from "core/utils";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
import { $console, isDebug } from "core";
|
||||
import {
|
||||
DataPermissions,
|
||||
type EntityData,
|
||||
@@ -13,10 +11,10 @@ import {
|
||||
import type { Handler } from "hono/types";
|
||||
import type { ModuleBuildContext } from "modules";
|
||||
import { Controller } from "modules/Controller";
|
||||
import { jsc, s } from "core/object/schema";
|
||||
import { jsc, s, describeRoute, schemaToSpec } from "core/object/schema";
|
||||
import * as SystemPermissions from "modules/permissions";
|
||||
import type { AppDataConfig } from "../data-schema";
|
||||
const { Type } = tbbox;
|
||||
import { omitKeys } from "core/utils";
|
||||
|
||||
export class DataController extends Controller {
|
||||
constructor(
|
||||
@@ -72,6 +70,7 @@ export class DataController extends Controller {
|
||||
override getController() {
|
||||
const { permission, auth } = this.middlewares;
|
||||
const hono = this.create().use(auth(), permission(SystemPermissions.accessApi));
|
||||
const entitiesEnum = this.getEntitiesEnum(this.em);
|
||||
|
||||
// @todo: sample implementation how to augment handler with additional info
|
||||
function handler<HH extends Handler>(name: string, h: HH): any {
|
||||
@@ -84,6 +83,10 @@ export class DataController extends Controller {
|
||||
// info
|
||||
hono.get(
|
||||
"/",
|
||||
describeRoute({
|
||||
summary: "Retrieve data configuration",
|
||||
tags: ["data"],
|
||||
}),
|
||||
handler("data info", (c) => {
|
||||
// sample implementation
|
||||
return c.json(this.em.toJSON());
|
||||
@@ -91,9 +94,22 @@ export class DataController extends Controller {
|
||||
);
|
||||
|
||||
// sync endpoint
|
||||
hono.get("/sync", permission(DataPermissions.databaseSync), async (c) => {
|
||||
const force = c.req.query("force") === "1";
|
||||
const drop = c.req.query("drop") === "1";
|
||||
hono.get(
|
||||
"/sync",
|
||||
permission(DataPermissions.databaseSync),
|
||||
describeRoute({
|
||||
summary: "Sync database schema",
|
||||
tags: ["data"],
|
||||
}),
|
||||
jsc(
|
||||
"query",
|
||||
s.partialObject({
|
||||
force: s.boolean(),
|
||||
drop: s.boolean(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const { force, drop } = c.req.valid("query");
|
||||
//console.log("force", force);
|
||||
const tables = await this.em.schema().introspect();
|
||||
//console.log("tables", tables);
|
||||
@@ -102,13 +118,21 @@ export class DataController extends Controller {
|
||||
drop,
|
||||
});
|
||||
return c.json({ tables: tables.map((t) => t.name), changes });
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Schema endpoints
|
||||
*/
|
||||
// read entity schema
|
||||
hono.get("/schema.json", permission(DataPermissions.entityRead), async (c) => {
|
||||
hono.get(
|
||||
"/schema.json",
|
||||
permission(DataPermissions.entityRead),
|
||||
describeRoute({
|
||||
summary: "Retrieve data schema",
|
||||
tags: ["data"],
|
||||
}),
|
||||
async (c) => {
|
||||
const $id = `${this.config.basepath}/schema.json`;
|
||||
const schemas = Object.fromEntries(
|
||||
this.em.entities.map((e) => [
|
||||
@@ -123,17 +147,22 @@ export class DataController extends Controller {
|
||||
$id,
|
||||
properties: schemas,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// read schema
|
||||
hono.get(
|
||||
"/schemas/:entity/:context?",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb(
|
||||
describeRoute({
|
||||
summary: "Retrieve entity schema",
|
||||
tags: ["data"],
|
||||
}),
|
||||
jsc(
|
||||
"param",
|
||||
Type.Object({
|
||||
entity: Type.String(),
|
||||
context: Type.Optional(StringEnum(["create", "update"])),
|
||||
s.object({
|
||||
entity: entitiesEnum,
|
||||
context: s.string({ enum: ["create", "update"], default: "create" }).optional(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -162,7 +191,15 @@ export class DataController extends Controller {
|
||||
/**
|
||||
* Info endpoints
|
||||
*/
|
||||
hono.get("/info/:entity", async (c) => {
|
||||
hono.get(
|
||||
"/info/:entity",
|
||||
permission(DataPermissions.entityRead),
|
||||
describeRoute({
|
||||
summary: "Retrieve entity info",
|
||||
tags: ["data"],
|
||||
}),
|
||||
jsc("param", s.object({ entity: entitiesEnum })),
|
||||
async (c) => {
|
||||
const { entity } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return this.notFound(c);
|
||||
@@ -185,7 +222,8 @@ export class DataController extends Controller {
|
||||
target: $rels(this.em.relations.targetRelationsOf(_entity)),
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return hono.all("*", (c) => c.notFound());
|
||||
}
|
||||
@@ -194,10 +232,7 @@ export class DataController extends Controller {
|
||||
const { permission } = this.middlewares;
|
||||
const hono = this.create();
|
||||
|
||||
const definedEntities = this.em.entities.map((e) => e.name);
|
||||
const tbNumber = Type.Transform(Type.String({ pattern: "^[1-9][0-9]{0,}$" }))
|
||||
.Decode(Number.parseInt)
|
||||
.Encode(String);
|
||||
const entitiesEnum = this.getEntitiesEnum(this.em);
|
||||
|
||||
/**
|
||||
* Function endpoints
|
||||
@@ -206,14 +241,19 @@ export class DataController extends Controller {
|
||||
hono.post(
|
||||
"/:entity/fn/count",
|
||||
permission(DataPermissions.entityRead),
|
||||
jsc("param", s.object({ entity: s.string() })),
|
||||
describeRoute({
|
||||
summary: "Count entities",
|
||||
tags: ["data"],
|
||||
}),
|
||||
jsc("param", s.object({ entity: entitiesEnum })),
|
||||
jsc("json", repoQuery.properties.where),
|
||||
async (c) => {
|
||||
const { entity } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
return this.notFound(c);
|
||||
}
|
||||
|
||||
const where = (await c.req.json()) as any;
|
||||
const where = c.req.valid("json") as any;
|
||||
const result = await this.em.repository(entity).count(where);
|
||||
return c.json({ entity, count: result.count });
|
||||
},
|
||||
@@ -223,14 +263,19 @@ export class DataController extends Controller {
|
||||
hono.post(
|
||||
"/:entity/fn/exists",
|
||||
permission(DataPermissions.entityRead),
|
||||
jsc("param", s.object({ entity: s.string() })),
|
||||
describeRoute({
|
||||
summary: "Check if entity exists",
|
||||
tags: ["data"],
|
||||
}),
|
||||
jsc("param", s.object({ entity: entitiesEnum })),
|
||||
jsc("json", repoQuery.properties.where),
|
||||
async (c) => {
|
||||
const { entity } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
return this.notFound(c);
|
||||
}
|
||||
|
||||
const where = c.req.json() as any;
|
||||
const where = c.req.valid("json") as any;
|
||||
const result = await this.em.repository(entity).exists(where);
|
||||
return c.json({ entity, exists: result.exists });
|
||||
},
|
||||
@@ -240,11 +285,29 @@ 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 saveRepoQueryParams = (pick: string[] = Object.keys(repoQuery.properties)) => [
|
||||
...(schemaToSpec(saveRepoQuery, "query").parameters?.filter(
|
||||
// @ts-ignore
|
||||
(p) => pick.includes(p.name),
|
||||
) as any),
|
||||
];
|
||||
|
||||
hono.get(
|
||||
"/:entity",
|
||||
describeRoute({
|
||||
summary: "Read many",
|
||||
parameters: saveRepoQueryParams(["limit", "offset", "sort", "select", "join"]),
|
||||
tags: ["data"],
|
||||
}),
|
||||
permission(DataPermissions.entityRead),
|
||||
jsc("param", s.object({ entity: s.string() })),
|
||||
jsc("query", repoQuery),
|
||||
jsc("param", s.object({ entity: entitiesEnum })),
|
||||
jsc("query", repoQuery, { skipOpenAPI: true }),
|
||||
async (c) => {
|
||||
const { entity } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
@@ -260,15 +323,20 @@ export class DataController extends Controller {
|
||||
// read one
|
||||
hono.get(
|
||||
"/:entity/:id",
|
||||
describeRoute({
|
||||
summary: "Read one",
|
||||
parameters: saveRepoQueryParams(["offset", "sort", "select"]),
|
||||
tags: ["data"],
|
||||
}),
|
||||
permission(DataPermissions.entityRead),
|
||||
jsc(
|
||||
"param",
|
||||
s.object({
|
||||
entity: s.string(),
|
||||
entity: entitiesEnum,
|
||||
id: s.string(),
|
||||
}),
|
||||
),
|
||||
jsc("query", repoQuery),
|
||||
jsc("query", repoQuery, { skipOpenAPI: true }),
|
||||
async (c) => {
|
||||
const { entity, id } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
@@ -284,16 +352,21 @@ export class DataController extends Controller {
|
||||
// read many by reference
|
||||
hono.get(
|
||||
"/:entity/:id/:reference",
|
||||
describeRoute({
|
||||
summary: "Read many by reference",
|
||||
parameters: saveRepoQueryParams(),
|
||||
tags: ["data"],
|
||||
}),
|
||||
permission(DataPermissions.entityRead),
|
||||
jsc(
|
||||
"param",
|
||||
s.object({
|
||||
entity: s.string(),
|
||||
entity: entitiesEnum,
|
||||
id: s.string(),
|
||||
reference: s.string(),
|
||||
}),
|
||||
),
|
||||
jsc("query", repoQuery),
|
||||
jsc("query", repoQuery, { skipOpenAPI: true }),
|
||||
async (c) => {
|
||||
const { entity, id, reference } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
@@ -310,17 +383,33 @@ export class DataController extends Controller {
|
||||
);
|
||||
|
||||
// func query
|
||||
const fnQuery = s.partialObject({
|
||||
...saveRepoQuery.properties,
|
||||
with: s.object({}),
|
||||
});
|
||||
hono.post(
|
||||
"/:entity/query",
|
||||
describeRoute({
|
||||
summary: "Query entities",
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: fnQuery.toJSON(),
|
||||
example: fnQuery.template({ withOptional: true }),
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ["data"],
|
||||
}),
|
||||
permission(DataPermissions.entityRead),
|
||||
jsc("param", s.object({ entity: s.string() })),
|
||||
jsc("json", repoQuery),
|
||||
jsc("param", s.object({ entity: entitiesEnum })),
|
||||
jsc("json", repoQuery, { skipOpenAPI: true }),
|
||||
async (c) => {
|
||||
const { entity } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
return this.notFound(c);
|
||||
}
|
||||
const options = (await c.req.valid("json")) as RepoQuery;
|
||||
const options = (await c.req.json()) as RepoQuery;
|
||||
const result = await this.em.repository(entity).findMany(options);
|
||||
|
||||
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
||||
@@ -333,10 +422,13 @@ export class DataController extends Controller {
|
||||
// insert one
|
||||
hono.post(
|
||||
"/:entity",
|
||||
describeRoute({
|
||||
summary: "Insert one or many",
|
||||
tags: ["data"],
|
||||
}),
|
||||
permission(DataPermissions.entityCreate),
|
||||
jsc("param", s.object({ entity: s.string() })),
|
||||
jsc("param", s.object({ entity: entitiesEnum })),
|
||||
jsc("json", s.anyOf([s.object({}), s.array(s.object({}))])),
|
||||
//tb("json", Type.Union([Type.Object({}), Type.Array(Type.Object({}))])),
|
||||
async (c) => {
|
||||
const { entity } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
@@ -357,8 +449,12 @@ export class DataController extends Controller {
|
||||
// update many
|
||||
hono.patch(
|
||||
"/:entity",
|
||||
describeRoute({
|
||||
summary: "Update many",
|
||||
tags: ["data"],
|
||||
}),
|
||||
permission(DataPermissions.entityUpdate),
|
||||
jsc("param", s.object({ entity: s.string() })),
|
||||
jsc("param", s.object({ entity: entitiesEnum })),
|
||||
jsc(
|
||||
"json",
|
||||
s.object({
|
||||
@@ -384,8 +480,13 @@ export class DataController extends Controller {
|
||||
// update one
|
||||
hono.patch(
|
||||
"/:entity/:id",
|
||||
describeRoute({
|
||||
summary: "Update one",
|
||||
tags: ["data"],
|
||||
}),
|
||||
permission(DataPermissions.entityUpdate),
|
||||
jsc("param", s.object({ entity: s.string(), id: s.number() })),
|
||||
jsc("param", s.object({ entity: entitiesEnum, id: s.number() })),
|
||||
jsc("json", s.object({})),
|
||||
async (c) => {
|
||||
const { entity, id } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
@@ -401,8 +502,12 @@ export class DataController extends Controller {
|
||||
// delete one
|
||||
hono.delete(
|
||||
"/:entity/:id",
|
||||
describeRoute({
|
||||
summary: "Delete one",
|
||||
tags: ["data"],
|
||||
}),
|
||||
permission(DataPermissions.entityDelete),
|
||||
jsc("param", s.object({ entity: s.string(), id: s.number() })),
|
||||
jsc("param", s.object({ entity: entitiesEnum, id: s.number() })),
|
||||
async (c) => {
|
||||
const { entity, id } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
@@ -417,15 +522,19 @@ export class DataController extends Controller {
|
||||
// delete many
|
||||
hono.delete(
|
||||
"/:entity",
|
||||
describeRoute({
|
||||
summary: "Delete many",
|
||||
tags: ["data"],
|
||||
}),
|
||||
permission(DataPermissions.entityDelete),
|
||||
jsc("param", s.object({ entity: s.string() })),
|
||||
jsc("param", s.object({ entity: entitiesEnum })),
|
||||
jsc("json", repoQuery.properties.where),
|
||||
async (c) => {
|
||||
const { entity } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
return this.notFound(c);
|
||||
}
|
||||
const where = c.req.valid("json") as RepoQuery["where"];
|
||||
const where = (await c.req.json()) as RepoQuery["where"];
|
||||
const result = await this.em.mutator(entity).deleteWhere(where);
|
||||
|
||||
return c.json(this.mutatorResult(result));
|
||||
|
||||
@@ -10,18 +10,6 @@ const stringIdentifier = s.string({
|
||||
// allow "id", "id,title" – but not "id," or "not allowed"
|
||||
pattern: "^(?:[a-zA-Z_$][\\w$]*)(?:,[a-zA-Z_$][\\w$]*)*$",
|
||||
});
|
||||
const numberOrString = <N extends s.UnionSchema>(c: N = {} as N) =>
|
||||
s.anyOf([s.number(), s.string()], {
|
||||
...c,
|
||||
coerse: function (this: s.TSchema, v): number {
|
||||
if (typeof v === "string") {
|
||||
const n = Number.parseInt(v);
|
||||
if (Number.isNaN(n)) return this.default ?? 10;
|
||||
return n;
|
||||
}
|
||||
return v as number;
|
||||
},
|
||||
}) as unknown as s.TSchemaInOut<number | string, number>;
|
||||
const stringArray = s.anyOf(
|
||||
[
|
||||
stringIdentifier,
|
||||
@@ -75,6 +63,13 @@ const sort = s.anyOf([s.string(), sortSchema], {
|
||||
// filter
|
||||
const where = s.anyOf([s.string(), s.object({})], {
|
||||
default: {},
|
||||
examples: [
|
||||
{
|
||||
attribute: {
|
||||
$eq: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
coerce: (value: unknown) => {
|
||||
const q = typeof value === "string" ? JSON.parse(value) : value;
|
||||
return WhereBuilder.convert(q);
|
||||
@@ -132,8 +127,8 @@ const withSchema = <In, Out = In>(self: s.TSchema): s.TSchemaInOut<In, Out> =>
|
||||
// REPO QUERY
|
||||
export const repoQuery = s.recursive((self) =>
|
||||
s.partialObject({
|
||||
limit: numberOrString({ default: 10 }),
|
||||
offset: numberOrString({ default: 0 }),
|
||||
limit: s.number({ default: 10 }),
|
||||
offset: s.number({ default: 0 }),
|
||||
sort,
|
||||
where,
|
||||
select: stringArray,
|
||||
|
||||
@@ -6,12 +6,7 @@ import { DataPermissions } from "data";
|
||||
import { Controller } from "modules/Controller";
|
||||
import type { AppMedia } from "../AppMedia";
|
||||
import { MediaField } from "../MediaField";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
const { Type } = tbbox;
|
||||
|
||||
const booleanLike = Type.Transform(Type.String())
|
||||
.Decode((v) => v === "1")
|
||||
.Encode((v) => (v ? "1" : "0"));
|
||||
import { jsc, s, describeRoute } from "core/object/schema";
|
||||
|
||||
export class MediaController extends Controller {
|
||||
constructor(private readonly media: AppMedia) {
|
||||
@@ -31,22 +26,40 @@ export class MediaController extends Controller {
|
||||
// @todo: implement range requests
|
||||
const { auth, permission } = this.middlewares;
|
||||
const hono = this.create().use(auth());
|
||||
const entitiesEnum = this.getEntitiesEnum(this.media.em);
|
||||
|
||||
// get files list (temporary)
|
||||
hono.get("/files", permission(MediaPermissions.listFiles), async (c) => {
|
||||
hono.get(
|
||||
"/files",
|
||||
describeRoute({
|
||||
summary: "Get the list of files",
|
||||
tags: ["media"],
|
||||
}),
|
||||
permission(MediaPermissions.listFiles),
|
||||
async (c) => {
|
||||
const files = await this.getStorageAdapter().listObjects();
|
||||
return c.json(files);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// get file by name
|
||||
// @todo: implement more aggressive cache? (configurable)
|
||||
hono.get("/file/:filename", permission(MediaPermissions.readFile), async (c) => {
|
||||
hono.get(
|
||||
"/file/:filename",
|
||||
describeRoute({
|
||||
summary: "Get a file by name",
|
||||
tags: ["media"],
|
||||
}),
|
||||
permission(MediaPermissions.readFile),
|
||||
async (c) => {
|
||||
const { filename } = c.req.param();
|
||||
if (!filename) {
|
||||
throw new Error("No file name provided");
|
||||
}
|
||||
|
||||
await this.getStorage().emgr.emit(new StorageEvents.FileAccessEvent({ name: filename }));
|
||||
await this.getStorage().emgr.emit(
|
||||
new StorageEvents.FileAccessEvent({ name: filename }),
|
||||
);
|
||||
const res = await this.getStorageAdapter().getObject(filename, c.req.raw.headers);
|
||||
|
||||
const headers = new Headers(res.headers);
|
||||
@@ -57,10 +70,18 @@ export class MediaController extends Controller {
|
||||
statusText: res.statusText,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// delete a file by name
|
||||
hono.delete("/file/:filename", permission(MediaPermissions.deleteFile), async (c) => {
|
||||
hono.delete(
|
||||
"/file/:filename",
|
||||
describeRoute({
|
||||
summary: "Delete a file by name",
|
||||
tags: ["media"],
|
||||
}),
|
||||
permission(MediaPermissions.deleteFile),
|
||||
async (c) => {
|
||||
const { filename } = c.req.param();
|
||||
if (!filename) {
|
||||
throw new Error("No file name provided");
|
||||
@@ -68,24 +89,64 @@ export class MediaController extends Controller {
|
||||
await this.getStorage().deleteFile(filename);
|
||||
|
||||
return c.json({ message: "File deleted" });
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const maxSize = this.getStorage().getConfig().body_max_size ?? Number.POSITIVE_INFINITY;
|
||||
|
||||
if (isDebug()) {
|
||||
hono.post("/inspect", async (c) => {
|
||||
hono.post(
|
||||
"/inspect",
|
||||
describeRoute({
|
||||
summary: "Inspect a file",
|
||||
tags: ["media"],
|
||||
}),
|
||||
async (c) => {
|
||||
const file = await getFileFromContext(c);
|
||||
return c.json({
|
||||
type: file?.type,
|
||||
name: file?.name,
|
||||
size: file?.size,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
content: {
|
||||
"multipart/form-data": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
file: {
|
||||
type: "string",
|
||||
format: "binary",
|
||||
},
|
||||
},
|
||||
required: ["file"],
|
||||
},
|
||||
},
|
||||
"application/octet-stream": {
|
||||
schema: {
|
||||
type: "string",
|
||||
format: "binary",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
// upload file
|
||||
// @todo: add required type for "upload endpoints"
|
||||
hono.post("/upload/:filename?", permission(MediaPermissions.uploadFile), async (c) => {
|
||||
hono.post(
|
||||
"/upload/:filename?",
|
||||
describeRoute({
|
||||
summary: "Upload a file",
|
||||
tags: ["media"],
|
||||
requestBody,
|
||||
}),
|
||||
jsc("param", s.object({ filename: s.string().optional() })),
|
||||
permission(MediaPermissions.uploadFile),
|
||||
async (c) => {
|
||||
const reqname = c.req.param("filename");
|
||||
|
||||
const body = await getFileFromContext(c);
|
||||
@@ -103,18 +164,27 @@ export class MediaController extends Controller {
|
||||
const res = await this.getStorage().uploadFile(body, filename);
|
||||
|
||||
return c.json(res, HttpStatus.CREATED);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// add upload file to entity
|
||||
// @todo: add required type for "upload endpoints"
|
||||
hono.post(
|
||||
"/entity/:entity/:id/:field",
|
||||
tb(
|
||||
"query",
|
||||
Type.Object({
|
||||
overwrite: Type.Optional(booleanLike),
|
||||
describeRoute({
|
||||
summary: "Add a file to an entity",
|
||||
tags: ["media"],
|
||||
requestBody,
|
||||
}),
|
||||
jsc(
|
||||
"param",
|
||||
s.object({
|
||||
entity: entitiesEnum,
|
||||
id: s.number(),
|
||||
field: s.string(),
|
||||
}),
|
||||
),
|
||||
jsc("query", s.object({ overwrite: s.boolean().optional() })),
|
||||
permission([DataPermissions.entityCreate, MediaPermissions.uploadFile]),
|
||||
async (c) => {
|
||||
const entity_name = c.req.param("entity");
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { App } from "App";
|
||||
import { type Context, type Env, Hono } from "hono";
|
||||
import * as middlewares from "modules/middlewares";
|
||||
import type { SafeUser } from "auth";
|
||||
import type { EntityManager } from "data";
|
||||
import { s } from "core/object/schema";
|
||||
|
||||
export type ServerEnv = Env & {
|
||||
Variables: {
|
||||
@@ -46,4 +48,9 @@ export class Controller {
|
||||
|
||||
return c.notFound();
|
||||
}
|
||||
|
||||
protected getEntitiesEnum(em: EntityManager<any>) {
|
||||
const entities = em.entities.map((e) => e.name);
|
||||
return entities.length > 0 ? s.string({ enum: entities }) : s.string();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,8 @@ import {
|
||||
import { getRuntimeKey } from "core/utils";
|
||||
import type { Context, Hono } from "hono";
|
||||
import { Controller } from "modules/Controller";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
const { Type } = tbbox;
|
||||
|
||||
import { openAPISpecs } from "jsonv-ts/hono";
|
||||
import { swaggerUI } from "@hono/swagger-ui";
|
||||
import {
|
||||
MODULE_NAMES,
|
||||
type ModuleConfigs,
|
||||
@@ -24,12 +23,8 @@ import {
|
||||
getDefaultConfig,
|
||||
} from "modules/ModuleManager";
|
||||
import * as SystemPermissions from "modules/permissions";
|
||||
import { generateOpenAPI } from "modules/server/openapi";
|
||||
|
||||
const booleanLike = Type.Transform(Type.String())
|
||||
.Decode((v) => v === "1")
|
||||
.Encode((v) => (v ? "1" : "0"));
|
||||
|
||||
import { jsc, s, describeRoute } from "core/object/schema";
|
||||
import { getVersion } from "core/env";
|
||||
export type ConfigUpdate<Key extends ModuleKey = ModuleKey> = {
|
||||
success: true;
|
||||
module: Key;
|
||||
@@ -61,20 +56,27 @@ export class SystemController extends Controller {
|
||||
|
||||
hono.use(permission(SystemPermissions.configRead));
|
||||
|
||||
hono.get("/raw", permission([SystemPermissions.configReadSecrets]), async (c) => {
|
||||
hono.get(
|
||||
"/raw",
|
||||
describeRoute({
|
||||
summary: "Get the raw config",
|
||||
tags: ["system"],
|
||||
}),
|
||||
permission([SystemPermissions.configReadSecrets]),
|
||||
async (c) => {
|
||||
// @ts-expect-error "fetch" is private
|
||||
return c.json(await this.app.modules.fetch());
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
hono.get(
|
||||
"/:module?",
|
||||
tb("param", Type.Object({ module: Type.Optional(StringEnum(MODULE_NAMES)) })),
|
||||
tb(
|
||||
"query",
|
||||
Type.Object({
|
||||
secrets: Type.Optional(booleanLike),
|
||||
describeRoute({
|
||||
summary: "Get the config for a module",
|
||||
tags: ["system"],
|
||||
}),
|
||||
),
|
||||
jsc("param", s.object({ module: s.string({ enum: MODULE_NAMES }).optional() })),
|
||||
jsc("query", s.object({ secrets: s.boolean().optional() })),
|
||||
async (c) => {
|
||||
// @todo: allow secrets if authenticated user is admin
|
||||
const { secrets } = c.req.valid("query");
|
||||
@@ -119,12 +121,7 @@ export class SystemController extends Controller {
|
||||
hono.post(
|
||||
"/set/:module",
|
||||
permission(SystemPermissions.configWrite),
|
||||
tb(
|
||||
"query",
|
||||
Type.Object({
|
||||
force: Type.Optional(booleanLike),
|
||||
}),
|
||||
),
|
||||
jsc("query", s.object({ force: s.boolean().optional() }), { skipOpenAPI: true }),
|
||||
async (c) => {
|
||||
const module = c.req.param("module") as any;
|
||||
const { force } = c.req.valid("query");
|
||||
@@ -230,13 +227,17 @@ export class SystemController extends Controller {
|
||||
|
||||
hono.get(
|
||||
"/schema/:module?",
|
||||
describeRoute({
|
||||
summary: "Get the schema for a module",
|
||||
tags: ["system"],
|
||||
}),
|
||||
permission(SystemPermissions.schemaRead),
|
||||
tb(
|
||||
jsc(
|
||||
"query",
|
||||
Type.Object({
|
||||
config: Type.Optional(booleanLike),
|
||||
secrets: Type.Optional(booleanLike),
|
||||
fresh: Type.Optional(booleanLike),
|
||||
s.partialObject({
|
||||
config: s.boolean(),
|
||||
secrets: s.boolean(),
|
||||
fresh: s.boolean(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -274,13 +275,11 @@ export class SystemController extends Controller {
|
||||
|
||||
hono.post(
|
||||
"/build",
|
||||
tb(
|
||||
"query",
|
||||
Type.Object({
|
||||
sync: Type.Optional(booleanLike),
|
||||
fetch: Type.Optional(booleanLike),
|
||||
describeRoute({
|
||||
summary: "Build the app",
|
||||
tags: ["system"],
|
||||
}),
|
||||
),
|
||||
jsc("query", s.object({ sync: s.boolean().optional(), fetch: s.boolean().optional() })),
|
||||
async (c) => {
|
||||
const options = c.req.valid("query") as Record<string, boolean>;
|
||||
this.ctx.guard.throwUnlessGranted(SystemPermissions.build, c);
|
||||
@@ -293,9 +292,22 @@ export class SystemController extends Controller {
|
||||
},
|
||||
);
|
||||
|
||||
hono.get("/ping", (c) => c.json({ pong: true }));
|
||||
hono.get(
|
||||
"/ping",
|
||||
describeRoute({
|
||||
summary: "Ping the server",
|
||||
tags: ["system"],
|
||||
}),
|
||||
(c) => c.json({ pong: true }),
|
||||
);
|
||||
|
||||
hono.get("/info", (c) =>
|
||||
hono.get(
|
||||
"/info",
|
||||
describeRoute({
|
||||
summary: "Get the server info",
|
||||
tags: ["system"],
|
||||
}),
|
||||
(c) =>
|
||||
c.json({
|
||||
version: c.get("app")?.version(),
|
||||
runtime: getRuntimeKey(),
|
||||
@@ -308,10 +320,16 @@ export class SystemController extends Controller {
|
||||
}),
|
||||
);
|
||||
|
||||
hono.get("/openapi.json", async (c) => {
|
||||
const config = getDefaultConfig();
|
||||
return c.json(generateOpenAPI(config));
|
||||
});
|
||||
hono.get(
|
||||
"/openapi.json",
|
||||
openAPISpecs(this.ctx.server, {
|
||||
info: {
|
||||
title: "bknd API",
|
||||
version: getVersion(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
hono.get("/swagger", swaggerUI({ url: "/api/system/openapi.json" }));
|
||||
|
||||
return hono.all("*", (c) => c.notFound());
|
||||
}
|
||||
|
||||
@@ -10,12 +10,11 @@ export function useSearch<Schema extends s.TAnySchema = s.TAnySchema>(
|
||||
) {
|
||||
const searchString = useWouterSearch();
|
||||
const [location, navigate] = useLocation();
|
||||
let value = (defaultValue ? parse(schema, defaultValue as any) : {}) as s.StaticCoerced<Schema>;
|
||||
|
||||
if (searchString.length > 0) {
|
||||
value = parse(schema, decodeSearch(searchString));
|
||||
//console.log("search:decode", value);
|
||||
}
|
||||
const initial = searchString.length > 0 ? decodeSearch(searchString) : (defaultValue ?? {});
|
||||
const value = parse(schema, initial, {
|
||||
withDefaults: true,
|
||||
clone: true,
|
||||
}) as s.StaticCoerced<Schema>;
|
||||
|
||||
// @todo: add option to set multiple keys at once
|
||||
function set<Key extends keyof s.StaticCoerced<Schema>>(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SegmentedControl, Tooltip } from "@mantine/core";
|
||||
import { IconKeyOff, IconSettings, IconUser } from "@tabler/icons-react";
|
||||
import { IconApi, IconKeyOff, IconSettings, IconUser } from "@tabler/icons-react";
|
||||
import {
|
||||
TbDatabase,
|
||||
TbFingerprint,
|
||||
@@ -159,6 +159,11 @@ function UserMenu() {
|
||||
|
||||
const items: DropdownItem[] = [
|
||||
{ label: "Settings", onClick: () => navigate("/settings"), icon: IconSettings },
|
||||
{
|
||||
label: "OpenAPI",
|
||||
onClick: () => window.open("/api/system/swagger", "_blank"),
|
||||
icon: IconApi,
|
||||
},
|
||||
];
|
||||
|
||||
if (config.auth.enabled) {
|
||||
|
||||
@@ -4,11 +4,13 @@ import { defineConfig } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import { devServerConfig } from "./src/adapter/vite/dev-server-config";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import pkg from "./package.json" with { type: "json" };
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
define: {
|
||||
__isDev: process.env.NODE_ENV === "production" ? "0" : "1",
|
||||
__version: JSON.stringify(pkg.version),
|
||||
},
|
||||
clearScreen: false,
|
||||
publicDir: "./src/ui/assets",
|
||||
|
||||
7
bun.lock
7
bun.lock
@@ -34,6 +34,7 @@
|
||||
"@codemirror/lang-html": "^6.4.9",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@hono/swagger-ui": "^0.5.1",
|
||||
"@libsql/client": "^0.15.2",
|
||||
"@mantine/core": "^7.17.1",
|
||||
"@mantine/hooks": "^7.17.1",
|
||||
@@ -49,7 +50,6 @@
|
||||
"json-schema-form-react": "^0.0.2",
|
||||
"json-schema-library": "10.0.0-rc7",
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"jsonv-ts": "^0.0.11",
|
||||
"kysely": "^0.27.6",
|
||||
"lodash-es": "^4.17.21",
|
||||
"oauth4webapi": "^2.11.1",
|
||||
@@ -84,6 +84,7 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"jotai": "^2.12.2",
|
||||
"jsdom": "^26.0.0",
|
||||
"jsonv-ts": "^0.0.14-alpha.6",
|
||||
"kysely-d1": "^0.3.0",
|
||||
"open": "^10.1.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
@@ -640,6 +641,8 @@
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.13.8", "", { "peerDependencies": { "hono": "^4" } }, "sha512-fsn8ucecsAXUoVxrUil0m13kOEq4mkX4/4QozCqmY+HpGfKl74OYSn8JcMA8GnG0ClfdRI4/ZSeG7zhFaVg+wg=="],
|
||||
|
||||
"@hono/swagger-ui": ["@hono/swagger-ui@0.5.1", "", { "peerDependencies": { "hono": "*" } }, "sha512-XpUCfszLJ9b1rtFdzqOSHfdg9pfBiC2J5piEjuSanYpDDTIwpMz0ciiv5N3WWUaQpz9fEgH8lttQqL41vIFuDA=="],
|
||||
|
||||
"@hono/typebox-validator": ["@hono/typebox-validator@0.3.2", "", { "peerDependencies": { "@sinclair/typebox": ">=0.31.15 <1", "hono": ">=3.9.0" } }, "sha512-MIxYk80vtuFnkvbNreMubZ/vLoNCCQivLH8n3vNDY5dFNsZ12BFaZV3FmsLJHGibNMMpmkO6y4w5gNWY4KzSdg=="],
|
||||
|
||||
"@hono/vite-dev-server": ["@hono/vite-dev-server@0.19.0", "", { "dependencies": { "@hono/node-server": "^1.12.0", "minimatch": "^9.0.3" }, "peerDependencies": { "hono": "*", "miniflare": "*", "wrangler": "*" }, "optionalPeers": ["miniflare", "wrangler"] }, "sha512-myMc4Nm0nFQSPaeE6I/a1ODyDR5KpQ4EHodX4Tu/7qlB31GfUemhUH/WsO91HJjDEpRRpsT4Zbg+PleMlpTljw=="],
|
||||
@@ -2518,7 +2521,7 @@
|
||||
|
||||
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
|
||||
|
||||
"jsonv-ts": ["jsonv-ts@0.0.11", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-W5WC6iwQvOuB0gRaAW9jAQKqT56pXjTA7XCjjAXZIM92/VBVNczTmV7iPtClqV1Zpgy4CtzaUsOJj4kWNeB5YQ=="],
|
||||
"jsonv-ts": ["jsonv-ts@0.0.14-alpha.6", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-pwMpjEbNtyq8Xi6QBXuQ8dOZm7WQAEwvCPu3vVf9b3aU2KRHW+cfTPqO53U01YYdjWSSRkqaTKcLSiYdfwBYRA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
|
||||
Reference in New Issue
Block a user