-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathBackendHandling.ts
More file actions
281 lines (235 loc) · 7.75 KB
/
Copy pathBackendHandling.ts
File metadata and controls
281 lines (235 loc) · 7.75 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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
/**
* **Backend Handling** - Parsing and processing different sort query formats
*
* The backend needs to handle various serialization formats from different clients.
* This file shows examples for popular backend frameworks.
*/
import type { SortDirection, SortField, SortRecord } from "./MultiSort"
/**
* ============================================================================
* NODEJS / EXPRESS WITH `qs` LIBRARY
* ============================================================================
*/
// Recommended approach: use `qs` to parse automatically
// npm install qs @types/qs
// Express middleware example:
/*
import qs from 'qs'
app.get('/api/users', (req, res) => {
// Parse query string with qs (handles all formats)
const query = qs.parse(req.url.split('?')[1])
// query.sort can be:
// - { status: 'asc', createdAt: 'desc' } (bracket or object notation)
// - ['status:asc', 'createdAt:desc'] (stacked pairs)
// - 'status:asc,createdAt:desc' (compact pairs)
const sorts = normalizeSorts(query.sort)
// => SortField[]
})
function normalizeSorts(sort: unknown): SortField[] {
if (!sort) return []
// If object: { field: direction, ... }
if (typeof sort === 'object' && !Array.isArray(sort)) {
return Object.entries(sort as Record<string, unknown>).map(([field, direction]) => ({
field,
direction: (direction as string).toLowerCase() as SortDirection,
}))
}
// If array of pairs: ['field:asc', 'status:desc']
if (Array.isArray(sort)) {
return sort
.map(s => {
const [field, direction] = String(s).split(':')
return { field, direction: (direction || 'asc') as SortDirection }
})
.filter(s => s.field)
}
// If string: single pair or comma-separated (shouldn't happen with qs)
if (typeof sort === 'string') {
return sort
.split(',')
.map(s => {
const [field, direction] = s.split(':')
return { field, direction: (direction || 'asc') as SortDirection }
})
.filter(s => s.field)
}
return []
}
*/
/**
* ============================================================================
* PYTHON / FLASK
* ============================================================================
*/
// Python example (pseudocode):
/*
from flask import Flask, request
from urllib.parse import parse_qs
@app.route('/api/users')
def list_users():
# parse_qs returns lists for repeated params
query = parse_qs(request.query_string.decode())
# Different formats:
# Compact: ?sort=status:asc,createdAt:desc
# query['sort'] = ['status:asc,createdAt:desc']
#
# Stacked: ?sort=status:asc&sort=createdAt:desc
# query['sort'] = ['status:asc', 'createdAt:desc']
#
# Bracket: ?sort[status]=asc&sort[createdAt]=desc
# query['sort[status]'] = ['asc']
# query['sort[createdAt]'] = ['desc']
sorts = normalize_sorts(query.get('sort', []))
return fetch_users(sorts)
def normalize_sorts(sort_values):
if not sort_values:
return []
sorts = []
# Check if first value contains colons (pair format)
if sort_values and ':' in sort_values[0]:
# Could be compact (comma-separated) or stacked
for val in sort_values:
# Handle both "field:direction" and "field1:dir1,field2:dir2"
for pair in val.split(','):
if ':' in pair:
field, direction = pair.split(':', 1)
sorts.append({'field': field, 'direction': direction})
return sorts
*/
/**
* ============================================================================
* DENO / FRESH / OAK
* ============================================================================
*/
// Deno example (pseudocode):
/*
import { parse } from 'https://deno.land/std@0.208.0/url/parse.ts'
export const handler: Handlers = {
GET(req, _ctx) {
const url = new URL(req.url)
const sortParam = url.searchParams.get('sort')
// If using qs library equivalent for Deno:
// const query = parse(url.search)
const sorts = normalizeSorts(sortParam)
// Process sorts
}
}
function normalizeSorts(sort: string | null): SortField[] {
if (!sort) return []
// Handle comma-separated pairs
return sort
.split(',')
.map(pair => {
const [field, direction] = pair.split(':')
return { field, direction: (direction || 'asc') as SortDirection }
})
.filter(s => s.field)
}
*/
/**
* ============================================================================
* DATABASE QUERY BUILDING
* ============================================================================
*
* Once normalized to SortField[], use with your ORM/query builder.
*/
// TypeORM / TypeORM example:
/*
async function listUsers(sorts: SortField[]): Promise<User[]> {
let query = User.createQueryBuilder('user')
// Build ORDER BY clauses
for (const sort of sorts) {
const [alias, ...fieldPath] = sort.field.split('.')
const column = fieldPath.join('.')
// Validate field is in sort definition (CustomSort approach)
if (!ALLOWED_SORT_FIELDS.includes(sort.field)) {
continue // Skip unauthorized fields
}
query = query.addOrderBy(
`${alias}.${column}`,
sort.direction.toUpperCase()
)
}
return query.getMany()
}
const ALLOWED_SORT_FIELDS = ['status', 'createdAt', 'name']
*/
// Prisma example:
/*
async function listUsers(sorts: SortField[]) {
const orderBy = sorts
.filter(s => ALLOWED_SORT_FIELDS.includes(s.field))
.map(s => ({
[s.field]: s.direction
}))
return prisma.user.findMany({
orderBy: orderBy.length > 0 ? orderBy : undefined,
})
}
const ALLOWED_SORT_FIELDS = ['status', 'createdAt', 'name']
*/
// Raw SQL example:
/*
async function listUsers(sorts: SortField[]): Promise<User[]> {
let query = 'SELECT * FROM users'
if (sorts.length > 0) {
const orderClauses = sorts
.filter(s => isValidField(s.field))
.map(s => `${s.field} ${s.direction.toUpperCase()}`)
if (orderClauses.length > 0) {
query += ' ORDER BY ' + orderClauses.join(', ')
}
}
return db.query(query)
}
function isValidField(field: string): boolean {
const ALLOWED = ['status', 'created_at', 'name']
// SQL injection protection: whitelist fields
return ALLOWED.includes(field.toLowerCase())
}
*/
/**
* ============================================================================
* VALIDATION AND SECURITY CONSIDERATIONS
* ============================================================================
*
* 1. **Always whitelist allowed fields**
* Never directly insert sort fields into WHERE/ORDER BY clauses.
* Use CustomSort definitions or explicit field lists.
*
* 2. **Validate direction values**
* Ensure direction is 'asc' or 'desc', normalize to uppercase/lowercase
* as your database requires.
*
* 3. **Handle nested fields carefully**
* If allowing dot-chained paths like "user.createdAt", validate
* that the relationship exists and is readable by the user.
*
* 4. **Set limits**
* Don't allow sorting by unlimited fields; enforce a maximum
* (e.g., max 3 sort fields) to prevent expensive queries.
*
* 5. **Consider expensive operations**
* Some fields may require JOINs or computation. Restrict these
* or add query optimization hints (indexes, preloading).
*/
// Example validation function:
/*
function validateSorts(
sorts: SortField[],
allowedFields: string[],
maxSorts = 3
): SortField[] {
if (sorts.length > maxSorts) {
console.warn(`Too many sort fields (${sorts.length}), limiting to ${maxSorts}`)
sorts = sorts.slice(0, maxSorts)
}
return sorts.filter(sort => {
const isAllowed = allowedFields.includes(sort.field)
if (!isAllowed) {
console.warn(`Unauthorized sort field: ${sort.field}`)
}
return isAllowed
})
}
*/