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:
68
gerbil-manager-web/src/api/gridify.ts
Normal file
68
gerbil-manager-web/src/api/gridify.ts
Normal 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}` : ''
|
||||
}
|
||||
95
gerbil-manager-web/src/api/types.ts
Normal file
95
gerbil-manager-web/src/api/types.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* API DTO types — mirror the backend (DATA-1/DATA-2) contract.
|
||||
*
|
||||
* Code is English/camelCase (matching the DTOs); German is UI-only (de.ts).
|
||||
* Enums are string unions matching the C# member names verbatim (assumes
|
||||
* JsonStringEnumConverter on the backend — confirmed with DATA-2).
|
||||
*/
|
||||
|
||||
/** Gerbil.Gender — C# enum { unknown, male, female }. */
|
||||
export type Gender = 'unknown' | 'male' | 'female'
|
||||
export const GENDERS: Gender[] = ['unknown', 'male', 'female']
|
||||
|
||||
/** Gerbil.Status — C# enum { Active, Deceased, GivenAway }. */
|
||||
export type GerbilStatus = 'Active' | 'Deceased' | 'GivenAway'
|
||||
export const GERBIL_STATUSES: GerbilStatus[] = ['Active', 'Deceased', 'GivenAway']
|
||||
|
||||
/** ISO date string "YYYY-MM-DD" (maps to C# DateOnly). */
|
||||
export type DateOnlyString = string
|
||||
|
||||
export interface Gerbil {
|
||||
id: string
|
||||
name: string
|
||||
gender: Gender
|
||||
status: GerbilStatus
|
||||
dateOfBirth: DateOnlyString | null
|
||||
dateOfDeath: DateOnlyString | null
|
||||
causeOfDeath: string | null
|
||||
goHomeDate: DateOnlyString | null
|
||||
litterId: string | null
|
||||
enclosureId: string | null
|
||||
colorVarietyId: string | null
|
||||
originContactId: string | null
|
||||
receiverContactId: string | null
|
||||
/** Compact GEN-1 genotype string, e.g. "Aa CC Dd EE GG Pp Spsp rere" (or "?"-wildcards). */
|
||||
genotype: string | null
|
||||
notes: string | null
|
||||
}
|
||||
|
||||
/** Payload for POST /gerbils. */
|
||||
export interface CreateGerbil {
|
||||
name: string
|
||||
gender: Gender
|
||||
status?: GerbilStatus
|
||||
dateOfBirth?: DateOnlyString | null
|
||||
dateOfDeath?: DateOnlyString | null
|
||||
causeOfDeath?: string | null
|
||||
goHomeDate?: DateOnlyString | null
|
||||
litterId?: string | null
|
||||
enclosureId?: string | null
|
||||
colorVarietyId?: string | null
|
||||
originContactId?: string | null
|
||||
receiverContactId?: string | null
|
||||
genotype?: string | null
|
||||
notes?: string | null
|
||||
}
|
||||
|
||||
/** Payload for PUT /gerbils/{id} (all optional / partial update). */
|
||||
export type UpdateGerbil = Partial<CreateGerbil>
|
||||
|
||||
export interface ColorVariety {
|
||||
id: string
|
||||
name: string
|
||||
canonicalGenotype: string | null
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
export interface Enclosure {
|
||||
id: string
|
||||
name: string
|
||||
notes: string | null
|
||||
}
|
||||
|
||||
export interface Contact {
|
||||
id: string
|
||||
name: string
|
||||
contactInfo: string | null
|
||||
notes: string | null
|
||||
}
|
||||
|
||||
export interface Litter {
|
||||
id: string
|
||||
name: string
|
||||
date: DateOnlyString
|
||||
totalBorn: number | null
|
||||
fatherId: string | null
|
||||
motherId: string | null
|
||||
}
|
||||
|
||||
/** Paged list envelope returned by Gridify list endpoints. */
|
||||
export interface Paged<T> {
|
||||
items: T[]
|
||||
totalCount: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
Reference in New Issue
Block a user