@pyreon/feature 0.3.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,501 +1,259 @@
1
- import { batch, signal } from "@pyreon/reactivity";
2
- import { useForm } from "@pyreon/form";
3
- import { zodSchema } from "@pyreon/validation";
4
- import { useMutation, useQuery, useQueryClient } from "@pyreon/query";
5
- import { getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useTable } from "@pyreon/table";
6
- import { defineStore } from "@pyreon/store";
7
-
8
- //#region src/schema.ts
9
- /** Symbol used to tag reference schema objects. */
1
+ import * as _pyreon_table0 from "@pyreon/table";
2
+ import { SortingState } from "@pyreon/table";
3
+ import { FormState, SchemaValidateFn } from "@pyreon/form";
4
+ import { QueryKey, UseMutationResult, UseQueryResult } from "@pyreon/query";
5
+ import { Computed, Signal } from "@pyreon/reactivity";
6
+ import { StoreApi } from "@pyreon/store";
10
7
 
8
+ //#region src/schema.d.ts
11
9
  /**
12
- * Check if a value is a reference schema created by `reference()`.
13
- */
14
- function isReference(value) {
15
- return value !== null && typeof value === "object" && value[REFERENCE_TAG] === true;
10
+ * Schema introspection utilities.
11
+ *
12
+ * Extracts field names, types, and metadata from Zod schemas at runtime
13
+ * without importing Zod types directly (duck-typed).
14
+ */
15
+ interface FieldInfo {
16
+ /** Field name (key in the schema object). */
17
+ name: string;
18
+ /** Inferred type: 'string' | 'number' | 'boolean' | 'date' | 'enum' | 'array' | 'object' | 'reference' | 'unknown'. */
19
+ type: FieldType;
20
+ /** Whether the field is optional. */
21
+ optional: boolean;
22
+ /** For enum fields, the list of allowed values. */
23
+ enumValues?: (string | number)[];
24
+ /** For reference fields, the name of the referenced feature. */
25
+ referenceTo?: string;
26
+ /** Human-readable label derived from field name. */
27
+ label: string;
16
28
  }
29
+ type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'enum' | 'array' | 'object' | 'reference' | 'unknown';
17
30
  /**
18
- * Create a reference field that links to another feature.
19
- *
20
- * Returns a Zod-compatible schema that validates as `string | number` and
21
- * carries metadata about the referenced feature for form dropdowns and table links.
22
- *
23
- * @example
24
- * ```ts
25
- * import { defineFeature, reference } from '@pyreon/feature'
26
- *
27
- * const posts = defineFeature({
28
- * name: 'posts',
29
- * schema: z.object({
30
- * title: z.string(),
31
- * authorId: reference(users),
32
- * }),
33
- * api: '/api/posts',
34
- * })
35
- * ```
36
- */
37
- function reference(feature) {
38
- const featureName = feature.name;
39
- function validateRef(value) {
40
- if (typeof value === "string" || typeof value === "number") return {
41
- success: true
31
+ * Metadata carried by a reference schema.
32
+ */
33
+ interface ReferenceSchema {
34
+ /** Marker symbol for detection. */
35
+ [key: symbol]: true;
36
+ /** Name of the referenced feature. */
37
+ _featureName: string;
38
+ /** Duck-typed Zod-like interface: validates as string | number. */
39
+ safeParse: (value: unknown) => {
40
+ success: boolean;
41
+ error?: {
42
+ issues: {
43
+ message: string;
44
+ }[];
42
45
  };
43
- return {
44
- success: false,
45
- error: {
46
- issues: [{
47
- message: `Expected string or number reference to ${featureName}, got ${typeof value}`
48
- }]
49
- }
46
+ };
47
+ /** Async variant for compatibility. */
48
+ safeParseAsync: (value: unknown) => Promise<{
49
+ success: boolean;
50
+ error?: {
51
+ issues: {
52
+ message: string;
53
+ }[];
50
54
  };
51
- }
52
- return {
53
- [REFERENCE_TAG]: true,
54
- _featureName: featureName,
55
- safeParse: validateRef,
56
- safeParseAsync: async value => validateRef(value),
57
- _def: {
58
- typeName: "ZodString"
59
- }
55
+ }>;
56
+ /** Shape-like marker for schema introspection. */
57
+ _def: {
58
+ typeName: string;
60
59
  };
61
60
  }
62
61
  /**
63
- * Convert a field name to a human-readable label.
64
- * e.g., 'firstName' → 'First Name', 'created_at' → 'Created At'
65
- */
66
- function nameToLabel(name) {
67
- return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ").replace(/\b\w/g, c => c.toUpperCase());
68
- }
62
+ * Check if a value is a reference schema created by `reference()`.
63
+ */
64
+ declare function isReference(value: unknown): value is ReferenceSchema;
69
65
  /**
70
- * Detect the field type from a Zod schema shape entry.
71
- * Duck-typed — works with Zod v3 and v4 without importing Zod.
72
- */
73
- function detectFieldType(zodField) {
74
- if (isReference(zodField)) return {
75
- type: "reference",
76
- optional: false,
77
- referenceTo: zodField._featureName
78
- };
79
- if (!zodField || typeof zodField !== "object") return {
80
- type: "unknown",
81
- optional: false
82
- };
83
- let inner = zodField;
84
- let optional = false;
85
- const getTypeName = obj => {
86
- const def = obj._def;
87
- if (def?.typeName && typeof def.typeName === "string") return def.typeName;
88
- const zodDef = obj._zod?.def;
89
- if (zodDef?.type && typeof zodDef.type === "string") return zodDef.type;
90
- };
91
- const typeName = getTypeName(inner);
92
- if (typeName === "ZodOptional" || typeName === "ZodNullable" || typeName === "optional" || typeName === "nullable") {
93
- optional = true;
94
- const innerType = inner._def?.innerType ?? inner._zod?.def;
95
- if (innerType && typeof innerType === "object") inner = innerType;
96
- }
97
- const innerTypeName = getTypeName(inner) ?? typeName;
98
- if (!innerTypeName) return {
99
- type: "unknown",
100
- optional
101
- };
102
- const type = {
103
- ZodString: "string",
104
- ZodNumber: "number",
105
- ZodBoolean: "boolean",
106
- ZodDate: "date",
107
- ZodEnum: "enum",
108
- ZodNativeEnum: "enum",
109
- ZodArray: "array",
110
- ZodObject: "object",
111
- string: "string",
112
- number: "number",
113
- boolean: "boolean",
114
- date: "date",
115
- enum: "enum",
116
- array: "array",
117
- object: "object"
118
- }[innerTypeName] ?? "string";
119
- let enumValues;
120
- if (type === "enum") {
121
- const def = inner._def;
122
- if (def?.values && Array.isArray(def.values)) enumValues = def.values;
123
- const zodDef = inner._zod?.def;
124
- if (zodDef?.values && Array.isArray(zodDef.values)) enumValues = zodDef.values;
125
- }
126
- return {
127
- type,
128
- optional,
129
- enumValues
130
- };
66
+ * Create a reference field that links to another feature.
67
+ *
68
+ * Returns a Zod-compatible schema that validates as `string | number` and
69
+ * carries metadata about the referenced feature for form dropdowns and table links.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * import { defineFeature, reference } from '@pyreon/feature'
74
+ *
75
+ * const posts = defineFeature({
76
+ * name: 'posts',
77
+ * schema: z.object({
78
+ * title: z.string(),
79
+ * authorId: reference(users),
80
+ * }),
81
+ * api: '/api/posts',
82
+ * })
83
+ * ```
84
+ */
85
+ declare function reference(feature: {
86
+ name: string;
87
+ }): ReferenceSchema;
88
+ /**
89
+ * Extract field information from a Zod object schema.
90
+ * Returns an array of FieldInfo objects describing each field.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const schema = z.object({ name: z.string(), age: z.number().optional() })
95
+ * const fields = extractFields(schema)
96
+ * // [
97
+ * // { name: 'name', type: 'string', optional: false, label: 'Name' },
98
+ * // { name: 'age', type: 'number', optional: true, label: 'Age' },
99
+ * // ]
100
+ * ```
101
+ */
102
+ declare function extractFields(schema: unknown): FieldInfo[];
103
+ /**
104
+ * Generate default initial values from a schema's field types.
105
+ */
106
+ declare function defaultInitialValues(fields: FieldInfo[]): Record<string, unknown>;
107
+ //#endregion
108
+ //#region src/types.d.ts
109
+ /**
110
+ * Configuration for defining a feature.
111
+ */
112
+ interface FeatureConfig<TValues extends Record<string, unknown>> {
113
+ /** Unique feature name — used for store ID and query key namespace. */
114
+ name: string;
115
+ /** Validation schema (Zod, Valibot, or ArkType). Duck-typed — must have `safeParseAsync` for auto-validation. */
116
+ schema: unknown;
117
+ /** Custom schema-level validation function. If provided, overrides auto-detection from schema. */
118
+ validate?: SchemaValidateFn<TValues>;
119
+ /** API base path (e.g., '/api/users'). */
120
+ api: string;
121
+ /** Default initial values for create forms. If not provided, auto-generated from schema field types. */
122
+ initialValues?: Partial<TValues>;
123
+ /** Custom fetch function. Defaults to global fetch. */
124
+ fetcher?: typeof fetch;
131
125
  }
132
126
  /**
133
- * Extract field information from a Zod object schema.
134
- * Returns an array of FieldInfo objects describing each field.
135
- *
136
- * @example
137
- * ```ts
138
- * const schema = z.object({ name: z.string(), age: z.number().optional() })
139
- * const fields = extractFields(schema)
140
- * // [
141
- * // { name: 'name', type: 'string', optional: false, label: 'Name' },
142
- * // { name: 'age', type: 'number', optional: true, label: 'Age' },
143
- * // ]
144
- * ```
145
- */
146
- function extractFields(schema) {
147
- if (!schema || typeof schema !== "object") return [];
148
- const s = schema;
149
- let shape;
150
- if (s.shape && typeof s.shape === "object") shape = s.shape;
151
- if (!shape) {
152
- const def = s._def;
153
- if (def?.shape) shape = typeof def.shape === "function" ? def.shape() : def.shape;
154
- }
155
- if (!shape) {
156
- const zodDef = s._zod?.def;
157
- if (zodDef?.shape && typeof zodDef.shape === "object") shape = zodDef.shape;
158
- }
159
- if (!shape) return [];
160
- return Object.entries(shape).map(([name, fieldSchema]) => {
161
- const {
162
- type,
163
- optional,
164
- enumValues,
165
- referenceTo
166
- } = detectFieldType(fieldSchema);
167
- const info = {
168
- name,
169
- type,
170
- optional,
171
- label: nameToLabel(name)
172
- };
173
- if (enumValues) info.enumValues = enumValues;
174
- if (referenceTo) info.referenceTo = referenceTo;
175
- return info;
176
- });
127
+ * Query options for useList.
128
+ */
129
+ interface ListOptions {
130
+ /** Additional query parameters appended to the URL. */
131
+ params?: Record<string, string | number | boolean>;
132
+ /** Reactive page number. When provided, `page` and `pageSize` are appended to query params. */
133
+ page?: number | Signal<number>;
134
+ /** Items per page. Defaults to 20 when `page` is provided. */
135
+ pageSize?: number;
136
+ /** Override stale time for this query. */
137
+ staleTime?: number;
138
+ /** Enable/disable the query. */
139
+ enabled?: boolean;
177
140
  }
178
141
  /**
179
- * Generate default initial values from a schema's field types.
180
- */
181
- function defaultInitialValues(fields) {
182
- const values = {};
183
- for (const field of fields) switch (field.type) {
184
- case "string":
185
- values[field.name] = "";
186
- break;
187
- case "number":
188
- values[field.name] = 0;
189
- break;
190
- case "boolean":
191
- values[field.name] = false;
192
- break;
193
- case "enum":
194
- values[field.name] = field.enumValues?.[0] ?? "";
195
- break;
196
- case "date":
197
- values[field.name] = "";
198
- break;
199
- default:
200
- values[field.name] = "";
201
- }
202
- return values;
142
+ * Form options for useForm.
143
+ */
144
+ interface FeatureFormOptions<TValues extends Record<string, unknown>> {
145
+ /** 'create' (default) or 'edit'. Edit mode uses PUT instead of POST. */
146
+ mode?: 'create' | 'edit';
147
+ /** Item ID — required when mode is 'edit'. Used to PUT to api/:id and auto-fetch data. */
148
+ id?: string | number;
149
+ /** Override initial values (merged with feature defaults). */
150
+ initialValues?: Partial<TValues>;
151
+ /** When to validate: 'blur' (default), 'change', or 'submit'. */
152
+ validateOn?: 'blur' | 'change' | 'submit';
153
+ /** Callback after successful create/update. */
154
+ onSuccess?: (result: unknown) => void;
155
+ /** Callback on submit error. */
156
+ onError?: (error: unknown) => void;
203
157
  }
204
-
205
- //#endregion
206
- //#region src/define-feature.ts
207
- function createFetcher(baseFetcher = fetch) {
208
- async function request(url, init) {
209
- const res = await baseFetcher(url, init);
210
- if (!res.ok) {
211
- let message = `${init?.method ?? "GET"} ${url} failed: ${res.status}`;
212
- try {
213
- const body = await res.json();
214
- if (body?.message) message = body.message;
215
- if (body?.errors) throw Object.assign(new Error(message), {
216
- status: res.status,
217
- errors: body.errors
218
- });
219
- } catch (e) {
220
- if (e instanceof Error && "errors" in e) throw e;
221
- }
222
- throw Object.assign(new Error(message), {
223
- status: res.status
224
- });
225
- }
226
- if (res.status === 204) return void 0;
227
- return res.json();
228
- }
229
- return {
230
- list(url, params) {
231
- return request(`${url}${params ? `?${new URLSearchParams(Object.entries(params).map(([k, v]) => [k, String(v)])).toString()}` : ""}`);
232
- },
233
- getById(url, id) {
234
- return request(`${url}/${id}`);
235
- },
236
- create(url, data) {
237
- return request(url, {
238
- method: "POST",
239
- headers: {
240
- "Content-Type": "application/json"
241
- },
242
- body: JSON.stringify(data)
243
- });
244
- },
245
- update(url, id, data) {
246
- return request(`${url}/${id}`, {
247
- method: "PUT",
248
- headers: {
249
- "Content-Type": "application/json"
250
- },
251
- body: JSON.stringify(data)
252
- });
253
- },
254
- delete(url, id) {
255
- return request(`${url}/${id}`, {
256
- method: "DELETE"
257
- });
258
- }
259
- };
158
+ /**
159
+ * Table options for useTable.
160
+ */
161
+ interface FeatureTableOptions<TValues extends Record<string, unknown>> {
162
+ /** Subset of schema fields to show as columns. If not provided, all fields are shown. */
163
+ columns?: (keyof TValues & string)[];
164
+ /** Per-column overrides (header text, cell renderer, size, etc.). */
165
+ columnOverrides?: Partial<Record<keyof TValues & string, Record<string, unknown>>>;
166
+ /** Page size for pagination. If not provided, pagination is disabled. */
167
+ pageSize?: number;
260
168
  }
261
- function createValidator(schema, customValidate) {
262
- if (customValidate) return customValidate;
263
- if (schema && typeof schema === "object" && "safeParseAsync" in schema && typeof schema.safeParseAsync === "function") return zodSchema(schema);
169
+ /**
170
+ * Return type of feature.useTable().
171
+ */
172
+ interface FeatureTableResult<TValues extends Record<string, unknown>> {
173
+ /** The reactive TanStack Table instance. */
174
+ table: Computed<_pyreon_table0.Table<TValues>>;
175
+ /** Sorting state signal — bind to UI controls. */
176
+ sorting: Signal<SortingState>;
177
+ /** Global filter signal — bind to search input. */
178
+ globalFilter: Signal<string>;
179
+ /** Column metadata from schema introspection. */
180
+ columns: FieldInfo[];
264
181
  }
265
- function resolvePageValue(page) {
266
- if (page === void 0) return void 0;
267
- if (typeof page === "function") return page();
268
- return page;
182
+ /**
183
+ * Reactive store for a feature's cached data.
184
+ */
185
+ interface FeatureStore<TValues extends Record<string, unknown>> {
186
+ /** Cached list of items. */
187
+ items: Signal<TValues[]>;
188
+ /** Currently selected item. */
189
+ selected: Signal<TValues | null>;
190
+ /** Loading state. */
191
+ loading: Signal<boolean>;
192
+ /** Set the selected item by ID (finds from items list). */
193
+ select: (id: string | number) => void;
194
+ /** Clear the current selection. */
195
+ clear: () => void;
196
+ /** Index signature for Record<string, unknown> compatibility. */
197
+ [key: string]: unknown;
269
198
  }
270
199
  /**
271
- * Define a schema-driven feature with auto-generated CRUD hooks.
272
- *
273
- * @example
274
- * ```ts
275
- * import { defineFeature } from '@pyreon/feature'
276
- * import { z } from 'zod'
277
- *
278
- * const users = defineFeature({
279
- * name: 'users',
280
- * schema: z.object({
281
- * name: z.string().min(2),
282
- * email: z.string().email(),
283
- * role: z.enum(['admin', 'editor', 'viewer']),
284
- * }),
285
- * api: '/api/users',
286
- * })
287
- * ```
288
- */
289
- function defineFeature(config) {
290
- const {
291
- name,
292
- schema,
293
- api,
294
- fetcher: customFetcher
295
- } = config;
296
- const http = createFetcher(customFetcher);
297
- const fields = extractFields(schema);
298
- const autoInitialValues = defaultInitialValues(fields);
299
- const initialValues = config.initialValues ? {
300
- ...autoInitialValues,
301
- ...config.initialValues
302
- } : autoInitialValues;
303
- const validate = createValidator(schema, config.validate);
304
- const queryKeyBase = [name];
305
- const queryKey = suffix => suffix !== void 0 ? [name, suffix] : [name];
306
- return {
307
- name,
308
- api,
309
- schema,
310
- fields,
311
- queryKey,
312
- useStore: defineStore(name, () => {
313
- const items = signal([]);
314
- const selected = signal(null);
315
- const loading = signal(false);
316
- const select = id => {
317
- const found = items.peek().find(item => {
318
- return item.id === id;
319
- });
320
- selected.set(found ?? null);
321
- };
322
- const clear = () => {
323
- selected.set(null);
324
- };
325
- return {
326
- items,
327
- selected,
328
- loading,
329
- select,
330
- clear
331
- };
332
- }),
333
- useList(options) {
334
- return useQuery(() => {
335
- const pageValue = resolvePageValue(options?.page);
336
- const pageSize = options?.pageSize ?? 20;
337
- const params = {
338
- ...(options?.params ?? {})
339
- };
340
- if (pageValue !== void 0) {
341
- params.page = pageValue;
342
- params.pageSize = pageSize;
343
- }
344
- return {
345
- queryKey: [...queryKeyBase, "list", params],
346
- queryFn: () => http.list(api, Object.keys(params).length > 0 ? params : void 0),
347
- staleTime: options?.staleTime,
348
- enabled: options?.enabled
349
- };
350
- });
351
- },
352
- useById(id) {
353
- return useQuery(() => ({
354
- queryKey: [name, id],
355
- queryFn: () => http.getById(api, id),
356
- enabled: id !== void 0 && id !== null
357
- }));
358
- },
359
- useSearch(searchTerm, options) {
360
- return useQuery(() => ({
361
- queryKey: [...queryKeyBase, "search", searchTerm()],
362
- queryFn: () => http.list(api, {
363
- ...options?.params,
364
- q: searchTerm()
365
- }),
366
- enabled: searchTerm().length > 0,
367
- staleTime: options?.staleTime
368
- }));
369
- },
370
- useCreate() {
371
- const client = useQueryClient();
372
- return useMutation({
373
- mutationFn: data => http.create(api, data),
374
- onSuccess: () => {
375
- client.invalidateQueries({
376
- queryKey: queryKeyBase
377
- });
378
- }
379
- });
380
- },
381
- useUpdate() {
382
- const client = useQueryClient();
383
- return useMutation({
384
- mutationFn: ({
385
- id,
386
- data
387
- }) => http.update(api, id, data),
388
- onMutate: async variables => {
389
- await client.cancelQueries({
390
- queryKey: [name, variables.id]
391
- });
392
- const previous = client.getQueryData([name, variables.id]);
393
- client.setQueryData([name, variables.id], old => {
394
- if (old && typeof old === "object") return {
395
- ...old,
396
- ...variables.data
397
- };
398
- return variables.data;
399
- });
400
- return {
401
- previous
402
- };
403
- },
404
- onError: (_err, variables, context) => {
405
- if (context?.previous) client.setQueryData([name, variables.id], context.previous);
406
- },
407
- onSuccess: (_data, variables) => {
408
- client.invalidateQueries({
409
- queryKey: queryKeyBase
410
- });
411
- client.invalidateQueries({
412
- queryKey: [name, variables.id]
413
- });
414
- }
415
- });
416
- },
417
- useDelete() {
418
- const client = useQueryClient();
419
- return useMutation({
420
- mutationFn: id => http.delete(api, id),
421
- onSuccess: () => {
422
- client.invalidateQueries({
423
- queryKey: queryKeyBase
424
- });
425
- }
426
- });
427
- },
428
- useForm(options) {
429
- const mode = options?.mode ?? "create";
430
- const form = useForm({
431
- initialValues: {
432
- ...initialValues,
433
- ...(options?.initialValues ?? {})
434
- },
435
- schema: validate,
436
- validateOn: options?.validateOn ?? "blur",
437
- onSubmit: async values => {
438
- try {
439
- let result;
440
- if (mode === "edit" && options?.id !== void 0) result = await http.update(api, options.id, values);else result = await http.create(api, values);
441
- options?.onSuccess?.(result);
442
- } catch (err) {
443
- options?.onError?.(err);
444
- throw err;
445
- }
446
- }
447
- });
448
- if (mode === "edit" && options?.id !== void 0) {
449
- form.isSubmitting.set(true);
450
- http.getById(api, options.id).then(data => {
451
- batch(() => {
452
- for (const key of Object.keys(data)) form.setFieldValue(key, data[key]);
453
- form.isSubmitting.set(false);
454
- });
455
- }, () => {
456
- form.isSubmitting.set(false);
457
- });
458
- }
459
- return form;
460
- },
461
- useTable(data, options) {
462
- const visibleFields = options?.columns ? fields.filter(f => options.columns.includes(f.name)) : fields;
463
- const columns = visibleFields.map(field => ({
464
- accessorKey: field.name,
465
- header: field.label,
466
- ...(options?.columnOverrides?.[field.name] ?? {})
467
- }));
468
- const sorting = signal([]);
469
- const globalFilter = signal("");
470
- return {
471
- table: useTable(() => ({
472
- data: typeof data === "function" ? data() : data,
473
- columns,
474
- state: {
475
- sorting: sorting(),
476
- globalFilter: globalFilter()
477
- },
478
- onSortingChange: updater => {
479
- sorting.set(typeof updater === "function" ? updater(sorting()) : updater);
480
- },
481
- onGlobalFilterChange: updater => {
482
- globalFilter.set(typeof updater === "function" ? updater(globalFilter()) : updater);
483
- },
484
- getCoreRowModel: getCoreRowModel(),
485
- getSortedRowModel: getSortedRowModel(),
486
- getFilteredRowModel: getFilteredRowModel(),
487
- ...(options?.pageSize ? {
488
- getPaginationRowModel: getPaginationRowModel()
489
- } : {})
490
- })),
491
- sorting,
492
- globalFilter,
493
- columns: visibleFields
494
- };
495
- }
496
- };
200
+ * The feature object returned by defineFeature().
201
+ */
202
+ interface Feature<TValues extends Record<string, unknown>> {
203
+ /** Feature name. */
204
+ name: string;
205
+ /** API base path. */
206
+ api: string;
207
+ /** The schema passed to defineFeature. */
208
+ schema: unknown;
209
+ /** Introspected field information from the schema. */
210
+ fields: FieldInfo[];
211
+ /** Fetch a paginated/filtered list. */
212
+ useList: (options?: ListOptions) => UseQueryResult<TValues[], unknown>;
213
+ /** Fetch a single item by ID. */
214
+ useById: (id: string | number) => UseQueryResult<TValues, unknown>;
215
+ /** Search with a reactive signal term. */
216
+ useSearch: (searchTerm: Signal<string>, options?: ListOptions) => UseQueryResult<TValues[], unknown>;
217
+ /** Create mutation — POST to api. */
218
+ useCreate: () => UseMutationResult<TValues, unknown, Partial<TValues>>;
219
+ /** Update mutation — PUT to api/:id with optimistic updates. */
220
+ useUpdate: () => UseMutationResult<TValues, unknown, {
221
+ id: string | number;
222
+ data: Partial<TValues>;
223
+ }>;
224
+ /** Delete mutation — DELETE to api/:id. */
225
+ useDelete: () => UseMutationResult<void, unknown, string | number>;
226
+ /** Create a form pre-wired with schema validation and API submit. In edit mode with an ID, auto-fetches data. */
227
+ useForm: (options?: FeatureFormOptions<TValues>) => FormState<TValues>;
228
+ /** Create a reactive table with columns inferred from schema. */
229
+ useTable: (data: TValues[] | (() => TValues[]), options?: FeatureTableOptions<TValues>) => FeatureTableResult<TValues>;
230
+ /** Reactive store for cached items, selection, and loading state. */
231
+ useStore: () => StoreApi<FeatureStore<TValues>>;
232
+ /** Generate namespaced query keys. */
233
+ queryKey: (suffix?: string | number) => QueryKey;
497
234
  }
498
-
499
235
  //#endregion
500
- export { defaultInitialValues, defineFeature, extractFields, isReference, reference };
501
- //# sourceMappingURL=index.d.ts.map
236
+ //#region src/define-feature.d.ts
237
+ /**
238
+ * Define a schema-driven feature with auto-generated CRUD hooks.
239
+ *
240
+ * @example
241
+ * ```ts
242
+ * import { defineFeature } from '@pyreon/feature'
243
+ * import { z } from 'zod'
244
+ *
245
+ * const users = defineFeature({
246
+ * name: 'users',
247
+ * schema: z.object({
248
+ * name: z.string().min(2),
249
+ * email: z.string().email(),
250
+ * role: z.enum(['admin', 'editor', 'viewer']),
251
+ * }),
252
+ * api: '/api/users',
253
+ * })
254
+ * ```
255
+ */
256
+ declare function defineFeature<TValues extends Record<string, unknown>>(config: FeatureConfig<TValues>): Feature<TValues>;
257
+ //#endregion
258
+ export { type Feature, type FeatureConfig, type FeatureFormOptions, type FeatureStore, type FeatureTableOptions, type FeatureTableResult, type FieldInfo, type FieldType, type ListOptions, type ReferenceSchema, defaultInitialValues, defineFeature, extractFields, isReference, reference };
259
+ //# sourceMappingURL=index2.d.ts.map