1
10
11import type { SortSpec } from "@factory/shared/dto";
12
13import type { RecordRow, PropertyInfo } from "../values/types.js";
14import { codec } from "../values/registry.js";
15
16export const MAX_SORT_KEYS = 5;
17
18export interface SortKey extends SortSpec {
19 nullOrder?: "last" | "first";
20}
21
22export class SortValidationError extends Error {
23 constructor(message: string) {
24 super(message);
25 this.name = "SortValidationError";
26 }
27}
28
29export function validateSortKeys(
30 keys: readonly SortKey[],
31 properties: Map<string, PropertyInfo>,
32): void {
33 if (keys.length > MAX_SORT_KEYS)
34 throw new SortValidationError(`at most ${MAX_SORT_KEYS} sort keys`);
35 const seen = new Set<string>();
36 for (const k of keys) {
37 if (!properties.has(k.propertyId))
38 throw new SortValidationError(`unknown sort property: ${k.propertyId}`);
39 if (seen.has(k.propertyId))
40 throw new SortValidationError(`duplicate sort property: ${k.propertyId}`);
41 seen.add(k.propertyId);
42 }
43}
44
45function hasValue(v: unknown): boolean {
46 if (v === undefined || v === null) return false;
47 if (Array.isArray(v)) return v.length > 0;
48 if (typeof v === "string") return v.length > 0;
49 return true;
50}
51
52export function sortRecords<T extends RecordRow>(
53 rows: T[],
54 keys: readonly SortKey[],
55 properties: Map<string, PropertyInfo>,
56 validate = true,
57): T[] {
58 if (validate) validateSortKeys(keys, properties);
59 const out = rows.slice();
60 out.sort((a, b) => {
61 for (const key of keys) {
62 const property = properties.get(key.propertyId);
63 if (!property) continue;
64 const c = codec(property.type);
65 const av = a.values[key.propertyId];
66 const bv = b.values[key.propertyId];
67 const aNull = !hasValue(av);
68 const bNull = !hasValue(bv);
69 const nullOrder = key.nullOrder ?? "last";
70 if (aNull && bNull) continue;
71 if (aNull) return nullOrder === "last" ? 1 : -1;
72 if (bNull) return nullOrder === "last" ? -1 : 1;
73 let r = c.compare(av, bv, property.config);
74 if (key.direction === "desc") r = -r;
75 if (r !== 0) return r;
76 }
77
78 if (a.position !== b.position) return a.position < b.position ? -1 : 1;
79 return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
80 });
81 return out;
82}
83