-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCustomSort.ts
More file actions
164 lines (146 loc) · 4.44 KB
/
Copy pathCustomSort.ts
File metadata and controls
164 lines (146 loc) · 4.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/**
* **CustomSort** - Limited sortable fields per endpoint
*
* Instead of letting the API consumer sort by any field (full diversity),
* the backend restricts which fields can be sorted. This is crucial when:
* - Fields require expensive DB operations (computed fields, joins)
* - Sorting by certain fields would expose internal structure
* - Different endpoints expose different subsets of the same resource
*
* The consumer defines which fields matter to them, the endpoint decides
* which of those it can actually sort by.
*/
import type { MultiSort } from "./MultiSort"
export enum SortDirection {
ASC = "asc",
DESC = "desc",
}
/**
* Define allowed sortable fields for an endpoint.
* Maps frontend-friendly names to actual backend properties.
*/
export interface SortDefinition {
// Frontend key => backend property path
[key: string]: string
}
export interface CustomSortField {
field: keyof SortDefinition | string // Consumer only knows the frontend key
direction: SortDirection
}
/**
* Example: Users endpoint allows sorting by predefined fields only
*/
export const UsersSortDefinition: SortDefinition = {
status: "status", // simple field
createdAt: "createdAt",
name: "profile.name", // nested field
email: "email",
// NOT exposing: "password", "internalId", etc.
}
/**
* Example: Orders endpoint with fewer exposed fields
*/
export const OrdersSortDefinition: SortDefinition = {
id: "id",
createdAt: "createdAt",
total: "totals.grand", // computed/nested
status: "status",
// Customer name is exposed but computed
customerName: "customer.name",
}
/**
* Validate and resolve frontend keys to backend properties.
* Returns fields that are allowed, ignores unknown ones (safe approach).
*/
export function resolveCustomSort(
sorts: CustomSortField[],
definition: SortDefinition,
): Array<{ field: string; direction: SortDirection }> {
return sorts
.filter((sort) => sort.field in definition)
.map((sort) => ({
field: definition[sort.field as string],
direction: sort.direction,
}))
}
/**
* Example usage:
*
* Frontend sends:
* ```ts
* const userSort: CustomSortField[] = [
* { field: "status", direction: "asc" },
* { field: "createdAt", direction: "desc" },
* { field: "password", direction: "asc" }, // Not in definition, ignored
* ]
* ```
*
* Backend resolves:
* ```ts
* const resolved = resolveCustomSort(userSort, UsersSortDefinition)
* // => [
* // { field: "status", direction: "asc" },
* // { field: "createdAt", direction: "desc" },
* // ]
* ```
*
* Then builds query string as with MultiSort.buildStackedSort(resolved)
*/
/**
* Stricter validation: throw error if unknown field is requested (fail‑fast).
* Use this if you want to catch frontend bugs immediately.
*/
export function resolveCustomSortStrict(
sorts: CustomSortField[],
definition: SortDefinition,
): Array<{ field: string; direction: SortDirection }> {
const unknownFields = sorts
.map((s) => s.field)
.filter((field) => !(field in definition))
if (unknownFields.length > 0) {
throw new Error(
`Invalid sort fields: ${unknownFields.join(", ")}. ` +
`Allowed: ${Object.keys(definition).join(", ")}`,
)
}
return sorts.map((sort) => ({
field: definition[sort.field as string],
direction: sort.direction,
}))
}
/**
* Build a customizable endpoint that enforces sort restrictions.
* This is a facade pattern - the restriction is transparent to the caller
* but enforced on the API boundary.
*/
export class CustomSortedEndpoint {
constructor(
private apiPath: string,
private sortDefinition: SortDefinition,
) { }
buildQuery(
sorts?: CustomSortField[],
options: { strict?: boolean } = {},
): URLSearchParams {
const params = new URLSearchParams()
if (!sorts || sorts.length === 0) {
return params
}
const resolved = options.strict
? resolveCustomSortStrict(sorts, this.sortDefinition)
: resolveCustomSort(sorts, this.sortDefinition)
// Use stacked format for clarity
for (const sort of resolved) {
params.append("sortBy", sort.field)
params.append("sortOrder", sort.direction)
}
return params
}
}
// Example:
// const usersEndpoint = new CustomSortedEndpoint("/api/users", UsersSortDefinition)
// const query = usersEndpoint.buildQuery([
// { field: "status", direction: "asc" },
// { field: "createdAt", direction: "desc" },
// ])
// => URLSearchParams with sortBy and sortOrder