simplesvelte 2.2.23 → 2.4.0

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.
@@ -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/)