simplesvelte 2.5.1 → 2.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +227 -214
- package/dist/AG_GRID_SERVER_API.md +373 -373
- package/dist/Grid.svelte +168 -168
- package/dist/Input.svelte +145 -145
- package/dist/Label.svelte +43 -43
- package/dist/Modal.svelte +39 -39
- package/dist/Select.svelte +697 -696
- package/dist/TextArea.svelte +45 -45
- package/dist/styles.css +15 -15
- package/package.json +73 -69
|
@@ -1,373 +1,373 @@
|
|
|
1
|
-
# AG Grid Server-Side Row Model (SSRM) - Server API
|
|
2
|
-
|
|
3
|
-
Clean, database-agnostic API for implementing AG Grid's Server-Side Row Model on the backend.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
This library provides a simple, strongly-typed interface for handling AG Grid SSRM requests on the server side. It works with any backend data source: Prisma, raw SQL, MongoDB, REST APIs, etc.
|
|
8
|
-
|
|
9
|
-
## Key Features
|
|
10
|
-
|
|
11
|
-
- ✅ **Database Agnostic** - Works with any data source
|
|
12
|
-
- ✅ **Type Safe** - Full TypeScript support with generics
|
|
13
|
-
- ✅ **Simple API** - Just implement `fetch` and `count`
|
|
14
|
-
- ✅ **Computed Fields** - Easy handling of virtual/calculated fields
|
|
15
|
-
- ✅ **Grouping Support** - Built-in row grouping support
|
|
16
|
-
- ✅ **Filter Transforms** - Automatic filter/sort transformation
|
|
17
|
-
- ✅ **Zero Boilerplate** - Clean, declarative configuration
|
|
18
|
-
|
|
19
|
-
## Quick Start
|
|
20
|
-
|
|
21
|
-
### 1. Basic Implementation (Prisma)
|
|
22
|
-
|
|
23
|
-
```typescript
|
|
24
|
-
import { createAGGridQuery } from '$lib/ag-grid'
|
|
25
|
-
import { query } from '$app/server'
|
|
26
|
-
import { DB } from '$lib/prisma.server'
|
|
27
|
-
|
|
28
|
-
export const getUsersPaginated = query(agGridRequestSchema, async (request) => {
|
|
29
|
-
return await createAGGridQuery({
|
|
30
|
-
async fetch(params) {
|
|
31
|
-
return await DB.user.findMany({
|
|
32
|
-
where: params.where,
|
|
33
|
-
orderBy: params.orderBy,
|
|
34
|
-
skip: params.skip,
|
|
35
|
-
take: params.take,
|
|
36
|
-
})
|
|
37
|
-
},
|
|
38
|
-
async count(params) {
|
|
39
|
-
return await DB.user.count({ where: params.where })
|
|
40
|
-
},
|
|
41
|
-
defaultSort: { createdAt: 'desc' },
|
|
42
|
-
})(request)
|
|
43
|
-
})
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
### 2. With Nested Relations (Auto-Handled!)
|
|
47
|
-
|
|
48
|
-
```typescript
|
|
49
|
-
export const getInterventionsPaginated = query(agGridRequestSchema, async (request) => {
|
|
50
|
-
return await createAGGridQuery({
|
|
51
|
-
async fetch(params) {
|
|
52
|
-
return await DB.intervention.findMany({
|
|
53
|
-
where: params.where,
|
|
54
|
-
orderBy: params.orderBy,
|
|
55
|
-
skip: params.skip,
|
|
56
|
-
take: params.take,
|
|
57
|
-
include: { location: true }, // Include the relation
|
|
58
|
-
})
|
|
59
|
-
},
|
|
60
|
-
async count(params) {
|
|
61
|
-
return await DB.intervention.count({ where: params.where })
|
|
62
|
-
},
|
|
63
|
-
// No computedFields needed! Just use field: 'location.name' in column definitions
|
|
64
|
-
// The system automatically:
|
|
65
|
-
// - Handles text filters: where.location.name contains 'X'
|
|
66
|
-
// - Handles set filters: where.location.name in [...]
|
|
67
|
-
// - Handles sorting: orderBy: { location: { name: 'asc' } }
|
|
68
|
-
// - Handles grouping: where.location.name = 'groupKey'
|
|
69
|
-
// - AG Grid displays nested values natively with dot notation
|
|
70
|
-
})(request)
|
|
71
|
-
})
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
**Column Definition:**
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
const columnDefs = [
|
|
78
|
-
{ field: 'id' },
|
|
79
|
-
{
|
|
80
|
-
field: 'location.name', // ✨ Use dot notation directly!
|
|
81
|
-
headerName: 'Location',
|
|
82
|
-
filter: 'agTextColumnFilter',
|
|
83
|
-
},
|
|
84
|
-
{ field: 'dateOfEngagement', filter: 'agDateColumnFilter' },
|
|
85
|
-
]
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### 3. With Complex Computed Fields
|
|
89
|
-
|
|
90
|
-
For truly computed/calculated values, provide custom handlers:
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
computedFields: [
|
|
94
|
-
{
|
|
95
|
-
columnId: 'weekEnding',
|
|
96
|
-
valueGetter: (record) => {
|
|
97
|
-
// Calculate week ending date from dateOfEngagement
|
|
98
|
-
const date = new Date(record.dateOfEngagement)
|
|
99
|
-
const day = date.getDay()
|
|
100
|
-
date.setDate(date.getDate() + (7 - day))
|
|
101
|
-
return date.toISOString().split('T')[0]
|
|
102
|
-
},
|
|
103
|
-
filterHandler: (filterValue, where) => {
|
|
104
|
-
// Custom filter logic for week ending
|
|
105
|
-
const filter = filterValue as { dateFrom?: string }
|
|
106
|
-
if (filter.dateFrom) {
|
|
107
|
-
const targetDate = new Date(filter.dateFrom)
|
|
108
|
-
const weekStart = new Date(targetDate)
|
|
109
|
-
weekStart.setDate(targetDate.getDate() - 6)
|
|
110
|
-
where.dateOfEngagement = { gte: weekStart, lte: targetDate }
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
]
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
**Column Definition:**
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
const columnDefs = [
|
|
121
|
-
{
|
|
122
|
-
field: 'weekEnding', // Custom computed field
|
|
123
|
-
headerName: 'Week Ending',
|
|
124
|
-
filter: 'agDateColumnFilter',
|
|
125
|
-
},
|
|
126
|
-
]
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### 4. Client-Side Usage
|
|
130
|
-
|
|
131
|
-
```svelte
|
|
132
|
-
<script lang="ts">
|
|
133
|
-
import { getUsersPaginated } from './user.remote'
|
|
134
|
-
import { createAGGridDatasource, createRemoteFetcher } from '$lib/ag-grid'
|
|
135
|
-
import { AgGridSvelte } from 'ag-grid-svelte'
|
|
136
|
-
|
|
137
|
-
let gridApi: GridApi
|
|
138
|
-
|
|
139
|
-
const columnDefs = [
|
|
140
|
-
{ field: 'id' },
|
|
141
|
-
{ field: 'name', filter: 'agTextColumnFilter' },
|
|
142
|
-
{ field: 'email', filter: 'agTextColumnFilter' },
|
|
143
|
-
// For nested relations, use dot notation directly:
|
|
144
|
-
{ field: 'profile.avatar', headerName: 'Avatar' },
|
|
145
|
-
{ field: 'department.name', headerName: 'Department', filter: 'agTextColumnFilter' },
|
|
146
|
-
]
|
|
147
|
-
|
|
148
|
-
function onGridReady(params: any) {
|
|
149
|
-
gridApi = params.api
|
|
150
|
-
|
|
151
|
-
const datasource = createAGGridDatasource(createRemoteFetcher(getUsersPaginated))
|
|
152
|
-
|
|
153
|
-
gridApi.setGridOption('serverSideDatasource', datasource)
|
|
154
|
-
}
|
|
155
|
-
</script>
|
|
156
|
-
|
|
157
|
-
<div class="ag-theme-alpine" style="height: 600px;">
|
|
158
|
-
<AgGridSvelte {columnDefs} rowModelType="serverSide" pagination={true} paginationPageSize={100} {onGridReady} />
|
|
159
|
-
</div>
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
## API Reference
|
|
163
|
-
|
|
164
|
-
### `createAGGridQuery(config)`
|
|
165
|
-
|
|
166
|
-
Main function to create a server-side query handler.
|
|
167
|
-
|
|
168
|
-
**Parameters:**
|
|
169
|
-
|
|
170
|
-
- `config: AGGridQueryConfig<TRecord, TWhereInput>`
|
|
171
|
-
- `fetch: (params) => Promise<TRecord[]>` - Function to fetch data rows
|
|
172
|
-
- `count: (params) => Promise<number>` - Function to count total rows
|
|
173
|
-
- `computedFields?: ComputedField[]` - Optional computed field configurations
|
|
174
|
-
- `defaultSort?: Record<string, 'asc' | 'desc'>` - Optional default sort
|
|
175
|
-
- `transformWhere?: (where, request) => TWhereInput` - Optional WHERE clause transformer
|
|
176
|
-
|
|
177
|
-
**Returns:** `(request: AGGridRequest) => Promise<AGGridResponse<TRecord>>`
|
|
178
|
-
|
|
179
|
-
### `AGGridQueryParams`
|
|
180
|
-
|
|
181
|
-
The params object passed to `fetch` and `count` functions:
|
|
182
|
-
|
|
183
|
-
```typescript
|
|
184
|
-
interface AGGridQueryParams {
|
|
185
|
-
where: TWhereInput // WHERE clause for filtering
|
|
186
|
-
orderBy: Record<string, unknown>[] // ORDER BY for sorting
|
|
187
|
-
skip: number // Pagination offset
|
|
188
|
-
take: number // Pagination limit
|
|
189
|
-
groupLevel: number // Current group level (for grouping)
|
|
190
|
-
groupKeys: string[] // Group drill-down keys
|
|
191
|
-
groupColumn?: AGGridColumn // Column being grouped
|
|
192
|
-
isGroupRequest: boolean // Whether this is a group or leaf request
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### `ComputedField`
|
|
197
|
-
|
|
198
|
-
Configuration for computed/virtual fields:
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
interface ComputedField {
|
|
202
|
-
columnId: string // Column ID in AG Grid
|
|
203
|
-
dbField?: string // DB field path (e.g., 'location.name')
|
|
204
|
-
// 👇 Optional - only needed for complex computed logic
|
|
205
|
-
valueGetter?: (record) => unknown // Compute display value
|
|
206
|
-
filterHandler?: (filterValue, where) => void // Handle filtering
|
|
207
|
-
groupHandler?: (groupKey, where) => void // Handle grouping
|
|
208
|
-
}
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
**✨ Auto-Magic for Nested Fields:**
|
|
212
|
-
|
|
213
|
-
- If `dbField` contains a `.` (e.g., `'location.name'`), the system automatically:
|
|
214
|
-
- Extracts nested values for display
|
|
215
|
-
- Handles text and set filters
|
|
216
|
-
- Handles sorting and grouping
|
|
217
|
-
- You only need `valueGetter`, `filterHandler`, and `groupHandler` for **complex computed logic**
|
|
218
|
-
|
|
219
|
-
## Common Patterns
|
|
220
|
-
|
|
221
|
-
### Pattern 1: Simple Nested Relations (Use Dot Notation!)
|
|
222
|
-
|
|
223
|
-
**Recommended:** Use dot notation directly in column definitions - no `computedFields` needed!
|
|
224
|
-
|
|
225
|
-
```typescript
|
|
226
|
-
// Server-side - just include the relation
|
|
227
|
-
return await createAGGridQuery({
|
|
228
|
-
async fetch(params) {
|
|
229
|
-
return await DB.model.findMany({
|
|
230
|
-
where: params.where,
|
|
231
|
-
orderBy: params.orderBy,
|
|
232
|
-
skip: params.skip,
|
|
233
|
-
take: params.take,
|
|
234
|
-
include: { location: true, category: true, user: true },
|
|
235
|
-
})
|
|
236
|
-
},
|
|
237
|
-
async count(params) {
|
|
238
|
-
return await DB.model.count({ where: params.where })
|
|
239
|
-
},
|
|
240
|
-
})(request)
|
|
241
|
-
|
|
242
|
-
// Client-side - use dot notation in field
|
|
243
|
-
const columnDefs = [
|
|
244
|
-
{ field: 'location.name', headerName: 'Location', filter: 'agTextColumnFilter' },
|
|
245
|
-
{ field: 'category.name', headerName: 'Category', filter: 'agTextColumnFilter' },
|
|
246
|
-
{ field: 'user.email', headerName: 'User', filter: 'agTextColumnFilter' },
|
|
247
|
-
]
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
The system automatically handles:
|
|
251
|
-
|
|
252
|
-
- **Display**: AG Grid extracts `record.location.name` natively
|
|
253
|
-
- **Text Filter**: `where.location.name = { contains: 'X', mode: 'insensitive' }`
|
|
254
|
-
- **Set Filter**: `where.location.name = { in: ['A', 'B'] }`
|
|
255
|
-
- **Sorting**: `orderBy: { location: { name: 'asc' } }`
|
|
256
|
-
- **Grouping**: `where.location.name = 'groupKey'`
|
|
257
|
-
|
|
258
|
-
### Pattern 2: Complex Calculated Fields (Custom Handlers Required)
|
|
259
|
-
|
|
260
|
-
```typescript
|
|
261
|
-
computedFields: [
|
|
262
|
-
{
|
|
263
|
-
columnId: 'weekEnding',
|
|
264
|
-
valueGetter: (record) => {
|
|
265
|
-
const date = new Date(record.date)
|
|
266
|
-
const day = date.getDay()
|
|
267
|
-
date.setDate(date.getDate() + (7 - day))
|
|
268
|
-
return date.toISOString().split('T')[0]
|
|
269
|
-
},
|
|
270
|
-
filterHandler: (filterValue, where) => {
|
|
271
|
-
// Custom filter logic based on calculated value
|
|
272
|
-
const targetDate = new Date(filterValue.dateFrom)
|
|
273
|
-
const weekStart = new Date(targetDate)
|
|
274
|
-
weekStart.setDate(targetDate.getDate() - 6)
|
|
275
|
-
where.date = { gte: weekStart, lte: targetDate }
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
|
-
]
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
### Pattern 3: Custom WHERE Transform
|
|
282
|
-
|
|
283
|
-
```typescript
|
|
284
|
-
transformWhere: (where, request) => {
|
|
285
|
-
// Add tenant filtering
|
|
286
|
-
where.tenantId = getCurrentTenantId()
|
|
287
|
-
|
|
288
|
-
// Add soft delete filter
|
|
289
|
-
where.deletedAt = null
|
|
290
|
-
|
|
291
|
-
return where
|
|
292
|
-
}
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
## Database Examples
|
|
296
|
-
|
|
297
|
-
### Prisma
|
|
298
|
-
|
|
299
|
-
```typescript
|
|
300
|
-
{
|
|
301
|
-
async fetch(params) {
|
|
302
|
-
return await DB.model.findMany({
|
|
303
|
-
where: params.where,
|
|
304
|
-
orderBy: params.orderBy,
|
|
305
|
-
skip: params.skip,
|
|
306
|
-
take: params.take,
|
|
307
|
-
})
|
|
308
|
-
},
|
|
309
|
-
async count(params) {
|
|
310
|
-
return await DB.model.count({ where: params.where })
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
## Advanced Features
|
|
316
|
-
|
|
317
|
-
### Row Grouping
|
|
318
|
-
|
|
319
|
-
The API automatically handles row grouping. When `isGroupRequest` is `true`, it returns group rows with counts. When `false`, it returns leaf data.
|
|
320
|
-
|
|
321
|
-
```typescript
|
|
322
|
-
if (params.isGroupRequest) {
|
|
323
|
-
// Return groups at current level
|
|
324
|
-
} else {
|
|
325
|
-
// Return actual data rows
|
|
326
|
-
}
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
### Type Safety
|
|
330
|
-
|
|
331
|
-
Use TypeScript generics for full type safety:
|
|
332
|
-
|
|
333
|
-
```typescript
|
|
334
|
-
type UserWhereInput = {
|
|
335
|
-
id?: number
|
|
336
|
-
name?: { contains: string; mode: 'insensitive' }
|
|
337
|
-
email?: { contains: string; mode: 'insensitive' }
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
createAGGridQuery<User, UserWhereInput>({
|
|
341
|
-
// params.where is now typed as UserWhereInput
|
|
342
|
-
async fetch(params) { ... },
|
|
343
|
-
async count(params) { ... }
|
|
344
|
-
})
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
## Nested Field Magic Explained
|
|
348
|
-
|
|
349
|
-
When you use dot notation in field names (e.g., `'location.name'`), the system automatically handles everything:
|
|
350
|
-
|
|
351
|
-
```typescript
|
|
352
|
-
// Column definition
|
|
353
|
-
{ field: 'location.name', headerName: 'Location', filter: 'agTextColumnFilter' }
|
|
354
|
-
|
|
355
|
-
// Server-side - just include the relation
|
|
356
|
-
include: { location: true }
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
**What happens automatically:**
|
|
360
|
-
|
|
361
|
-
1. **Display**: AG Grid extracts `record.location.name` natively
|
|
362
|
-
2. **Text Filtering**: `where.location = { name: { contains: 'Office', mode: 'insensitive' } }`
|
|
363
|
-
3. **Set Filtering**: `where.location = { name: { in: ['Office A', 'Office B'] } }`
|
|
364
|
-
4. **Sorting**: `orderBy: [{ location: { name: 'asc' } }]`
|
|
365
|
-
5. **Grouping**: `where.location = { name: 'Office A' }`
|
|
366
|
-
|
|
367
|
-
**You write:** Column definition with dot notation
|
|
368
|
-
|
|
369
|
-
**You get:** All filtering, sorting, grouping, and display automatically! ✨
|
|
370
|
-
|
|
371
|
-
## See Also
|
|
372
|
-
|
|
373
|
-
- [AG Grid SSRM Documentation](https://www.ag-grid.com/javascript-data-grid/server-side-model/)
|
|
1
|
+
# AG Grid Server-Side Row Model (SSRM) - Server API
|
|
2
|
+
|
|
3
|
+
Clean, database-agnostic API for implementing AG Grid's Server-Side Row Model on the backend.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This library provides a simple, strongly-typed interface for handling AG Grid SSRM requests on the server side. It works with any backend data source: Prisma, raw SQL, MongoDB, REST APIs, etc.
|
|
8
|
+
|
|
9
|
+
## Key Features
|
|
10
|
+
|
|
11
|
+
- ✅ **Database Agnostic** - Works with any data source
|
|
12
|
+
- ✅ **Type Safe** - Full TypeScript support with generics
|
|
13
|
+
- ✅ **Simple API** - Just implement `fetch` and `count`
|
|
14
|
+
- ✅ **Computed Fields** - Easy handling of virtual/calculated fields
|
|
15
|
+
- ✅ **Grouping Support** - Built-in row grouping support
|
|
16
|
+
- ✅ **Filter Transforms** - Automatic filter/sort transformation
|
|
17
|
+
- ✅ **Zero Boilerplate** - Clean, declarative configuration
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### 1. Basic Implementation (Prisma)
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { createAGGridQuery } from '$lib/ag-grid'
|
|
25
|
+
import { query } from '$app/server'
|
|
26
|
+
import { DB } from '$lib/prisma.server'
|
|
27
|
+
|
|
28
|
+
export const getUsersPaginated = query(agGridRequestSchema, async (request) => {
|
|
29
|
+
return await createAGGridQuery({
|
|
30
|
+
async fetch(params) {
|
|
31
|
+
return await DB.user.findMany({
|
|
32
|
+
where: params.where,
|
|
33
|
+
orderBy: params.orderBy,
|
|
34
|
+
skip: params.skip,
|
|
35
|
+
take: params.take,
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
async count(params) {
|
|
39
|
+
return await DB.user.count({ where: params.where })
|
|
40
|
+
},
|
|
41
|
+
defaultSort: { createdAt: 'desc' },
|
|
42
|
+
})(request)
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2. With Nested Relations (Auto-Handled!)
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
export const getInterventionsPaginated = query(agGridRequestSchema, async (request) => {
|
|
50
|
+
return await createAGGridQuery({
|
|
51
|
+
async fetch(params) {
|
|
52
|
+
return await DB.intervention.findMany({
|
|
53
|
+
where: params.where,
|
|
54
|
+
orderBy: params.orderBy,
|
|
55
|
+
skip: params.skip,
|
|
56
|
+
take: params.take,
|
|
57
|
+
include: { location: true }, // Include the relation
|
|
58
|
+
})
|
|
59
|
+
},
|
|
60
|
+
async count(params) {
|
|
61
|
+
return await DB.intervention.count({ where: params.where })
|
|
62
|
+
},
|
|
63
|
+
// No computedFields needed! Just use field: 'location.name' in column definitions
|
|
64
|
+
// The system automatically:
|
|
65
|
+
// - Handles text filters: where.location.name contains 'X'
|
|
66
|
+
// - Handles set filters: where.location.name in [...]
|
|
67
|
+
// - Handles sorting: orderBy: { location: { name: 'asc' } }
|
|
68
|
+
// - Handles grouping: where.location.name = 'groupKey'
|
|
69
|
+
// - AG Grid displays nested values natively with dot notation
|
|
70
|
+
})(request)
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Column Definition:**
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const columnDefs = [
|
|
78
|
+
{ field: 'id' },
|
|
79
|
+
{
|
|
80
|
+
field: 'location.name', // ✨ Use dot notation directly!
|
|
81
|
+
headerName: 'Location',
|
|
82
|
+
filter: 'agTextColumnFilter',
|
|
83
|
+
},
|
|
84
|
+
{ field: 'dateOfEngagement', filter: 'agDateColumnFilter' },
|
|
85
|
+
]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 3. With Complex Computed Fields
|
|
89
|
+
|
|
90
|
+
For truly computed/calculated values, provide custom handlers:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
computedFields: [
|
|
94
|
+
{
|
|
95
|
+
columnId: 'weekEnding',
|
|
96
|
+
valueGetter: (record) => {
|
|
97
|
+
// Calculate week ending date from dateOfEngagement
|
|
98
|
+
const date = new Date(record.dateOfEngagement)
|
|
99
|
+
const day = date.getDay()
|
|
100
|
+
date.setDate(date.getDate() + (7 - day))
|
|
101
|
+
return date.toISOString().split('T')[0]
|
|
102
|
+
},
|
|
103
|
+
filterHandler: (filterValue, where) => {
|
|
104
|
+
// Custom filter logic for week ending
|
|
105
|
+
const filter = filterValue as { dateFrom?: string }
|
|
106
|
+
if (filter.dateFrom) {
|
|
107
|
+
const targetDate = new Date(filter.dateFrom)
|
|
108
|
+
const weekStart = new Date(targetDate)
|
|
109
|
+
weekStart.setDate(targetDate.getDate() - 6)
|
|
110
|
+
where.dateOfEngagement = { gte: weekStart, lte: targetDate }
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Column Definition:**
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const columnDefs = [
|
|
121
|
+
{
|
|
122
|
+
field: 'weekEnding', // Custom computed field
|
|
123
|
+
headerName: 'Week Ending',
|
|
124
|
+
filter: 'agDateColumnFilter',
|
|
125
|
+
},
|
|
126
|
+
]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 4. Client-Side Usage
|
|
130
|
+
|
|
131
|
+
```svelte
|
|
132
|
+
<script lang="ts">
|
|
133
|
+
import { getUsersPaginated } from './user.remote'
|
|
134
|
+
import { createAGGridDatasource, createRemoteFetcher } from '$lib/ag-grid'
|
|
135
|
+
import { AgGridSvelte } from 'ag-grid-svelte'
|
|
136
|
+
|
|
137
|
+
let gridApi: GridApi
|
|
138
|
+
|
|
139
|
+
const columnDefs = [
|
|
140
|
+
{ field: 'id' },
|
|
141
|
+
{ field: 'name', filter: 'agTextColumnFilter' },
|
|
142
|
+
{ field: 'email', filter: 'agTextColumnFilter' },
|
|
143
|
+
// For nested relations, use dot notation directly:
|
|
144
|
+
{ field: 'profile.avatar', headerName: 'Avatar' },
|
|
145
|
+
{ field: 'department.name', headerName: 'Department', filter: 'agTextColumnFilter' },
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
function onGridReady(params: any) {
|
|
149
|
+
gridApi = params.api
|
|
150
|
+
|
|
151
|
+
const datasource = createAGGridDatasource(createRemoteFetcher(getUsersPaginated))
|
|
152
|
+
|
|
153
|
+
gridApi.setGridOption('serverSideDatasource', datasource)
|
|
154
|
+
}
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<div class="ag-theme-alpine" style="height: 600px;">
|
|
158
|
+
<AgGridSvelte {columnDefs} rowModelType="serverSide" pagination={true} paginationPageSize={100} {onGridReady} />
|
|
159
|
+
</div>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## API Reference
|
|
163
|
+
|
|
164
|
+
### `createAGGridQuery(config)`
|
|
165
|
+
|
|
166
|
+
Main function to create a server-side query handler.
|
|
167
|
+
|
|
168
|
+
**Parameters:**
|
|
169
|
+
|
|
170
|
+
- `config: AGGridQueryConfig<TRecord, TWhereInput>`
|
|
171
|
+
- `fetch: (params) => Promise<TRecord[]>` - Function to fetch data rows
|
|
172
|
+
- `count: (params) => Promise<number>` - Function to count total rows
|
|
173
|
+
- `computedFields?: ComputedField[]` - Optional computed field configurations
|
|
174
|
+
- `defaultSort?: Record<string, 'asc' | 'desc'>` - Optional default sort
|
|
175
|
+
- `transformWhere?: (where, request) => TWhereInput` - Optional WHERE clause transformer
|
|
176
|
+
|
|
177
|
+
**Returns:** `(request: AGGridRequest) => Promise<AGGridResponse<TRecord>>`
|
|
178
|
+
|
|
179
|
+
### `AGGridQueryParams`
|
|
180
|
+
|
|
181
|
+
The params object passed to `fetch` and `count` functions:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
interface AGGridQueryParams {
|
|
185
|
+
where: TWhereInput // WHERE clause for filtering
|
|
186
|
+
orderBy: Record<string, unknown>[] // ORDER BY for sorting
|
|
187
|
+
skip: number // Pagination offset
|
|
188
|
+
take: number // Pagination limit
|
|
189
|
+
groupLevel: number // Current group level (for grouping)
|
|
190
|
+
groupKeys: string[] // Group drill-down keys
|
|
191
|
+
groupColumn?: AGGridColumn // Column being grouped
|
|
192
|
+
isGroupRequest: boolean // Whether this is a group or leaf request
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `ComputedField`
|
|
197
|
+
|
|
198
|
+
Configuration for computed/virtual fields:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
interface ComputedField {
|
|
202
|
+
columnId: string // Column ID in AG Grid
|
|
203
|
+
dbField?: string // DB field path (e.g., 'location.name')
|
|
204
|
+
// 👇 Optional - only needed for complex computed logic
|
|
205
|
+
valueGetter?: (record) => unknown // Compute display value
|
|
206
|
+
filterHandler?: (filterValue, where) => void // Handle filtering
|
|
207
|
+
groupHandler?: (groupKey, where) => void // Handle grouping
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**✨ Auto-Magic for Nested Fields:**
|
|
212
|
+
|
|
213
|
+
- If `dbField` contains a `.` (e.g., `'location.name'`), the system automatically:
|
|
214
|
+
- Extracts nested values for display
|
|
215
|
+
- Handles text and set filters
|
|
216
|
+
- Handles sorting and grouping
|
|
217
|
+
- You only need `valueGetter`, `filterHandler`, and `groupHandler` for **complex computed logic**
|
|
218
|
+
|
|
219
|
+
## Common Patterns
|
|
220
|
+
|
|
221
|
+
### Pattern 1: Simple Nested Relations (Use Dot Notation!)
|
|
222
|
+
|
|
223
|
+
**Recommended:** Use dot notation directly in column definitions - no `computedFields` needed!
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// Server-side - just include the relation
|
|
227
|
+
return await createAGGridQuery({
|
|
228
|
+
async fetch(params) {
|
|
229
|
+
return await DB.model.findMany({
|
|
230
|
+
where: params.where,
|
|
231
|
+
orderBy: params.orderBy,
|
|
232
|
+
skip: params.skip,
|
|
233
|
+
take: params.take,
|
|
234
|
+
include: { location: true, category: true, user: true },
|
|
235
|
+
})
|
|
236
|
+
},
|
|
237
|
+
async count(params) {
|
|
238
|
+
return await DB.model.count({ where: params.where })
|
|
239
|
+
},
|
|
240
|
+
})(request)
|
|
241
|
+
|
|
242
|
+
// Client-side - use dot notation in field
|
|
243
|
+
const columnDefs = [
|
|
244
|
+
{ field: 'location.name', headerName: 'Location', filter: 'agTextColumnFilter' },
|
|
245
|
+
{ field: 'category.name', headerName: 'Category', filter: 'agTextColumnFilter' },
|
|
246
|
+
{ field: 'user.email', headerName: 'User', filter: 'agTextColumnFilter' },
|
|
247
|
+
]
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The system automatically handles:
|
|
251
|
+
|
|
252
|
+
- **Display**: AG Grid extracts `record.location.name` natively
|
|
253
|
+
- **Text Filter**: `where.location.name = { contains: 'X', mode: 'insensitive' }`
|
|
254
|
+
- **Set Filter**: `where.location.name = { in: ['A', 'B'] }`
|
|
255
|
+
- **Sorting**: `orderBy: { location: { name: 'asc' } }`
|
|
256
|
+
- **Grouping**: `where.location.name = 'groupKey'`
|
|
257
|
+
|
|
258
|
+
### Pattern 2: Complex Calculated Fields (Custom Handlers Required)
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
computedFields: [
|
|
262
|
+
{
|
|
263
|
+
columnId: 'weekEnding',
|
|
264
|
+
valueGetter: (record) => {
|
|
265
|
+
const date = new Date(record.date)
|
|
266
|
+
const day = date.getDay()
|
|
267
|
+
date.setDate(date.getDate() + (7 - day))
|
|
268
|
+
return date.toISOString().split('T')[0]
|
|
269
|
+
},
|
|
270
|
+
filterHandler: (filterValue, where) => {
|
|
271
|
+
// Custom filter logic based on calculated value
|
|
272
|
+
const targetDate = new Date(filterValue.dateFrom)
|
|
273
|
+
const weekStart = new Date(targetDate)
|
|
274
|
+
weekStart.setDate(targetDate.getDate() - 6)
|
|
275
|
+
where.date = { gte: weekStart, lte: targetDate }
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
]
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Pattern 3: Custom WHERE Transform
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
transformWhere: (where, request) => {
|
|
285
|
+
// Add tenant filtering
|
|
286
|
+
where.tenantId = getCurrentTenantId()
|
|
287
|
+
|
|
288
|
+
// Add soft delete filter
|
|
289
|
+
where.deletedAt = null
|
|
290
|
+
|
|
291
|
+
return where
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Database Examples
|
|
296
|
+
|
|
297
|
+
### Prisma
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
{
|
|
301
|
+
async fetch(params) {
|
|
302
|
+
return await DB.model.findMany({
|
|
303
|
+
where: params.where,
|
|
304
|
+
orderBy: params.orderBy,
|
|
305
|
+
skip: params.skip,
|
|
306
|
+
take: params.take,
|
|
307
|
+
})
|
|
308
|
+
},
|
|
309
|
+
async count(params) {
|
|
310
|
+
return await DB.model.count({ where: params.where })
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Advanced Features
|
|
316
|
+
|
|
317
|
+
### Row Grouping
|
|
318
|
+
|
|
319
|
+
The API automatically handles row grouping. When `isGroupRequest` is `true`, it returns group rows with counts. When `false`, it returns leaf data.
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
if (params.isGroupRequest) {
|
|
323
|
+
// Return groups at current level
|
|
324
|
+
} else {
|
|
325
|
+
// Return actual data rows
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Type Safety
|
|
330
|
+
|
|
331
|
+
Use TypeScript generics for full type safety:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
type UserWhereInput = {
|
|
335
|
+
id?: number
|
|
336
|
+
name?: { contains: string; mode: 'insensitive' }
|
|
337
|
+
email?: { contains: string; mode: 'insensitive' }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
createAGGridQuery<User, UserWhereInput>({
|
|
341
|
+
// params.where is now typed as UserWhereInput
|
|
342
|
+
async fetch(params) { ... },
|
|
343
|
+
async count(params) { ... }
|
|
344
|
+
})
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Nested Field Magic Explained
|
|
348
|
+
|
|
349
|
+
When you use dot notation in field names (e.g., `'location.name'`), the system automatically handles everything:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
// Column definition
|
|
353
|
+
{ field: 'location.name', headerName: 'Location', filter: 'agTextColumnFilter' }
|
|
354
|
+
|
|
355
|
+
// Server-side - just include the relation
|
|
356
|
+
include: { location: true }
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**What happens automatically:**
|
|
360
|
+
|
|
361
|
+
1. **Display**: AG Grid extracts `record.location.name` natively
|
|
362
|
+
2. **Text Filtering**: `where.location = { name: { contains: 'Office', mode: 'insensitive' } }`
|
|
363
|
+
3. **Set Filtering**: `where.location = { name: { in: ['Office A', 'Office B'] } }`
|
|
364
|
+
4. **Sorting**: `orderBy: [{ location: { name: 'asc' } }]`
|
|
365
|
+
5. **Grouping**: `where.location = { name: 'Office A' }`
|
|
366
|
+
|
|
367
|
+
**You write:** Column definition with dot notation
|
|
368
|
+
|
|
369
|
+
**You get:** All filtering, sorting, grouping, and display automatically! ✨
|
|
370
|
+
|
|
371
|
+
## See Also
|
|
372
|
+
|
|
373
|
+
- [AG Grid SSRM Documentation](https://www.ag-grid.com/javascript-data-grid/server-side-model/)
|