FEAT-1: add Gridify query builder + API DTO types

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 23:50:30 +02:00
parent 3629e89e87
commit e04b69ce49
2 changed files with 163 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
/**
* Gridify query-string builder.
*
* The backend list endpoints accept Gridify query params: `filter`, `orderBy`,
* `page`, `pageSize`. This module turns typed UI filter state into a Gridify
* `filter` string and assembles the query string.
*
* Gridify filter syntax (subset we use):
* field==value equals
* field=*value* contains (append /i for case-insensitive)
* a==1,b==2 AND (comma)
* a==1|b==2 OR (pipe)
* Special characters in values (comma, pipe, parens, =, <, >, !, asterisk,
* backslash) must be escaped with a leading backslash.
*/
export interface GridifyQuery {
/** Gridify filter expression, already composed. */
filter?: string
/** Gridify orderBy expression, e.g. "name" or "dateOfBirth desc". */
orderBy?: string
/** 1-based page number. */
page?: number
pageSize?: number
}
/** Escape a value for use inside a Gridify filter expression. */
export function escapeGridifyValue(value: string): string {
return value.replace(/([,|()=<>!*\\])/g, '\\$1')
}
export type GridifyOp = '==' | '!=' | '>' | '<' | '>=' | '<=' | 'contains'
export interface FilterCondition {
field: string
op: GridifyOp
value: string | number | boolean
/** Case-insensitive match (appends /i). Only meaningful for string ops. */
caseInsensitive?: boolean
}
/** Build a single Gridify condition, e.g. a name-contains or `status==Active`. */
export function condition(c: FilterCondition): string {
const escaped = escapeGridifyValue(String(c.value))
if (c.op === 'contains') {
// Gridify "contains" operator is =* ; default to case-insensitive.
const suffix = c.caseInsensitive === false ? '' : '/i'
return `${c.field}=*${escaped}*${suffix}`
}
const suffix = c.caseInsensitive ? '/i' : ''
return `${c.field}${c.op}${escaped}${suffix}`
}
/** Join conditions with AND (comma). Falsy/empty conditions are dropped. */
export function andFilter(...conditions: Array<string | null | undefined | false>): string {
return conditions.filter((c): c is string => Boolean(c)).join(',')
}
/** Assemble the URL query string ("?...") from a GridifyQuery. */
export function toQueryString(q: GridifyQuery): string {
const params = new URLSearchParams()
if (q.filter) params.set('filter', q.filter)
if (q.orderBy) params.set('orderBy', q.orderBy)
if (q.page != null) params.set('page', String(q.page))
if (q.pageSize != null) params.set('pageSize', String(q.pageSize))
const s = params.toString()
return s ? `?${s}` : ''
}