Compare commits

..

2 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
21 changed files with 153 additions and 139 deletions
+1 -5
View File
@@ -142,7 +142,6 @@ const adapters = {
}, },
nextjs: { nextjs: {
dir: path.join(basePath, "examples/nextjs"), dir: path.join(basePath, "examples/nextjs"),
env: "TEST_TIMEOUT=20000",
clean: async function () { clean: async function () {
const cwd = path.relative(process.cwd(), this.dir); const cwd = path.relative(process.cwd(), this.dir);
await $`cd ${cwd} && rm -rf .nextjs data.db`; await $`cd ${cwd} && rm -rf .nextjs data.db`;
@@ -196,8 +195,7 @@ async function testAdapter(name: keyof typeof adapters) {
console.log("proc:", proc.pid, "data:", c.cyan(data)); console.log("proc:", proc.pid, "data:", c.cyan(data));
//proc.kill();process.exit(0); //proc.kill();process.exit(0);
const add_env = "env" in config && config.env ? config.env : ""; await $`TEST_URL=${data} TEST_ADAPTER=${name} bun run test:e2e`;
await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
console.log("DONE!"); console.log("DONE!");
while (!proc.killed) { while (!proc.killed) {
@@ -207,8 +205,6 @@ 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) { if (process.env.TEST_ADAPTER) {
await testAdapter(process.env.TEST_ADAPTER as any); await testAdapter(process.env.TEST_ADAPTER as any);
} else { } else {
+3 -3
View File
@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"version": "0.14.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.", "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", "homepage": "https://bknd.io",
"repository": { "repository": {
@@ -70,7 +70,8 @@
"oauth4webapi": "^2.11.1", "oauth4webapi": "^2.11.1",
"object-path-immutable": "^4.1.2", "object-path-immutable": "^4.1.2",
"radix-ui": "^1.1.3", "radix-ui": "^1.1.3",
"swr": "^2.3.3" "swr": "^2.3.3",
"uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"@aws-sdk/client-s3": "^3.758.0", "@aws-sdk/client-s3": "^3.758.0",
@@ -120,7 +121,6 @@
"tsc-alias": "^1.8.11", "tsc-alias": "^1.8.11",
"tsup": "^8.4.0", "tsup": "^8.4.0",
"tsx": "^4.19.3", "tsx": "^4.19.3",
"uuid": "^11.1.0",
"vite": "^6.3.5", "vite": "^6.3.5",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.9", "vitest": "^3.0.9",
+1 -2
View File
@@ -3,7 +3,6 @@ import { defineConfig, devices } from "@playwright/test";
const baseUrl = process.env.TEST_URL || "http://localhost:28623"; const baseUrl = process.env.TEST_URL || "http://localhost:28623";
const startCommand = process.env.TEST_START_COMMAND || "bun run dev"; const startCommand = process.env.TEST_START_COMMAND || "bun run dev";
const autoStart = ["1", "true", undefined].includes(process.env.TEST_AUTO_START); 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({ export default defineConfig({
testMatch: "**/*.e2e-spec.ts", testMatch: "**/*.e2e-spec.ts",
@@ -13,7 +12,7 @@ export default defineConfig({
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
reporter: "html", reporter: "html",
timeout, timeout: 20000,
use: { use: {
baseURL: baseUrl, baseURL: baseUrl,
trace: "on-first-retry", trace: "on-first-retry",
+1 -1
View File
@@ -92,7 +92,7 @@ export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
args?: CfMakeConfigArgs<Env>, args?: CfMakeConfigArgs<Env>,
) { ) {
if (!media_registered) { if (!media_registered) {
registerMedia(args?.env as any); registerMedia(args as any);
media_registered = true; media_registered = true;
} }
@@ -22,6 +22,7 @@ export class D1Connection<
> extends SqliteConnection { > extends SqliteConnection {
protected override readonly supported = { protected override readonly supported = {
batching: true, batching: true,
counts: false,
}; };
constructor(private config: D1ConnectionConfig<DB>) { constructor(private config: D1ConnectionConfig<DB>) {
+15 -11
View File
@@ -1,17 +1,24 @@
import { serveStatic } from "@hono/node-server/serve-static"; 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 { 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 { registerLocalMediaAdapter } from "bknd/adapter/node";
import { devServerConfig } from "./dev-server-config"; import { devServerConfig } from "./dev-server-config";
import type { MiddlewareHandler } from "hono";
export type ViteEnv = NodeJS.ProcessEnv; 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( return html.replace(
"</head>", "</head>",
`<script type="module"> `<script type="module">
@@ -41,10 +48,7 @@ async function createApp<ViteEnv>(
mainPath: "/src/main.tsx", mainPath: "/src/main.tsx",
}, },
}, },
serveStatic: config.serveStatic || [ serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })],
"/assets/*",
serveStatic({ root: config.distPath ?? "./" }),
],
}, },
env, env,
opts, opts,
+1 -1
View File
@@ -316,7 +316,7 @@ export class DataController extends Controller {
return this.notFound(c); return this.notFound(c);
} }
const options = c.req.valid("query") as RepoQuery; 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 }); return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
}, },
+1
View File
@@ -82,6 +82,7 @@ export abstract class Connection<DB = any> {
kysely: Kysely<DB>; kysely: Kysely<DB>;
protected readonly supported = { protected readonly supported = {
batching: false, batching: false,
counts: true,
}; };
constructor( constructor(
@@ -3,6 +3,7 @@ import { Connection, type FieldSpec, type SchemaResponse } from "./Connection";
export class DummyConnection extends Connection { export class DummyConnection extends Connection {
protected override readonly supported = { protected override readonly supported = {
batching: true, batching: true,
counts: true,
}; };
constructor() { constructor() {
@@ -28,13 +28,13 @@ export class LibsqlConnection extends SqliteConnection {
private client: Client; private client: Client;
protected override readonly supported = { protected override readonly supported = {
batching: true, batching: true,
counts: false,
}; };
constructor(client: Client); constructor(client: Client);
constructor(credentials: LibSqlCredentials); constructor(credentials: LibSqlCredentials);
constructor(clientOrCredentials: Client | LibSqlCredentials) { constructor(clientOrCredentials: Client | LibSqlCredentials) {
let client: Client; let client: Client;
let batching_enabled = true;
if (clientOrCredentials && "url" in clientOrCredentials) { if (clientOrCredentials && "url" in clientOrCredentials) {
let { url, authToken, protocol } = clientOrCredentials; let { url, authToken, protocol } = clientOrCredentials;
if (protocol && LIBSQL_PROTOCOLS.includes(protocol)) { if (protocol && LIBSQL_PROTOCOLS.includes(protocol)) {
@@ -56,7 +56,6 @@ export class LibsqlConnection extends SqliteConnection {
super(kysely, {}, plugins); super(kysely, {}, plugins);
this.client = client; this.client = client;
this.supported.batching = batching_enabled;
} }
getClient(): Client { getClient(): Client {
+21
View File
@@ -27,6 +27,7 @@ export type RepositoryResponse<T = EntityData[]> = RepositoryRawResponse & {
data: T; data: T;
meta: { meta: {
items: number; items: number;
has_more?: boolean;
total?: number; total?: number;
count?: number; count?: number;
time?: number; time?: number;
@@ -61,6 +62,10 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
public entity: Entity, public entity: Entity,
protected options: RepositoryOptions = {}, protected options: RepositoryOptions = {},
) { ) {
this.options = {
...options,
includeCounts: options?.includeCounts ?? this.em.connection.supports("counts"),
};
this.emgr = options?.emgr ?? new EventManager(MutatorEvents); 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.limit) validated.limit = options.limit;
if (options.offset) validated.offset = options.offset; 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; 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> { protected async performQuery(qb: RepositoryQB): Promise<RepositoryResponse> {
const entity = this.entity; const entity = this.entity;
const compiled = qb.compile(); const compiled = qb.compile();
const payload = { const payload = {
@@ -444,6 +455,16 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
await this.triggerFindBefore(this.entity, options); await this.triggerFindBefore(this.entity, options);
const res = await this.performQuery(qb); 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); await this.triggerFindAfter(this.entity, options, res.data);
return res as any; return res as any;
+1 -1
View File
@@ -333,7 +333,7 @@ export class SchemaManager {
if (config.force) { if (config.force) {
try { try {
$console.debug("[SchemaManager]", sql); $console.log("[SchemaManager]", sql);
await qb.execute(); await qb.execute();
} catch (e) { } catch (e) {
throw new Error(`Failed to execute query: ${sql}: ${(e as any).message}`); throw new Error(`Failed to execute query: ${sql}: ${(e as any).message}`);
+1 -2
View File
@@ -51,7 +51,6 @@ export class Controller {
protected getEntitiesEnum(em: EntityManager<any>) { protected getEntitiesEnum(em: EntityManager<any>) {
const entities = em.entities.map((e) => e.name); const entities = em.entities.map((e) => e.name);
// @todo: current workaround to allow strings (sometimes building is not fast enough to get the entities) return entities.length > 0 ? s.string({ enum: entities }) : s.string();
return entities.length > 0 ? s.anyOf([s.string({ enum: entities }), s.string()]) : s.string();
} }
} }
+1 -1
View File
@@ -50,7 +50,7 @@ export class AdminController extends Controller {
} }
get basepath() { get basepath() {
return this.withAdminBasePath(); return this.options.basepath ?? "/";
} }
private withBasePath(route: string = "") { private withBasePath(route: string = "") {
+64 -39
View File
@@ -34,6 +34,7 @@ export type DataTableProps<Data> = {
checkable?: boolean; checkable?: boolean;
onClickRow?: (row: Data) => void; onClickRow?: (row: Data) => void;
onClickPage?: (page: number) => void; onClickPage?: (page: number) => void;
hasMore?: boolean;
total?: number; total?: number;
page?: number; page?: number;
perPage?: number; perPage?: number;
@@ -59,6 +60,7 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
onClickRow, onClickRow,
onClickPage, onClickPage,
onClickSort, onClickSort,
hasMore,
total, total,
sort, sort,
page = 1, page = 1,
@@ -75,7 +77,7 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
page = page || 1; page = page || 1;
const select = columns && columns.length > 0 ? columns : Object.keys(data?.[0] || {}); 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; const CellRender = renderValue || CellValue;
return ( return (
@@ -104,17 +106,15 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
<button <button
type="button" type="button"
className={twMerge( className={twMerge(
"py-1.5 rounded-md inline-flex flex-row justify-start items-center gap-1", "link hover:bg-primary/5 py-1.5 rounded-md inline-flex flex-row justify-start items-center gap-1",
onClickSort onClickSort ? "pl-2.5 pr-1" : "px-2.5",
? "link hover:bg-primary/5 pl-2.5 pr-1"
: "px-2.5",
)} )}
onClick={() => onClickSort?.(property)} onClick={() => onClickSort?.(property)}
> >
<span className="text-left text-nowrap whitespace-nowrap"> <span className="text-left text-nowrap whitespace-nowrap">
{label} {label}
</span> </span>
{(onClickSort || (sort && sort.by === property)) && ( {onClickSort && (
<SortIndicator sort={sort} field={property} /> <SortIndicator sort={sort} field={property} />
)} )}
</button> </button>
@@ -198,12 +198,14 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
</div> </div>
<div className="flex flex-row items-center justify-between"> <div className="flex flex-row items-center justify-between">
<div className="hidden md:flex text-primary/40"> <div className="hidden md:flex text-primary/40">
<TableDisplay {hasMore === undefined ? (
perPage={perPage} <TableDisplay
page={page} perPage={perPage}
items={data?.length || 0} page={page}
total={total} items={data?.length || 0}
/> total={total}
/>
) : null}
</div> </div>
<div className="flex flex-row gap-2 md:gap-10 items-center"> <div className="flex flex-row gap-2 md:gap-10 items-center">
{perPageOptions && ( {perPageOptions && (
@@ -222,11 +224,16 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
</div> </div>
)} )}
<div className="text-primary/40"> <div className="text-primary/40">
Page {page} of {pages} Page {page} {pages ? `of ${pages}` : ""}
</div> </div>
{onClickPage && ( {onClickPage && (
<div className="flex flex-row gap-1.5"> <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>
)} )}
</div> </div>
@@ -276,41 +283,59 @@ const TableDisplay = ({ perPage, page, items, total }) => {
return <>Showing 1 row</>; return <>Showing 1 row</>;
} }
return ( const text = `Showing ${perPage * (page - 1) + 1}-${perPage * (page - 1) + items}`;
<> if (total) {
Showing {perPage * (page - 1) + 1}-{perPage * (page - 1) + items} of {total} rows return `${text} of ${total} rows`;
</> }
);
return text;
}; };
type TableNavProps = { type TableNavProps = {
current: number; current: number;
total: number; total?: number;
hasMore?: boolean;
onClick?: (page: number) => void; 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 = [ const navMap = [
{ value: 1, Icon: TbChevronsLeft, disabled: current === 1 }, { value: 1, Icon: TbChevronsLeft, disabled: current === 1 },
{ value: current - 1, Icon: TbChevronLeft, disabled: current === 1 }, { value: current - 1, Icon: TbChevronLeft, disabled: current === 1 },
{ value: current + 1, Icon: TbChevronRight, disabled: current === total }, { value: current + 1, Icon: TbChevronRight, disabled: !hasMore },
{ value: total, Icon: TbChevronsRight, disabled: current === total }, {
value: _hasMore === undefined ? total : undefined,
Icon: TbChevronsRight,
disabled: !hasMore,
},
] as const; ] as const;
return navMap.map((nav, key) => ( return navMap.map((nav, key) => {
<button const page = nav.value;
role="button" if (page === undefined) return null;
type="button"
key={key} return (
disabled={nav.disabled} <button
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" role="button"
onClick={() => { type="button"
const page = nav.value; key={key}
const safePage = page < 1 ? 1 : page > total ? total : page; disabled={nav.disabled}
onClick?.(safePage); 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;
<nav.Icon /> onClick?.(safePage);
</button> }}
)); >
<nav.Icon />
</button>
);
});
}; };
+19 -31
View File
@@ -1,44 +1,32 @@
import { decodeSearch, encodeSearch, mergeObject } from "core/utils"; import { decodeSearch, encodeSearch, mergeObject, parseDecode } from "core/utils";
import { isEqual, transform } from "lodash-es"; import { isEqual, transform } from "lodash-es";
import { useLocation, useSearch as useWouterSearch } from "wouter"; import { useLocation, useSearch as useWouterSearch } from "wouter";
import { type s, parse } from "core/object/schema"; import { type s, parse, cloneSchema } 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>( export function useSearch<Schema extends s.TAnySchema = s.TAnySchema>(
schema: Schema, _schema: Schema,
options?: UseSearchOptions<Schema>, defaultValue?: Partial<s.StaticCoerced<Schema>>,
) { ) {
const schema = cloneSchema(_schema as any) as s.TSchema;
const searchString = useWouterSearch(); const searchString = useWouterSearch();
const [location, navigate] = useLocation(); const [location, navigate] = useLocation();
const [value, setValue] = useState<s.StaticCoerced<Schema>>( const initial = searchString.length > 0 ? decodeSearch(searchString) : (defaultValue ?? {});
options?.defaultValue ?? ({} as any), const value = parse(schema, initial, {
); withDefaults: true,
clone: true,
}) as s.StaticCoerced<Schema>;
const defaults = useMemo(() => { // @ts-ignore
return mergeObject( const _defaults = mergeObject(schema.template({ withOptional: true }), defaultValue ?? {});
// @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 { function set<Update extends Partial<s.StaticCoerced<Schema>>>(update: Update): void {
const search = getWithoutDefaults(Object.assign({}, value, update), defaults); // @ts-ignore
const prepared = options?.beforeEncode?.(search) ?? search; if (schema.validate(update).valid) {
const encoded = encodeSearch(prepared, { encode: false }); const search = getWithoutDefaults(mergeObject(value, update), _defaults);
navigate(location + (encoded.length > 0 ? "?" + encoded : "")); const encoded = encodeSearch(search, { encode: false });
navigate(location + (encoded.length > 0 ? "?" + encoded : ""));
}
} }
return { return {
+7 -18
View File
@@ -257,20 +257,15 @@ function EntityDetailInner({
}) { }) {
const other = relation.other(entity); const other = relation.other(entity);
const [navigate] = useNavigate(); const [navigate] = useNavigate();
const [search, setSearch] = useState({
const search = {
select: other.entity.getSelect(undefined, "table"), select: other.entity.getSelect(undefined, "table"),
sort: other.entity.getDefaultSort(),
limit: 10, limit: 10,
offset: 0, offset: 0,
}); };
// @todo: add custom key for invalidation // @todo: add custom key for invalidation
const $q = useApiQuery( const $q = useApiQuery((api) =>
(api) => api.data.readManyByReference(entity.name, id, other.reference, search), api.data.readManyByReference(entity.name, id, other.reference, search),
{
keepPreviousData: true,
revalidateOnFocus: true,
},
); );
function handleClickRow(row: Record<string, any>) { function handleClickRow(row: Record<string, any>) {
@@ -305,17 +300,11 @@ function EntityDetailInner({
select={search.select} select={search.select}
data={$q.data ?? null} data={$q.data ?? null}
entity={other.entity} entity={other.entity}
sort={search.sort}
onClickRow={handleClickRow} onClickRow={handleClickRow}
onClickNew={handleClickNew} onClickNew={handleClickNew}
page={Math.floor(search.offset / search.limit) + 1} page={1}
total={$q.data?.body?.meta?.count ?? 1} total={$q.data?.body?.meta?.count ?? 1}
onClickPage={(page) => { /*onClickPage={handleClickPage}*/
setSearch((s) => ({
...s,
offset: (page - 1) * s.limit,
}));
}}
/> />
</div> </div>
); );
+3 -13
View File
@@ -35,19 +35,8 @@ export function DataEntityList({ params }) {
useBrowserTitle(["Data", entity?.label ?? params.entity]); useBrowserTitle(["Data", entity?.label ?? params.entity]);
const [navigate] = useNavigate(); const [navigate] = useNavigate();
const search = useSearch(searchSchema, { const search = useSearch(searchSchema, {
defaultValue: { select: entity.getSelect(undefined, "table"),
select: entity.getSelect(undefined, "table"), sort: entity.getDefaultSort(),
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( const $q = useApiQuery(
@@ -142,6 +131,7 @@ export function DataEntityList({ params }) {
perPage={search.value.perPage} perPage={search.value.perPage}
perPageOptions={PER_PAGE_OPTIONS} perPageOptions={PER_PAGE_OPTIONS}
total={meta?.count} total={meta?.count}
hasMore={meta?.has_more}
onClickPage={handleClickPage} onClickPage={handleClickPage}
onClickPerPage={handleClickPerPage} onClickPerPage={handleClickPerPage}
/> />
+7 -7
View File
@@ -114,12 +114,12 @@ Example using `@neondatabase/serverless`:
import { createCustomPostgresConnection } from "@bknd/postgres"; import { createCustomPostgresConnection } from "@bknd/postgres";
import { NeonDialect } from "kysely-neon"; import { NeonDialect } from "kysely-neon";
const neon = createCustomPostgresConnection(NeonDialect); const connection = createCustomPostgresConnection(NeonDialect)({
connectionString: process.env.NEON,
});
serve({ serve({
connection: neon({ connection: connection,
connectionString: process.env.NEON,
}),
}); });
``` ```
@@ -137,14 +137,14 @@ const xata = new client({
branch: process.env.XATA_BRANCH, branch: process.env.XATA_BRANCH,
}); });
const xataConnection = createCustomPostgresConnection(XataDialect, { const connection = createCustomPostgresConnection(XataDialect, {
supports: { supports: {
batching: false, batching: false,
}, },
}); })({ xata });
serve({ serve({
connection: xataConnection({ xata }), connection: connection,
}); });
``` ```
+2 -2
View File
@@ -5,6 +5,6 @@ import react from "@astrojs/react";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
output: "server", output: "hybrid",
integrations: [react()], integrations: [react()]
}); });
@@ -16,6 +16,7 @@ export const plugins = [new ParseJSONResultsPlugin()];
export abstract class PostgresConnection<DB = any> extends Connection<DB> { export abstract class PostgresConnection<DB = any> extends Connection<DB> {
protected override readonly supported = { protected override readonly supported = {
batching: true, batching: true,
counts: true,
}; };
constructor(kysely: Kysely<DB>, fn?: Partial<DbFunctions>, _plugins?: KyselyPlugin[]) { constructor(kysely: Kysely<DB>, fn?: Partial<DbFunctions>, _plugins?: KyselyPlugin[]) {