forked from Yara724/api
Fix SMS, and added pagination+sorting+searching for GETs
This commit is contained in:
51
src/helpers/list-query-v2.spec.ts
Normal file
51
src/helpers/list-query-v2.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { applyListQueryV2 } from "./list-query-v2";
|
||||
|
||||
describe("applyListQueryV2", () => {
|
||||
const rows = [
|
||||
{ publicId: "B100", createdAt: "2026-01-01", status: "OPEN" },
|
||||
{ publicId: "A200", createdAt: "2026-02-01", status: "DONE" },
|
||||
{ publicId: "C050", createdAt: "2026-03-01", status: "OPEN" },
|
||||
];
|
||||
|
||||
const getters = {
|
||||
publicId: (r: (typeof rows)[0]) => r.publicId,
|
||||
createdAt: (r: (typeof rows)[0]) => r.createdAt,
|
||||
status: (r: (typeof rows)[0]) => r.status,
|
||||
};
|
||||
|
||||
it("returns all rows when no pagination query", () => {
|
||||
const r = applyListQueryV2(rows, getters, {});
|
||||
expect(r.total).toBe(3);
|
||||
expect(r.list).toHaveLength(3);
|
||||
expect(r.page).toBeUndefined();
|
||||
});
|
||||
|
||||
it("filters by search on publicId", () => {
|
||||
const r = applyListQueryV2(rows, getters, { search: "a200" });
|
||||
expect(r.total).toBe(1);
|
||||
expect(r.list[0].publicId).toBe("A200");
|
||||
});
|
||||
|
||||
it("sorts by publicId ascending", () => {
|
||||
const r = applyListQueryV2(rows, getters, {
|
||||
sortBy: "publicId",
|
||||
sortOrder: "asc",
|
||||
});
|
||||
expect(r.list.map((x) => x.publicId)).toEqual(["A200", "B100", "C050"]);
|
||||
});
|
||||
|
||||
it("paginates when page and limit are set", () => {
|
||||
const r = applyListQueryV2(rows, getters, {
|
||||
sortBy: "publicId",
|
||||
sortOrder: "asc",
|
||||
page: 2,
|
||||
limit: 1,
|
||||
});
|
||||
expect(r.total).toBe(3);
|
||||
expect(r.list).toHaveLength(1);
|
||||
expect(r.list[0].publicId).toBe("B100");
|
||||
expect(r.page).toBe(2);
|
||||
expect(r.limit).toBe(1);
|
||||
expect(r.totalPages).toBe(3);
|
||||
});
|
||||
});
|
||||
116
src/helpers/list-query-v2.ts
Normal file
116
src/helpers/list-query-v2.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||
|
||||
const MAX_LIMIT = 100;
|
||||
const DEFAULT_LIMIT_WHEN_PAGE_SET = 20;
|
||||
|
||||
export type ListItemGetters<T> = {
|
||||
publicId: (row: T) => string | undefined;
|
||||
createdAt: (row: T) => Date | string | number | undefined;
|
||||
requestNo?: (row: T) => string | undefined;
|
||||
status: (row: T) => string | undefined;
|
||||
/** Extra strings to match against `search` (e.g. claimRequestId, vehicle). */
|
||||
searchExtras?: (row: T) => string[];
|
||||
};
|
||||
|
||||
function normalizeSearch(raw?: string): string | undefined {
|
||||
const t = raw?.trim().toLowerCase();
|
||||
return t || undefined;
|
||||
}
|
||||
|
||||
function toTime(v: Date | string | number | undefined): number {
|
||||
if (v == null) return 0;
|
||||
const n = v instanceof Date ? v.getTime() : new Date(v as string | number).getTime();
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter, sort, and optionally paginate in-memory list results.
|
||||
* Pagination applies only when `query.page` or `query.limit` is set.
|
||||
*/
|
||||
export function applyListQueryV2<T>(
|
||||
rows: T[],
|
||||
getters: ListItemGetters<T>,
|
||||
query: ListQueryV2Dto,
|
||||
): {
|
||||
list: T[];
|
||||
total: number;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
totalPages?: number;
|
||||
} {
|
||||
const search = normalizeSearch(query.search);
|
||||
let filtered = rows;
|
||||
if (search) {
|
||||
filtered = rows.filter((row) => {
|
||||
const chunks: string[] = [];
|
||||
const pid = getters.publicId(row);
|
||||
if (pid) chunks.push(pid);
|
||||
const rno = getters.requestNo?.(row);
|
||||
if (rno) chunks.push(rno);
|
||||
const st = getters.status(row);
|
||||
if (st) chunks.push(st);
|
||||
const extras = getters.searchExtras?.(row) ?? [];
|
||||
for (const e of extras) {
|
||||
if (e) chunks.push(e);
|
||||
}
|
||||
const hay = chunks.join(" ").toLowerCase();
|
||||
return hay.includes(search);
|
||||
});
|
||||
}
|
||||
|
||||
const sortBy = query.sortBy ?? "createdAt";
|
||||
let sortOrder = query.sortOrder;
|
||||
if (!sortOrder) {
|
||||
sortOrder =
|
||||
sortBy === "publicId" || sortBy === "requestNo" ? "asc" : "desc";
|
||||
}
|
||||
const dir = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
const sorted = [...filtered].sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case "publicId": {
|
||||
const va = String(getters.publicId(a) ?? "");
|
||||
const vb = String(getters.publicId(b) ?? "");
|
||||
return dir * va.localeCompare(vb, "en", { numeric: true, sensitivity: "base" });
|
||||
}
|
||||
case "requestNo": {
|
||||
const va = String(
|
||||
getters.requestNo?.(a) ?? getters.publicId(a) ?? "",
|
||||
);
|
||||
const vb = String(
|
||||
getters.requestNo?.(b) ?? getters.publicId(b) ?? "",
|
||||
);
|
||||
return dir * va.localeCompare(vb, "en", { numeric: true, sensitivity: "base" });
|
||||
}
|
||||
case "status": {
|
||||
const va = String(getters.status(a) ?? "");
|
||||
const vb = String(getters.status(b) ?? "");
|
||||
return dir * va.localeCompare(vb, "en", { sensitivity: "base" });
|
||||
}
|
||||
case "createdAt":
|
||||
default: {
|
||||
const ta = toTime(getters.createdAt(a));
|
||||
const tb = toTime(getters.createdAt(b));
|
||||
return dir * (ta - tb);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const total = sorted.length;
|
||||
const wantsPage = query.page != null;
|
||||
const wantsLimit = query.limit != null;
|
||||
if (!wantsPage && !wantsLimit) {
|
||||
return { list: sorted, total };
|
||||
}
|
||||
|
||||
const limit = Math.min(
|
||||
MAX_LIMIT,
|
||||
Math.max(1, query.limit ?? (wantsPage ? DEFAULT_LIMIT_WHEN_PAGE_SET : MAX_LIMIT)),
|
||||
);
|
||||
const page = Math.max(1, query.page ?? 1);
|
||||
const skip = (page - 1) * limit;
|
||||
const list = sorted.slice(skip, skip + limit);
|
||||
const totalPages = Math.max(1, Math.ceil(total / limit));
|
||||
|
||||
return { list, total, page, limit, totalPages };
|
||||
}
|
||||
Reference in New Issue
Block a user