@pyreon/feature 0.10.0 → 0.11.1

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