@pyreon/feature 0.11.4 → 0.11.6

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,10 +1,10 @@
1
- import { QueryClient, QueryClientProvider } from "@pyreon/query"
2
- import { signal } from "@pyreon/reactivity"
3
- import { mount } from "@pyreon/runtime-dom"
4
- import { resetAllStores } from "@pyreon/store"
5
- import { z } from "zod"
6
- import { defineFeature } from "../define-feature"
7
- import { defaultInitialValues, extractFields, isReference, reference } from "../schema"
1
+ import { QueryClient, QueryClientProvider } from '@pyreon/query'
2
+ import { signal } from '@pyreon/reactivity'
3
+ import { mount } from '@pyreon/runtime-dom'
4
+ import { resetAllStores } from '@pyreon/store'
5
+ import { z } from 'zod'
6
+ import { defineFeature } from '../define-feature'
7
+ import { defaultInitialValues, extractFields, isReference, reference } from '../schema'
8
8
 
9
9
  // ─── Helpers ──────────────────────────────────────────────────────────────────
10
10
 
@@ -15,7 +15,7 @@ function Capture<T>({ fn }: { fn: () => T }) {
15
15
 
16
16
  function mountWith<T>(client: QueryClient, fn: () => T): { result: T; unmount: () => void } {
17
17
  let result: T | undefined
18
- const el = document.createElement("div")
18
+ const el = document.createElement('div')
19
19
  document.body.appendChild(el)
20
20
  const unmount = mount(
21
21
  <QueryClientProvider client={client}>
@@ -41,7 +41,7 @@ function mountWith<T>(client: QueryClient, fn: () => T): { result: T; unmount: (
41
41
  const userSchema = z.object({
42
42
  name: z.string().min(2),
43
43
  email: z.string().email(),
44
- role: z.enum(["admin", "editor", "viewer"]),
44
+ role: z.enum(['admin', 'editor', 'viewer']),
45
45
  age: z.number().optional(),
46
46
  active: z.boolean(),
47
47
  })
@@ -52,21 +52,21 @@ type UserValues = z.infer<typeof userSchema>
52
52
 
53
53
  function createMockFetch(responses: Record<string, { status?: number; body?: unknown }>) {
54
54
  return async (url: string | URL | Request, init?: RequestInit): Promise<Response> => {
55
- const urlStr = typeof url === "string" ? url : url.toString()
56
- const method = init?.method ?? "GET"
55
+ const urlStr = typeof url === 'string' ? url : url.toString()
56
+ const method = init?.method ?? 'GET'
57
57
  const key = `${method} ${urlStr}`
58
58
 
59
59
  const match = responses[key] ?? Object.entries(responses).find(([k]) => key.startsWith(k))?.[1]
60
60
 
61
61
  if (!match) {
62
- return new Response(JSON.stringify({ message: "Not found" }), {
62
+ return new Response(JSON.stringify({ message: 'Not found' }), {
63
63
  status: 404,
64
64
  })
65
65
  }
66
66
 
67
67
  return new Response(match.body !== undefined ? JSON.stringify(match.body) : null, {
68
68
  status: match.status ?? 200,
69
- headers: { "Content-Type": "application/json" },
69
+ headers: { 'Content-Type': 'application/json' },
70
70
  })
71
71
  }
72
72
  }
@@ -79,94 +79,94 @@ afterEach(() => {
79
79
 
80
80
  // ─── Schema introspection ──────────────────────────────────────────────────────
81
81
 
82
- describe("extractFields", () => {
83
- it("extracts field names and types from a Zod schema", () => {
82
+ describe('extractFields', () => {
83
+ it('extracts field names and types from a Zod schema', () => {
84
84
  const fields = extractFields(userSchema)
85
85
 
86
86
  expect(fields).toHaveLength(5)
87
87
  expect(fields[0]).toEqual({
88
- name: "name",
89
- type: "string",
88
+ name: 'name',
89
+ type: 'string',
90
90
  optional: false,
91
- label: "Name",
91
+ label: 'Name',
92
92
  })
93
93
  expect(fields[1]).toEqual({
94
- name: "email",
95
- type: "string",
94
+ name: 'email',
95
+ type: 'string',
96
96
  optional: false,
97
- label: "Email",
97
+ label: 'Email',
98
98
  })
99
99
  expect(fields[2]).toMatchObject({
100
- name: "role",
101
- type: "enum",
100
+ name: 'role',
101
+ type: 'enum',
102
102
  optional: false,
103
- label: "Role",
103
+ label: 'Role',
104
104
  })
105
105
  })
106
106
 
107
- it("detects optional fields", () => {
107
+ it('detects optional fields', () => {
108
108
  const fields = extractFields(userSchema)
109
- const ageField = fields.find((f) => f.name === "age")
109
+ const ageField = fields.find((f) => f.name === 'age')
110
110
  expect(ageField?.optional).toBe(true)
111
- expect(ageField?.type).toBe("number")
111
+ expect(ageField?.type).toBe('number')
112
112
  })
113
113
 
114
- it("detects boolean fields", () => {
114
+ it('detects boolean fields', () => {
115
115
  const fields = extractFields(userSchema)
116
- const activeField = fields.find((f) => f.name === "active")
117
- expect(activeField?.type).toBe("boolean")
116
+ const activeField = fields.find((f) => f.name === 'active')
117
+ expect(activeField?.type).toBe('boolean')
118
118
  })
119
119
 
120
- it("converts field names to labels", () => {
120
+ it('converts field names to labels', () => {
121
121
  const schema = z.object({
122
122
  firstName: z.string(),
123
123
  last_name: z.string(),
124
124
  email_address: z.string(),
125
125
  })
126
126
  const fields = extractFields(schema)
127
- expect(fields[0]!.label).toBe("First Name")
128
- expect(fields[1]!.label).toBe("Last Name")
129
- expect(fields[2]!.label).toBe("Email Address")
127
+ expect(fields[0]!.label).toBe('First Name')
128
+ expect(fields[1]!.label).toBe('Last Name')
129
+ expect(fields[2]!.label).toBe('Email Address')
130
130
  })
131
131
 
132
- it("returns empty array for non-object input", () => {
132
+ it('returns empty array for non-object input', () => {
133
133
  expect(extractFields(null)).toEqual([])
134
134
  expect(extractFields(undefined)).toEqual([])
135
- expect(extractFields("string")).toEqual([])
135
+ expect(extractFields('string')).toEqual([])
136
136
  })
137
137
 
138
- it("handles Zod v3-style schema with _def.typeName", () => {
138
+ it('handles Zod v3-style schema with _def.typeName', () => {
139
139
  // Mock v3 schema shape
140
140
  const mockSchema = {
141
141
  _def: {
142
142
  shape: () => ({
143
143
  title: {
144
- _def: { typeName: "ZodString" },
144
+ _def: { typeName: 'ZodString' },
145
145
  },
146
146
  count: {
147
- _def: { typeName: "ZodNumber" },
147
+ _def: { typeName: 'ZodNumber' },
148
148
  },
149
149
  done: {
150
- _def: { typeName: "ZodBoolean" },
150
+ _def: { typeName: 'ZodBoolean' },
151
151
  },
152
152
  }),
153
153
  },
154
154
  }
155
155
  const fields = extractFields(mockSchema)
156
156
  expect(fields).toHaveLength(3)
157
- expect(fields[0]).toMatchObject({ name: "title", type: "string" })
158
- expect(fields[1]).toMatchObject({ name: "count", type: "number" })
159
- expect(fields[2]).toMatchObject({ name: "done", type: "boolean" })
157
+ expect(fields[0]).toMatchObject({ name: 'title', type: 'string' })
158
+ expect(fields[1]).toMatchObject({ name: 'count', type: 'number' })
159
+ expect(fields[2]).toMatchObject({ name: 'done', type: 'boolean' })
160
160
  })
161
161
 
162
- it("handles Zod v3-style optional with _def.typeName ZodOptional", () => {
162
+ it('handles Zod v3-style optional with _def.typeName ZodOptional', () => {
163
163
  const mockSchema = {
164
164
  _def: {
165
165
  shape: () => ({
166
166
  name: {
167
167
  _def: {
168
- typeName: "ZodOptional",
169
- innerType: { _def: { typeName: "ZodString" } },
168
+ typeName: 'ZodOptional',
169
+ innerType: { _def: { typeName: 'ZodString' } },
170
170
  },
171
171
  },
172
172
  }),
@@ -174,20 +174,20 @@ describe("extractFields", () => {
174
174
  }
175
175
  const fields = extractFields(mockSchema)
176
176
  expect(fields[0]).toMatchObject({
177
- name: "name",
178
- type: "string",
177
+ name: 'name',
178
+ type: 'string',
179
179
  optional: true,
180
180
  })
181
181
  })
182
182
 
183
- it("handles Zod v3-style enum with _def.values", () => {
183
+ it('handles Zod v3-style enum with _def.values', () => {
184
184
  const mockSchema = {
185
185
  _def: {
186
186
  shape: () => ({
187
187
  status: {
188
188
  _def: {
189
- typeName: "ZodEnum",
190
- values: ["active", "inactive"],
189
+ typeName: 'ZodEnum',
190
+ values: ['active', 'inactive'],
191
191
  },
192
192
  },
193
193
  }),
@@ -195,42 +195,42 @@ describe("extractFields", () => {
195
195
  }
196
196
  const fields = extractFields(mockSchema)
197
197
  expect(fields[0]).toMatchObject({
198
- name: "status",
199
- type: "enum",
200
- enumValues: ["active", "inactive"],
198
+ name: 'status',
199
+ type: 'enum',
200
+ enumValues: ['active', 'inactive'],
201
201
  })
202
202
  })
203
203
 
204
- it("returns unknown for unrecognized field type", () => {
204
+ it('returns unknown for unrecognized field type', () => {
205
205
  const mockSchema = {
206
206
  shape: {
207
- weird: { _def: { typeName: "ZodSomethingNew" } },
207
+ weird: { _def: { typeName: 'ZodSomethingNew' } },
208
208
  },
209
209
  }
210
210
  const fields = extractFields(mockSchema)
211
- expect(fields[0]).toMatchObject({ name: "weird", type: "string" })
211
+ expect(fields[0]).toMatchObject({ name: 'weird', type: 'string' })
212
212
  })
213
213
 
214
- it("handles schema with static _def.shape object (not function)", () => {
214
+ it('handles schema with static _def.shape object (not function)', () => {
215
215
  const mockSchema = {
216
216
  _def: {
217
217
  shape: {
218
- name: { _def: { typeName: "ZodString" } },
218
+ name: { _def: { typeName: 'ZodString' } },
219
219
  },
220
220
  },
221
221
  }
222
222
  const fields = extractFields(mockSchema)
223
223
  expect(fields).toHaveLength(1)
224
- expect(fields[0]).toMatchObject({ name: "name", type: "string" })
224
+ expect(fields[0]).toMatchObject({ name: 'name', type: 'string' })
225
225
  })
226
226
 
227
- it("handles schema with _zod.def.shape (v4 path)", () => {
227
+ it('handles schema with _zod.def.shape (v4 path)', () => {
228
228
  const mockSchema = {
229
229
  _zod: {
230
230
  def: {
231
231
  shape: {
232
232
  email: {
233
- _zod: { def: { type: "string" } },
233
+ _zod: { def: { type: 'string' } },
234
234
  },
235
235
  },
236
236
  },
@@ -238,27 +238,27 @@ describe("extractFields", () => {
238
238
  }
239
239
  const fields = extractFields(mockSchema)
240
240
  expect(fields).toHaveLength(1)
241
- expect(fields[0]).toMatchObject({ name: "email", type: "string" })
241
+ expect(fields[0]).toMatchObject({ name: 'email', type: 'string' })
242
242
  })
243
243
 
244
- it("handles getTypeName returning undefined", () => {
244
+ it('handles getTypeName returning undefined', () => {
245
245
  const mockSchema = {
246
246
  shape: {
247
247
  field: {},
248
248
  },
249
249
  }
250
250
  const fields = extractFields(mockSchema)
251
- expect(fields[0]).toMatchObject({ name: "field", type: "unknown" })
251
+ expect(fields[0]).toMatchObject({ name: 'field', type: 'unknown' })
252
252
  })
253
253
  })
254
254
 
255
- describe("defaultInitialValues", () => {
256
- it("generates defaults from field types", () => {
255
+ describe('defaultInitialValues', () => {
256
+ it('generates defaults from field types', () => {
257
257
  const fields = extractFields(userSchema)
258
258
  const values = defaultInitialValues(fields)
259
259
 
260
- expect(values.name).toBe("")
261
- expect(values.email).toBe("")
260
+ expect(values.name).toBe('')
261
+ expect(values.email).toBe('')
262
262
  expect(values.age).toBe(0)
263
263
  expect(values.active).toBe(false)
264
264
  })
@@ -266,52 +266,52 @@ describe("defaultInitialValues", () => {
266
266
 
267
267
  // ─── defineFeature ─────────────────────────────────────────────────────────────
268
268
 
269
- describe("defineFeature", () => {
270
- it("returns a feature with name, api, schema, fields, and queryKey", () => {
269
+ describe('defineFeature', () => {
270
+ it('returns a feature with name, api, schema, fields, and queryKey', () => {
271
271
  const users = defineFeature<UserValues>({
272
- name: "users",
272
+ name: 'users',
273
273
  schema: userSchema,
274
- api: "/api/users",
274
+ api: '/api/users',
275
275
  })
276
276
 
277
- expect(users.name).toBe("users")
278
- expect(users.api).toBe("/api/users")
277
+ expect(users.name).toBe('users')
278
+ expect(users.api).toBe('/api/users')
279
279
  expect(users.schema).toBe(userSchema)
280
280
  expect(users.fields.length).toBeGreaterThan(0)
281
- expect(users.fields[0]!.name).toBe("name")
282
- expect(users.queryKey()).toEqual(["users"])
283
- expect(users.queryKey("123")).toEqual(["users", "123"])
284
- expect(users.queryKey(42)).toEqual(["users", 42])
281
+ expect(users.fields[0]!.name).toBe('name')
282
+ expect(users.queryKey()).toEqual(['users'])
283
+ expect(users.queryKey('123')).toEqual(['users', '123'])
284
+ expect(users.queryKey(42)).toEqual(['users', 42])
285
285
  })
286
286
 
287
- it("has all hooks", () => {
287
+ it('has all hooks', () => {
288
288
  const users = defineFeature<UserValues>({
289
- name: "users",
289
+ name: 'users',
290
290
  schema: userSchema,
291
- api: "/api/users",
291
+ api: '/api/users',
292
292
  })
293
293
 
294
- expect(typeof users.useList).toBe("function")
295
- expect(typeof users.useById).toBe("function")
296
- expect(typeof users.useSearch).toBe("function")
297
- expect(typeof users.useCreate).toBe("function")
298
- expect(typeof users.useUpdate).toBe("function")
299
- expect(typeof users.useDelete).toBe("function")
300
- expect(typeof users.useForm).toBe("function")
301
- expect(typeof users.useTable).toBe("function")
302
- expect(typeof users.useStore).toBe("function")
294
+ expect(typeof users.useList).toBe('function')
295
+ expect(typeof users.useById).toBe('function')
296
+ expect(typeof users.useSearch).toBe('function')
297
+ expect(typeof users.useCreate).toBe('function')
298
+ expect(typeof users.useUpdate).toBe('function')
299
+ expect(typeof users.useDelete).toBe('function')
300
+ expect(typeof users.useForm).toBe('function')
301
+ expect(typeof users.useTable).toBe('function')
302
+ expect(typeof users.useStore).toBe('function')
303
303
  })
304
304
 
305
- it("auto-generates initial values from schema", () => {
305
+ it('auto-generates initial values from schema', () => {
306
306
  const users = defineFeature<UserValues>({
307
- name: "users-auto-init",
307
+ name: 'users-auto-init',
308
308
  schema: userSchema,
309
- api: "/api/users",
309
+ api: '/api/users',
310
310
  })
311
311
 
312
312
  const client = new QueryClient()
313
313
  const { result: form, unmount } = mountWith(client, () => users.useForm())
314
- expect(form.values().name).toBe("")
314
+ expect(form.values().name).toBe('')
315
315
  expect(form.values().active).toBe(false)
316
316
  unmount()
317
317
  })
@@ -319,16 +319,16 @@ describe("defineFeature", () => {
319
319
 
320
320
  // ─── useList ────────────────────────────────────────────────────────────────────
321
321
 
322
- describe("useList", () => {
323
- it("fetches list from API", async () => {
324
- const mockUsers = [{ name: "Alice", email: "a@t.com", role: "admin", active: true }]
322
+ describe('useList', () => {
323
+ it('fetches list from API', async () => {
324
+ const mockUsers = [{ name: 'Alice', email: 'a@t.com', role: 'admin', active: true }]
325
325
 
326
326
  const users = defineFeature<UserValues>({
327
- name: "users-list",
327
+ name: 'users-list',
328
328
  schema: userSchema,
329
- api: "/api/users",
329
+ api: '/api/users',
330
330
  fetcher: createMockFetch({
331
- "GET /api/users": { body: mockUsers },
331
+ 'GET /api/users': { body: mockUsers },
332
332
  }) as typeof fetch,
333
333
  })
334
334
 
@@ -343,15 +343,15 @@ describe("useList", () => {
343
343
  unmount()
344
344
  })
345
345
 
346
- it("passes query params to URL", async () => {
347
- let capturedUrl = ""
346
+ it('passes query params to URL', async () => {
347
+ let capturedUrl = ''
348
348
  const users = defineFeature<UserValues>({
349
- name: "users-params",
349
+ name: 'users-params',
350
350
  schema: userSchema,
351
- api: "/api/users",
351
+ api: '/api/users',
352
352
  fetcher: (async (url: string) => {
353
353
  capturedUrl = url
354
- return new Response("[]", { status: 200 })
354
+ return new Response('[]', { status: 200 })
355
355
  }) as typeof fetch,
356
356
  })
357
357
 
@@ -360,23 +360,23 @@ describe("useList", () => {
360
360
  })
361
361
  const { unmount } = mountWith(client, () => users.useList({ params: { page: 2 } }))
362
362
  await new Promise((r) => setTimeout(r, 50))
363
- expect(capturedUrl).toContain("page=2")
363
+ expect(capturedUrl).toContain('page=2')
364
364
  unmount()
365
365
  })
366
366
  })
367
367
 
368
368
  // ─── useList pagination ─────────────────────────────────────────────────────────
369
369
 
370
- describe("useList pagination", () => {
371
- it("appends page and pageSize when page is provided as number", async () => {
372
- let capturedUrl = ""
370
+ describe('useList pagination', () => {
371
+ it('appends page and pageSize when page is provided as number', async () => {
372
+ let capturedUrl = ''
373
373
  const users = defineFeature<UserValues>({
374
- name: "users-page-num",
374
+ name: 'users-page-num',
375
375
  schema: userSchema,
376
- api: "/api/users",
376
+ api: '/api/users',
377
377
  fetcher: (async (url: string) => {
378
378
  capturedUrl = url
379
- return new Response("[]", { status: 200 })
379
+ return new Response('[]', { status: 200 })
380
380
  }) as typeof fetch,
381
381
  })
382
382
 
@@ -385,20 +385,20 @@ describe("useList pagination", () => {
385
385
  })
386
386
  const { unmount } = mountWith(client, () => users.useList({ page: 1, pageSize: 10 }))
387
387
  await new Promise((r) => setTimeout(r, 50))
388
- expect(capturedUrl).toContain("page=1")
389
- expect(capturedUrl).toContain("pageSize=10")
388
+ expect(capturedUrl).toContain('page=1')
389
+ expect(capturedUrl).toContain('pageSize=10')
390
390
  unmount()
391
391
  })
392
392
 
393
- it("defaults pageSize to 20 when page is provided", async () => {
394
- let capturedUrl = ""
393
+ it('defaults pageSize to 20 when page is provided', async () => {
394
+ let capturedUrl = ''
395
395
  const users = defineFeature<UserValues>({
396
- name: "users-page-default-size",
396
+ name: 'users-page-default-size',
397
397
  schema: userSchema,
398
- api: "/api/users",
398
+ api: '/api/users',
399
399
  fetcher: (async (url: string) => {
400
400
  capturedUrl = url
401
- return new Response("[]", { status: 200 })
401
+ return new Response('[]', { status: 200 })
402
402
  }) as typeof fetch,
403
403
  })
404
404
 
@@ -407,20 +407,20 @@ describe("useList pagination", () => {
407
407
  })
408
408
  const { unmount } = mountWith(client, () => users.useList({ page: 3 }))
409
409
  await new Promise((r) => setTimeout(r, 50))
410
- expect(capturedUrl).toContain("page=3")
411
- expect(capturedUrl).toContain("pageSize=20")
410
+ expect(capturedUrl).toContain('page=3')
411
+ expect(capturedUrl).toContain('pageSize=20')
412
412
  unmount()
413
413
  })
414
414
 
415
- it("accepts reactive page signal", async () => {
415
+ it('accepts reactive page signal', async () => {
416
416
  const capturedUrls: string[] = []
417
417
  const users = defineFeature<UserValues>({
418
- name: "users-page-signal",
418
+ name: 'users-page-signal',
419
419
  schema: userSchema,
420
- api: "/api/users",
420
+ api: '/api/users',
421
421
  fetcher: (async (url: string) => {
422
422
  capturedUrls.push(url)
423
- return new Response("[]", { status: 200 })
423
+ return new Response('[]', { status: 200 })
424
424
  }) as typeof fetch,
425
425
  })
426
426
 
@@ -430,17 +430,17 @@ describe("useList pagination", () => {
430
430
  })
431
431
  const { unmount } = mountWith(client, () => users.useList({ page, pageSize: 5 }))
432
432
  await new Promise((r) => setTimeout(r, 50))
433
- expect(capturedUrls.some((u) => u.includes("page=1"))).toBe(true)
433
+ expect(capturedUrls.some((u) => u.includes('page=1'))).toBe(true)
434
434
  unmount()
435
435
  })
436
436
 
437
- it("includes page in query key for independent caching", () => {
437
+ it('includes page in query key for independent caching', () => {
438
438
  const users = defineFeature<UserValues>({
439
- name: "users-page-key",
439
+ name: 'users-page-key',
440
440
  schema: userSchema,
441
- api: "/api/users",
441
+ api: '/api/users',
442
442
  fetcher: (async () => {
443
- return new Response("[]", { status: 200 })
443
+ return new Response('[]', { status: 200 })
444
444
  }) as typeof fetch,
445
445
  })
446
446
 
@@ -453,21 +453,21 @@ describe("useList pagination", () => {
453
453
 
454
454
  // ─── useById ────────────────────────────────────────────────────────────────────
455
455
 
456
- describe("useById", () => {
457
- it("fetches single item by ID", async () => {
456
+ describe('useById', () => {
457
+ it('fetches single item by ID', async () => {
458
458
  const mockUser = {
459
- name: "Alice",
460
- email: "a@t.com",
461
- role: "admin",
459
+ name: 'Alice',
460
+ email: 'a@t.com',
461
+ role: 'admin',
462
462
  active: true,
463
463
  }
464
464
 
465
465
  const users = defineFeature<UserValues>({
466
- name: "users-by-id",
466
+ name: 'users-by-id',
467
467
  schema: userSchema,
468
- api: "/api/users",
468
+ api: '/api/users',
469
469
  fetcher: createMockFetch({
470
- "GET /api/users/1": { body: mockUser },
470
+ 'GET /api/users/1': { body: mockUser },
471
471
  }) as typeof fetch,
472
472
  })
473
473
 
@@ -483,21 +483,21 @@ describe("useById", () => {
483
483
 
484
484
  // ─── useCreate ──────────────────────────────────────────────────────────────────
485
485
 
486
- describe("useCreate", () => {
487
- it("posts to API", async () => {
486
+ describe('useCreate', () => {
487
+ it('posts to API', async () => {
488
488
  const users = defineFeature<UserValues>({
489
- name: "users-create",
489
+ name: 'users-create',
490
490
  schema: userSchema,
491
- api: "/api/users",
491
+ api: '/api/users',
492
492
  fetcher: createMockFetch({
493
- "POST /api/users": { body: { name: "Created" } },
493
+ 'POST /api/users': { body: { name: 'Created' } },
494
494
  }) as typeof fetch,
495
495
  })
496
496
 
497
497
  const client = new QueryClient()
498
498
  const { result: mutation, unmount } = mountWith(client, () => users.useCreate())
499
499
 
500
- mutation.mutate({ name: "New" })
500
+ mutation.mutate({ name: 'New' })
501
501
  await new Promise((r) => setTimeout(r, 50))
502
502
  expect(mutation.isSuccess()).toBe(true)
503
503
  unmount()
@@ -506,30 +506,30 @@ describe("useCreate", () => {
506
506
 
507
507
  // ─── useUpdate ──────────────────────────────────────────────────────────────────
508
508
 
509
- describe("useUpdate", () => {
510
- it("sends PUT with id and data", async () => {
511
- let capturedUrl = ""
512
- let capturedMethod = ""
509
+ describe('useUpdate', () => {
510
+ it('sends PUT with id and data', async () => {
511
+ let capturedUrl = ''
512
+ let capturedMethod = ''
513
513
 
514
514
  const users = defineFeature<UserValues>({
515
- name: "users-update",
515
+ name: 'users-update',
516
516
  schema: userSchema,
517
- api: "/api/users",
517
+ api: '/api/users',
518
518
  fetcher: (async (url: string, init?: RequestInit) => {
519
519
  capturedUrl = url
520
- capturedMethod = init?.method ?? "GET"
521
- return new Response("{}", { status: 200 })
520
+ capturedMethod = init?.method ?? 'GET'
521
+ return new Response('{}', { status: 200 })
522
522
  }) as typeof fetch,
523
523
  })
524
524
 
525
525
  const client = new QueryClient()
526
526
  const { result: mutation, unmount } = mountWith(client, () => users.useUpdate())
527
527
 
528
- mutation.mutate({ id: 42, data: { name: "Updated" } })
528
+ mutation.mutate({ id: 42, data: { name: 'Updated' } })
529
529
  await new Promise((r) => setTimeout(r, 50))
530
530
 
531
- expect(capturedUrl).toBe("/api/users/42")
532
- expect(capturedMethod).toBe("PUT")
531
+ expect(capturedUrl).toBe('/api/users/42')
532
+ expect(capturedMethod).toBe('PUT')
533
533
  expect(mutation.isSuccess()).toBe(true)
534
534
  unmount()
535
535
  })
@@ -537,36 +537,36 @@ describe("useUpdate", () => {
537
537
 
538
538
  // ─── Optimistic updates ─────────────────────────────────────────────────────────
539
539
 
540
- describe("optimistic updates", () => {
541
- it("updates cache optimistically before server responds", async () => {
540
+ describe('optimistic updates', () => {
541
+ it('updates cache optimistically before server responds', async () => {
542
542
  let resolveUpdate: ((value: Response) => void) | undefined
543
543
  const mockUser = {
544
544
  id: 1,
545
- name: "Alice",
546
- email: "a@t.com",
547
- role: "admin",
545
+ name: 'Alice',
546
+ email: 'a@t.com',
547
+ role: 'admin',
548
548
  active: true,
549
549
  }
550
550
 
551
551
  const users = defineFeature<UserValues & { id: number }>({
552
- name: "users-optimistic",
552
+ name: 'users-optimistic',
553
553
  schema: z.object({
554
554
  id: z.number(),
555
555
  name: z.string().min(2),
556
556
  email: z.string().email(),
557
- role: z.enum(["admin", "editor", "viewer"]),
557
+ role: z.enum(['admin', 'editor', 'viewer']),
558
558
  active: z.boolean(),
559
559
  }),
560
- api: "/api/users",
560
+ api: '/api/users',
561
561
  fetcher: (async (_url: string, init?: RequestInit) => {
562
- if (init?.method === "PUT") {
562
+ if (init?.method === 'PUT') {
563
563
  return new Promise<Response>((resolve) => {
564
564
  resolveUpdate = resolve
565
565
  })
566
566
  }
567
567
  return new Response(JSON.stringify(mockUser), {
568
568
  status: 200,
569
- headers: { "Content-Type": "application/json" },
569
+ headers: { 'Content-Type': 'application/json' },
570
570
  })
571
571
  }) as typeof fetch,
572
572
  })
@@ -576,23 +576,23 @@ describe("optimistic updates", () => {
576
576
  })
577
577
 
578
578
  // Pre-populate cache
579
- client.setQueryData(["users-optimistic", 1], mockUser)
579
+ client.setQueryData(['users-optimistic', 1], mockUser)
580
580
 
581
581
  const { result: mutation, unmount } = mountWith(client, () => users.useUpdate())
582
582
 
583
583
  // Start mutation — should optimistically update cache
584
- mutation.mutate({ id: 1, data: { name: "Bob" } })
584
+ mutation.mutate({ id: 1, data: { name: 'Bob' } })
585
585
  await new Promise((r) => setTimeout(r, 20))
586
586
 
587
587
  // Cache should be optimistically updated
588
- const cached = client.getQueryData(["users-optimistic", 1]) as Record<string, unknown>
589
- expect(cached.name).toBe("Bob")
588
+ const cached = client.getQueryData(['users-optimistic', 1]) as Record<string, unknown>
589
+ expect(cached.name).toBe('Bob')
590
590
 
591
591
  // Resolve server response
592
592
  resolveUpdate!(
593
- new Response(JSON.stringify({ ...mockUser, name: "Bob" }), {
593
+ new Response(JSON.stringify({ ...mockUser, name: 'Bob' }), {
594
594
  status: 200,
595
- headers: { "Content-Type": "application/json" },
595
+ headers: { 'Content-Type': 'application/json' },
596
596
  }),
597
597
  )
598
598
  await new Promise((r) => setTimeout(r, 50))
@@ -600,35 +600,35 @@ describe("optimistic updates", () => {
600
600
  unmount()
601
601
  })
602
602
 
603
- it("rolls back cache on server error", async () => {
603
+ it('rolls back cache on server error', async () => {
604
604
  const mockUser = {
605
605
  id: 2,
606
- name: "Alice",
607
- email: "a@t.com",
608
- role: "admin",
606
+ name: 'Alice',
607
+ email: 'a@t.com',
608
+ role: 'admin',
609
609
  active: true,
610
610
  }
611
611
 
612
612
  const users = defineFeature<UserValues & { id: number }>({
613
- name: "users-rollback",
613
+ name: 'users-rollback',
614
614
  schema: z.object({
615
615
  id: z.number(),
616
616
  name: z.string().min(2),
617
617
  email: z.string().email(),
618
- role: z.enum(["admin", "editor", "viewer"]),
618
+ role: z.enum(['admin', 'editor', 'viewer']),
619
619
  active: z.boolean(),
620
620
  }),
621
- api: "/api/users",
621
+ api: '/api/users',
622
622
  fetcher: (async (_url: string, init?: RequestInit) => {
623
- if (init?.method === "PUT") {
624
- return new Response(JSON.stringify({ message: "Server error" }), {
623
+ if (init?.method === 'PUT') {
624
+ return new Response(JSON.stringify({ message: 'Server error' }), {
625
625
  status: 500,
626
- headers: { "Content-Type": "application/json" },
626
+ headers: { 'Content-Type': 'application/json' },
627
627
  })
628
628
  }
629
629
  return new Response(JSON.stringify(mockUser), {
630
630
  status: 200,
631
- headers: { "Content-Type": "application/json" },
631
+ headers: { 'Content-Type': 'application/json' },
632
632
  })
633
633
  }) as typeof fetch,
634
634
  })
@@ -638,16 +638,16 @@ describe("optimistic updates", () => {
638
638
  })
639
639
 
640
640
  // Pre-populate cache
641
- client.setQueryData(["users-rollback", 2], mockUser)
641
+ client.setQueryData(['users-rollback', 2], mockUser)
642
642
 
643
643
  const { result: mutation, unmount } = mountWith(client, () => users.useUpdate())
644
644
 
645
- mutation.mutate({ id: 2, data: { name: "Should rollback" } })
645
+ mutation.mutate({ id: 2, data: { name: 'Should rollback' } })
646
646
  await new Promise((r) => setTimeout(r, 100))
647
647
 
648
648
  // Should roll back to original
649
- const cached = client.getQueryData(["users-rollback", 2]) as Record<string, unknown>
650
- expect(cached.name).toBe("Alice")
649
+ const cached = client.getQueryData(['users-rollback', 2]) as Record<string, unknown>
650
+ expect(cached.name).toBe('Alice')
651
651
  expect(mutation.isError()).toBe(true)
652
652
  unmount()
653
653
  })
@@ -655,18 +655,18 @@ describe("optimistic updates", () => {
655
655
 
656
656
  // ─── useDelete ──────────────────────────────────────────────────────────────────
657
657
 
658
- describe("useDelete", () => {
659
- it("sends DELETE with id", async () => {
660
- let capturedUrl = ""
661
- let capturedMethod = ""
658
+ describe('useDelete', () => {
659
+ it('sends DELETE with id', async () => {
660
+ let capturedUrl = ''
661
+ let capturedMethod = ''
662
662
 
663
663
  const users = defineFeature<UserValues>({
664
- name: "users-delete",
664
+ name: 'users-delete',
665
665
  schema: userSchema,
666
- api: "/api/users",
666
+ api: '/api/users',
667
667
  fetcher: (async (url: string, init?: RequestInit) => {
668
668
  capturedUrl = url
669
- capturedMethod = init?.method ?? "GET"
669
+ capturedMethod = init?.method ?? 'GET'
670
670
  return new Response(null, { status: 204 })
671
671
  }) as typeof fetch,
672
672
  })
@@ -677,8 +677,8 @@ describe("useDelete", () => {
677
677
  mutation.mutate(7)
678
678
  await new Promise((r) => setTimeout(r, 50))
679
679
 
680
- expect(capturedUrl).toBe("/api/users/7")
681
- expect(capturedMethod).toBe("DELETE")
680
+ expect(capturedUrl).toBe('/api/users/7')
681
+ expect(capturedMethod).toBe('DELETE')
682
682
  expect(mutation.isSuccess()).toBe(true)
683
683
  unmount()
684
684
  })
@@ -686,71 +686,71 @@ describe("useDelete", () => {
686
686
 
687
687
  // ─── useForm ────────────────────────────────────────────────────────────────────
688
688
 
689
- describe("useForm", () => {
690
- it("creates form with schema validation", () => {
689
+ describe('useForm', () => {
690
+ it('creates form with schema validation', () => {
691
691
  const users = defineFeature<UserValues>({
692
- name: "users-form",
692
+ name: 'users-form',
693
693
  schema: userSchema,
694
- api: "/api/users",
694
+ api: '/api/users',
695
695
  })
696
696
 
697
697
  const client = new QueryClient()
698
698
  const { result: form, unmount } = mountWith(client, () => users.useForm())
699
699
 
700
- expect(typeof form.handleSubmit).toBe("function")
701
- expect(typeof form.register).toBe("function")
700
+ expect(typeof form.handleSubmit).toBe('function')
701
+ expect(typeof form.register).toBe('function')
702
702
  unmount()
703
703
  })
704
704
 
705
- it("submits as POST in create mode", async () => {
706
- let capturedMethod = ""
705
+ it('submits as POST in create mode', async () => {
706
+ let capturedMethod = ''
707
707
  const users = defineFeature<UserValues>({
708
- name: "users-form-post",
708
+ name: 'users-form-post',
709
709
  schema: userSchema,
710
- api: "/api/users",
710
+ api: '/api/users',
711
711
  fetcher: (async (_url: string, init?: RequestInit) => {
712
- capturedMethod = init?.method ?? "GET"
713
- return new Response("{}", { status: 200 })
712
+ capturedMethod = init?.method ?? 'GET'
713
+ return new Response('{}', { status: 200 })
714
714
  }) as typeof fetch,
715
715
  })
716
716
 
717
717
  const client = new QueryClient()
718
718
  const { result: form, unmount } = mountWith(client, () => users.useForm())
719
719
 
720
- form.setFieldValue("name", "Al")
721
- form.setFieldValue("email", "a@t.com")
722
- form.setFieldValue("role", "admin")
723
- form.setFieldValue("active", true)
720
+ form.setFieldValue('name', 'Al')
721
+ form.setFieldValue('email', 'a@t.com')
722
+ form.setFieldValue('role', 'admin')
723
+ form.setFieldValue('active', true)
724
724
  await form.handleSubmit()
725
725
  await new Promise((r) => setTimeout(r, 50))
726
726
 
727
- expect(capturedMethod).toBe("POST")
727
+ expect(capturedMethod).toBe('POST')
728
728
  unmount()
729
729
  })
730
730
 
731
- it("submits as PUT in edit mode", async () => {
732
- let capturedMethod = ""
733
- let capturedUrl = ""
731
+ it('submits as PUT in edit mode', async () => {
732
+ let capturedMethod = ''
733
+ let capturedUrl = ''
734
734
  const users = defineFeature<UserValues>({
735
- name: "users-form-put",
735
+ name: 'users-form-put',
736
736
  schema: userSchema,
737
- api: "/api/users",
737
+ api: '/api/users',
738
738
  fetcher: (async (url: string, init?: RequestInit) => {
739
739
  capturedUrl = url
740
- capturedMethod = init?.method ?? "GET"
741
- return new Response("{}", { status: 200 })
740
+ capturedMethod = init?.method ?? 'GET'
741
+ return new Response('{}', { status: 200 })
742
742
  }) as typeof fetch,
743
743
  })
744
744
 
745
745
  const client = new QueryClient()
746
746
  const { result: form, unmount } = mountWith(client, () =>
747
747
  users.useForm({
748
- mode: "edit",
748
+ mode: 'edit',
749
749
  id: 42,
750
750
  initialValues: {
751
- name: "Al",
752
- email: "a@t.com",
753
- role: "admin",
751
+ name: 'Al',
752
+ email: 'a@t.com',
753
+ role: 'admin',
754
754
  active: true,
755
755
  },
756
756
  }),
@@ -762,19 +762,19 @@ describe("useForm", () => {
762
762
  await form.handleSubmit()
763
763
  await new Promise((r) => setTimeout(r, 50))
764
764
 
765
- expect(capturedMethod).toBe("PUT")
766
- expect(capturedUrl).toBe("/api/users/42")
765
+ expect(capturedMethod).toBe('PUT')
766
+ expect(capturedUrl).toBe('/api/users/42')
767
767
  unmount()
768
768
  })
769
769
 
770
- it("calls onSuccess callback", async () => {
770
+ it('calls onSuccess callback', async () => {
771
771
  let successResult: unknown = null
772
772
  const users = defineFeature<UserValues>({
773
- name: "users-form-cb",
773
+ name: 'users-form-cb',
774
774
  schema: userSchema,
775
- api: "/api/users",
775
+ api: '/api/users',
776
776
  fetcher: createMockFetch({
777
- "POST /api/users": { body: { id: 1 } },
777
+ 'POST /api/users': { body: { id: 1 } },
778
778
  }) as typeof fetch,
779
779
  })
780
780
 
@@ -787,10 +787,10 @@ describe("useForm", () => {
787
787
  }),
788
788
  )
789
789
 
790
- form.setFieldValue("name", "Al")
791
- form.setFieldValue("email", "a@t.com")
792
- form.setFieldValue("role", "admin")
793
- form.setFieldValue("active", true)
790
+ form.setFieldValue('name', 'Al')
791
+ form.setFieldValue('email', 'a@t.com')
792
+ form.setFieldValue('role', 'admin')
793
+ form.setFieldValue('active', true)
794
794
  await form.handleSubmit()
795
795
  await new Promise((r) => setTimeout(r, 50))
796
796
 
@@ -801,28 +801,28 @@ describe("useForm", () => {
801
801
 
802
802
  // ─── Auto-fetch edit form ────────────────────────────────────────────────────
803
803
 
804
- describe("auto-fetch edit form", () => {
805
- it("populates form fields from API when mode is edit", async () => {
804
+ describe('auto-fetch edit form', () => {
805
+ it('populates form fields from API when mode is edit', async () => {
806
806
  const mockUser = {
807
- name: "Alice",
808
- email: "alice@example.com",
809
- role: "admin",
807
+ name: 'Alice',
808
+ email: 'alice@example.com',
809
+ role: 'admin',
810
810
  active: true,
811
811
  }
812
812
 
813
813
  const users = defineFeature<UserValues>({
814
- name: "users-form-autofetch",
814
+ name: 'users-form-autofetch',
815
815
  schema: userSchema,
816
- api: "/api/users",
816
+ api: '/api/users',
817
817
  fetcher: createMockFetch({
818
- "GET /api/users/42": { body: mockUser },
819
- "PUT /api/users/42": { body: mockUser },
818
+ 'GET /api/users/42': { body: mockUser },
819
+ 'PUT /api/users/42': { body: mockUser },
820
820
  }) as typeof fetch,
821
821
  })
822
822
 
823
823
  const client = new QueryClient()
824
824
  const { result: form, unmount } = mountWith(client, () =>
825
- users.useForm({ mode: "edit", id: 42 }),
825
+ users.useForm({ mode: 'edit', id: 42 }),
826
826
  )
827
827
 
828
828
  // Should be loading initially
@@ -832,26 +832,26 @@ describe("auto-fetch edit form", () => {
832
832
 
833
833
  // Should have populated values
834
834
  expect(form.isSubmitting()).toBe(false)
835
- expect(form.values().name).toBe("Alice")
836
- expect(form.values().email).toBe("alice@example.com")
837
- expect(form.values().role).toBe("admin")
835
+ expect(form.values().name).toBe('Alice')
836
+ expect(form.values().email).toBe('alice@example.com')
837
+ expect(form.values().role).toBe('admin')
838
838
  expect(form.values().active).toBe(true)
839
839
  unmount()
840
840
  })
841
841
 
842
- it("clears loading state on fetch error", async () => {
842
+ it('clears loading state on fetch error', async () => {
843
843
  const users = defineFeature<UserValues>({
844
- name: "users-form-autofetch-err",
844
+ name: 'users-form-autofetch-err',
845
845
  schema: userSchema,
846
- api: "/api/users",
846
+ api: '/api/users',
847
847
  fetcher: createMockFetch({
848
- "GET /api/users/999": { status: 404, body: { message: "Not found" } },
848
+ 'GET /api/users/999': { status: 404, body: { message: 'Not found' } },
849
849
  }) as typeof fetch,
850
850
  })
851
851
 
852
852
  const client = new QueryClient()
853
853
  const { result: form, unmount } = mountWith(client, () =>
854
- users.useForm({ mode: "edit", id: 999 }),
854
+ users.useForm({ mode: 'edit', id: 999 }),
855
855
  )
856
856
 
857
857
  expect(form.isSubmitting()).toBe(true)
@@ -863,12 +863,12 @@ describe("auto-fetch edit form", () => {
863
863
 
864
864
  // ─── useStore ──────────────────────────────────────────────────────────────────
865
865
 
866
- describe("useStore", () => {
867
- it("returns a store with items, selected, and loading signals", () => {
866
+ describe('useStore', () => {
867
+ it('returns a store with items, selected, and loading signals', () => {
868
868
  const users = defineFeature<UserValues>({
869
- name: "users-store",
869
+ name: 'users-store',
870
870
  schema: userSchema,
871
- api: "/api/users",
871
+ api: '/api/users',
872
872
  })
873
873
 
874
874
  const client = new QueryClient()
@@ -880,17 +880,17 @@ describe("useStore", () => {
880
880
  unmount()
881
881
  })
882
882
 
883
- it("select() finds item by id from items list", () => {
883
+ it('select() finds item by id from items list', () => {
884
884
  const users = defineFeature<UserValues & { id: number }>({
885
- name: "users-store-select",
885
+ name: 'users-store-select',
886
886
  schema: z.object({
887
887
  id: z.number(),
888
888
  name: z.string().min(2),
889
889
  email: z.string().email(),
890
- role: z.enum(["admin", "editor", "viewer"]),
890
+ role: z.enum(['admin', 'editor', 'viewer']),
891
891
  active: z.boolean(),
892
892
  }),
893
- api: "/api/users",
893
+ api: '/api/users',
894
894
  })
895
895
 
896
896
  const client = new QueryClient()
@@ -899,37 +899,37 @@ describe("useStore", () => {
899
899
  const items = [
900
900
  {
901
901
  id: 1,
902
- name: "Alice",
903
- email: "a@t.com",
904
- role: "admin" as const,
902
+ name: 'Alice',
903
+ email: 'a@t.com',
904
+ role: 'admin' as const,
905
905
  active: true,
906
906
  },
907
907
  {
908
908
  id: 2,
909
- name: "Bob",
910
- email: "b@t.com",
911
- role: "editor" as const,
909
+ name: 'Bob',
910
+ email: 'b@t.com',
911
+ role: 'editor' as const,
912
912
  active: false,
913
913
  },
914
914
  ]
915
915
  storeApi.store.items.set(items)
916
916
  storeApi.store.select(2)
917
917
 
918
- expect(storeApi.store.selected()?.name).toBe("Bob")
918
+ expect(storeApi.store.selected()?.name).toBe('Bob')
919
919
  unmount()
920
920
  })
921
921
 
922
- it("select() sets null when id not found", () => {
922
+ it('select() sets null when id not found', () => {
923
923
  const users = defineFeature<UserValues & { id: number }>({
924
- name: "users-store-select-miss",
924
+ name: 'users-store-select-miss',
925
925
  schema: z.object({
926
926
  id: z.number(),
927
927
  name: z.string().min(2),
928
928
  email: z.string().email(),
929
- role: z.enum(["admin", "editor", "viewer"]),
929
+ role: z.enum(['admin', 'editor', 'viewer']),
930
930
  active: z.boolean(),
931
931
  }),
932
- api: "/api/users",
932
+ api: '/api/users',
933
933
  })
934
934
 
935
935
  const client = new QueryClient()
@@ -938,9 +938,9 @@ describe("useStore", () => {
938
938
  storeApi.store.items.set([
939
939
  {
940
940
  id: 1,
941
- name: "Alice",
942
- email: "a@t.com",
943
- role: "admin" as const,
941
+ name: 'Alice',
942
+ email: 'a@t.com',
943
+ role: 'admin' as const,
944
944
  active: true,
945
945
  },
946
946
  ])
@@ -950,17 +950,17 @@ describe("useStore", () => {
950
950
  unmount()
951
951
  })
952
952
 
953
- it("clear() resets selection to null", () => {
953
+ it('clear() resets selection to null', () => {
954
954
  const users = defineFeature<UserValues & { id: number }>({
955
- name: "users-store-clear",
955
+ name: 'users-store-clear',
956
956
  schema: z.object({
957
957
  id: z.number(),
958
958
  name: z.string().min(2),
959
959
  email: z.string().email(),
960
- role: z.enum(["admin", "editor", "viewer"]),
960
+ role: z.enum(['admin', 'editor', 'viewer']),
961
961
  active: z.boolean(),
962
962
  }),
963
- api: "/api/users",
963
+ api: '/api/users',
964
964
  })
965
965
 
966
966
  const client = new QueryClient()
@@ -969,9 +969,9 @@ describe("useStore", () => {
969
969
  storeApi.store.items.set([
970
970
  {
971
971
  id: 1,
972
- name: "Alice",
973
- email: "a@t.com",
974
- role: "admin" as const,
972
+ name: 'Alice',
973
+ email: 'a@t.com',
974
+ role: 'admin' as const,
975
975
  active: true,
976
976
  },
977
977
  ])
@@ -983,11 +983,11 @@ describe("useStore", () => {
983
983
  unmount()
984
984
  })
985
985
 
986
- it("loading signal reflects loading state", () => {
986
+ it('loading signal reflects loading state', () => {
987
987
  const users = defineFeature<UserValues>({
988
- name: "users-store-loading",
988
+ name: 'users-store-loading',
989
989
  schema: userSchema,
990
- api: "/api/users",
990
+ api: '/api/users',
991
991
  })
992
992
 
993
993
  const client = new QueryClient()
@@ -999,11 +999,11 @@ describe("useStore", () => {
999
999
  unmount()
1000
1000
  })
1001
1001
 
1002
- it("is singleton — same store returned on multiple calls", () => {
1002
+ it('is singleton — same store returned on multiple calls', () => {
1003
1003
  const users = defineFeature<UserValues>({
1004
- name: "users-store-singleton",
1004
+ name: 'users-store-singleton',
1005
1005
  schema: userSchema,
1006
- api: "/api/users",
1006
+ api: '/api/users',
1007
1007
  })
1008
1008
 
1009
1009
  const client = new QueryClient()
@@ -1020,54 +1020,54 @@ describe("useStore", () => {
1020
1020
 
1021
1021
  // ─── useTable ───────────────────────────────────────────────────────────────────
1022
1022
 
1023
- describe("useTable", () => {
1024
- it("creates table with schema-inferred columns", () => {
1023
+ describe('useTable', () => {
1024
+ it('creates table with schema-inferred columns', () => {
1025
1025
  const users = defineFeature<UserValues>({
1026
- name: "users-table",
1026
+ name: 'users-table',
1027
1027
  schema: userSchema,
1028
- api: "/api/users",
1028
+ api: '/api/users',
1029
1029
  })
1030
1030
 
1031
- const data: UserValues[] = [{ name: "Alice", email: "a@t.com", role: "admin", active: true }]
1031
+ const data: UserValues[] = [{ name: 'Alice', email: 'a@t.com', role: 'admin', active: true }]
1032
1032
 
1033
1033
  const client = new QueryClient()
1034
1034
  const { result, unmount } = mountWith(client, () => users.useTable(data))
1035
1035
 
1036
1036
  expect(result.columns.length).toBeGreaterThan(0)
1037
- expect(result.columns[0]!.name).toBe("name")
1038
- expect(result.columns[0]!.label).toBe("Name")
1037
+ expect(result.columns[0]!.name).toBe('name')
1038
+ expect(result.columns[0]!.label).toBe('Name')
1039
1039
  expect(result.table().getRowModel().rows).toHaveLength(1)
1040
1040
  unmount()
1041
1041
  })
1042
1042
 
1043
- it("filters columns by name", () => {
1043
+ it('filters columns by name', () => {
1044
1044
  const users = defineFeature<UserValues>({
1045
- name: "users-table-cols",
1045
+ name: 'users-table-cols',
1046
1046
  schema: userSchema,
1047
- api: "/api/users",
1047
+ api: '/api/users',
1048
1048
  })
1049
1049
 
1050
1050
  const client = new QueryClient()
1051
1051
  const { result, unmount } = mountWith(client, () =>
1052
- users.useTable([{ name: "Alice", email: "a@t.com", role: "admin", active: true }], {
1053
- columns: ["name", "email"],
1052
+ users.useTable([{ name: 'Alice', email: 'a@t.com', role: 'admin', active: true }], {
1053
+ columns: ['name', 'email'],
1054
1054
  }),
1055
1055
  )
1056
1056
 
1057
1057
  expect(result.columns).toHaveLength(2)
1058
- expect(result.columns.map((c) => c.name)).toEqual(["name", "email"])
1058
+ expect(result.columns.map((c) => c.name)).toEqual(['name', 'email'])
1059
1059
  unmount()
1060
1060
  })
1061
1061
 
1062
- it("accepts reactive data function", () => {
1062
+ it('accepts reactive data function', () => {
1063
1063
  const users = defineFeature<UserValues>({
1064
- name: "users-table-fn",
1064
+ name: 'users-table-fn',
1065
1065
  schema: userSchema,
1066
- api: "/api/users",
1066
+ api: '/api/users',
1067
1067
  })
1068
1068
 
1069
1069
  const data = signal<UserValues[]>([
1070
- { name: "Alice", email: "a@t.com", role: "admin", active: true },
1070
+ { name: 'Alice', email: 'a@t.com', role: 'admin', active: true },
1071
1071
  ])
1072
1072
 
1073
1073
  const client = new QueryClient()
@@ -1080,41 +1080,41 @@ describe("useTable", () => {
1080
1080
 
1081
1081
  // ─── useSearch ──────────────────────────────────────────────────────────────────
1082
1082
 
1083
- describe("useSearch", () => {
1084
- it("passes search term as query param", async () => {
1085
- let capturedUrl = ""
1083
+ describe('useSearch', () => {
1084
+ it('passes search term as query param', async () => {
1085
+ let capturedUrl = ''
1086
1086
  const users = defineFeature<UserValues>({
1087
- name: "users-search",
1087
+ name: 'users-search',
1088
1088
  schema: userSchema,
1089
- api: "/api/users",
1089
+ api: '/api/users',
1090
1090
  fetcher: (async (url: string) => {
1091
1091
  capturedUrl = url
1092
- return new Response("[]", { status: 200 })
1092
+ return new Response('[]', { status: 200 })
1093
1093
  }) as typeof fetch,
1094
1094
  })
1095
1095
 
1096
- const term = signal("alice")
1096
+ const term = signal('alice')
1097
1097
  const client = new QueryClient({
1098
1098
  defaultOptions: { queries: { retry: false } },
1099
1099
  })
1100
1100
  const { unmount } = mountWith(client, () => users.useSearch(term))
1101
1101
 
1102
1102
  await new Promise((r) => setTimeout(r, 50))
1103
- expect(capturedUrl).toContain("q=alice")
1103
+ expect(capturedUrl).toContain('q=alice')
1104
1104
  unmount()
1105
1105
  })
1106
1106
 
1107
- it("disables query when search term is empty", () => {
1107
+ it('disables query when search term is empty', () => {
1108
1108
  const users = defineFeature<UserValues>({
1109
- name: "users-search-empty",
1109
+ name: 'users-search-empty',
1110
1110
  schema: userSchema,
1111
- api: "/api/users",
1111
+ api: '/api/users',
1112
1112
  fetcher: (() => {
1113
- throw new Error("Should not fetch")
1113
+ throw new Error('Should not fetch')
1114
1114
  }) as typeof fetch,
1115
1115
  })
1116
1116
 
1117
- const term = signal("")
1117
+ const term = signal('')
1118
1118
  const client = new QueryClient()
1119
1119
  const { result: query, unmount } = mountWith(client, () => users.useSearch(term))
1120
1120
 
@@ -1126,14 +1126,14 @@ describe("useSearch", () => {
1126
1126
 
1127
1127
  // ─── Error handling ────────────────────────────────────────────────────────────
1128
1128
 
1129
- describe("error handling", () => {
1130
- it("handles API errors in useList", async () => {
1129
+ describe('error handling', () => {
1130
+ it('handles API errors in useList', async () => {
1131
1131
  const users = defineFeature<UserValues>({
1132
- name: "users-err",
1132
+ name: 'users-err',
1133
1133
  schema: userSchema,
1134
- api: "/api/users",
1134
+ api: '/api/users',
1135
1135
  fetcher: createMockFetch({
1136
- "GET /api/users": { status: 500, body: { message: "Server error" } },
1136
+ 'GET /api/users': { status: 500, body: { message: 'Server error' } },
1137
1137
  }) as typeof fetch,
1138
1138
  })
1139
1139
 
@@ -1147,15 +1147,15 @@ describe("error handling", () => {
1147
1147
  unmount()
1148
1148
  })
1149
1149
 
1150
- it("parses structured error body from API", async () => {
1150
+ it('parses structured error body from API', async () => {
1151
1151
  const users = defineFeature<UserValues>({
1152
- name: "users-err-struct",
1152
+ name: 'users-err-struct',
1153
1153
  schema: userSchema,
1154
- api: "/api/users",
1154
+ api: '/api/users',
1155
1155
  fetcher: createMockFetch({
1156
- "POST /api/users": {
1156
+ 'POST /api/users': {
1157
1157
  status: 422,
1158
- body: { message: "Validation failed", errors: { email: "Taken" } },
1158
+ body: { message: 'Validation failed', errors: { email: 'Taken' } },
1159
1159
  },
1160
1160
  }) as typeof fetch,
1161
1161
  })
@@ -1163,49 +1163,49 @@ describe("error handling", () => {
1163
1163
  const client = new QueryClient()
1164
1164
  const { result: mutation, unmount } = mountWith(client, () => users.useCreate())
1165
1165
 
1166
- mutation.mutate({ name: "Test" })
1166
+ mutation.mutate({ name: 'Test' })
1167
1167
  await new Promise((r) => setTimeout(r, 50))
1168
1168
 
1169
1169
  expect(mutation.isError()).toBe(true)
1170
1170
  const err = mutation.error() as Error & { errors?: unknown }
1171
- expect(err.message).toBe("Validation failed")
1172
- expect(err.errors).toEqual({ email: "Taken" })
1171
+ expect(err.message).toBe('Validation failed')
1172
+ expect(err.errors).toEqual({ email: 'Taken' })
1173
1173
  unmount()
1174
1174
  })
1175
1175
  })
1176
1176
 
1177
1177
  // ─── References ─────────────────────────────────────────────────────────────────
1178
1178
 
1179
- describe("reference", () => {
1180
- it("creates a reference schema object", () => {
1179
+ describe('reference', () => {
1180
+ it('creates a reference schema object', () => {
1181
1181
  const users = defineFeature<UserValues>({
1182
- name: "users-ref",
1182
+ name: 'users-ref',
1183
1183
  schema: userSchema,
1184
- api: "/api/users",
1184
+ api: '/api/users',
1185
1185
  })
1186
1186
 
1187
1187
  const ref = reference(users)
1188
1188
  expect(isReference(ref)).toBe(true)
1189
- expect(ref._featureName).toBe("users-ref")
1189
+ expect(ref._featureName).toBe('users-ref')
1190
1190
  })
1191
1191
 
1192
- it("validates string and number IDs", () => {
1192
+ it('validates string and number IDs', () => {
1193
1193
  const users = defineFeature<UserValues>({
1194
- name: "users-ref-validate",
1194
+ name: 'users-ref-validate',
1195
1195
  schema: userSchema,
1196
- api: "/api/users",
1196
+ api: '/api/users',
1197
1197
  })
1198
1198
 
1199
1199
  const ref = reference(users)
1200
1200
  expect(ref.safeParse(42).success).toBe(true)
1201
- expect(ref.safeParse("abc-123").success).toBe(true)
1201
+ expect(ref.safeParse('abc-123').success).toBe(true)
1202
1202
  expect(ref.safeParse(null).success).toBe(false)
1203
1203
  expect(ref.safeParse(undefined).success).toBe(false)
1204
1204
  expect(ref.safeParse({}).success).toBe(false)
1205
1205
  })
1206
1206
 
1207
- it("safeParseAsync works the same as safeParse", async () => {
1208
- const ref = reference({ name: "test" })
1207
+ it('safeParseAsync works the same as safeParse', async () => {
1208
+ const ref = reference({ name: 'test' })
1209
1209
  const result = await ref.safeParseAsync(42)
1210
1210
  expect(result.success).toBe(true)
1211
1211
 
@@ -1213,20 +1213,20 @@ describe("reference", () => {
1213
1213
  expect(fail.success).toBe(false)
1214
1214
  })
1215
1215
 
1216
- it("isReference returns false for non-reference objects", () => {
1216
+ it('isReference returns false for non-reference objects', () => {
1217
1217
  expect(isReference(null)).toBe(false)
1218
1218
  expect(isReference(undefined)).toBe(false)
1219
1219
  expect(isReference(42)).toBe(false)
1220
- expect(isReference("string")).toBe(false)
1220
+ expect(isReference('string')).toBe(false)
1221
1221
  expect(isReference({})).toBe(false)
1222
1222
  expect(isReference(z.string())).toBe(false)
1223
1223
  })
1224
1224
 
1225
- it("reference fields detected in schema introspection", () => {
1225
+ it('reference fields detected in schema introspection', () => {
1226
1226
  const users = defineFeature<UserValues>({
1227
- name: "users-ref-introspect",
1227
+ name: 'users-ref-introspect',
1228
1228
  schema: userSchema,
1229
- api: "/api/users",
1229
+ api: '/api/users',
1230
1230
  })
1231
1231
 
1232
1232
  const postSchema = z.object({
@@ -1236,63 +1236,63 @@ describe("reference", () => {
1236
1236
  })
1237
1237
 
1238
1238
  const fields = extractFields(postSchema)
1239
- const authorField = fields.find((f) => f.name === "authorId")
1239
+ const authorField = fields.find((f) => f.name === 'authorId')
1240
1240
  expect(authorField).toBeDefined()
1241
- expect(authorField!.type).toBe("reference")
1242
- expect(authorField!.referenceTo).toBe("users-ref-introspect")
1241
+ expect(authorField!.type).toBe('reference')
1242
+ expect(authorField!.referenceTo).toBe('users-ref-introspect')
1243
1243
  })
1244
1244
 
1245
- it("reference fields in defineFeature schema produce reference FieldInfo", () => {
1245
+ it('reference fields in defineFeature schema produce reference FieldInfo', () => {
1246
1246
  const users = defineFeature<UserValues>({
1247
- name: "users-ref-in-feat",
1247
+ name: 'users-ref-in-feat',
1248
1248
  schema: userSchema,
1249
- api: "/api/users",
1249
+ api: '/api/users',
1250
1250
  })
1251
1251
 
1252
1252
  type PostValues = { title: string; body: string; authorId: string }
1253
1253
  const posts = defineFeature<PostValues>({
1254
- name: "posts-ref",
1254
+ name: 'posts-ref',
1255
1255
  schema: z.object({
1256
1256
  title: z.string(),
1257
1257
  body: z.string(),
1258
1258
  authorId: reference(users) as any,
1259
1259
  }),
1260
- api: "/api/posts",
1260
+ api: '/api/posts',
1261
1261
  })
1262
1262
 
1263
- const authorField = posts.fields.find((f) => f.name === "authorId")
1263
+ const authorField = posts.fields.find((f) => f.name === 'authorId')
1264
1264
  expect(authorField).toBeDefined()
1265
- expect(authorField!.type).toBe("reference")
1266
- expect(authorField!.referenceTo).toBe("users-ref-in-feat")
1265
+ expect(authorField!.type).toBe('reference')
1266
+ expect(authorField!.referenceTo).toBe('users-ref-in-feat')
1267
1267
  })
1268
1268
  })
1269
1269
 
1270
1270
  // ─── Edge case coverage ────────────────────────────────────────────────────────
1271
1271
 
1272
- describe("edge cases", () => {
1273
- it("defineFeature works without a Zod schema (no validation)", () => {
1272
+ describe('edge cases', () => {
1273
+ it('defineFeature works without a Zod schema (no validation)', () => {
1274
1274
  const items = defineFeature<{ title: string }>({
1275
- name: "items-no-zod",
1275
+ name: 'items-no-zod',
1276
1276
  schema: { notAZodSchema: true } as { _output?: { title: string } },
1277
- api: "/api/items",
1277
+ api: '/api/items',
1278
1278
  })
1279
1279
 
1280
1280
  const client = new QueryClient()
1281
1281
  const { result: form, unmount } = mountWith(client, () => items.useForm())
1282
- expect(typeof form.handleSubmit).toBe("function")
1282
+ expect(typeof form.handleSubmit).toBe('function')
1283
1283
  unmount()
1284
1284
  })
1285
1285
 
1286
- it("useForm onError callback fires on submit failure", async () => {
1286
+ it('useForm onError callback fires on submit failure', async () => {
1287
1287
  let caughtError: unknown = null
1288
1288
  const users = defineFeature<UserValues>({
1289
- name: "users-form-onerror",
1289
+ name: 'users-form-onerror',
1290
1290
  schema: userSchema,
1291
- api: "/api/users",
1291
+ api: '/api/users',
1292
1292
  fetcher: createMockFetch({
1293
- "POST /api/users": {
1293
+ 'POST /api/users': {
1294
1294
  status: 500,
1295
- body: { message: "Server broke" },
1295
+ body: { message: 'Server broke' },
1296
1296
  },
1297
1297
  }) as typeof fetch,
1298
1298
  })
@@ -1306,10 +1306,10 @@ describe("edge cases", () => {
1306
1306
  }),
1307
1307
  )
1308
1308
 
1309
- form.setFieldValue("name", "Al")
1310
- form.setFieldValue("email", "a@t.com")
1311
- form.setFieldValue("role", "admin")
1312
- form.setFieldValue("active", true)
1309
+ form.setFieldValue('name', 'Al')
1310
+ form.setFieldValue('email', 'a@t.com')
1311
+ form.setFieldValue('role', 'admin')
1312
+ form.setFieldValue('active', true)
1313
1313
 
1314
1314
  try {
1315
1315
  await form.handleSubmit()
@@ -1322,32 +1322,32 @@ describe("edge cases", () => {
1322
1322
  unmount()
1323
1323
  })
1324
1324
 
1325
- it("useTable sorting via direct state object (non-function updater)", () => {
1325
+ it('useTable sorting via direct state object (non-function updater)', () => {
1326
1326
  const users = defineFeature<UserValues>({
1327
- name: "users-table-sort-direct",
1327
+ name: 'users-table-sort-direct',
1328
1328
  schema: userSchema,
1329
- api: "/api/users",
1329
+ api: '/api/users',
1330
1330
  })
1331
1331
 
1332
1332
  const data: UserValues[] = [
1333
- { name: "Bob", email: "b@t.com", role: "editor", active: true },
1334
- { name: "Alice", email: "a@t.com", role: "admin", active: true },
1333
+ { name: 'Bob', email: 'b@t.com', role: 'editor', active: true },
1334
+ { name: 'Alice', email: 'a@t.com', role: 'admin', active: true },
1335
1335
  ]
1336
1336
 
1337
1337
  const client = new QueryClient()
1338
1338
  const { result, unmount } = mountWith(client, () => users.useTable(data))
1339
1339
 
1340
1340
  // Trigger sorting via the table's toggle handler (function updater)
1341
- result.table().getColumn("name")!.toggleSorting(false)
1341
+ result.table().getColumn('name')!.toggleSorting(false)
1342
1342
  expect(result.sorting().length).toBe(1)
1343
1343
 
1344
1344
  // Also set sorting directly (non-function updater path)
1345
- result.sorting.set([{ id: "email", desc: true }])
1346
- expect(result.sorting()).toEqual([{ id: "email", desc: true }])
1345
+ result.sorting.set([{ id: 'email', desc: true }])
1346
+ expect(result.sorting()).toEqual([{ id: 'email', desc: true }])
1347
1347
 
1348
1348
  // Set globalFilter directly
1349
- result.globalFilter.set("alice")
1350
- expect(result.globalFilter()).toBe("alice")
1349
+ result.globalFilter.set('alice')
1350
+ expect(result.globalFilter()).toBe('alice')
1351
1351
 
1352
1352
  unmount()
1353
1353
  })