mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 906f80b11e | |||
| 7a3691a1a7 |
@@ -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>) {
|
||||||
|
|||||||
@@ -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 });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 (
|
||||||
@@ -196,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">
|
||||||
|
{hasMore === undefined ? (
|
||||||
<TableDisplay
|
<TableDisplay
|
||||||
perPage={perPage}
|
perPage={perPage}
|
||||||
page={page}
|
page={page}
|
||||||
items={data?.length || 0}
|
items={data?.length || 0}
|
||||||
total={total}
|
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 && (
|
||||||
@@ -220,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>
|
||||||
@@ -274,28 +283,46 @@ 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) => {
|
||||||
|
const page = nav.value;
|
||||||
|
if (page === undefined) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
role="button"
|
role="button"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -303,12 +330,12 @@ const TableNav: React.FC<TableNavProps> = ({ current, total, onClick }: TableNav
|
|||||||
disabled={nav.disabled}
|
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"
|
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={() => {
|
onClick={() => {
|
||||||
const page = nav.value;
|
|
||||||
const safePage = page < 1 ? 1 : page > total ? total : page;
|
const safePage = page < 1 ? 1 : page > total ? total : page;
|
||||||
onClick?.(safePage);
|
onClick?.(safePage);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<nav.Icon />
|
<nav.Icon />
|
||||||
</button>
|
</button>
|
||||||
));
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -131,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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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[]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user