mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c4aac8843 | |||
| d5bb6ffa61 | |||
| fc513bb413 | |||
| 4162b9878a | |||
| c75f8d0937 | |||
| 88419548c7 |
+5
-1
@@ -142,6 +142,7 @@ const adapters = {
|
||||
},
|
||||
nextjs: {
|
||||
dir: path.join(basePath, "examples/nextjs"),
|
||||
env: "TEST_TIMEOUT=20000",
|
||||
clean: async function () {
|
||||
const cwd = path.relative(process.cwd(), this.dir);
|
||||
await $`cd ${cwd} && rm -rf .nextjs data.db`;
|
||||
@@ -195,7 +196,8 @@ async function testAdapter(name: keyof typeof adapters) {
|
||||
console.log("proc:", proc.pid, "data:", c.cyan(data));
|
||||
//proc.kill();process.exit(0);
|
||||
|
||||
await $`TEST_URL=${data} TEST_ADAPTER=${name} bun run test:e2e`;
|
||||
const add_env = "env" in config && config.env ? config.env : "";
|
||||
await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
|
||||
console.log("DONE!");
|
||||
|
||||
while (!proc.killed) {
|
||||
@@ -205,6 +207,8 @@ async function testAdapter(name: keyof typeof adapters) {
|
||||
}
|
||||
}
|
||||
|
||||
// run with: TEST_ADAPTER=astro bun run e2e/adapters.ts
|
||||
// (modify `test:e2e` to `test:e2e:ui` to see the UI)
|
||||
if (process.env.TEST_ADAPTER) {
|
||||
await testAdapter(process.env.TEST_ADAPTER as any);
|
||||
} else {
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"bin": "./dist/cli/index.js",
|
||||
"version": "0.14.0-rc.2",
|
||||
"version": "0.14.0",
|
||||
"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": {
|
||||
@@ -70,8 +70,7 @@
|
||||
"oauth4webapi": "^2.11.1",
|
||||
"object-path-immutable": "^4.1.2",
|
||||
"radix-ui": "^1.1.3",
|
||||
"swr": "^2.3.3",
|
||||
"uuid": "^11.1.0"
|
||||
"swr": "^2.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-s3": "^3.758.0",
|
||||
@@ -121,6 +120,7 @@
|
||||
"tsc-alias": "^1.8.11",
|
||||
"tsup": "^8.4.0",
|
||||
"tsx": "^4.19.3",
|
||||
"uuid": "^11.1.0",
|
||||
"vite": "^6.3.5",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.9",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { defineConfig, devices } from "@playwright/test";
|
||||
const baseUrl = process.env.TEST_URL || "http://localhost:28623";
|
||||
const startCommand = process.env.TEST_START_COMMAND || "bun run dev";
|
||||
const autoStart = ["1", "true", undefined].includes(process.env.TEST_AUTO_START);
|
||||
const timeout = process.env.TEST_TIMEOUT ? Number.parseInt(process.env.TEST_TIMEOUT) : 5000;
|
||||
|
||||
export default defineConfig({
|
||||
testMatch: "**/*.e2e-spec.ts",
|
||||
@@ -12,7 +13,7 @@ export default defineConfig({
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: "html",
|
||||
timeout: 20000,
|
||||
timeout,
|
||||
use: {
|
||||
baseURL: baseUrl,
|
||||
trace: "on-first-retry",
|
||||
|
||||
@@ -92,7 +92,7 @@ export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
args?: CfMakeConfigArgs<Env>,
|
||||
) {
|
||||
if (!media_registered) {
|
||||
registerMedia(args as any);
|
||||
registerMedia(args?.env as any);
|
||||
media_registered = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import { serveStatic } from "@hono/node-server/serve-static";
|
||||
import {
|
||||
type DevServerOptions,
|
||||
default as honoViteDevServer,
|
||||
} from "@hono/vite-dev-server";
|
||||
import { type DevServerOptions, default as honoViteDevServer } from "@hono/vite-dev-server";
|
||||
import type { App } from "bknd";
|
||||
import {
|
||||
type RuntimeBkndConfig,
|
||||
createRuntimeApp,
|
||||
type FrameworkOptions,
|
||||
} from "bknd/adapter";
|
||||
import { type RuntimeBkndConfig, createRuntimeApp, type FrameworkOptions } from "bknd/adapter";
|
||||
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
||||
import { devServerConfig } from "./dev-server-config";
|
||||
import type { MiddlewareHandler } from "hono";
|
||||
|
||||
export type ViteEnv = NodeJS.ProcessEnv;
|
||||
export type ViteBkndConfig<Env = ViteEnv> = RuntimeBkndConfig<Env> & {};
|
||||
export type ViteBkndConfig<Env = ViteEnv> = RuntimeBkndConfig<Env> & {
|
||||
serveStatic?: false | MiddlewareHandler;
|
||||
};
|
||||
|
||||
export function addViteScript(
|
||||
html: string,
|
||||
addBkndContext: boolean = true,
|
||||
) {
|
||||
export function addViteScript(html: string, addBkndContext: boolean = true) {
|
||||
return html.replace(
|
||||
"</head>",
|
||||
`<script type="module">
|
||||
@@ -48,7 +41,10 @@ async function createApp<ViteEnv>(
|
||||
mainPath: "/src/main.tsx",
|
||||
},
|
||||
},
|
||||
serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })],
|
||||
serveStatic: config.serveStatic || [
|
||||
"/assets/*",
|
||||
serveStatic({ root: config.distPath ?? "./" }),
|
||||
],
|
||||
},
|
||||
env,
|
||||
opts,
|
||||
|
||||
@@ -333,7 +333,7 @@ export class SchemaManager {
|
||||
|
||||
if (config.force) {
|
||||
try {
|
||||
$console.log("[SchemaManager]", sql);
|
||||
$console.debug("[SchemaManager]", sql);
|
||||
await qb.execute();
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to execute query: ${sql}: ${(e as any).message}`);
|
||||
|
||||
@@ -51,6 +51,7 @@ export class Controller {
|
||||
|
||||
protected getEntitiesEnum(em: EntityManager<any>) {
|
||||
const entities = em.entities.map((e) => e.name);
|
||||
return entities.length > 0 ? s.string({ enum: entities }) : s.string();
|
||||
// @todo: current workaround to allow strings (sometimes building is not fast enough to get the entities)
|
||||
return entities.length > 0 ? s.anyOf([s.string({ enum: entities }), s.string()]) : s.string();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export class AdminController extends Controller {
|
||||
}
|
||||
|
||||
get basepath() {
|
||||
return this.options.basepath ?? "/";
|
||||
return this.withAdminBasePath();
|
||||
}
|
||||
|
||||
private withBasePath(route: string = "") {
|
||||
|
||||
@@ -104,15 +104,17 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
|
||||
<button
|
||||
type="button"
|
||||
className={twMerge(
|
||||
"link hover:bg-primary/5 py-1.5 rounded-md inline-flex flex-row justify-start items-center gap-1",
|
||||
onClickSort ? "pl-2.5 pr-1" : "px-2.5",
|
||||
"py-1.5 rounded-md inline-flex flex-row justify-start items-center gap-1",
|
||||
onClickSort
|
||||
? "link hover:bg-primary/5 pl-2.5 pr-1"
|
||||
: "px-2.5",
|
||||
)}
|
||||
onClick={() => onClickSort?.(property)}
|
||||
>
|
||||
<span className="text-left text-nowrap whitespace-nowrap">
|
||||
{label}
|
||||
</span>
|
||||
{onClickSort && (
|
||||
{(onClickSort || (sort && sort.by === property)) && (
|
||||
<SortIndicator sort={sort} field={property} />
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
import { decodeSearch, encodeSearch, mergeObject, parseDecode } from "core/utils";
|
||||
import { decodeSearch, encodeSearch, mergeObject } from "core/utils";
|
||||
import { isEqual, transform } from "lodash-es";
|
||||
import { useLocation, useSearch as useWouterSearch } from "wouter";
|
||||
import { type s, parse, cloneSchema } from "core/object/schema";
|
||||
import { type s, parse } from "core/object/schema";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
export type UseSearchOptions<Schema extends s.TAnySchema = s.TAnySchema> = {
|
||||
defaultValue?: Partial<s.StaticCoerced<Schema>>;
|
||||
beforeEncode?: (search: Partial<s.StaticCoerced<Schema>>) => object;
|
||||
};
|
||||
|
||||
// @todo: migrate to Typebox
|
||||
export function useSearch<Schema extends s.TAnySchema = s.TAnySchema>(
|
||||
_schema: Schema,
|
||||
defaultValue?: Partial<s.StaticCoerced<Schema>>,
|
||||
schema: Schema,
|
||||
options?: UseSearchOptions<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 ?? {});
|
||||
const value = parse(schema, initial, {
|
||||
withDefaults: true,
|
||||
clone: true,
|
||||
}) as s.StaticCoerced<Schema>;
|
||||
const [value, setValue] = useState<s.StaticCoerced<Schema>>(
|
||||
options?.defaultValue ?? ({} as any),
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
const _defaults = mergeObject(schema.template({ withOptional: true }), defaultValue ?? {});
|
||||
const defaults = useMemo(() => {
|
||||
return mergeObject(
|
||||
// @ts-ignore
|
||||
schema.template({ withOptional: true }),
|
||||
options?.defaultValue ?? {},
|
||||
);
|
||||
}, [JSON.stringify({ schema, dflt: options?.defaultValue })]);
|
||||
|
||||
useEffect(() => {
|
||||
const initial =
|
||||
searchString.length > 0 ? decodeSearch(searchString) : (options?.defaultValue ?? {});
|
||||
const v = parse(schema, Object.assign({}, defaults, initial)) as any;
|
||||
setValue(v);
|
||||
}, [searchString, JSON.stringify(options?.defaultValue), location]);
|
||||
|
||||
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 : ""));
|
||||
}
|
||||
const search = getWithoutDefaults(Object.assign({}, value, update), defaults);
|
||||
const prepared = options?.beforeEncode?.(search) ?? search;
|
||||
const encoded = encodeSearch(prepared, { encode: false });
|
||||
navigate(location + (encoded.length > 0 ? "?" + encoded : ""));
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -257,15 +257,20 @@ function EntityDetailInner({
|
||||
}) {
|
||||
const other = relation.other(entity);
|
||||
const [navigate] = useNavigate();
|
||||
|
||||
const search = {
|
||||
const [search, setSearch] = useState({
|
||||
select: other.entity.getSelect(undefined, "table"),
|
||||
sort: other.entity.getDefaultSort(),
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
};
|
||||
});
|
||||
|
||||
// @todo: add custom key for invalidation
|
||||
const $q = useApiQuery((api) =>
|
||||
api.data.readManyByReference(entity.name, id, other.reference, search),
|
||||
const $q = useApiQuery(
|
||||
(api) => api.data.readManyByReference(entity.name, id, other.reference, search),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
revalidateOnFocus: true,
|
||||
},
|
||||
);
|
||||
|
||||
function handleClickRow(row: Record<string, any>) {
|
||||
@@ -300,11 +305,17 @@ function EntityDetailInner({
|
||||
select={search.select}
|
||||
data={$q.data ?? null}
|
||||
entity={other.entity}
|
||||
sort={search.sort}
|
||||
onClickRow={handleClickRow}
|
||||
onClickNew={handleClickNew}
|
||||
page={1}
|
||||
page={Math.floor(search.offset / search.limit) + 1}
|
||||
total={$q.data?.body?.meta?.count ?? 1}
|
||||
/*onClickPage={handleClickPage}*/
|
||||
onClickPage={(page) => {
|
||||
setSearch((s) => ({
|
||||
...s,
|
||||
offset: (page - 1) * s.limit,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -35,8 +35,19 @@ export function DataEntityList({ params }) {
|
||||
useBrowserTitle(["Data", entity?.label ?? params.entity]);
|
||||
const [navigate] = useNavigate();
|
||||
const search = useSearch(searchSchema, {
|
||||
select: entity.getSelect(undefined, "table"),
|
||||
sort: entity.getDefaultSort(),
|
||||
defaultValue: {
|
||||
select: entity.getSelect(undefined, "table"),
|
||||
sort: entity.getDefaultSort(),
|
||||
},
|
||||
beforeEncode: (v) => {
|
||||
if ("sort" in v && v.sort) {
|
||||
return {
|
||||
...v,
|
||||
sort: `${v.sort.dir === "asc" ? "" : "-"}${v.sort.by}`,
|
||||
};
|
||||
}
|
||||
return v;
|
||||
},
|
||||
});
|
||||
|
||||
const $q = useApiQuery(
|
||||
|
||||
@@ -114,12 +114,12 @@ Example using `@neondatabase/serverless`:
|
||||
import { createCustomPostgresConnection } from "@bknd/postgres";
|
||||
import { NeonDialect } from "kysely-neon";
|
||||
|
||||
const connection = createCustomPostgresConnection(NeonDialect)({
|
||||
connectionString: process.env.NEON,
|
||||
});
|
||||
const neon = createCustomPostgresConnection(NeonDialect);
|
||||
|
||||
serve({
|
||||
connection: connection,
|
||||
connection: neon({
|
||||
connectionString: process.env.NEON,
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -137,14 +137,14 @@ const xata = new client({
|
||||
branch: process.env.XATA_BRANCH,
|
||||
});
|
||||
|
||||
const connection = createCustomPostgresConnection(XataDialect, {
|
||||
const xataConnection = createCustomPostgresConnection(XataDialect, {
|
||||
supports: {
|
||||
batching: false,
|
||||
},
|
||||
})({ xata });
|
||||
});
|
||||
|
||||
serve({
|
||||
connection: connection,
|
||||
connection: xataConnection({ xata }),
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -5,6 +5,6 @@ import react from "@astrojs/react";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
output: "hybrid",
|
||||
integrations: [react()]
|
||||
output: "server",
|
||||
integrations: [react()],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user