Compare commits

...

36 Commits

Author SHA1 Message Date
dswbx 906f80b11e test hiding counts 2025-06-09 20:28:09 +02:00
dswbx 7a3691a1a7 test hiding counts 2025-06-09 20:27:24 +02:00
dswbx 12c955155d bump rc 2025-06-09 20:27:02 +02:00
dswbx 15af9f5261 fix use-search 2025-06-09 20:25:57 +02:00
dswbx 39efb911e9 admin header: added links to docs and version 2025-06-09 07:29:20 +02:00
dswbx 4729203d47 fix useSearch and admin path registration 2025-06-09 07:21:32 +02:00
dswbx 0db052acca Merge pull request #182 from bknd-io/feat/postgres-improvements
postgres: added `pg` and `postgres`, and examples for xata and neon
2025-06-07 11:14:45 +02:00
dswbx 6e08f45857 updated docs on new postgres instructions 2025-06-07 11:11:42 +02:00
dswbx 7bff84d601 updated postgres package build 2025-06-07 10:31:22 +02:00
dswbx e66e05b2b0 postgres: make sure to store id as varchar if uuid 2025-06-07 09:59:50 +02:00
dswbx 5b318ce485 Merge remote-tracking branch 'origin/release/0.14' into feat/postgres-improvements 2025-06-07 09:41:32 +02:00
dswbx 22de16fe17 Merge pull request #183 from bknd-io/feat/uuid
feat: adding initial uuid support
2025-06-07 09:40:43 +02:00
dswbx 5d26673bc6 Merge remote-tracking branch 'origin/release/0.14' into feat/uuid
# Conflicts:
#	app/tsconfig.json
#	bun.lock
2025-06-07 09:38:59 +02:00
dswbx dfa4bc0e33 Merge pull request #181 from bknd-io/feat/cf-sessions
adding d1 session support
2025-06-07 09:32:16 +02:00
dswbx e51b89a18a store session cookie/header name in constants, updated docs 2025-06-07 09:29:56 +02:00
dswbx 63988e0c5f store session cookie/header name in constants, updated docs 2025-06-07 09:29:38 +02:00
dswbx 297fd85a4f Merge branch 'release/0.14' into feat/postgres-improvements 2025-06-07 09:01:49 +02:00
dswbx a82fbe7400 Merge branch 'release/0.14' into feat/cf-sessions 2025-06-07 09:01:05 +02:00
dswbx 74e1e9a03f Merge pull request #179 from bknd-io/fix/hono-cookie-body-disturb
fix: lock hono version because of changed header setting in 4.7.7
2025-06-07 08:59:44 +02:00
dswbx 05a81b5bdc Merge remote-tracking branch 'origin/release/0.14' into fix/hono-cookie-body-disturb 2025-06-07 08:52:54 +02:00
dswbx 9ff49103fb bump rc 2025-06-07 08:50:23 +02:00
dswbx a5b59c004e fix logout by setting the api route as logout target 2025-06-07 08:48:28 +02:00
dswbx a0edcf483b added more tests, updated json schema generation 2025-06-07 08:24:21 +02:00
dswbx 9a18e354cd feat: adding initial uuid support 2025-06-06 20:46:01 +02:00
dswbx 58c7aba1a4 postgres: added pg and postgres, and examples for xata and neon 2025-06-06 16:52:07 +02:00
dswbx f8aa242d2b fix tests 2025-06-06 11:30:27 +02:00
dswbx 3da9570abe reduce worker config type 2025-06-06 11:18:44 +02:00
dswbx 6ee898e606 adding d1 session support 2025-06-06 11:13:35 +02:00
dswbx abbd372ddf Merge pull request #180 from bknd-io/docs/bknd-config-ts
docs: added docs about how to use bknd.config.ts
2025-06-05 17:14:30 +02:00
dswbx 3e77982996 docs: added docs about how to use bknd.config.ts 2025-06-05 17:11:50 +02:00
dswbx 3fbea8ace7 fix: cookie setting by only setting on admin routes 2025-06-03 13:11:18 +02:00
dswbx a41f943b43 fix: cookie setting by only setting on admin routes 2025-06-03 13:06:49 +02:00
dswbx 15a9c549e7 fix: cookie setting by only setting on admin routes 2025-06-03 13:04:03 +02:00
dswbx 50cadbaa8e fix: lock hono version because of changed header setting in 4.7.7 2025-06-03 11:12:47 +02:00
dswbx 7b128c9701 Merge pull request #176 from bknd-io/chores/repo-cleanup
repo chores: fixed root dir, removed unused class, added .env.example
2025-05-27 20:27:38 +02:00
dswbx 061181d59d repo chores: fixed root dir, removed unused class, added .env.example 2025-05-27 20:25:37 +02:00
89 changed files with 2075 additions and 610 deletions
+2 -3
View File
@@ -1,5 +1,4 @@
[![npm version](https://img.shields.io/npm/v/bknd.svg)](https://npmjs.org/package/bknd)
[![npm downloads](https://img.shields.io/npm/dm/bknd)](https://www.npmjs.com/package/bknd)
![bknd](https://raw.githubusercontent.com/bknd-io/bknd/refs/heads/main/docs/_assets/poster.png)
@@ -18,14 +17,14 @@ bknd simplifies app development by providing a fully functional backend for data
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
## Size
![gzipped size of bknd](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/index.js?compression=gzip&label=bknd)
![gzipped size of bknd](https://img.shields.io/bundlejs/size/bknd?label=bknd)
![gzipped size of bknd/client](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/client/index.js?compression=gzip&label=bknd/client)
![gzipped size of bknd/elements](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/elements/index.js?compression=gzip&label=bknd/elements)
![gzipped size of bknd/ui](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/index.js?compression=gzip&label=bknd/ui)
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
Depending on what you use, the size can be higher as additional dependencies are getting pulled in. The minimal size of a full `bknd` app as an API is around 212 kB gzipped (e.g. deployed as Cloudflare Worker).
Depending on what you use, the size can be higher as additional dependencies are getting pulled in. The minimal size of a full `bknd` app as an API is around 300 kB gzipped (e.g. deployed as Cloudflare Worker).
## Motivation
Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them.
+42
View File
@@ -0,0 +1,42 @@
# ===== DB Settings =====
VITE_DB_URL=:memory:
# you can set a location for a database here, it'll overwrite the previous setting
# ideally use the ".db" folder (create it first), it's git ignored
VITE_DB_URL=file:.db/dev.db
# alternatively, you can use url/token combination
#VITE_DB_URL=
#VITE_DB_TOKEN=
# ===== DEV Server =====
# restart the dev server on every change (enable with "1")
VITE_APP_FRESH=
# displays react-scan widget (enable with "1")
VITE_DEBUG_RERENDERS=
# console logs registered routes on start (enable with "1")
VITE_SHOW_ROUTES=
# ===== Test Credentials =====
RESEND_API_KEY=
R2_TOKEN=
R2_ACCESS_KEY=
R2_SECRET_ACCESS_KEY=
R2_URL=
AWS_ACCESS_KEY=
AWS_SECRET_KEY=
AWS_S3_URL=
OAUTH_CLIENT_ID=
OAUTH_CLIENT_SECRET=
PUBLIC_POSTHOG_KEY=
PUBLIC_POSTHOG_HOST=
# ===== Internals =====
BKND_CLI_CREATE_REF=main
BKND_CLI_LOG_LEVEL=debug
BKND_MODULES_DEBUG=1
+14
View File
@@ -110,4 +110,18 @@ describe("some tests", async () => {
new EntityManager([entity, entity2], connection);
}).toThrow();
});
test("primary uuid", async () => {
const entity = new Entity("users", [
new PrimaryField("id", { format: "uuid" }),
new TextField("username"),
]);
const em = new EntityManager([entity], getDummyConnection().dummyConnection);
await em.schema().sync({ force: true });
const mutator = em.mutator(entity);
const data = await mutator.insertOne({ username: "test" });
expect(data.data.id).toBeDefined();
expect(data.data.id).toBeString();
});
});
@@ -39,4 +39,28 @@ describe("[data] PrimaryField", async () => {
expect(field.transformPersist(1)).rejects.toThrow();
expect(field.transformRetrieve(1)).toBe(1);
});
test("format", () => {
const uuid = new PrimaryField("uuid", { format: "uuid" });
expect(uuid.format).toBe("uuid");
expect(uuid.fieldType).toBe("text");
expect(uuid.getNewValue()).toBeString();
expect(uuid.toType()).toEqual({
required: true,
comment: undefined,
type: "Generated<string>",
import: [{ package: "kysely", name: "Generated" }],
});
const integer = new PrimaryField("integer", { format: "integer" });
expect(integer.format).toBe("integer");
expect(integer.fieldType).toBe("integer");
expect(integer.getNewValue()).toBeUndefined();
expect(integer.toType()).toEqual({
required: true,
comment: undefined,
type: "Generated<number>",
import: [{ package: "kysely", name: "Generated" }],
});
});
});
+5
View File
@@ -153,6 +153,7 @@ describe("AppAuth", () => {
});
await app.build();
app.registerAdminController();
const spy = spyOn(app.module.auth.authenticator, "requestCookieRefresh");
// register custom route
@@ -162,6 +163,10 @@ describe("AppAuth", () => {
await app.server.request("/api/system/ping");
await app.server.request("/test");
expect(spy.mock.calls.length).toBe(0);
// admin route
await app.server.request("/");
expect(spy.mock.calls.length).toBe(1);
});
+11 -9
View File
@@ -3,7 +3,7 @@
"type": "module",
"sideEffects": false,
"bin": "./dist/cli/index.js",
"version": "0.13.0",
"version": "0.14.0-rc.2",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io",
"repository": {
@@ -61,23 +61,24 @@
"bcryptjs": "^3.0.2",
"dayjs": "^1.11.13",
"fast-xml-parser": "^5.0.8",
"hono": "^4.7.4",
"json-schema-form-react": "^0.0.2",
"json-schema-library": "10.0.0-rc7",
"json-schema-to-ts": "^3.1.1",
"kysely": "^0.27.6",
"hono": "^4.7.11",
"lodash-es": "^4.17.21",
"oauth4webapi": "^2.11.1",
"object-path-immutable": "^4.1.2",
"radix-ui": "^1.1.3",
"swr": "^2.3.3"
"swr": "^2.3.3",
"uuid": "^11.1.0"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.758.0",
"@bluwy/giget-core": "^0.1.2",
"@dagrejs/dagre": "^1.1.4",
"@hono/typebox-validator": "^0.3.2",
"@hono/vite-dev-server": "^0.19.0",
"@hono/typebox-validator": "^0.3.3",
"@hono/vite-dev-server": "^0.19.1",
"@hookform/resolvers": "^4.1.3",
"@libsql/kysely-libsql": "^0.4.1",
"@mantine/modals": "^7.17.1",
@@ -99,7 +100,7 @@
"dotenv": "^16.4.7",
"jotai": "^2.12.2",
"jsdom": "^26.0.0",
"jsonv-ts": "^0.0.14-alpha.6",
"jsonv-ts": "^0.1.0",
"kysely-d1": "^0.3.0",
"open": "^10.1.0",
"openapi-types": "^12.1.3",
@@ -120,13 +121,14 @@
"tsc-alias": "^1.8.11",
"tsup": "^8.4.0",
"tsx": "^4.19.3",
"vite": "^6.2.1",
"vite": "^6.3.5",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.9",
"wouter": "^3.6.0"
"wouter": "^3.6.0",
"@cloudflare/workers-types": "^4.20250606.0"
},
"optionalDependencies": {
"@hono/node-server": "^1.13.8"
"@hono/node-server": "^1.14.3"
},
"peerDependencies": {
"react": ">=19",
+5
View File
@@ -253,6 +253,11 @@ export class App {
break;
}
});
// call server init if set
if (this.options?.manager?.onServerInit) {
this.options.manager.onServerInit(server);
}
}
}
@@ -1,6 +1,6 @@
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import { makeApp } from "./modes/fresh";
import { makeConfig } from "./config";
import { makeConfig, type CfMakeConfigArgs } from "./config";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
@@ -23,7 +23,7 @@ describe("cf adapter", () => {
{
connection: { url: DB_URL },
},
{},
$ctx({ DB_URL }),
),
).toEqual({ connection: { url: DB_URL } });
@@ -34,15 +34,15 @@ describe("cf adapter", () => {
connection: { url: env.DB_URL },
}),
},
{
DB_URL,
},
$ctx({ DB_URL }),
),
).toEqual({ connection: { url: DB_URL } });
});
adapterTestSuite<CloudflareBkndConfig, object>(bunTestRunner, {
makeApp,
adapterTestSuite<CloudflareBkndConfig, CfMakeConfigArgs<any>>(bunTestRunner, {
makeApp: async (c, a, o) => {
return await makeApp(c, { env: a } as any, o);
},
makeHandler: (c, a, o) => {
return async (request: any) => {
const app = await makeApp(
@@ -50,7 +50,7 @@ describe("cf adapter", () => {
c ?? {
connection: { url: DB_URL },
},
a,
a!,
o,
);
return app.fetch(request);
@@ -9,7 +9,13 @@ import { getDurable } from "./modes/durable";
import type { App } from "bknd";
import { $console } from "core";
export type CloudflareEnv = object;
declare global {
namespace Cloudflare {
interface Env {}
}
}
export type CloudflareEnv = Cloudflare.Env;
export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & {
mode?: "warm" | "fresh" | "cache" | "durable";
bindings?: (args: Env) => {
@@ -17,6 +23,11 @@ export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> &
dobj?: DurableObjectNamespace;
db?: D1Database;
};
d1?: {
session?: boolean;
transport?: "header" | "cookie";
first?: D1SessionConstraint;
};
static?: "kv" | "assets";
key?: string;
keepAliveSeconds?: number;
+120 -19
View File
@@ -1,47 +1,148 @@
/// <reference types="@cloudflare/workers-types" />
import { registerMedia } from "./storage/StorageR2Adapter";
import { getBinding } from "./bindings";
import { D1Connection } from "./D1Connection";
import { D1Connection } from "./connection/D1Connection";
import type { CloudflareBkndConfig, CloudflareEnv } from ".";
import { App } from "bknd";
import { makeConfig as makeAdapterConfig } from "bknd/adapter";
import type { ExecutionContext } from "hono";
import type { Context, ExecutionContext } from "hono";
import { $console } from "core";
import { setCookie } from "hono/cookie";
export const constants = {
exec_async_event_id: "cf_register_waituntil",
cache_endpoint: "/__bknd/cache",
do_endpoint: "/__bknd/do",
d1_session: {
cookie: "cf_d1_session",
header: "x-cf-d1-session",
},
};
export type CfMakeConfigArgs<Env extends CloudflareEnv = CloudflareEnv> = {
env: Env;
ctx?: ExecutionContext;
request?: Request;
};
function getCookieValue(cookies: string | null, name: string) {
if (!cookies) return null;
for (const cookie of cookies.split("; ")) {
const [key, value] = cookie.split("=");
if (key === name && value) {
return decodeURIComponent(value);
}
}
return null;
}
export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
const headerKey = constants.d1_session.header;
const cookieKey = constants.d1_session.cookie;
const transport = config.d1?.transport;
return {
get: (request?: Request): D1SessionBookmark | undefined => {
if (!request || !config.d1?.session) return undefined;
if (!transport || transport === "cookie") {
const cookies = request.headers.get("Cookie");
if (cookies) {
const cookie = getCookieValue(cookies, cookieKey);
if (cookie) {
return cookie;
}
}
}
if (!transport || transport === "header") {
if (request.headers.has(headerKey)) {
return request.headers.get(headerKey) as any;
}
}
return undefined;
},
set: (c: Context, d1?: D1DatabaseSession) => {
if (!d1 || !config.d1?.session) return;
const session = d1.getBookmark();
if (session) {
if (!transport || transport === "header") {
c.header(headerKey, session);
}
if (!transport || transport === "cookie") {
setCookie(c, cookieKey, session, {
httpOnly: true,
secure: true,
sameSite: "Lax",
maxAge: 60 * 5, // 5 minutes
});
}
}
},
};
}
let media_registered: boolean = false;
export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>,
args: Env = {} as Env,
args?: CfMakeConfigArgs<Env>,
) {
if (!media_registered) {
registerMedia(args as any);
media_registered = true;
}
const appConfig = makeAdapterConfig(config, args);
const bindings = config.bindings?.(args);
if (!appConfig.connection) {
let db: D1Database | undefined;
if (bindings?.db) {
$console.log("Using database from bindings");
db = bindings.db;
} else if (Object.keys(args).length > 0) {
const binding = getBinding(args, "D1Database");
if (binding) {
$console.log(`Using database from env "${binding.key}"`);
db = binding.value;
const appConfig = makeAdapterConfig(config, args?.env);
if (args?.env) {
const bindings = config.bindings?.(args?.env);
const sessionHelper = d1SessionHelper(config);
const sessionId = sessionHelper.get(args.request);
let session: D1DatabaseSession | undefined;
if (!appConfig.connection) {
let db: D1Database | undefined;
if (bindings?.db) {
$console.log("Using database from bindings");
db = bindings.db;
} else if (Object.keys(args).length > 0) {
const binding = getBinding(args.env, "D1Database");
if (binding) {
$console.log(`Using database from env "${binding.key}"`);
db = binding.value;
}
}
if (db) {
if (config.d1?.session) {
session = db.withSession(sessionId ?? config.d1?.first);
appConfig.connection = new D1Connection({ binding: session });
} else {
appConfig.connection = new D1Connection({ binding: db });
}
} else {
throw new Error("No database connection given");
}
}
if (db) {
appConfig.connection = new D1Connection({ binding: db });
} else {
throw new Error("No database connection given");
if (config.d1?.session) {
appConfig.options = {
...appConfig.options,
manager: {
...appConfig.options?.manager,
onServerInit: (server) => {
server.use(async (c, next) => {
sessionHelper.set(c, session);
await next();
});
},
},
};
}
}
@@ -5,8 +5,8 @@ import type { QB } from "data/connection/Connection";
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin } from "kysely";
import { D1Dialect } from "kysely-d1";
export type D1ConnectionConfig = {
binding: D1Database;
export type D1ConnectionConfig<DB extends D1Database | D1DatabaseSession = D1Database> = {
binding: DB;
};
class CustomD1Dialect extends D1Dialect {
@@ -17,22 +17,25 @@ class CustomD1Dialect extends D1Dialect {
}
}
export class D1Connection extends SqliteConnection {
export class D1Connection<
DB extends D1Database | D1DatabaseSession = D1Database,
> extends SqliteConnection {
protected override readonly supported = {
batching: true,
counts: false,
};
constructor(private config: D1ConnectionConfig) {
constructor(private config: D1ConnectionConfig<DB>) {
const plugins = [new ParseJSONResultsPlugin()];
const kysely = new Kysely({
dialect: new CustomD1Dialect({ database: config.binding }),
dialect: new CustomD1Dialect({ database: config.binding as D1Database }),
plugins,
});
super(kysely, {}, plugins);
}
get client(): D1Database {
get client(): DB {
return this.config.binding;
}
+2 -1
View File
@@ -1,4 +1,4 @@
import { D1Connection, type D1ConnectionConfig } from "./D1Connection";
import { D1Connection, type D1ConnectionConfig } from "./connection/D1Connection";
export * from "./cloudflare-workers.adapter";
export { makeApp, getFresh } from "./modes/fresh";
@@ -12,6 +12,7 @@ export {
type GetBindingType,
type BindingMap,
} from "./bindings";
export { constants } from "./config";
export function d1(config: D1ConnectionConfig) {
return new D1Connection(config);
+4 -3
View File
@@ -5,8 +5,9 @@ import { makeConfig, registerAsyncsExecutionContext, constants } from "../config
export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>,
{ env, ctx, ...args }: Context<Env>,
args: Context<Env>,
) {
const { env, ctx } = args;
const { kv } = config.bindings?.(env)!;
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
const key = config.key ?? "app";
@@ -20,7 +21,7 @@ export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
const app = await createRuntimeApp(
{
...makeConfig(config, env),
...makeConfig(config, args),
initialConfig,
onBuilt: async (app) => {
registerAsyncsExecutionContext(app, ctx);
@@ -41,7 +42,7 @@ export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
await config.beforeBuild?.(app);
},
},
{ env, ctx, ...args },
args,
);
if (!cachedConfig) {
+4 -4
View File
@@ -1,13 +1,13 @@
import { createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
import { makeConfig, registerAsyncsExecutionContext } from "../config";
import { makeConfig, registerAsyncsExecutionContext, type CfMakeConfigArgs } from "../config";
export async function makeApp<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>,
args: Env = {} as Env,
args?: CfMakeConfigArgs<Env>,
opts?: RuntimeOptions,
) {
return await createRuntimeApp<Env>(makeConfig(config, args), args, opts);
return await createRuntimeApp<Env>(makeConfig(config, args), args?.env, opts);
}
export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>(
@@ -23,7 +23,7 @@ export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>(
await config.onBuilt?.(app);
},
},
ctx.env,
ctx,
opts,
);
}
-1
View File
@@ -3,7 +3,6 @@ import { isNode } from "bknd/utils";
import type { NextApiRequest } from "next";
type NextjsEnv = NextApiRequest["env"];
export type NextjsBkndConfig<Env = NextjsEnv> = FrameworkBkndConfig<Env> & {
cleanRequest?: { searchParams?: string[] };
};
+1
View File
@@ -121,6 +121,7 @@ export class AuthController extends Controller {
const claims = c.get("auth")?.user;
if (claims) {
const { data: user } = await this.userRepo.findId(claims.id);
await this.auth.authenticator?.requestCookieRefresh(c);
return c.json({ user });
}
@@ -347,6 +347,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
}
async logout(c: Context<ServerEnv>) {
$console.info("Logging out");
c.set("auth", undefined);
const cookie = await this.getAuthCookie(c);
+1 -5
View File
@@ -60,11 +60,7 @@ export const auth = (options?: {
}
await next();
if (!skipped) {
// renew cookie if applicable
authenticator?.requestCookieRefresh(c);
}
// @todo: potentially add cookie refresh if content-type html and about to expire
// release
authCtx.skip = false;
+2 -2
View File
@@ -3,9 +3,9 @@
*/
import type { Generated } from "kysely";
export type PrimaryFieldType<IdType extends number = number> = IdType | Generated<IdType>;
export type PrimaryFieldType<IdType = number | string> = IdType | Generated<IdType>;
export interface AppEntity<IdType extends number = number> {
export interface AppEntity<IdType = number | string> {
id: PrimaryFieldType<IdType>;
}
+6
View File
@@ -23,6 +23,12 @@ function hasColors() {
}
const __consoles = {
critical: {
prefix: "CRT",
color: colors.red,
args_color: colors.red,
original: console.error,
},
error: {
prefix: "ERR",
color: colors.red,
+1 -1
View File
@@ -27,7 +27,7 @@ export type ParseOptions = {
clone?: boolean;
};
const cloneSchema = <S extends s.TSchema>(schema: S): S => {
export const cloneSchema = <S extends s.TSchema>(schema: S): S => {
const json = schema.toJSON();
return s.fromSchema(json) as S;
};
-29
View File
@@ -1,29 +0,0 @@
import type { Env, Input, MiddlewareHandler, ValidationTargets } from "hono";
import { validator } from "hono/validator";
import type { Static, TSchema } from "simple-jsonschema-ts";
export const honoValidator = <
Target extends keyof ValidationTargets,
E extends Env,
P extends string,
const Schema extends TSchema = TSchema,
Out = Static<Schema>,
I extends Input = {
in: { [K in Target]: Static<Schema> };
out: { [K in Target]: Static<Schema> };
},
>(
target: Target,
schema: Schema,
): MiddlewareHandler<E, P, I> => {
// @ts-expect-error not typed well
return validator(target, async (value, c) => {
const coersed = schema.coerce(value);
const result = schema.validate(coersed);
if (!result.valid) {
return c.json({ ...result, schema }, 400);
}
return coersed as Out;
});
};
+4 -2
View File
@@ -117,7 +117,9 @@ async function detectMimeType(
return;
}
export async function getFileFromContext(c: Context<any>): Promise<File> {
type HonoAnyContext = Context<any, any, any>;
export async function getFileFromContext(c: HonoAnyContext): Promise<File> {
const contentType = c.req.header("Content-Type") ?? "application/octet-stream";
if (
@@ -149,7 +151,7 @@ export async function getFileFromContext(c: Context<any>): Promise<File> {
throw new Error("No file found in request");
}
export async function getBodyFromContext(c: Context<any>): Promise<ReadableStream | File> {
export async function getBodyFromContext(c: HonoAnyContext): Promise<ReadableStream | File> {
const contentType = c.req.header("Content-Type") ?? "application/octet-stream";
if (
+1 -1
View File
@@ -36,7 +36,7 @@ export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"
severities.forEach((severity) => {
console[severity] = () => null;
});
$console.setLevel("error");
$console.setLevel("critical");
}
export function enableConsoleLog() {
+7 -1
View File
@@ -1,4 +1,10 @@
import { v4, v7 } from "uuid";
// generates v4
export function uuid(): string {
return crypto.randomUUID();
return v4();
}
export function uuidv7(): string {
return v7();
}
+11 -9
View File
@@ -233,6 +233,8 @@ export class DataController extends Controller {
const hono = this.create();
const entitiesEnum = this.getEntitiesEnum(this.em);
// @todo: make dynamic based on entity
const idType = s.anyOf([s.number(), s.string()], { coerce: (v) => v as any });
/**
* Function endpoints
@@ -314,7 +316,7 @@ export class DataController extends Controller {
return this.notFound(c);
}
const options = c.req.valid("query") as RepoQuery;
const result = await this.em.repository(entity).findMany(options);
const result = await this.em.repo(entity).findMany(options);
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
},
@@ -333,7 +335,7 @@ export class DataController extends Controller {
"param",
s.object({
entity: entitiesEnum,
id: s.string(),
id: idType,
}),
),
jsc("query", repoQuery, { skipOpenAPI: true }),
@@ -343,7 +345,7 @@ export class DataController extends Controller {
return this.notFound(c);
}
const options = c.req.valid("query") as RepoQuery;
const result = await this.em.repository(entity).findId(Number(id), options);
const result = await this.em.repository(entity).findId(id, options);
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
},
@@ -362,7 +364,7 @@ export class DataController extends Controller {
"param",
s.object({
entity: entitiesEnum,
id: s.string(),
id: idType,
reference: s.string(),
}),
),
@@ -376,7 +378,7 @@ export class DataController extends Controller {
const options = c.req.valid("query") as RepoQuery;
const result = await this.em
.repository(entity)
.findManyByReference(Number(id), reference, options);
.findManyByReference(id, reference, options);
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
},
@@ -485,7 +487,7 @@ export class DataController extends Controller {
tags: ["data"],
}),
permission(DataPermissions.entityUpdate),
jsc("param", s.object({ entity: entitiesEnum, id: s.number() })),
jsc("param", s.object({ entity: entitiesEnum, id: idType })),
jsc("json", s.object({})),
async (c) => {
const { entity, id } = c.req.valid("param");
@@ -493,7 +495,7 @@ export class DataController extends Controller {
return this.notFound(c);
}
const body = (await c.req.json()) as EntityData;
const result = await this.em.mutator(entity).updateOne(Number(id), body);
const result = await this.em.mutator(entity).updateOne(id, body);
return c.json(this.mutatorResult(result));
},
@@ -507,13 +509,13 @@ export class DataController extends Controller {
tags: ["data"],
}),
permission(DataPermissions.entityDelete),
jsc("param", s.object({ entity: entitiesEnum, id: s.number() })),
jsc("param", s.object({ entity: entitiesEnum, id: idType })),
async (c) => {
const { entity, id } = c.req.valid("param");
if (!this.entityExists(entity)) {
return this.notFound(c);
}
const result = await this.em.mutator(entity).deleteOne(Number(id));
const result = await this.em.mutator(entity).deleteOne(id);
return c.json(this.mutatorResult(result));
},
+21 -1
View File
@@ -2,6 +2,8 @@ import {
type AliasableExpression,
type ColumnBuilderCallback,
type ColumnDataType,
type DatabaseIntrospector,
type Dialect,
type Expression,
type Kysely,
type KyselyPlugin,
@@ -12,7 +14,8 @@ import {
type Simplify,
sql,
} from "kysely";
import type { BaseIntrospector } from "./BaseIntrospector";
import type { BaseIntrospector, BaseIntrospectorConfig } from "./BaseIntrospector";
import type { Constructor } from "core";
export type QB = SelectQueryBuilder<any, any, any>;
@@ -79,6 +82,7 @@ export abstract class Connection<DB = any> {
kysely: Kysely<DB>;
protected readonly supported = {
batching: false,
counts: true,
};
constructor(
@@ -159,3 +163,19 @@ export abstract class Connection<DB = any> {
// no-op by default
}
}
export function customIntrospector<T extends Constructor<Dialect>>(
dialect: T,
introspector: Constructor<DatabaseIntrospector>,
options: BaseIntrospectorConfig = {},
) {
return {
create(...args: ConstructorParameters<T>) {
return new (class extends dialect {
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
return new introspector(db, options);
}
})(...args);
},
};
}
@@ -3,6 +3,7 @@ import { Connection, type FieldSpec, type SchemaResponse } from "./Connection";
export class DummyConnection extends Connection {
protected override readonly supported = {
batching: true,
counts: true,
};
constructor() {
+1
View File
@@ -5,6 +5,7 @@ export {
type IndexSpec,
type DbFunctions,
type SchemaResponse,
customIntrospector,
} from "./Connection";
// sqlite
@@ -28,13 +28,13 @@ export class LibsqlConnection extends SqliteConnection {
private client: Client;
protected override readonly supported = {
batching: true,
counts: false,
};
constructor(client: Client);
constructor(credentials: LibSqlCredentials);
constructor(clientOrCredentials: Client | LibSqlCredentials) {
let client: Client;
let batching_enabled = true;
if (clientOrCredentials && "url" in clientOrCredentials) {
let { url, authToken, protocol } = clientOrCredentials;
if (protocol && LIBSQL_PROTOCOLS.includes(protocol)) {
@@ -56,7 +56,6 @@ export class LibsqlConnection extends SqliteConnection {
super(kysely, {}, plugins);
this.client = client;
this.supported.batching = batching_enabled;
}
getClient(): Client {
@@ -31,7 +31,11 @@ export class SqliteConnection extends Connection {
type,
(col: ColumnDefinitionBuilder) => {
if (spec.primary) {
return col.primaryKey().notNull().autoIncrement();
if (spec.type === "integer") {
return col.primaryKey().notNull().autoIncrement();
}
return col.primaryKey().notNull();
}
if (spec.references) {
let relCol = col.references(spec.references);
+5 -1
View File
@@ -1,4 +1,4 @@
import { type Static, StringRecord, objectTransform } from "core/utils";
import { type Static, StringEnum, StringRecord, objectTransform } from "core/utils";
import * as tb from "@sinclair/typebox";
import {
FieldClassMap,
@@ -8,6 +8,7 @@ import {
entityTypes,
} from "data";
import { MediaField, mediaFieldConfigSchema } from "../media/MediaField";
import { primaryFieldTypes } from "./fields";
export const FIELDS = {
...FieldClassMap,
@@ -72,6 +73,9 @@ export const indicesSchema = tb.Type.Object(
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: {} })),
+16 -2
View File
@@ -6,7 +6,13 @@ import {
snakeToPascalWithSpaces,
transformObject,
} from "core/utils";
import { type Field, PrimaryField, type TActionContext, type TRenderContext } from "../fields";
import {
type Field,
PrimaryField,
primaryFieldTypes,
type TActionContext,
type TRenderContext,
} from "../fields";
import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox;
@@ -18,6 +24,7 @@ export const entityConfigSchema = Type.Object(
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,
@@ -68,7 +75,14 @@ export class Entity<
if (primary_count > 1) {
throw new Error(`Entity "${name}" has more than one primary field`);
}
this.fields = primary_count === 1 ? [] : [new PrimaryField()];
this.fields =
primary_count === 1
? []
: [
new PrimaryField(undefined, {
format: this.config.primary_format,
}),
];
if (fields) {
fields.forEach((field) => this.addField(field));
+13 -3
View File
@@ -143,7 +143,7 @@ export class Mutator<
// if listener returned, take what's returned
const _data = result.returned ? result.params.data : data;
const validatedData = {
let validatedData = {
...entity.getDefaultObject(),
...(await this.getValidatedData(_data, "create")),
};
@@ -159,6 +159,16 @@ export class Mutator<
}
}
// primary
const primary = entity.getPrimaryField();
const primary_value = primary.getNewValue();
if (primary_value) {
validatedData = {
[primary.name]: primary_value,
...validatedData,
};
}
const query = this.conn
.insertInto(entity.name)
.values(validatedData)
@@ -175,7 +185,7 @@ export class Mutator<
async updateOne(id: PrimaryFieldType, data: Partial<Input>): Promise<MutatorResponse<Output>> {
const entity = this.entity;
if (!Number.isInteger(id)) {
if (!id) {
throw new Error("ID must be provided for update");
}
@@ -212,7 +222,7 @@ export class Mutator<
async deleteOne(id: PrimaryFieldType): Promise<MutatorResponse<Output>> {
const entity = this.entity;
if (!Number.isInteger(id)) {
if (!id) {
throw new Error("ID must be provided for deletion");
}
+21
View File
@@ -27,6 +27,7 @@ export type RepositoryResponse<T = EntityData[]> = RepositoryRawResponse & {
data: T;
meta: {
items: number;
has_more?: boolean;
total?: number;
count?: number;
time?: number;
@@ -61,6 +62,10 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
public entity: Entity,
protected options: RepositoryOptions = {},
) {
this.options = {
...options,
includeCounts: options?.includeCounts ?? this.em.connection.supports("counts"),
};
this.emgr = options?.emgr ?? new EventManager(MutatorEvents);
}
@@ -179,6 +184,11 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
if (options.limit) validated.limit = options.limit;
if (options.offset) validated.offset = options.offset;
// if counts disabled, add +1 to limit
if (this.options?.includeCounts === false) {
validated.limit = (validated.limit ?? 10) + 1;
}
return validated;
}
@@ -210,6 +220,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
protected async performQuery(qb: RepositoryQB): Promise<RepositoryResponse> {
const entity = this.entity;
const compiled = qb.compile();
const payload = {
@@ -444,6 +455,16 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
await this.triggerFindBefore(this.entity, options);
const res = await this.performQuery(qb);
if (this.options?.includeCounts === false) {
const hasMore = res.data.length === (options.limit ?? 10);
res.meta.has_more = hasMore;
if (hasMore) {
res.data = res.data.slice(0, -1);
res.result = res.result.slice(0, -1);
res.meta.items = res.data.length;
}
}
await this.triggerFindAfter(this.entity, options, res.data);
return res as any;
+31 -6
View File
@@ -1,13 +1,17 @@
import { config } from "core";
import type { Static } from "core/utils";
import { StringEnum, uuidv7, type Static } from "core/utils";
import { Field, baseFieldConfigSchema } from "./Field";
import * as tbbox from "@sinclair/typebox";
import type { TFieldTSType } from "data/entities/EntityTypescript";
const { Type } = tbbox;
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)),
}),
]);
@@ -21,8 +25,8 @@ export class PrimaryField<Required extends true | false = false> extends Field<
> {
override readonly type = "primary";
constructor(name: string = config.data.default_primary_field) {
super(name, { fillable: false, required: false });
constructor(name: string = config.data.default_primary_field, cfg?: PrimaryFieldConfig) {
super(name, { fillable: false, required: false, ...cfg });
}
override isRequired(): boolean {
@@ -30,32 +34,53 @@ export class PrimaryField<Required extends true | false = false> extends Field<
}
protected getSchema() {
return baseFieldConfigSchema;
return primaryFieldConfigSchema;
}
get format() {
return this.config.format ?? "integer";
}
get fieldType() {
return this.format === "integer" ? "integer" : "text";
}
override schema() {
return Object.freeze({
type: "integer",
type: this.fieldType,
name: this.name,
primary: true,
nullable: false,
});
}
getNewValue(): any {
if (this.format === "uuid") {
return uuidv7();
}
return undefined;
}
override async transformPersist(value: any): Promise<number> {
throw new Error("PrimaryField: This function should not be called");
}
override toJsonSchema() {
if (this.format === "uuid") {
return this.toSchemaWrapIfRequired(Type.String({ writeOnly: undefined }));
}
return this.toSchemaWrapIfRequired(Type.Number({ writeOnly: undefined }));
}
override toType(): TFieldTSType {
const type = this.format === "integer" ? "number" : "string";
return {
...super.toType(),
required: true,
import: [{ package: "kysely", name: "Generated" }],
type: "Generated<number>",
type: `Generated<${type}>`,
};
}
}
+2 -1
View File
@@ -9,6 +9,7 @@ import {
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;
const directions = ["source", "target"] as const;
@@ -72,7 +73,7 @@ export abstract class EntityRelation<
reference: string,
): KyselyQueryBuilder;
getReferenceQuery(entity: Entity, id: number, reference: string): Partial<RepoQuery> {
getReferenceQuery(entity: Entity, id: PrimaryFieldType, reference: string): Partial<RepoQuery> {
return {};
}
+4 -2
View File
@@ -1,6 +1,6 @@
import { type Static, StringEnum } from "core/utils";
import type { EntityManager } from "../entities";
import { Field, baseFieldConfigSchema } from "../fields";
import { Field, baseFieldConfigSchema, primaryFieldTypes } from "../fields";
import type { EntityRelation } from "./EntityRelation";
import type { EntityRelationAnchor } from "./EntityRelationAnchor";
import * as tbbox from "@sinclair/typebox";
@@ -15,6 +15,7 @@ export const relationFieldConfigSchema = Type.Composite([
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" })),
}),
]);
@@ -45,6 +46,7 @@ export class RelationField extends Field<RelationFieldConfig> {
reference: target.reference,
target: target.entity.name,
target_field: target.entity.getPrimaryField().name,
target_field_type: target.entity.getPrimaryField().fieldType,
});
}
@@ -63,7 +65,7 @@ export class RelationField extends Field<RelationFieldConfig> {
override schema() {
return Object.freeze({
...super.schema()!,
type: "integer",
type: this.config.target_field_type ?? "integer",
references: `${this.config.target}.${this.config.target_field}`,
onDelete: this.config.on_delete ?? "set null",
});
+2
View File
@@ -2,6 +2,7 @@ import type { CompiledQuery, TableMetadata } from "kysely";
import type { IndexMetadata, SchemaResponse } from "../connection/Connection";
import type { Entity, EntityManager } from "../entities";
import { PrimaryField } from "../fields";
import { $console } from "core";
type IntrospectedTable = TableMetadata & {
indices: IndexMetadata[];
@@ -332,6 +333,7 @@ export class SchemaManager {
if (config.force) {
try {
$console.log("[SchemaManager]", sql);
await qb.execute();
} catch (e) {
throw new Error(`Failed to execute query: ${sql}: ${(e as any).message}`);
+1 -1
View File
@@ -183,7 +183,7 @@ export class ModuleManager {
const context = this.ctx(true);
for (const key in MODULES) {
const moduleConfig = key in initial ? initial[key] : {};
const moduleConfig = initial && key in initial ? initial[key] : {};
const module = new MODULES[key](moduleConfig, context) as Module;
module.setListener(async (c) => {
await this.onModuleConfigUpdated(key, c);
+50 -41
View File
@@ -54,7 +54,7 @@ export class AdminController extends Controller {
}
private withBasePath(route: string = "") {
return (this.basepath + route).replace(/(?<!:)\/+/g, "/");
return (this.options.basepath + route).replace(/(?<!:)\/+/g, "/");
}
private withAdminBasePath(route: string = "") {
@@ -80,25 +80,51 @@ export class AdminController extends Controller {
loggedOut: configs.auth.cookie.pathLoggedOut ?? this.withAdminBasePath("/"),
login: this.withAdminBasePath("/auth/login"),
register: this.withAdminBasePath("/auth/register"),
logout: this.withAdminBasePath("/auth/logout"),
logout: "/api/auth/logout",
};
hono.use("*", async (c, next) => {
const obj = {
user: c.get("auth")?.user,
logout_route: authRoutes.logout,
admin_basepath: this.options.adminBasepath,
};
const html = await this.getHtml(obj);
if (!html) {
console.warn("Couldn't generate HTML for admin UI");
// re-casting to void as a return is not required
return c.notFound() as unknown as void;
}
c.set("html", html);
const paths = ["/", "/data/*", "/auth/*", "/media/*", "/flows/*", "/settings/*"];
if (isDebug()) {
paths.push("/test/*");
}
await next();
});
for (const path of paths) {
hono.get(
path,
permission(SystemPermissions.accessAdmin, {
onDenied: async (c) => {
if (!path.startsWith("/auth")) {
addFlashMessage(c, "You are not authorized to access the Admin UI", "error");
$console.log("redirecting", authRoutes.login);
return c.redirect(authRoutes.login);
}
return;
},
}),
permission(SystemPermissions.schemaRead, {
onDenied: async (c) => {
addFlashMessage(c, "You not allowed to read the schema", "warning");
},
}),
async (c) => {
const obj = {
user: c.get("auth")?.user,
logout_route: authRoutes.logout,
admin_basepath: this.options.adminBasepath,
};
const html = await this.getHtml(obj);
if (!html) {
console.warn("Couldn't generate HTML for admin UI");
// re-casting to void as a return is not required
return c.notFound() as unknown as void;
}
await auth.authenticator?.requestCookieRefresh(c);
return c.html(html);
},
);
}
if (auth_enabled) {
const redirectRouteParams = [
@@ -126,27 +152,6 @@ export class AdminController extends Controller {
});
}
// @todo: only load known paths
hono.get(
"/*",
permission(SystemPermissions.accessAdmin, {
onDenied: async (c) => {
addFlashMessage(c, "You are not authorized to access the Admin UI", "error");
$console.log("redirecting");
return c.redirect(authRoutes.login);
},
}),
permission(SystemPermissions.schemaRead, {
onDenied: async (c) => {
addFlashMessage(c, "You not allowed to read the schema", "warning");
},
}),
async (c) => {
return c.html(c.get("html")!);
},
);
return hono;
}
@@ -194,9 +199,13 @@ export class AdminController extends Controller {
}).then((res) => res.default);
}
// @todo: load all marked as entry (incl. css)
assets.js = manifest["src/ui/main.tsx"].file;
assets.css = manifest["src/ui/main.tsx"].css[0] as any;
try {
// @todo: load all marked as entry (incl. css)
assets.js = manifest["src/ui/main.tsx"].file;
assets.css = manifest["src/ui/main.tsx"].css[0] as any;
} catch (e) {
$console.warn("Couldn't find assets in manifest", e);
}
}
const favicon = isProd ? this.options.assetsPath + "favicon.ico" : "/favicon.ico";
+1 -1
View File
@@ -331,6 +331,6 @@ export class SystemController extends Controller {
);
hono.get("/swagger", swaggerUI({ url: "/api/system/openapi.json" }));
return hono.all("*", (c) => c.notFound());
return hono;
}
}
+61 -34
View File
@@ -34,6 +34,7 @@ export type DataTableProps<Data> = {
checkable?: boolean;
onClickRow?: (row: Data) => void;
onClickPage?: (page: number) => void;
hasMore?: boolean;
total?: number;
page?: number;
perPage?: number;
@@ -59,6 +60,7 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
onClickRow,
onClickPage,
onClickSort,
hasMore,
total,
sort,
page = 1,
@@ -75,7 +77,7 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
page = page || 1;
const select = columns && columns.length > 0 ? columns : Object.keys(data?.[0] || {});
const pages = Math.max(Math.ceil(total / perPage), 1);
const pages = hasMore === undefined ? Math.max(Math.ceil(total / perPage), 1) : undefined;
const CellRender = renderValue || CellValue;
return (
@@ -196,12 +198,14 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
</div>
<div className="flex flex-row items-center justify-between">
<div className="hidden md:flex text-primary/40">
<TableDisplay
perPage={perPage}
page={page}
items={data?.length || 0}
total={total}
/>
{hasMore === undefined ? (
<TableDisplay
perPage={perPage}
page={page}
items={data?.length || 0}
total={total}
/>
) : null}
</div>
<div className="flex flex-row gap-2 md:gap-10 items-center">
{perPageOptions && (
@@ -220,11 +224,16 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
</div>
)}
<div className="text-primary/40">
Page {page} of {pages}
Page {page} {pages ? `of ${pages}` : ""}
</div>
{onClickPage && (
<div className="flex flex-row gap-1.5">
<TableNav current={page} total={pages} onClick={onClickPage} />
<TableNav
current={page}
total={pages}
hasMore={hasMore}
onClick={onClickPage}
/>
</div>
)}
</div>
@@ -274,41 +283,59 @@ const TableDisplay = ({ perPage, page, items, total }) => {
return <>Showing 1 row</>;
}
return (
<>
Showing {perPage * (page - 1) + 1}-{perPage * (page - 1) + items} of {total} rows
</>
);
const text = `Showing ${perPage * (page - 1) + 1}-${perPage * (page - 1) + items}`;
if (total) {
return `${text} of ${total} rows`;
}
return text;
};
type TableNavProps = {
current: number;
total: number;
total?: number;
hasMore?: boolean;
onClick?: (page: number) => void;
};
const TableNav: React.FC<TableNavProps> = ({ current, total, onClick }: TableNavProps) => {
const TableNav: React.FC<TableNavProps> = ({
current,
total: _total,
hasMore: _hasMore,
onClick,
}: TableNavProps) => {
const total = _total !== undefined ? _total : _hasMore !== undefined ? current + 1 : current;
const hasMore = _hasMore !== undefined ? _hasMore : current < (total ?? 0);
const navMap = [
{ value: 1, Icon: TbChevronsLeft, disabled: current === 1 },
{ value: current - 1, Icon: TbChevronLeft, disabled: current === 1 },
{ value: current + 1, Icon: TbChevronRight, disabled: current === total },
{ value: total, Icon: TbChevronsRight, disabled: current === total },
{ value: current + 1, Icon: TbChevronRight, disabled: !hasMore },
{
value: _hasMore === undefined ? total : undefined,
Icon: TbChevronsRight,
disabled: !hasMore,
},
] as const;
return navMap.map((nav, key) => (
<button
role="button"
type="button"
key={key}
disabled={nav.disabled}
className="px-2 py-2 border-muted border rounded-md enabled:link text-lg enabled:hover:bg-primary/5 text-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => {
const page = nav.value;
const safePage = page < 1 ? 1 : page > total ? total : page;
onClick?.(safePage);
}}
>
<nav.Icon />
</button>
));
return navMap.map((nav, key) => {
const page = nav.value;
if (page === undefined) return null;
return (
<button
role="button"
type="button"
key={key}
disabled={nav.disabled}
className="px-2 py-2 border-muted border rounded-md enabled:link text-lg enabled:hover:bg-primary/5 text-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => {
const safePage = page < 1 ? 1 : page > total ? total : page;
onClick?.(safePage);
}}
>
<nav.Icon />
</button>
);
});
};
@@ -1,9 +1,10 @@
import type { Api } from "bknd/client";
import type { PrimaryFieldType } from "core";
import type { RepoQueryIn } from "data";
import type { MediaFieldSchema } from "media/AppMedia";
import type { TAppMediaConfig } from "media/media-schema";
import { useId, useEffect, useRef, useState } from "react";
import { useApi, useApiInfiniteQuery, useApiQuery, useInvalidate } from "ui/client";
import { useApi, useApiInfiniteQuery, useApiQuery, useInvalidate } from "bknd/client";
import { useEvent } from "ui/hooks/use-event";
import { Dropzone, type DropzoneProps } from "./Dropzone";
import { mediaItemsToFileStates } from "./helper";
@@ -14,7 +15,7 @@ export type DropzoneContainerProps = {
infinite?: boolean;
entity?: {
name: string;
id: number;
id: PrimaryFieldType;
field: string;
};
media?: Pick<TAppMediaConfig, "entity_name" | "storage">;
+25 -20
View File
@@ -1,13 +1,14 @@
import { decodeSearch, encodeSearch, parseDecode } from "core/utils";
import { decodeSearch, encodeSearch, mergeObject, parseDecode } from "core/utils";
import { isEqual, transform } from "lodash-es";
import { useLocation, useSearch as useWouterSearch } from "wouter";
import { type s, parse } from "core/object/schema";
import { type s, parse, cloneSchema } from "core/object/schema";
// @todo: migrate to Typebox
export function useSearch<Schema extends s.TAnySchema = s.TAnySchema>(
schema: Schema,
_schema: Schema,
defaultValue?: Partial<s.StaticCoerced<Schema>>,
) {
const schema = cloneSchema(_schema as any) as s.TSchema;
const searchString = useWouterSearch();
const [location, navigate] = useLocation();
const initial = searchString.length > 0 ? decodeSearch(searchString) : (defaultValue ?? {});
@@ -16,23 +17,16 @@ export function useSearch<Schema extends s.TAnySchema = s.TAnySchema>(
clone: true,
}) as s.StaticCoerced<Schema>;
// @todo: add option to set multiple keys at once
function set<Key extends keyof s.StaticCoerced<Schema>>(
key: Key,
value: s.StaticCoerced<Schema>[Key],
): void {
//console.log("set", key, value);
const update = parse(schema, { ...decodeSearch(searchString), [key]: value });
const search = transform(
update as any,
(result, value, key) => {
if (defaultValue && isEqual(value, defaultValue[key])) return;
result[key] = value;
},
{} as s.StaticCoerced<Schema>,
);
const encoded = encodeSearch(search, { encode: false });
navigate(location + (encoded.length > 0 ? "?" + encoded : ""));
// @ts-ignore
const _defaults = mergeObject(schema.template({ withOptional: true }), defaultValue ?? {});
function set<Update extends Partial<s.StaticCoerced<Schema>>>(update: Update): void {
// @ts-ignore
if (schema.validate(update).valid) {
const search = getWithoutDefaults(mergeObject(value, update), _defaults);
const encoded = encodeSearch(search, { encode: false });
navigate(location + (encoded.length > 0 ? "?" + encoded : ""));
}
}
return {
@@ -40,3 +34,14 @@ export function useSearch<Schema extends s.TAnySchema = s.TAnySchema>(
set,
};
}
function getWithoutDefaults(value: object, defaultValue: object) {
return transform(
value as any,
(result, value, key) => {
if (defaultValue && isEqual(value, defaultValue[key])) return;
result[key] = value;
},
{} as object,
);
}
+12 -1
View File
@@ -1,5 +1,5 @@
import { SegmentedControl, Tooltip } from "@mantine/core";
import { IconApi, IconKeyOff, IconSettings, IconUser } from "@tabler/icons-react";
import { IconApi, IconBook, IconKeyOff, IconSettings, IconUser } from "@tabler/icons-react";
import {
TbDatabase,
TbFingerprint,
@@ -24,6 +24,7 @@ import { useLocation } from "wouter";
import { NavLink } from "./AppShell";
import { autoFormatString } from "core/utils";
import { appShellStore } from "ui/store";
import { getVersion } from "core/env";
export function HeaderNavigation() {
const [location, navigate] = useLocation();
@@ -164,6 +165,11 @@ function UserMenu() {
onClick: () => window.open("/api/system/swagger", "_blank"),
icon: IconApi,
},
{
label: "Docs",
onClick: () => window.open("https://docs.bknd.io", "_blank"),
icon: IconBook,
},
];
if (config.auth.enabled) {
@@ -182,6 +188,11 @@ function UserMenu() {
if (!options.theme) {
items.push(() => <UserMenuThemeToggler />);
}
items.push(() => (
<div className="font-mono leading-none text-xs text-primary/50 text-center pb-1 pt-2 mt-1 border-t border-primary/5">
{getVersion()}
</div>
));
return (
<>
@@ -22,6 +22,7 @@ import { EntityRelationalFormField } from "./fields/EntityRelationalFormField";
import ErrorBoundary from "ui/components/display/ErrorBoundary";
import { Alert } from "ui/components/display/Alert";
import { bkndModals } from "ui/modals";
import type { PrimaryFieldType } from "core";
// simplify react form types 🤦
export type FormApi = ReactFormExtendedApi<any, any, any, any, any, any, any, any, any, any>;
@@ -30,7 +31,7 @@ export type TFieldApi = FieldApi<any, any, any, any, any, any, any, any, any, an
type EntityFormProps = {
entity: Entity;
entityId?: number;
entityId?: PrimaryFieldType;
data?: EntityData;
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
fieldsDisabled: boolean;
@@ -225,7 +226,7 @@ function EntityMediaFormField({
formApi: FormApi;
field: MediaField;
entity: Entity;
entityId?: number;
entityId?: PrimaryFieldType;
disabled?: boolean;
}) {
if (!entityId) return;
@@ -11,12 +11,14 @@ import {
type EntityFieldsFormRef,
} 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";
const schema = entitiesSchema;
type Schema = Static<typeof schema>;
export function StepEntityFields() {
const { nextStep, stepBack, state, setState } = useStepContext<TCreateModalSchema>();
const { config } = useBkndData();
const entity = state.entities?.create?.[0]!;
const defaultFields = { id: { type: "primary", name: "id" } } as const;
const ref = useRef<EntityFieldsFormRef>(null);
@@ -82,6 +84,8 @@ export function StepEntityFields() {
ref={ref}
fields={initial.fields as any}
onChange={updateListener}
defaultPrimaryFormat={config?.default_primary_format}
isNew={true}
/>
</div>
</div>
@@ -10,12 +10,13 @@ import {
entitySchema,
useStepContext,
} from "./CreateModal";
import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect";
export function StepEntity() {
const focusTrapRef = useFocusTrap();
const { nextStep, stepBack, state, setState } = useStepContext<TCreateModalSchema>();
const { register, handleSubmit, formState, watch } = useForm({
const { register, handleSubmit, formState, watch, control } = useForm({
mode: "onTouched",
resolver: typeboxResolver(entitySchema),
defaultValues: state.entities?.create?.[0] ?? {},
@@ -56,7 +57,6 @@ export function StepEntity() {
label="What's the name of the entity?"
description="Use plural form, and all lowercase. It will be used as the database table."
/>
{/*<input type="submit" value="submit" />*/}
<TextInput
{...register("config.name")}
error={formState.errors.config?.name?.message}
+4 -3
View File
@@ -1,3 +1,4 @@
import type { PrimaryFieldType } from "core";
import { ucFirst } from "core/utils";
import type { Entity, EntityData, EntityRelation } from "data";
import { Fragment, useState } from "react";
@@ -24,7 +25,7 @@ export function DataEntityUpdate({ params }) {
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
}
const entityId = Number.parseInt(params.id as string);
const entityId = params.id as PrimaryFieldType;
const [error, setError] = useState<string | null>(null);
const [navigate] = useNavigate();
useBrowserTitle(["Data", entity.label, `#${entityId}`]);
@@ -202,7 +203,7 @@ function EntityDetailRelations({
entity,
relations,
}: {
id: number;
id: PrimaryFieldType;
entity: Entity;
relations: EntityRelation[];
}) {
@@ -250,7 +251,7 @@ function EntityDetailInner({
entity,
relation,
}: {
id: number;
id: PrimaryFieldType;
entity: Entity;
relation: EntityRelation;
}) {
@@ -23,7 +23,7 @@ const searchSchema = s.partialObject({
perPage: s.number({ default: 10 }).optional(),
});
const PER_PAGE_OPTIONS = [5, 10, 25];
const PER_PAGE_OPTIONS = [5, 10, 25, 50, 100];
export function DataEntityList({ params }) {
const { $data } = useBkndData();
@@ -61,19 +61,18 @@ export function DataEntityList({ params }) {
}
function handleClickPage(page: number) {
search.set("page", page);
search.set({ page });
}
function handleSortClick(name: string) {
const sort = search.value.sort!;
const newSort = { by: name, dir: sort.by === name && sort.dir === "asc" ? "desc" : "asc" };
search.set("sort", newSort as any);
search.set({ sort: newSort as any });
}
function handleClickPerPage(perPage: number) {
// @todo: also reset page to 1
search.set("perPage", perPage);
search.set({ perPage, page: 1 });
}
const isUpdating = $q.isLoading || $q.isValidating;
@@ -132,6 +131,7 @@ export function DataEntityList({ params }) {
perPage={search.value.perPage}
perPageOptions={PER_PAGE_OPTIONS}
total={meta?.count}
hasMore={meta?.has_more}
onClickPage={handleClickPage}
onClickPerPage={handleClickPerPage}
/>
@@ -148,7 +148,7 @@ export function DataSchemaEntity({ params }) {
const Fields = ({ entity }: { entity: Entity }) => {
const [submitting, setSubmitting] = useState(false);
const [updates, setUpdates] = useState(0);
const { actions, $data } = useBkndData();
const { actions, $data, config } = useBkndData();
const [res, setRes] = useState<any>();
const ref = useRef<EntityFieldsFormRef>(null);
async function handleUpdate() {
@@ -201,6 +201,8 @@ const Fields = ({ entity }: { entity: Entity }) => {
}
},
}))}
defaultPrimaryFormat={config?.default_primary_format}
isNew={false}
/>
{isDebug() && (
@@ -28,6 +28,8 @@ import { type TFieldSpec, fieldSpecs } from "ui/modules/data/components/fields-s
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;
const fieldsSchemaObject = originalFieldsSchemaObject;
@@ -65,6 +67,8 @@ export type EntityFieldsFormProps = {
sortable?: boolean;
additionalFieldTypes?: (TFieldSpec & { onClick: () => void })[];
routePattern?: string;
defaultPrimaryFormat?: TPrimaryFieldFormat;
isNew?: boolean;
};
export type EntityFieldsFormRef = {
@@ -77,7 +81,7 @@ export type EntityFieldsFormRef = {
export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsFormProps>(
function EntityFieldsForm(
{ fields: _fields, sortable, additionalFieldTypes, routePattern, ...props },
{ fields: _fields, sortable, additionalFieldTypes, routePattern, isNew, ...props },
ref,
) {
const entityFields = Object.entries(_fields).map(([name, field]) => ({
@@ -172,6 +176,10 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
remove={remove}
dnd={dnd}
routePattern={routePattern}
primary={{
defaultFormat: props.defaultPrimaryFormat,
editable: isNew,
}}
/>
)}
/>
@@ -186,6 +194,10 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
errors={errors}
remove={remove}
routePattern={routePattern}
primary={{
defaultFormat: props.defaultPrimaryFormat,
editable: isNew,
}}
/>
))}
</div>
@@ -281,6 +293,7 @@ function EntityField({
errors,
dnd,
routePattern,
primary,
}: {
field: FieldArrayWithId<TFieldsFormSchema, "fields", "id">;
index: number;
@@ -292,6 +305,10 @@ function EntityField({
errors: any;
dnd?: SortableItemProps;
routePattern?: string;
primary?: {
defaultFormat?: TPrimaryFieldFormat;
editable?: boolean;
};
}) {
const prefix = `fields.${index}.field` as const;
const type = field.field.type;
@@ -363,15 +380,29 @@ function EntityField({
</div>
)}
<div className="flex-col gap-1 hidden md:flex">
<span className="text-xs text-primary/50 leading-none">Required</span>
{is_primary ? (
<Switch size="sm" defaultChecked disabled />
<>
<MantineSelect
data={["integer", "uuid"]}
defaultValue={primary?.defaultFormat}
disabled={!primary?.editable}
placeholder="Select format"
name={`${prefix}.config.format`}
allowDeselect={false}
control={control}
size="xs"
className="w-22"
/>
</>
) : (
<MantineSwitch
size="sm"
name={`${prefix}.config.required`}
control={control}
/>
<>
<span className="text-xs text-primary/50 leading-none">Required</span>
<MantineSwitch
size="sm"
name={`${prefix}.config.required`}
control={control}
/>
</>
)}
</div>
</div>
+9 -3
View File
@@ -1,5 +1,11 @@
{
"extends": "./tsconfig.json",
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"exclude": ["./node_modules", "./__test__", "./e2e"]
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./dist/types",
"rootDir": "./src",
"baseUrl": ".",
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
},
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"exclude": ["./node_modules", "./__test__", "./e2e"]
}
+10 -4
View File
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"types": ["bun-types", "@cloudflare/workers-types"],
"types": ["bun-types"],
"composite": false,
"incremental": true,
"module": "ESNext",
@@ -26,12 +26,18 @@
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"rootDir": "./src",
"outDir": "./dist/types",
"rootDir": ".",
"baseUrl": ".",
"outDir": "./dist/types",
"paths": {
"*": ["./src/*"],
"bknd": ["./src/*"]
"bknd": ["./src/index.ts"],
"bknd/core": ["./src/core/index.ts"],
"bknd/adapter": ["./src/adapter/index.ts"],
"bknd/client": ["./src/ui/client/index.ts"],
"bknd/data": ["./src/data/index.ts"],
"bknd/media": ["./src/media/index.ts"],
"bknd/auth": ["./src/auth/index.ts"]
}
},
"include": [
+4 -1
View File
@@ -23,7 +23,10 @@ export default defineConfig({
},
plugins: [
react(),
tsconfigPaths(),
tsconfigPaths({
// otherwise it'll throw an error because of examples/astro
ignoreConfigErrors: true,
}),
devServer({
...devServerConfig,
entry: "./vite.dev.ts",
+277 -42
View File
@@ -6,7 +6,6 @@
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@clack/prompts": "^0.10.0",
"@cloudflare/workers-types": "^4.20240620.0",
"@tsconfig/strictest": "^2.0.5",
"@types/lodash-es": "^4.17.12",
"bun-types": "^1.1.18",
@@ -27,7 +26,7 @@
},
"app": {
"name": "bknd",
"version": "0.12.0",
"version": "0.14.0-rc.0",
"bin": "./dist/cli/index.js",
"dependencies": {
"@cfworker/json-schema": "^4.1.1",
@@ -46,7 +45,7 @@
"bcryptjs": "^3.0.2",
"dayjs": "^1.11.13",
"fast-xml-parser": "^5.0.8",
"hono": "^4.7.4",
"hono": "^4.7.11",
"json-schema-form-react": "^0.0.2",
"json-schema-library": "10.0.0-rc7",
"json-schema-to-ts": "^3.1.1",
@@ -56,13 +55,15 @@
"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",
"@bluwy/giget-core": "^0.1.2",
"@cloudflare/workers-types": "^4.20250606.0",
"@dagrejs/dagre": "^1.1.4",
"@hono/typebox-validator": "^0.3.2",
"@hono/vite-dev-server": "^0.19.0",
"@hono/typebox-validator": "^0.3.3",
"@hono/vite-dev-server": "^0.19.1",
"@hookform/resolvers": "^4.1.3",
"@libsql/kysely-libsql": "^0.4.1",
"@mantine/modals": "^7.17.1",
@@ -84,7 +85,7 @@
"dotenv": "^16.4.7",
"jotai": "^2.12.2",
"jsdom": "^26.0.0",
"jsonv-ts": "^0.0.14-alpha.6",
"jsonv-ts": "^0.1.0",
"kysely-d1": "^0.3.0",
"open": "^10.1.0",
"openapi-types": "^12.1.3",
@@ -105,13 +106,13 @@
"tsc-alias": "^1.8.11",
"tsup": "^8.4.0",
"tsx": "^4.19.3",
"vite": "^6.2.1",
"vite": "^6.3.5",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.9",
"wouter": "^3.6.0",
},
"optionalDependencies": {
"@hono/node-server": "^1.13.8",
"@hono/node-server": "^1.14.3",
},
"peerDependencies": {
"react": ">=19",
@@ -132,7 +133,6 @@
"version": "0.5.1",
"devDependencies": {
"@types/bun": "latest",
"bknd": "workspace:*",
"tsdx": "^0.14.1",
"typescript": "^5.0.0",
},
@@ -149,19 +149,24 @@
},
"packages/postgres": {
"name": "@bknd/postgres",
"version": "0.0.1",
"dependencies": {
"kysely": "^0.27.6",
"pg": "^8.14.0",
},
"version": "0.1.0",
"devDependencies": {
"@types/bun": "^1.2.5",
"@types/node": "^22.13.10",
"@types/pg": "^8.11.11",
"@xata.io/client": "^0.0.0-next.v93343b9646f57a1e5c51c35eccf0767c2bb80baa",
"@xata.io/kysely": "^0.2.1",
"bknd": "workspace:*",
"kysely-neon": "^1.3.0",
"tsup": "^8.4.0",
"typescript": "^5.8.2",
},
"optionalDependencies": {
"kysely": "^0.27.6",
"kysely-postgres-js": "^2.0.0",
"pg": "^8.14.0",
"postgres": "^3.4.7",
},
},
"packages/sqlocal": {
"name": "@bknd/sqlocal",
@@ -523,7 +528,7 @@
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250224.0", "", { "os": "win32", "cpu": "x64" }, "sha512-x2iF1CsmYmmPEorWb1GRpAAouX5rRjmhuHMC259ojIlozR4G0LarlB9XfmeLEvtw537Ea0kJ6SOhjvUcWzxSvA=="],
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250310.0", "", {}, "sha512-SNE2ohlL9/VxFbcHQc28n3Nj70FiS1Ea0wrUhCXUIbR2lsr4ceRVndNxhuzhcF9EZd2UXm2wwow34RIS1mm+Mg=="],
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250606.0", "", {}, "sha512-9T/Y/Mxe57UVzqgfjJKheiMplnStj/3CmCHlgoZNLU8JW2waRbXvpY3EEeliiYAJfeHZTjeAaKO2pCabxAoyCw=="],
"@cnakazawa/watch": ["@cnakazawa/watch@1.0.4", "", { "dependencies": { "exec-sh": "^0.3.2", "minimist": "^1.2.0" }, "bin": { "watch": "cli.js" } }, "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ=="],
@@ -639,13 +644,13 @@
"@hello-pangea/dnd": ["@hello-pangea/dnd@18.0.1", "", { "dependencies": { "@babel/runtime": "^7.26.7", "css-box-model": "^1.2.1", "raf-schd": "^4.0.3", "react-redux": "^9.2.0", "redux": "^5.0.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ=="],
"@hono/node-server": ["@hono/node-server@1.13.8", "", { "peerDependencies": { "hono": "^4" } }, "sha512-fsn8ucecsAXUoVxrUil0m13kOEq4mkX4/4QozCqmY+HpGfKl74OYSn8JcMA8GnG0ClfdRI4/ZSeG7zhFaVg+wg=="],
"@hono/node-server": ["@hono/node-server@1.14.3", "", { "peerDependencies": { "hono": "^4" } }, "sha512-KuDMwwghtFYSmIpr4WrKs1VpelTrptvJ+6x6mbUcZnFcc213cumTF5BdqfHyW93B19TNI4Vaev14vOI2a0Ie3w=="],
"@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/typebox-validator": ["@hono/typebox-validator@0.3.3", "", { "peerDependencies": { "@sinclair/typebox": ">=0.31.15 <1", "hono": ">=3.9.0" } }, "sha512-BH6TOkVKlLIYYX4qfadpkNZDP/knxtCXp4210T9apKioA7q8mq1m3ELEvMMLhrtZBhzPOAlTr83A6RLZ3awDtg=="],
"@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=="],
"@hono/vite-dev-server": ["@hono/vite-dev-server@0.19.1", "", { "dependencies": { "@hono/node-server": "^1.14.2", "minimatch": "^9.0.3" }, "peerDependencies": { "hono": "*", "miniflare": "*", "wrangler": "*" }, "optionalPeers": ["miniflare", "wrangler"] }, "sha512-hh+0u3IxHErEyj4YwHk/U+2f+qAHEQZ9EIQtadG9jeHfxEXH6r/ZecjnpyEkQbDK7JtgEEoVAq/JGOkd3Dvqww=="],
"@hookform/resolvers": ["@hookform/resolvers@4.1.3", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ=="],
@@ -791,6 +796,8 @@
"@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="],
"@neondatabase/serverless": ["@neondatabase/serverless@0.4.26", "", { "dependencies": { "@types/pg": "8.6.6" } }, "sha512-6DYEKos2GYn8NTgcJf33BLAx//LcgqzHVavQWe6ZkaDqmEq0I0Xtub6pzwFdq9iayNdCj7e2b0QKr5a8QKB8kQ=="],
"@next/env": ["@next/env@15.2.1", "", {}, "sha512-JmY0qvnPuS2NCWOz2bbby3Pe0VzdAQ7XpEB6uLIHmtXNfAsAO0KLQLkuAoc42Bxbo3/jMC3dcn9cdf+piCcG2Q=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aWXT+5KEREoy3K5AKtiKwioeblmOvFFjd+F3dVleLvvLiQ/mD//jOOuUcx5hzcO9ISSw4lrqtUPntTpK32uXXQ=="],
@@ -1227,7 +1234,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.9", "", { "dependencies": { "bun-types": "1.2.9" } }, "sha512-epShhLGQYc4Bv/aceHbmBhOz1XgUnuTZgcxjxk+WXwNyDXavv5QHD1QEFV0FwbTSQtNq6g4ZcV6y0vZakTjswg=="],
"@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
@@ -1395,6 +1402,10 @@
"@web3-storage/multipart-parser": ["@web3-storage/multipart-parser@1.0.0", "", {}, "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw=="],
"@xata.io/client": ["@xata.io/client@0.0.0-next.v93343b9646f57a1e5c51c35eccf0767c2bb80baa", "", { "peerDependencies": { "typescript": ">=4.5" } }, "sha512-4Js4SAKwmmOPmZVIS1l2K8XVGGkUOi8L1jXuagDfeUX56n95wfA4xYMSmsVS0RLMmRWI4UM4bp5UcFJxwbFYGw=="],
"@xata.io/kysely": ["@xata.io/kysely@0.2.1", "", { "dependencies": { "@xata.io/client": "0.30.1" }, "peerDependencies": { "kysely": "*" } }, "sha512-0+WBcFkBSNEu11wVTyJyeNMOPUuolDKJMjXQr1nheHTNZLfsL0qKshTZOKIC/bGInjepGA7DQ/HFeKDHe5CDpA=="],
"@xyflow/react": ["@xyflow/react@12.4.4", "", { "dependencies": { "@xyflow/system": "0.0.52", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-9RZ9dgKZNJOlbrXXST5HPb5TcXPOIDGondjwcjDro44OQRPl1E0ZRPTeWPGaQtVjbg4WpR4BUYwOeshNI2TuVg=="],
"@xyflow/system": ["@xyflow/system@0.0.52", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-pJBMaoh/GEebIABWEIxAai0yf57dm+kH7J/Br+LnLFPuJL87Fhcmm4KFWd/bCUy/kCWUg+2/yFAGY0AUHRPOnQ=="],
@@ -2075,7 +2086,7 @@
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
"fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
"fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="],
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
@@ -2225,7 +2236,7 @@
"headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="],
"hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="],
"hono": ["hono@4.7.11", "", {}, "sha512-rv0JMwC0KALbbmwJDEnxvQCeJh+xbS3KEWW5PC9cMJ08Ur9xgatI0HmtgYZfOdOSOeYsp5LO2cOhdI8cLEbDEQ=="],
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
@@ -2521,7 +2532,7 @@
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
"jsonv-ts": ["jsonv-ts@0.0.14-alpha.6", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-pwMpjEbNtyq8Xi6QBXuQ8dOZm7WQAEwvCPu3vVf9b3aU2KRHW+cfTPqO53U01YYdjWSSRkqaTKcLSiYdfwBYRA=="],
"jsonv-ts": ["jsonv-ts@0.1.0", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-wJ+79o49MNie2Xk9w1hPN8ozjqemVWXOfWUTdioLui/SeGDC7C+QKXTDxsmUaIay86lorkjb3CCGo6JDKbyTZQ=="],
"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=="],
@@ -2543,6 +2554,10 @@
"kysely-d1": ["kysely-d1@0.3.0", "", { "peerDependencies": { "kysely": "*" } }, "sha512-9wTbE6ooLiYtBa4wPg9e4fjfcmvRtgE/2j9pAjYrIq+iz+EsH/Hj9YbtxpEXA6JoRgfulVQ1EtGj6aycGGRpYw=="],
"kysely-neon": ["kysely-neon@1.3.0", "", { "peerDependencies": { "@neondatabase/serverless": "^0.4.3", "kysely": "0.x.x", "ws": "^8.13.0" }, "optionalPeers": ["ws"] }, "sha512-CIIlbmqpIXVJDdBEYtEOwbmALag0jmqYrGfBeM4cHKb9AgBGs+X1SvXUZ8TqkDacQEqEZN2XtsDoUkcMIISjHw=="],
"kysely-postgres-js": ["kysely-postgres-js@2.0.0", "", { "peerDependencies": { "kysely": ">= 0.24.0 < 1", "postgres": ">= 3.4.0 < 4" } }, "sha512-R1tWx6/x3tSatWvsmbHJxpBZYhNNxcnMw52QzZaHKg7ZOWtHib4iZyEaw4gb2hNKVctWQ3jfMxZT/ZaEMK6kBQ=="],
"language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
@@ -2905,7 +2920,7 @@
"pg-protocol": ["pg-protocol@1.8.0", "", {}, "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g=="],
"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=="],
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
@@ -2961,13 +2976,15 @@
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
"postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
"postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="],
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="],
"postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
"postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="],
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="],
@@ -3603,7 +3620,7 @@
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
"v8-compile-cache": ["v8-compile-cache@2.4.0", "", {}, "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw=="],
@@ -3635,7 +3652,7 @@
"verror": ["verror@1.10.0", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw=="],
"vite": ["vite@6.2.1", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q=="],
"vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
"vite-node": ["vite-node@3.0.8", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.6.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg=="],
@@ -3847,12 +3864,12 @@
"@babel/runtime/regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
"@bknd/postgres/@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="],
"@bundled-es-modules/cookie/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"@bundled-es-modules/tough-cookie/tough-cookie": ["tough-cookie@4.1.4", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="],
"@cypress/request/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
"@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@inquirer/core/cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
@@ -3909,6 +3926,8 @@
"@libsql/kysely-libsql/@libsql/client": ["@libsql/client@0.8.1", "", { "dependencies": { "@libsql/core": "^0.8.1", "@libsql/hrana-client": "^0.6.2", "js-base64": "^3.7.5", "libsql": "^0.3.10", "promise-limit": "^2.7.0" } }, "sha512-xGg0F4iTDFpeBZ0r4pA6icGsYa5rG6RAG+i/iLDnpCAnSuTqEWMDdPlVseiq4Z/91lWI9jvvKKiKpovqJ1kZWA=="],
"@neondatabase/serverless/@types/pg": ["@types/pg@8.6.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw=="],
"@plasmicapp/query/swr": ["swr@1.3.0", "", { "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0" } }, "sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw=="],
"@remix-run/node/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
@@ -4059,12 +4078,14 @@
"@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.9", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw=="],
"@types/bun/bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="],
"@types/jest/jest-diff": ["jest-diff@25.5.0", "", { "dependencies": { "chalk": "^3.0.0", "diff-sequences": "^25.2.6", "jest-get-type": "^25.2.6", "pretty-format": "^25.5.0" } }, "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A=="],
"@types/jest/pretty-format": ["pretty-format@25.5.0", "", { "dependencies": { "@jest/types": "^25.5.0", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^16.12.0" } }, "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ=="],
"@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=="],
"@typescript-eslint/experimental-utils/eslint-utils": ["eslint-utils@2.1.0", "", { "dependencies": { "eslint-visitor-keys": "^1.1.0" } }, "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg=="],
"@typescript-eslint/typescript-estree/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
@@ -4125,6 +4146,8 @@
"@wdio/utils/decamelize": ["decamelize@6.0.0", "", {}, "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA=="],
"@xata.io/kysely/@xata.io/client": ["@xata.io/client@0.30.1", "", { "peerDependencies": { "typescript": ">=4.5" } }, "sha512-dAzDPHmIfenVIpF39m1elmW5ngjWu2mO8ZqJBN7dmYdXr98uhPANfLdVZnc3mUNG+NH37LqY1dSO862hIo2oRw=="],
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"acorn-globals/acorn": ["acorn@6.4.2", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ=="],
@@ -4159,6 +4182,8 @@
"bknd-cli/@libsql/client": ["@libsql/client@0.14.0", "", { "dependencies": { "@libsql/core": "^0.14.0", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.4.4", "promise-limit": "^2.7.0" } }, "sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q=="],
"bknd-cli/hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="],
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
@@ -4467,8 +4492,6 @@
"peek-stream/duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="],
"pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
@@ -4609,6 +4632,8 @@
"through2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"tinyglobby/fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
"to-object-path/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="],
"ts-jest/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
@@ -4657,6 +4682,12 @@
"vite/esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="],
"vite/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"vite-node/vite": ["vite@6.2.1", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q=="],
"vitest/vite": ["vite@6.2.1", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q=="],
"webdriver/@types/node": ["@types/node@20.17.24", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA=="],
"webdriver/undici": ["undici@6.21.1", "", {}, "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ=="],
@@ -4741,6 +4772,14 @@
"@types/jest/pretty-format/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"@types/pg/pg-types/postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
"@types/pg/pg-types/postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="],
"@types/pg/pg-types/postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="],
"@types/pg/pg-types/postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="],
"@typescript-eslint/typescript-estree/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"@verdaccio/local-storage-legacy/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
@@ -4765,6 +4804,8 @@
"@verdaccio/middleware/express/path-to-regexp": ["path-to-regexp@0.1.10", "", {}, "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="],
"@vitest/coverage-v8/vitest/vite": ["vite@6.2.1", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q=="],
"@wdio/logger/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"@wdio/repl/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
@@ -4777,6 +4818,8 @@
"bknd-cli/@libsql/client/libsql": ["libsql@0.4.7", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.4.7", "@libsql/darwin-x64": "0.4.7", "@libsql/linux-arm64-gnu": "0.4.7", "@libsql/linux-arm64-musl": "0.4.7", "@libsql/linux-x64-gnu": "0.4.7", "@libsql/linux-x64-musl": "0.4.7", "@libsql/win32-x64-msvc": "0.4.7" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw=="],
"bknd/vitest/vite": ["vite@6.2.1", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q=="],
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"class-utils/define-property/is-descriptor": ["is-descriptor@0.1.7", "", { "dependencies": { "is-accessor-descriptor": "^1.0.1", "is-data-descriptor": "^1.0.1" } }, "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg=="],
@@ -4929,14 +4972,6 @@
"peek-stream/duplexify/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
"pg/pg-types/postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"pg/pg-types/postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"progress-estimator/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
"progress-estimator/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
@@ -5119,6 +5154,8 @@
"verdaccio-audit/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"vite-node/vite/esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="],
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="],
"vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="],
@@ -5167,6 +5204,8 @@
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="],
"vitest/vite/esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="],
"webdriver/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
"webdriverio/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
@@ -5265,6 +5304,8 @@
"@verdaccio/middleware/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"@vitest/coverage-v8/vitest/vite/esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="],
"bknd-cli/@libsql/client/libsql/@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.4.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yOL742IfWUlUevnI5PdnIT4fryY3LYTdLm56bnY0wXBw7dhFcnjuA7jrH3oSVz2mjZTHujxoITgAE7V6Z+eAbg=="],
"bknd-cli/@libsql/client/libsql/@libsql/darwin-x64": ["@libsql/darwin-x64@0.4.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-ezc7V75+eoyyH07BO9tIyJdqXXcRfZMbKcLCeF8+qWK5nP8wWuMcfOVywecsXGRbT99zc5eNra4NEx6z5PkSsA=="],
@@ -5281,6 +5322,8 @@
"bknd-cli/@libsql/client/libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
"bknd/vitest/vite/esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="],
"eslint/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
"eslint/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
@@ -5353,8 +5396,200 @@
"verdaccio-audit/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"vite-node/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="],
"vite-node/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="],
"vite-node/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.1", "", { "os": "android", "cpu": "arm64" }, "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="],
"vite-node/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.1", "", { "os": "android", "cpu": "x64" }, "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw=="],
"vite-node/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ=="],
"vite-node/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA=="],
"vite-node/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A=="],
"vite-node/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww=="],
"vite-node/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.1", "", { "os": "linux", "cpu": "arm" }, "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ=="],
"vite-node/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ=="],
"vite-node/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ=="],
"vite-node/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg=="],
"vite-node/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg=="],
"vite-node/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg=="],
"vite-node/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ=="],
"vite-node/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ=="],
"vite-node/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA=="],
"vite-node/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.1", "", { "os": "none", "cpu": "x64" }, "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA=="],
"vite-node/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg=="],
"vite-node/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw=="],
"vite-node/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg=="],
"vite-node/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ=="],
"vite-node/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A=="],
"vite-node/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="],
"vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="],
"vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="],
"vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.1", "", { "os": "android", "cpu": "arm64" }, "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="],
"vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.1", "", { "os": "android", "cpu": "x64" }, "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw=="],
"vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ=="],
"vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA=="],
"vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A=="],
"vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww=="],
"vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.1", "", { "os": "linux", "cpu": "arm" }, "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ=="],
"vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ=="],
"vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ=="],
"vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg=="],
"vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg=="],
"vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg=="],
"vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ=="],
"vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ=="],
"vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA=="],
"vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.1", "", { "os": "none", "cpu": "x64" }, "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA=="],
"vitest/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg=="],
"vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw=="],
"vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg=="],
"vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ=="],
"vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A=="],
"vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="],
"@jest/core/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.1", "", { "os": "android", "cpu": "arm64" }, "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.1", "", { "os": "android", "cpu": "x64" }, "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.1", "", { "os": "linux", "cpu": "arm" }, "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.1", "", { "os": "none", "cpu": "x64" }, "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A=="],
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="],
"bknd/vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="],
"bknd/vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="],
"bknd/vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.1", "", { "os": "android", "cpu": "arm64" }, "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="],
"bknd/vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.1", "", { "os": "android", "cpu": "x64" }, "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw=="],
"bknd/vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ=="],
"bknd/vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA=="],
"bknd/vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A=="],
"bknd/vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww=="],
"bknd/vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.1", "", { "os": "linux", "cpu": "arm" }, "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ=="],
"bknd/vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ=="],
"bknd/vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ=="],
"bknd/vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg=="],
"bknd/vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg=="],
"bknd/vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg=="],
"bknd/vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ=="],
"bknd/vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ=="],
"bknd/vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA=="],
"bknd/vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.1", "", { "os": "none", "cpu": "x64" }, "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA=="],
"bknd/vitest/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg=="],
"bknd/vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw=="],
"bknd/vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg=="],
"bknd/vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ=="],
"bknd/vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A=="],
"bknd/vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="],
"eslint/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
"flat-cache/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
+153
View File
@@ -0,0 +1,153 @@
---
title: bknd.config.ts
---
The central configuration file to extend bknd should be placed in the root of your project, so that the [CLI](/usage/cli#using-configuration-file-bknd-config) can automatically pick it up. It allows to:
* define your database connection centrally
* pass in initial configuration or data seeds when booting the first time
* add plugins to the app
* hook into system events
* define custom routes and endpoints
A simple example of a `bknd.config.ts` file:
```typescript bknd.config.ts
import type { BkndConfig } from "bknd/adapter";
export default {
connection: {
url: "file:data.db",
}
} satisfies BkndConfig;
```
## Overview
The `BkndConfig` extends the [`CreateAppConfig`](/usage/introduction#configuration-createappconfig) type with the following properties:
```typescript
export type BkndConfig = CreateAppConfig & {
// return the app configuration as object or from a function
app?: CreateAppConfig | ((args: Args) => CreateAppConfig);
// called before the app is built
beforeBuild?: (app: App) => Promise<void>;
// called after the app has been built
onBuilt?: (app: App) => Promise<void>;
// passed as the first argument to the `App.build` method
buildConfig?: Parameters<App["build"]>[0];
// force the app to be recreated
force?: boolean;
// the id of the app, defaults to `app`
id?: string;
}
```
The supported configuration file extensions are `js`, `ts`, `mjs`, `cjs` and `json`. Throughout the documentation, we'll use `ts` for the file extension.
### `app` (CreateAppConfig)
The `app` property is a function that returns a `CreateAppConfig` object. It allows to pass in the environment variables to the configuration object.
```typescript
import type { BkndConfig } from "bknd/adapter";
export default {
app: ({ env }) => ({
connection: {
url: env.DB_URL,
}
})
} satisfies BkndConfig;
```
See [Database](/usage/database) for more information on how to configure the database connection.
### `beforeBuild`
The `beforeBuild` property is an async function that is called before the app is built. It allows to modify the app instance that may influence the build process.
```typescript
import type { BkndConfig } from "bknd/adapter";
export default {
beforeBuild: async (app: App) => {
// do something that has to happen before the app is built
}
}
```
### `onBuilt`
The `onBuilt` property is an async function that is called after the app has been built. It allows to hook into the app after it has been built. This is useful for defining event listeners or register custom routes, as both the event manager and the server are recreated during the build process.
```typescript
import { type App, AppEvents } from "bknd";
import type { BkndConfig } from "bknd/adapter";
export default {
onBuilt: async (app: App) => {
console.log("App built", app.version());
// registering an event listener
app.emgr.onEvent(AppEvents.AppRequest, (event) => {
console.log("Request received", event.request.url);
})
// registering a custom route
app.server.get("/hello", (c) => c.text("Hello World"));
}
}
```
### `force` & `id`
The `force` property is a boolean that forces the app to be recreated. This is mainly useful for serverless environments where the execution environment is re-used, and you may or may not want to recreate the app on every request.
The `id` property is the reference in a cache map. You may create multiple instances of apps in the same process by using different ids (e.g. multi tenant applications).
## Framework & Runtime configuration
Depending on which framework or runtime you're using to run bknd, the configuration object will extend the `BkndConfig` type with additional properties.
### `RuntimeBkndConfig`
[Runtime adapters](/integration/runtime) need additional configuration to serve static assets for the admin UI.
```typescript
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
// the path to the dist folder to serve static assets for the admin UI
distPath?: string;
// custom middleware to serve static assets for the admin UI
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
// options for the admin UI
adminOptions?: AdminControllerOptions | false;
};
```
### `FrameworkBkndConfig`
[Framework adapters](/integration/framework) may need additional configuration based on the framework's requirements. For example, the `NextjsBkndConfig` type extends the `BkndConfig` type with the following additional properties:
```typescript
type NextjsEnv = NextApiRequest["env"];
export type NextjsBkndConfig<Env = NextjsEnv> = FrameworkBkndConfig<Env> & {
cleanRequest?: { searchParams?: string[] };
};
```
Next.js adds the mounted path to the request object, so that the `cleanRequest` property can be used to remove the mounted path from the request URL. See other frameworks for more information on how to configure them.
## Using the configuration file
The configuration file is automatically picked up if you're using the [CLI](/usage/cli). This allows interacting with your application using the `bknd` command. For example, you can run the following command in the root of your project to start an instance:
```bash
npx bknd run
```
When serving your application, you need to make sure to import the contents of your configuration file. If you're using Next.js for example, it's recommended to follow these steps:
1. create a `bknd.config.ts` file in the root of your project which defines the connection to the database, adds event listeners and custom routes.
2. create a `bknd.ts` file inside your app folder which exports helper functions to instantiate the bknd instance and retrieve the API.
3. create a catch-all route file at `src/api/[[...bknd]]/route.ts` which serves the bknd API.
This way, your application and the CLI are using the same configuration.
+127
View File
@@ -0,0 +1,127 @@
---
title: Events & Hooks
---
bknd comes with a powerful built-in event system that allows you to hook into the app lifecycle and extend its functionality. You can hook into these events in two ways:
- `async`: Your listener is not blocking the main execution flow. E.g. on Cloudflare Workers, by default, the `ExecutionContext`'s `waitUntil` method is used so that the listeners runs after the response is sent.
- `sync`: Your listener is blocking the main execution flow. This allows to abort the request in your custom conditions. Some events also allow to return a modified event payload.
<Note>
Don't mistake `async` with JavaScript's `async` keyword. The `async` keyword is used to indicate that the listener is not blocking the main execution flow.
</Note>
## Overview
### Listening to events
You can listen to events by using the `EventManager` exposed at `app.emgr`. To register a listener, you can use either of these methods:
- `onEvent`: Register a listener for a specific event with a typed event.
- `on`: Register a listener for an event by its slug.
- `onAny`: Register a listener for all events.
```typescript
import { createApp, AppEvents } from "bknd";
const app = createApp();
app.emgr.onEvent(AppEvents.AppRequest, async (event) => {
// ^? AppRequest
console.log("Request received", event.request.url);
});
app.emgr.on("app-request", async (event) => {
console.log("Request received", event.request.url);
});
app.emgr.onAny(async (event) => {
console.log("Event received", event.slug);
});
```
You may want to register your listeners inside [`bknd.config.ts`](/extending/config) to make sure they are registered before the app is built:
```typescript bknd.config.ts
import { AppEvents } from "bknd";
export default {
onBuilt: (app) => {
app.emgr.onEvent(AppEvents.AppRequest, async (event) => {
console.log("Request received", event.request.url);
});
}
}
```
### Setting a mode
By default, listeners are registered as `async` listeners, meaning that the listener is not blocking the main execution flow. You can change this by passing the mode as the third argument:
```typescript
app.emgr.onEvent(AppEvents.AppRequest, async (event) => {
console.log("Request received", event.request.url);
}, { mode: "sync" });
```
This works for all three methods.
## App Events
These events are emitted by the `App` class and are available on the `AppEvents` object.
```typescript
import { AppEvents } from "bknd";
```
Available events:
| Event | Params | Description |
|:------|:--------|:------------|
| `AppConfigUpdatedEvent` | `{ app: App }` | Emitted when the app configuration is updated |
| `AppBuiltEvent` | `{ app: App }` | Emitted when the app is built |
| `AppFirstBoot` | `{ app: App }` | Emitted when the app is first booted |
| `AppRequest` | `{ app: App, request: Request }` | Emitted when a request is received |
| `AppBeforeResponse` | `{ app: App, request: Request, response: Response }` | Emitted before a response is sent |
## Database Events
These events are emitted by the `Database` class and are available on the `DatabaseEvents` object. These are divided by events triggered by the `Mutator` and `Repository` classes.
```typescript
import { DatabaseEvents } from "bknd/data";
```
### Mutator Events
These events are emitted during database mutations (insert, update, delete operations).
| Event | Params | Description |
|:------|:--------|:------------|
| `MutatorInsertBefore` | `{ entity: Entity, data: EntityData }` | Emitted before inserting a new record. Can modify the data. |
| `MutatorInsertAfter` | `{ entity: Entity, data: EntityData, changed: EntityData }` | Emitted after inserting a new record. |
| `MutatorUpdateBefore` | `{ entity: Entity, entityId: PrimaryFieldType, data: EntityData }` | Emitted before updating a record. Can modify the data. |
| `MutatorUpdateAfter` | `{ entity: Entity, entityId: PrimaryFieldType, data: EntityData, changed: EntityData }` | Emitted after updating a record. |
| `MutatorDeleteBefore` | `{ entity: Entity, entityId: PrimaryFieldType }` | Emitted before deleting a record. |
| `MutatorDeleteAfter` | `{ entity: Entity, entityId: PrimaryFieldType, data: EntityData }` | Emitted after deleting a record. |
### Repository Events
These events are emitted during database queries (find operations).
| Event | Params | Description |
|:------|:--------|:------------|
| `RepositoryFindOneBefore` | `{ entity: Entity, options: RepoQuery }` | Emitted before finding a single record. |
| `RepositoryFindOneAfter` | `{ entity: Entity, options: RepoQuery, data: EntityData }` | Emitted after finding a single record. |
| `RepositoryFindManyBefore` | `{ entity: Entity, options: RepoQuery }` | Emitted before finding multiple records. |
| `RepositoryFindManyAfter` | `{ entity: Entity, options: RepoQuery, data: EntityData }` | Emitted after finding multiple records. |
## Media Events
These events are emitted by the `Storage` class and are available on the `MediaEvents` object.
```typescript
import { MediaEvents } from "bknd/media";
```
| Event | Params | Description |
|:------|:--------|:------------|
| `FileUploadedEvent` | `{ file: FileBody } & FileUploadPayload` | Emitted when a file is successfully uploaded. Can modify the event data. |
| `FileDeletedEvent` | `{ name: string }` | Emitted when a file is deleted. |
| `FileAccessEvent` | `{ name: string }` | Emitted when a file is accessed. |
## Auth Events
Coming soon.
+54 -3
View File
@@ -28,14 +28,14 @@ To get started with Astro and bknd you can either install the package manually,
npx astro add react
```
You also need to make sure to set the output to `hybrid` in your Astro config:
You also need to make sure to set the output to `server` in your Astro config:
```js {6}
// astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
export default defineConfig({
output: "hybrid",
output: "server",
integrations: [react()]
});
```
@@ -48,8 +48,58 @@ To get started with Astro and bknd you can either install the package manually,
</Tab>
</Tabs>
## Configuration
Now create a `bknd.config.ts` file in the root of your project. If you created the project using the CLI starter, this file is already created for you.
```typescript bknd.config.ts
import type { AstroBkndConfig } from "bknd/adapter/astro";
export default {
connection: {
url: "file:data.db"
},
} satisfies AstroBkndConfig;
```
See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `AstroBkndConfig` type extends the `BkndConfig` type with the following additional properties:
```typescript
type AstroEnv = NodeJS.ProcessEnv;
export type AstroBkndConfig<Env = AstroEnv> = FrameworkBkndConfig<Env>;
```
## Serve the API
Create a new catch-all route at `src/pages/api/[...api].ts`:
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configurationfrom the `bknd.config.ts` file:
```ts src/bknd.ts
import type { AstroGlobal } from "astro";
import { getApp as getBkndApp } from "bknd/adapter/astro";
import config from "../bknd.config";
export { config };
export async function getApp() {
return await getBkndApp(config);
}
export async function getApi(
astro: AstroGlobal,
opts?: { mode: "static" } | { mode?: "dynamic"; verify?: boolean },
) {
const app = await getApp();
if (opts?.mode !== "static" && opts?.verify) {
const api = app.getApi({ headers: astro.request.headers });
await api.verifyAuth();
return api;
}
return app.getApi();
}
```
Create a new catch-all route at `src/pages/api/[...api].ts`.
```ts src/pages/api/[...api].ts
import { serve } from "bknd/adapter/astro";
@@ -63,6 +113,7 @@ export const ALL = serve({
}
});
```
For more information about the connection object, refer to the [Database](/usage/database) guide. In the
special case of astro, you may also use your Astro DB credentials since it's also using LibSQL
under the hood. Refer to the [Astro DB documentation](https://docs.astro.build/en/guides/astro-db/) for more information.
+30
View File
@@ -205,4 +205,34 @@ new_classes = ["DurableBkndApp"]
tag = "v2"
renamed_classes = [{from = "DurableBkndApp", to = "CustomDurableBkndApp"}]
deleted_classes = ["DurableBkndApp"]
```
## D1 Sessions (experimental)
D1 now supports to enable [global read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/). This allows to reduce latency by reading from the closest region. In order for this to work, D1 has to be started from a bookmark. You can enable this behavior on bknd by setting the `d1.session` property:
```typescript src/index.ts
import { serve } from "bknd/adapter/cloudflare";
export default serve({
// currently recommended to use "fresh" mode
// otherwise consecutive requests will use the same bookmark
mode: "fresh",
// ...
d1: {
// enables D1 sessions
session: true,
// (optional) restrict the transport, options: "header" | "cookie"
// if not specified, it supports both
transport: "cookie",
// (optional) choose session constraint if not bookmark present
// options: "first-primary" | "first-unconstrained"
first: "first-primary"
}
});
```
If bknd is used in a stateful user context (like in a browser), it'll automatically send the session cookie to the server to set the correct bookmark. If you need to manually set the bookmark, you can do so by setting the `x-cf-d1-session` header:
```bash
curl -H "x-cf-d1-session: <bookmark>" ...
```
+28 -7
View File
@@ -22,21 +22,42 @@ To get started with Next.js and bknd you can either install the package manually
</Tab>
</Tabs>
## Configuration
Now create a `bknd.config.ts` file in the root of your project. If you created the project using the CLI starter, this file is already created for you.
```typescript bknd.config.ts
import type { NextjsBkndConfig } from "bknd/adapter/nextjs";
export default {
connection: {
url: "file:data.db"
},
} satisfies NextjsBkndConfig;
```
See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `NextjsBkndConfig` type extends the `BkndConfig` type with the following additional properties:
```typescript
type NextjsEnv = NextApiRequest["env"];
export type NextjsBkndConfig<Env = NextjsEnv> = FrameworkBkndConfig<Env> & {
cleanRequest?: { searchParams?: string[] };
};
```
## Serve the API
Create a helper file to instantiate the bknd instance and retrieve the API:
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configurationfrom the `bknd.config.ts` file:
```ts src/bknd.ts
import { type NextjsBkndConfig, getApp as getBkndApp } from "bknd/adapter/nextjs";
import { headers } from "next/headers";
import config from "../bknd.config";
export const config = {
connection: {
url: "file:data.db"
},
} as const satisfies NextjsBkndConfig;
export { config };
export async function getApp() {
return await getBkndApp(config);
return await getBkndApp(config, process.env);
}
export async function getApi(opts?: { verify?: boolean }) {
+32 -9
View File
@@ -22,21 +22,44 @@ To get started with React Router and bknd you can either install the package man
</Tab>
</Tabs>
## Configuration
Now create a `bknd.config.ts` file in the root of your project. If you created the project using the CLI starter, this file is already created for you.
```typescript bknd.config.ts
import type { ReactRouterBkndConfig } from "bknd/adapter/react-router";
export default {
connection: {
url: "file:data.db"
},
} satisfies ReactRouterBkndConfig;
```
See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `ReactRouterBkndConfig` type extends the `BkndConfig` type with the following additional properties:
```typescript
type ReactRouterEnv = NodeJS.ProcessEnv;
type ReactRouterFunctionArgs = {
request: Request;
};
export type ReactRouterBkndConfig<Env = ReactRouterEnv> = FrameworkBkndConfig<Env>;
```
## Serve the API
Create a helper file to instantiate the bknd instance and retrieve the API:
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configurationfrom the `bknd.config.ts` file:
```ts app/bknd.ts
import { type ReactRouterBkndConfig, getApp as getBkndApp } from "bknd/adapter/react-router";
import config from "../bknd.config";
const config = {
connection: {
url: "file:data.db"
}
} as const satisfies ReactRouterBkndConfig;
export { config };
export async function getApp(args?: { request: Request }) {
return await getBkndApp(config, args);
// you may adjust this function depending on your runtime environment.
// e.g. when deploying to cloudflare workers, you'd want the FunctionArgs to be passed in
// to resolve environment variables
export async function getApp() {
return await getBkndApp(config, process.env as any);
}
export async function getApi(
@@ -60,7 +83,7 @@ Create a new api splat route file at `app/routes/api.$.ts`:
import { getApp } from "~/bknd";
const handler = async (args: { request: Request }) => {
const app = await getApp(args);
const app = await getApp();
return app.fetch(args.request);
};
+46 -19
View File
@@ -16,25 +16,42 @@ Additionally, install required dependencies:
npm install @hono/vite-dev-server
```
## Configuration
Now create a `bknd.config.ts` file in the root of your project.
```typescript bknd.config.ts
import type { ViteBkndConfig } from "bknd/adapter/vite";
export default {
connection: {
url: "file:data.db"
}
} satisfies ViteBkndConfig;
```
See [bknd.config.ts](/extending/config) for more information on how to configure bknd.
The `ViteBkndConfig` type extends the `BkndConfig` type with the following additional properties:
```typescript
export type ViteEnv = NodeJS.ProcessEnv;
export type ViteBkndConfig<Env = ViteEnv> = RuntimeBkndConfig<Env> & {};
```
## Serve the API
To serve the **bknd** API, you first have to create a local server file for you vite environment.
Create a `server.ts` file:
```ts
```typescript
import { serve } from "bknd/adapter/vite";
import config from "./bknd.config";
// the configuration given is optional
export default serve({
mode: "cached", // that's the default
connection: {
url: ":memory:"
}
})
export default serve(config)
```
For more information about the connection object, refer to the [Database](/usage/database) guide.
You can also run your vite server in `mode: "fresh"`, this will re-create the app on every fetch.
This is only useful for when working on the `bknd` repository directly.
For more information about the connection object, refer to the [Database](/usage/database) guide.
Next, adjust your `vite.config.ts` to look like the following:
```ts
import { devServer } from "bknd/adapter/vite";
@@ -82,17 +99,22 @@ options you can make use of to adjust it according to your setup.
There might be cases you want to be sure to be in control over the HTML that is being used.
`bknd` generates it automatically, but you use your own one as follows:
```ts server.ts
```typescript server.ts
import { serve, addViteScript } from "bknd/adapter/vite";
import { readFile } from "node:fs/promises"
import config from "./bknd.config";
let html = await readFile("./index.html", "utf-8");
// add vite scripts
html = addViteScript(html);
// then add it as an option
export default serve({ html })
export default serve({
...config,
adminOptions: {
html: addViteScript(html),
// optionally, you can change the base path for the admin UI
adminBasePath: "/admin"
}
})
```
The vite scripts has to be added manually currently, as adding them automatically with
@@ -101,13 +123,18 @@ The vite scripts has to be added manually currently, as adding them automaticall
### Use a custom entry point
By default, the entry point `/src/main.tsx` is used and should fit most cases. If that's not you,
you can supply a different one like so:
```ts server.ts
```typescript server.ts
import { serve } from "bknd/adapter/vite";
import config from "./bknd.config";
// the configuration given is optional
export default serve({
forceDev: {
mainPath: "/src/special.tsx"
}
...config,
adminOptions: {
forceDev: {
mainPath: "/src/special.tsx"
}
}
});
```
+4
View File
@@ -74,6 +74,10 @@
"usage/elements"
]
},
{
"group": "Extending",
"pages": ["extending/config", "extending/events"]
},
{
"group": "Integration",
"pages": [
+76 -8
View File
@@ -14,20 +14,21 @@ Here is the output:
$ npx bknd
Usage: bknd [options] [command]
⚡ bknd cli v0.10.3-rc.1
⚡ bknd cli v0.13.0
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
config [options] get default config
copy-assets [options] copy static assets
create [options] create a new project
debug <subject> debug bknd
run [options] run an instance
user <action> create/update users, or generate a token (auth)
types [options] generate types
schema [options] get schema
user <action> create and update user (auth)
run [options] run an instance
debug <subject> debug bknd
create [options] create a new project
copy-assets [options] copy static assets
config [options] get default config
help [command] display help for command
```
@@ -123,4 +124,71 @@ To start an instance with an ephemeral in-memory database, run the following:
```
npx bknd run --memory
```
Keep in mind that the database is not persisted and will be lost when the process is terminated.
Keep in mind that the database is not persisted and will be lost when the process is terminated.
## Generating types (`types`)
To see all available `types` options, execute `npx bknd types --help`.
```
$ npx bknd types --help
Usage: bknd types [options]
generate types
Options:
-o, --outfile <outfile> output file (default: "bknd-types.d.ts")
--no-write do not write to file
-h, --help display help for command
```
To generate types for the database, run the following:
```
npx bknd types
```
This will generate types for your database schema in `bknd-types.d.ts`. The generated file could look like this:
```typescript bknd-types.d.ts
import type { DB } from "bknd/core";
import type { Insertable, Selectable, Updateable, Generated } from "kysely";
declare global {
type BkndEntity<T extends keyof DB> = Selectable<DB[T]>;
type BkndEntityCreate<T extends keyof DB> = Insertable<DB[T]>;
type BkndEntityUpdate<T extends keyof DB> = Updateable<DB[T]>;
}
export interface Todos {
id: Generated<number>;
title?: string;
done?: boolean;
}
interface Database {
todos: Todos;
}
declare module "bknd/core" {
interface DB extends Database {}
}
```
Make sure to add the generated file in your `tsconfig.json` file:
```json tsconfig.json
{
"include": [
"bknd-types.d.ts"
]
}
```
You can then use the types by importing them from `bknd/core`:
```typescript
import type { DB } from "bknd/core";
type Todo = DB["todos"];
```
All bknd methods that involve your database schema will be automatically typed.
+84 -17
View File
@@ -3,13 +3,8 @@ title: 'Database'
description: 'Choosing the right database configuration'
---
In order to use **bknd**, you need to prepare access information to your database and install
the dependencies.
In order to use **bknd**, you need to prepare access information to your database and install the dependencies. Connections to the database are managed using Kysely. Therefore, all [its dialects](https://kysely.dev/docs/dialects) are theoretically supported.
<Note>
Connections to the database are managed using Kysely. Therefore, all its dialects are
theoretically supported. However, only the `SQLite` dialect is implemented as of now.
</Note>
## Database
### SQLite in-memory
@@ -56,7 +51,9 @@ connection object to your new database:
```
### Cloudflare D1
Using the [Cloudflare Adapter](/integration/cloudflare), you can choose to use a D1 database binding. To do so, you only need to add a D1 database to your `wrangler.toml` and it'll pick up automatically. To manually specify which D1 database to take, you can specify it manually:
Using the [Cloudflare Adapter](/integration/cloudflare), you can choose to use a D1 database binding. To do so, you only need to add a D1 database to your `wrangler.toml` and it'll pick up automatically.
To manually specify which D1 database to take, you can specify it explicitly:
```ts
import { serve, d1 } from "bknd/adapter/cloudflare";
@@ -73,17 +70,19 @@ To use bknd with Postgres, you need to install the `@bknd/postgres` package. You
npm install @bknd/postgres
```
This package uses `pg` under the hood. If you'd like to see `postgres` or any other flavor, please create an [issue on Github](https://github.com/bknd-io/bknd/issues/new).
You can connect to your Postgres database using `pg` or `postgres` dialects. Additionally, you may also define your custom connection.
To establish a connection to your database, you can use any connection options available on the [`pg`](https://node-postgres.com/apis/client) package. Here is a quick example using the [Node.js Adapter](http://localhost:3000/integration/node):
#### Using `pg`
To establish a connection to your database, you can use any connection options available on the [`pg`](https://node-postgres.com/apis/client) package.
```js
import { serve } from "bknd/adapter/node";
import { PostgresConnection } from "@bknd/postgres";
import { pg } from "@bknd/postgres";
/** @type {import("bknd/adapter/node").NodeBkndConfig} */
const config = {
connection: new PostgresConnection({
connection: pg({
connectionString:
"postgresql://user:password@localhost:5432/database",
}),
@@ -92,6 +91,64 @@ const config = {
serve(config);
```
#### Using `postgres`
To establish a connection to your database, you can use any connection options available on the [`postgres`](https://github.com/porsager/postgres) package.
```js
import { serve } from "bknd/adapter/node";
import { postgresJs } from "@bknd/postgres";
serve({
connection: postgresJs("postgresql://user:password@localhost:5432/database"),
});
```
#### Using custom connection
Several Postgres hosting providers offer their own clients to connect to their database, e.g. suitable for serverless environments.
Example using `@neondatabase/serverless`:
```js
import { createCustomPostgresConnection } from "@bknd/postgres";
import { NeonDialect } from "kysely-neon";
const connection = createCustomPostgresConnection(NeonDialect)({
connectionString: process.env.NEON,
});
serve({
connection: connection,
});
```
Example using `@xata.io/client`:
```js
import { createCustomPostgresConnection } from "@bknd/postgres";
import { XataDialect } from "@xata.io/kysely";
import { buildClient } from "@xata.io/client";
const client = buildClient();
const xata = new client({
databaseURL: process.env.XATA_URL,
apiKey: process.env.XATA_API_KEY,
branch: process.env.XATA_BRANCH,
});
const connection = createCustomPostgresConnection(XataDialect, {
supports: {
batching: false,
},
})({ xata });
serve({
connection: connection,
});
```
### SQLocal
To use bknd with `sqlocal` for a offline expierence, you need to install the `@bknd/sqlocal` package. You can do so by running the following command:
@@ -138,7 +195,11 @@ const app = createApp({ connection })
## Initial Structure
To provide an initial database structure, you can pass `initialConfig` to the creation of an app. This will only be used if there isn't an existing configuration found in the database given. Here is a quick example:
```ts
<Note>
The initial structure is only respected if the database is empty! If you made updates, ensure to delete the database first, or perform updates through the Admin UI.
</Note>
```typescript
import { createApp } from "bknd";
import { em, entity, text, number } from "bknd/data";
@@ -193,13 +254,17 @@ Note that we didn't add relational fields directly to the entity, but instead de
</Note>
### Type completion
To get type completion, there are two options:
1. Use the CLI to [generate the types](/usage/cli#generating-types-types)
2. If you have an initial structure created with the prototype functions, you can extend the `DB` interface with your own schema.
All entity related functions use the types defined in `DB` from `bknd/core`. To get type completion, you can extend that interface with your own schema:
```ts
```typescript
import { em } from "bknd/data";
import { Api } from "bknd/client";
const schema = em({ /* ... */ });
const schema = em({ /* ... */ });
type Database = (typeof schema)["DB"];
declare module "bknd/core" {
@@ -217,10 +282,12 @@ The type completion is available for the API as well as all provided [React hook
To seed your database with initial data, you can pass a `seed` function to the configuration. It
provides the `ModuleBuildContext` as the first argument.
Note that the seed function will only be executed on app's first boot. If a configuration
already exists in the database, it will not be executed.
<Note>
Note that the seed function will only be executed on app's first boot. If a configuration
already exists in the database, it will not be executed.
</Note>
```ts
```typescript
import { createApp, type ModuleBuildContext } from "bknd";
const app = createApp({
+2 -2
View File
@@ -18,7 +18,7 @@ There is also a fourth option, which is running it inside a
Regardless of the method you choose, at the end all adapters come down to the actual
instantiation of the `App`, which in raw looks like this:
```ts
```typescript
import { createApp, type CreateAppConfig } from "bknd";
// create the app
@@ -38,7 +38,7 @@ implements the `Fetch` API.
## Configuration (`CreateAppConfig`)
The `CreateAppConfig` type is the main configuration object for the `createApp` function. It has
the following properties:
```ts
```typescript
import type { App, InitialModuleConfigs, ModuleBuildContext } from "bknd";
import type { Connection } from "bknd/data";
import type { Config } from "@libsql/client";
+3 -4
View File
@@ -9,11 +9,10 @@
},
"dependencies": {
"bknd": "file:../../app",
"kysely-d1": "^0.3.0"
"kysely-d1": "^0.4.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240620.0",
"typescript": "^5.5.3",
"wrangler": "^4.4.0"
"typescript": "^5.8.3",
"wrangler": "^4.19.1"
}
}
+2 -4
View File
@@ -1,10 +1,8 @@
/// <reference types="@cloudflare/workers-types" />
import { serve } from "bknd/adapter/cloudflare";
export default serve({
mode: "warm",
onBuilt: async (app) => {
app.modules.server.get("/custom", (c) => c.json({ hello: "world" }));
d1: {
session: true,
},
});
+1 -1
View File
@@ -5,7 +5,7 @@
"jsx": "react-jsx",
"module": "es2022",
"moduleResolution": "Bundler",
"types": ["@cloudflare/workers-types/2023-07-01"],
"types": ["./worker-configuration.d.ts"],
"resolveJsonModule": true,
"allowJs": true,
"checkJs": false,
+7 -11
View File
@@ -1,12 +1,8 @@
// Generated by Wrangler
// After adding bindings to `wrangler.toml`, regenerate this interface via `npm run cf-typegen`
interface Env {
DB_URL: string;
DB_TOKEN: string;
}
declare module "__STATIC_CONTENT_MANIFEST" {
const value: string;
export default value;
// placeholder, run generation again
declare namespace Cloudflare {
interface Env {
BUCKET: R2Bucket;
DB: D1Database;
}
}
interface Env extends Cloudflare.Env {}
+2 -2
View File
@@ -15,8 +15,8 @@
"d1_databases": [
{
"binding": "DB",
"database_name": "bknd-cf-example",
"database_id": "7ad67953-2bbf-47fc-8696-f4517dbfe674"
"database_name": "bknd-dev-weur",
"database_id": "81d8dfcc-4eaf-4453-8f0f-8f6d463fb867"
}
],
"r2_buckets": [
+2 -6
View File
@@ -21,7 +21,6 @@
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@clack/prompts": "^0.10.0",
"@cloudflare/workers-types": "^4.20240620.0",
"@tsconfig/strictest": "^2.0.5",
"@types/lodash-es": "^4.17.12",
"bun-types": "^1.1.18",
@@ -42,8 +41,5 @@
"engines": {
"node": ">=20.0.0"
},
"workspaces": [
"app",
"packages/*"
]
}
"workspaces": ["app", "packages/*"]
}
+75 -23
View File
@@ -1,5 +1,8 @@
# Postgres adapter for `bknd` (experimental)
This packages adds an adapter to use a Postgres database with [`bknd`](https://github.com/bknd-io/bknd). It is based on [`pg`](https://github.com/brianc/node-postgres) and the driver included in [`kysely`](https://github.com/kysely-org/kysely).
This packages adds an adapter to use a Postgres database with [`bknd`](https://github.com/bknd-io/bknd). It works with both `pg` and `postgres` drivers, and supports custom postgres connections.
* works with any Postgres database (tested with Supabase, Neon, Xata, and RDS)
* choose between `pg` and `postgres` drivers
* create custom postgres connections with any kysely postgres dialect
## Installation
Install the adapter with:
@@ -7,44 +10,93 @@ Install the adapter with:
npm install @bknd/postgres
```
## Usage
## Using `pg` driver
Install the [`pg`](https://github.com/brianc/node-postgres) driver with:
```bash
npm install pg
```
Create a connection:
```ts
import { PostgresConnection } from "@bknd/postgres";
import { pg } from "@bknd/postgres";
const connection = new PostgresConnection({
// accepts `pg` configuration
const connection = pg({
host: "localhost",
port: 5432,
user: "postgres",
password: "postgres",
database: "bknd",
database: "postgres",
});
// or with a connection string
const connection = pg({
connectionString: "postgres://postgres:postgres@localhost:5432/postgres",
});
```
Use the connection depending on which framework or runtime you are using. E.g., when using `createApp`, you can use the connection as follows:
## Using `postgres` driver
```ts
import { createApp } from "bknd";
import { PostgresConnection } from "@bknd/postgres";
const connection = new PostgresConnection();
const app = createApp({ connection });
Install the [`postgres`](https://github.com/porsager/postgres) driver with:
```bash
npm install postgres
```
Or if you're using it with a framework, say Next.js, you can add the connection object to where you're initializating the app:
Create a connection:
```ts
// e.g. in src/app/api/[[...bknd]]/route.ts
import { serve } from "bknd/adapter/nextjs";
import { PostgresConnection } from "@bknd/postgres";
import { postgresJs } from "@bknd/postgres";
const connection = new PostgresConnection();
const handler = serve({
connection
})
// ...
// accepts `postgres` configuration
const connection = postgresJs("postgres://postgres:postgres@localhost:5432/postgres");
```
For more information about how to integrate Next.js in general, check out the [Next.js documentation](https://docs.bknd.io/integration/nextjs).
## Using custom postgres dialects
You can create a custom kysely postgres dialect by using the `createCustomPostgresConnection` function.
```ts
import { createCustomPostgresConnection } from "@bknd/postgres";
const connection = createCustomPostgresConnection(MyDialect)({
// your custom dialect configuration
supports: {
batching: true
},
excludeTables: ["my_table"],
plugins: [new MyKyselyPlugin()],
});
```
### Custom `neon` connection
```typescript
import { createCustomPostgresConnection } from "@bknd/postgres";
import { NeonDialect } from "kysely-neon";
const connection = createCustomPostgresConnection(NeonDialect)({
connectionString: process.env.NEON,
});
```
### Custom `xata` connection
```typescript
import { createCustomPostgresConnection } from "@bknd/postgres";
import { XataDialect } from "@xata.io/kysely";
import { buildClient } from "@xata.io/client";
const client = buildClient();
const xata = new client({
databaseURL: process.env.XATA_URL,
apiKey: process.env.XATA_API_KEY,
branch: process.env.XATA_BRANCH,
});
const connection = createCustomPostgresConnection(XataDialect, {
supports: {
batching: false,
},
})({ xata });
```
+14
View File
@@ -0,0 +1,14 @@
import { serve } from "bknd/adapter/bun";
import { createCustomPostgresConnection } from "../src";
import { NeonDialect } from "kysely-neon";
const neon = createCustomPostgresConnection(NeonDialect);
export default serve({
connection: neon({
connectionString: process.env.NEON,
}),
// ignore this, it's only required within this repository
// because bknd is installed via "workspace:*"
distPath: "../../app/dist",
});
+24
View File
@@ -0,0 +1,24 @@
import { serve } from "bknd/adapter/bun";
import { createCustomPostgresConnection } from "../src";
import { XataDialect } from "@xata.io/kysely";
import { buildClient } from "@xata.io/client";
const client = buildClient();
const xata = new client({
databaseURL: process.env.XATA_URL,
apiKey: process.env.XATA_API_KEY,
branch: process.env.XATA_BRANCH,
});
const connection = createCustomPostgresConnection(XataDialect, {
supports: {
batching: false,
},
})({ xata });
export default serve({
connection,
// ignore this, it's only required within this repository
// because bknd is installed via "workspace:*"
distPath: "../../../app/dist",
});
+10 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@bknd/postgres",
"version": "0.0.1",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
@@ -17,15 +17,20 @@
"docker:start": "docker run --rm --name bknd-test-postgres -d -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres -e POSTGRES_DB=bknd -p 5430:5432 postgres:17",
"docker:stop": "docker stop bknd-test-postgres"
},
"dependencies": {
"optionalDependencies": {
"kysely": "^0.27.6",
"kysely-postgres-js": "^2.0.0",
"pg": "^8.14.0",
"kysely": "^0.27.6"
"postgres": "^3.4.7"
},
"devDependencies": {
"@types/bun": "^1.2.5",
"@types/node": "^22.13.10",
"@types/pg": "^8.11.11",
"@xata.io/client": "^0.0.0-next.v93343b9646f57a1e5c51c35eccf0767c2bb80baa",
"@xata.io/kysely": "^0.2.1",
"bknd": "workspace:*",
"kysely-neon": "^1.3.0",
"tsup": "^8.4.0",
"typescript": "^5.8.2"
},
@@ -33,10 +38,11 @@
"entry": ["src/index.ts"],
"format": ["esm"],
"target": "es2022",
"metafile": true,
"clean": true,
"minify": true,
"dts": true,
"external": ["bknd", "pg", "kysely"]
"external": ["bknd", "pg", "postgres", "kysely", "kysely-postgres-js"]
},
"files": ["dist", "README.md", "!*.map", "!metafile*.json"]
}
@@ -0,0 +1,32 @@
import { Kysely, PostgresDialect } from "kysely";
import { PostgresIntrospector } from "./PostgresIntrospector";
import { PostgresConnection, plugins } from "./PostgresConnection";
import { customIntrospector } from "bknd/data";
import $pg from "pg";
export type PgPostgresConnectionConfig = $pg.PoolConfig;
export class PgPostgresConnection extends PostgresConnection {
private pool: $pg.Pool;
constructor(config: PgPostgresConnectionConfig) {
const pool = new $pg.Pool(config);
const kysely = new Kysely({
dialect: customIntrospector(PostgresDialect, PostgresIntrospector, {
excludeTables: [],
}).create({ pool }),
plugins,
});
super(kysely);
this.pool = pool;
}
override async close(): Promise<void> {
await this.pool.end();
}
}
export function pg(config: PgPostgresConnectionConfig): PgPostgresConnection {
return new PgPostgresConnection(config);
}
+17 -37
View File
@@ -1,61 +1,45 @@
import { Connection, type FieldSpec, type SchemaResponse } from "bknd/data";
import { Connection, type DbFunctions, type FieldSpec, type SchemaResponse } from "bknd/data";
import {
ParseJSONResultsPlugin,
type ColumnDataType,
type ColumnDefinitionBuilder,
type DatabaseIntrospector,
Kysely,
ParseJSONResultsPlugin,
PostgresDialect,
type Kysely,
type KyselyPlugin,
type SelectQueryBuilder,
} from "kysely";
import { jsonArrayFrom, jsonBuildObject, jsonObjectFrom } from "kysely/helpers/postgres";
import pg from "pg";
import { PostgresIntrospector } from "./PostgresIntrospector";
export type PostgresConnectionConfig = pg.PoolConfig;
export type QB = SelectQueryBuilder<any, any, any>;
const plugins = [new ParseJSONResultsPlugin()];
export const plugins = [new ParseJSONResultsPlugin()];
class CustomPostgresDialect extends PostgresDialect {
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
return new PostgresIntrospector(db, {
excludeTables: [],
});
}
}
export class PostgresConnection extends Connection {
export abstract class PostgresConnection<DB = any> extends Connection<DB> {
protected override readonly supported = {
batching: true,
counts: true,
};
private pool: pg.Pool;
constructor(config: PostgresConnectionConfig) {
const pool = new pg.Pool(config);
const kysely = new Kysely({
dialect: new CustomPostgresDialect({
pool,
}),
plugins,
//log: ["query", "error"],
});
constructor(kysely: Kysely<DB>, fn?: Partial<DbFunctions>, _plugins?: KyselyPlugin[]) {
super(
kysely,
{
fn ?? {
jsonArrayFrom,
jsonBuildObject,
jsonObjectFrom,
},
plugins,
_plugins ?? plugins,
);
this.pool = pool;
}
override getFieldSchema(spec: FieldSpec): SchemaResponse {
this.validateFieldSpecType(spec.type);
let type: ColumnDataType = spec.primary ? "serial" : spec.type;
let type: ColumnDataType = spec.type;
if (spec.primary) {
if (spec.type === "integer") {
type = "serial";
}
}
switch (spec.type) {
case "blob":
@@ -90,10 +74,6 @@ export class PostgresConnection extends Connection {
];
}
override async close(): Promise<void> {
await this.pool.end();
}
protected override async batch<Queries extends QB[]>(
queries: [...Queries],
): Promise<{
@@ -0,0 +1,41 @@
import { Kysely } from "kysely";
import { PostgresIntrospector } from "./PostgresIntrospector";
import { PostgresConnection, plugins } from "./PostgresConnection";
import { customIntrospector } from "bknd/data";
import { PostgresJSDialect } from "kysely-postgres-js";
import $postgresJs, { type Sql, type Options, type PostgresType } from "postgres";
export type PostgresJsConfig = Options<Record<string, PostgresType>>;
export class PostgresJsConnection extends PostgresConnection {
private postgres: Sql;
constructor(opts: { postgres: Sql }) {
const kysely = new Kysely({
dialect: customIntrospector(PostgresJSDialect, PostgresIntrospector, {
excludeTables: [],
}).create({ postgres: opts.postgres }),
plugins,
});
super(kysely);
this.postgres = opts.postgres;
}
override async close(): Promise<void> {
await this.postgres.end();
}
}
export function postgresJs(
connectionString: string,
config?: PostgresJsConfig,
): PostgresJsConnection;
export function postgresJs(config: PostgresJsConfig): PostgresJsConnection;
export function postgresJs(
first: PostgresJsConfig | string,
second?: PostgresJsConfig,
): PostgresJsConnection {
const postgres = typeof first === "string" ? $postgresJs(first, second) : $postgresJs(first);
return new PostgresJsConnection({ postgres });
}
+43
View File
@@ -0,0 +1,43 @@
import type { Constructor } from "bknd/core";
import { customIntrospector, type DbFunctions } from "bknd/data";
import { Kysely, type Dialect, type KyselyPlugin } from "kysely";
import { plugins, PostgresConnection } from "./PostgresConnection";
import { PostgresIntrospector } from "./PostgresIntrospector";
export type CustomPostgresConnection = {
supports?: PostgresConnection["supported"];
fn?: Partial<DbFunctions>;
plugins?: KyselyPlugin[];
excludeTables?: string[];
};
export function createCustomPostgresConnection<
T extends Constructor<Dialect>,
C extends ConstructorParameters<T>[0],
>(
dialect: Constructor<Dialect>,
options?: CustomPostgresConnection,
): (config: C) => PostgresConnection<any> {
const supported = {
batching: true,
...((options?.supports ?? {}) as any),
};
return (config: C) =>
new (class extends PostgresConnection<any> {
protected override readonly supported = supported;
constructor(config: C) {
super(
new Kysely({
dialect: customIntrospector(dialect, PostgresIntrospector, {
excludeTables: options?.excludeTables ?? [],
}).create(config),
plugins: options?.plugins ?? plugins,
}),
options?.fn,
options?.plugins,
);
}
})(config);
}
+4 -1
View File
@@ -1,2 +1,5 @@
export { PostgresConnection, type PostgresConnectionConfig } from "./PostgresConnection";
export { pg, PgPostgresConnection, type PgPostgresConnectionConfig } from "./PgPostgresConnection";
export { PostgresIntrospector } from "./PostgresIntrospector";
export { PostgresConnection, type QB, plugins } from "./PostgresConnection";
export { postgresJs, PostgresJsConnection, type PostgresJsConfig } from "./PostgresJsConnection";
export { createCustomPostgresConnection } from "./custom";
-19
View File
@@ -1,19 +0,0 @@
import { describe, it, expect } from "bun:test";
import { PostgresConnection } from "../src";
import { createConnection, cleanDatabase } from "./setup";
describe(PostgresConnection, () => {
it("should connect to the database", async () => {
const connection = createConnection();
expect(await connection.ping()).toBe(true);
});
it("should clean the database", async () => {
const connection = createConnection();
await cleanDatabase(connection);
const tables = await connection.getIntrospector().getTables();
expect(tables).toEqual([]);
});
});
-113
View File
@@ -1,113 +0,0 @@
import { describe, it, expect, beforeAll, afterAll, afterEach } from "bun:test";
import { createApp } from "bknd";
import * as proto from "bknd/data";
import { createConnection, cleanDatabase } from "./setup";
import type { PostgresConnection } from "../src";
let connection: PostgresConnection;
beforeAll(async () => {
connection = createConnection();
await cleanDatabase(connection);
});
afterEach(async () => {
await cleanDatabase(connection);
});
afterAll(async () => {
await connection.close();
});
describe("integration", () => {
it("should create app and ping", async () => {
const app = createApp({
connection,
});
await app.build();
expect(app.version()).toBeDefined();
expect(await app.em.ping()).toBe(true);
});
it("should create a basic schema", async () => {
const schema = proto.em(
{
posts: proto.entity("posts", {
title: proto.text().required(),
content: proto.text(),
}),
comments: proto.entity("comments", {
content: proto.text(),
}),
},
(fns, s) => {
fns.relation(s.comments).manyToOne(s.posts);
fns.index(s.posts).on(["title"], true);
},
);
const app = createApp({
connection,
initialConfig: {
data: schema.toJSON(),
},
});
await app.build();
expect(app.em.entities.length).toBe(2);
expect(app.em.entities.map((e) => e.name)).toEqual(["posts", "comments"]);
const api = app.getApi();
expect(
(
await api.data.createMany("posts", [
{
title: "Hello",
content: "World",
},
{
title: "Hello 2",
content: "World 2",
},
])
).data,
).toEqual([
{
id: 1,
title: "Hello",
content: "World",
},
{
id: 2,
title: "Hello 2",
content: "World 2",
},
] as any);
// try to create an existing
expect(
(
await api.data.createOne("posts", {
title: "Hello",
})
).ok,
).toBe(false);
// add a comment to a post
await api.data.createOne("comments", {
content: "Hello",
posts_id: 1,
});
// and then query using a `with` property
const result = await api.data.readMany("posts", { with: ["comments"] });
expect(result.length).toBe(2);
expect(result[0].comments.length).toBe(1);
expect(result[0].comments[0].content).toBe("Hello");
expect(result[1].comments.length).toBe(0);
});
});
+16
View File
@@ -0,0 +1,16 @@
import { describe } from "bun:test";
import { pg } from "../src/PgPostgresConnection";
import { testSuite } from "./suite";
describe("pg", () => {
testSuite({
createConnection: () =>
pg({
host: "localhost",
port: 5430,
user: "postgres",
password: "postgres",
database: "bknd",
}),
});
});
+16
View File
@@ -0,0 +1,16 @@
import { describe } from "bun:test";
import { postgresJs } from "../src/PostgresJsConnection";
import { testSuite } from "./suite";
describe("postgresjs", () => {
testSuite({
createConnection: () =>
postgresJs({
host: "localhost",
port: 5430,
user: "postgres",
password: "postgres",
database: "bknd",
}),
});
});
-25
View File
@@ -1,25 +0,0 @@
import type { Kysely } from "kysely";
import { PostgresConnection, PostgresIntrospector, type PostgresConnectionConfig } from "../src";
export const info = {
host: "localhost",
port: 5430,
user: "postgres",
password: "postgres",
database: "bknd",
};
export function createConnection(config: PostgresConnectionConfig = {}) {
return new PostgresConnection({
...info,
...config,
});
}
export async function cleanDatabase(connection: PostgresConnection) {
const kysely = connection.kysely;
// drop all tables & create new schema
await kysely.schema.dropSchema("public").ifExists().cascade().execute();
await kysely.schema.createSchema("public").execute();
}
+197
View File
@@ -0,0 +1,197 @@
import { describe, beforeAll, afterAll, expect, it, afterEach } from "bun:test";
import type { PostgresConnection } from "../src";
import { createApp } from "bknd";
import * as proto from "bknd/data";
import { disableConsoleLog, enableConsoleLog } from "bknd/utils";
export type TestSuiteConfig = {
createConnection: () => InstanceType<typeof PostgresConnection>;
cleanDatabase?: (connection: InstanceType<typeof PostgresConnection>) => Promise<void>;
};
export async function defaultCleanDatabase(connection: InstanceType<typeof PostgresConnection>) {
const kysely = connection.kysely;
// drop all tables & create new schema
await kysely.schema.dropSchema("public").ifExists().cascade().execute();
await kysely.schema.createSchema("public").execute();
}
async function cleanDatabase(
connection: InstanceType<typeof PostgresConnection>,
config: TestSuiteConfig,
) {
if (config.cleanDatabase) {
await config.cleanDatabase(connection);
} else {
await defaultCleanDatabase(connection);
}
}
export function testSuite(config: TestSuiteConfig) {
beforeAll(() => disableConsoleLog(["log", "warn", "error"]));
afterAll(() => enableConsoleLog());
describe("base", () => {
it("should connect to the database", async () => {
const connection = config.createConnection();
expect(await connection.ping()).toBe(true);
});
it("should clean the database", async () => {
const connection = config.createConnection();
await cleanDatabase(connection, config);
const tables = await connection.getIntrospector().getTables();
expect(tables).toEqual([]);
});
});
describe("integration", () => {
let connection: PostgresConnection;
beforeAll(async () => {
connection = config.createConnection();
await cleanDatabase(connection, config);
});
afterEach(async () => {
await cleanDatabase(connection, config);
});
afterAll(async () => {
await connection.close();
});
it("should create app and ping", async () => {
const app = createApp({
connection,
});
await app.build();
expect(app.version()).toBeDefined();
expect(await app.em.ping()).toBe(true);
});
it("should create a basic schema", async () => {
const schema = proto.em(
{
posts: proto.entity("posts", {
title: proto.text().required(),
content: proto.text(),
}),
comments: proto.entity("comments", {
content: proto.text(),
}),
},
(fns, s) => {
fns.relation(s.comments).manyToOne(s.posts);
fns.index(s.posts).on(["title"], true);
},
);
const app = createApp({
connection,
initialConfig: {
data: schema.toJSON(),
},
});
await app.build();
expect(app.em.entities.length).toBe(2);
expect(app.em.entities.map((e) => e.name)).toEqual(["posts", "comments"]);
const api = app.getApi();
expect(
(
await api.data.createMany("posts", [
{
title: "Hello",
content: "World",
},
{
title: "Hello 2",
content: "World 2",
},
])
).data,
).toEqual([
{
id: 1,
title: "Hello",
content: "World",
},
{
id: 2,
title: "Hello 2",
content: "World 2",
},
] as any);
// try to create an existing
expect(
(
await api.data.createOne("posts", {
title: "Hello",
})
).ok,
).toBe(false);
// add a comment to a post
await api.data.createOne("comments", {
content: "Hello",
posts_id: 1,
});
// and then query using a `with` property
const result = await api.data.readMany("posts", { with: ["comments"] });
expect(result.length).toBe(2);
expect(result[0].comments.length).toBe(1);
expect(result[0].comments[0].content).toBe("Hello");
expect(result[1].comments.length).toBe(0);
});
it("should support uuid", async () => {
const schema = proto.em(
{
posts: proto.entity(
"posts",
{
title: proto.text().required(),
content: proto.text(),
},
{
primary_format: "uuid",
},
),
comments: proto.entity("comments", {
content: proto.text(),
}),
},
(fns, s) => {
fns.relation(s.comments).manyToOne(s.posts);
fns.index(s.posts).on(["title"], true);
},
);
const app = createApp({
connection,
initialConfig: {
data: schema.toJSON(),
},
});
await app.build();
const config = app.toJSON();
// @ts-expect-error
expect(config.data.entities?.posts.fields?.id.config?.format).toBe("uuid");
const em = app.em;
const mutator = em.mutator(em.entity("posts"));
const data = await mutator.insertOne({ title: "Hello", content: "World" });
expect(data.data.id).toBeString();
expect(String(data.data.id).length).toBe(36);
});
});
}