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
9 changed files with 89 additions and 37 deletions
@@ -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>) {
+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;
+42 -15
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 (
@@ -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[]) {