@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.
package/README.md CHANGED
@@ -33,7 +33,9 @@ function UserList() {
33
33
  return (
34
34
  <ul>
35
35
  {data()!.map((u) => (
36
- <li>{u.name} ({u.email})</li>
36
+ <li>
37
+ {u.name} ({u.email})
38
+ </li>
37
39
  ))}
38
40
  </ul>
39
41
  )
@@ -76,40 +78,40 @@ function EditUser({ id }: { id: number }) {
76
78
 
77
79
  Creates a feature object with all CRUD hooks, form, table, and store.
78
80
 
79
- | Parameter | Type | Description |
80
- | --- | --- | --- |
81
- | `name` | `string` | Unique feature name -- used for store ID and query key namespace |
82
- | `schema` | `ZodSchema` | Validation schema -- passed to `zodSchema()` for form validation |
83
- | `api` | `string` | API base path (e.g., `/api/users`) |
84
- | `initialValues?` | `Partial<TValues>` | Default values for create forms (auto-generated from schema if omitted) |
85
- | `validate?` | `SchemaValidateFn<TValues>` | Custom schema-level validation (overrides auto-detection) |
86
- | `fetcher?` | `typeof fetch` | Custom fetch function (defaults to global `fetch`) |
81
+ | Parameter | Type | Description |
82
+ | ---------------- | --------------------------- | ----------------------------------------------------------------------- |
83
+ | `name` | `string` | Unique feature name -- used for store ID and query key namespace |
84
+ | `schema` | `ZodSchema` | Validation schema -- passed to `zodSchema()` for form validation |
85
+ | `api` | `string` | API base path (e.g., `/api/users`) |
86
+ | `initialValues?` | `Partial<TValues>` | Default values for create forms (auto-generated from schema if omitted) |
87
+ | `validate?` | `SchemaValidateFn<TValues>` | Custom schema-level validation (overrides auto-detection) |
88
+ | `fetcher?` | `typeof fetch` | Custom fetch function (defaults to global `fetch`) |
87
89
 
88
90
  ### Returned Feature Object
89
91
 
90
- | Property / Hook | Returns | Description |
91
- | --- | --- | --- |
92
- | `name` | `string` | Feature name |
93
- | `api` | `string` | API base path |
94
- | `schema` | `unknown` | The schema passed to `defineFeature` |
95
- | `fields` | `FieldInfo[]` | Introspected field metadata from the schema |
96
- | `queryKey(suffix?)` | `QueryKey` | Generate namespaced query keys |
97
- | `useList(opts?)` | `UseQueryResult<T[]>` | GET `api` -- list query with optional pagination and params |
98
- | `useById(id)` | `UseQueryResult<T>` | GET `api/:id` -- single item query |
99
- | `useSearch(term, opts?)` | `UseQueryResult<T[]>` | GET `api?q=term` -- search with reactive signal |
100
- | `useCreate()` | `UseMutationResult` | POST `api` -- invalidates list on success |
101
- | `useUpdate()` | `UseMutationResult` | PUT `api/:id` -- optimistic update with rollback on error |
102
- | `useDelete()` | `UseMutationResult` | DELETE `api/:id` -- invalidates list on success |
103
- | `useForm(opts?)` | `FormState<T>` | Form with schema validation + API submit |
104
- | `useTable(data, opts?)` | `FeatureTableResult<T>` | Reactive table with schema-inferred columns |
105
- | `useStore()` | `StoreApi<FeatureStore<T>>` | Reactive store for items, selection, and loading state |
92
+ | Property / Hook | Returns | Description |
93
+ | ------------------------ | --------------------------- | ----------------------------------------------------------- |
94
+ | `name` | `string` | Feature name |
95
+ | `api` | `string` | API base path |
96
+ | `schema` | `unknown` | The schema passed to `defineFeature` |
97
+ | `fields` | `FieldInfo[]` | Introspected field metadata from the schema |
98
+ | `queryKey(suffix?)` | `QueryKey` | Generate namespaced query keys |
99
+ | `useList(opts?)` | `UseQueryResult<T[]>` | GET `api` -- list query with optional pagination and params |
100
+ | `useById(id)` | `UseQueryResult<T>` | GET `api/:id` -- single item query |
101
+ | `useSearch(term, opts?)` | `UseQueryResult<T[]>` | GET `api?q=term` -- search with reactive signal |
102
+ | `useCreate()` | `UseMutationResult` | POST `api` -- invalidates list on success |
103
+ | `useUpdate()` | `UseMutationResult` | PUT `api/:id` -- optimistic update with rollback on error |
104
+ | `useDelete()` | `UseMutationResult` | DELETE `api/:id` -- invalidates list on success |
105
+ | `useForm(opts?)` | `FormState<T>` | Form with schema validation + API submit |
106
+ | `useTable(data, opts?)` | `FeatureTableResult<T>` | Reactive table with schema-inferred columns |
107
+ | `useStore()` | `StoreApi<FeatureStore<T>>` | Reactive store for items, selection, and loading state |
106
108
 
107
109
  ### `reference(feature)`
108
110
 
109
111
  Creates a typed foreign key field for cross-feature relationships.
110
112
 
111
- | Parameter | Type | Description |
112
- | --- | --- | --- |
113
+ | Parameter | Type | Description |
114
+ | --------- | ------------------ | ------------------------------------------------------------- |
113
115
  | `feature` | `{ name: string }` | The referenced feature (or any object with a `name` property) |
114
116
 
115
117
  Returns a Zod-compatible schema that validates as `string | number` and carries metadata about the referenced feature.
@@ -118,9 +120,9 @@ Returns a Zod-compatible schema that validates as `string | number` and carries
118
120
 
119
121
  Extracts field metadata from a Zod object schema.
120
122
 
121
- | Parameter | Type | Description |
122
- | --- | --- | --- |
123
- | `schema` | `unknown` | A Zod object schema (duck-typed, works with v3 and v4) |
123
+ | Parameter | Type | Description |
124
+ | --------- | --------- | ------------------------------------------------------ |
125
+ | `schema` | `unknown` | A Zod object schema (duck-typed, works with v3 and v4) |
124
126
 
125
127
  Returns `FieldInfo[]` with `name`, `type`, `optional`, `enumValues`, `referenceTo`, and `label` for each field.
126
128
 
@@ -151,14 +153,10 @@ function UserManager() {
151
153
  <div>
152
154
  <ul>
153
155
  {store.items().map((u) => (
154
- <li onClick={() => store.select(u.id)}>
155
- {u.name}
156
- </li>
156
+ <li onClick={() => store.select(u.id)}>{u.name}</li>
157
157
  ))}
158
158
  </ul>
159
- {store.selected() && (
160
- <div>Selected: {store.selected()!.name}</div>
161
- )}
159
+ {store.selected() && <div>Selected: {store.selected()!.name}</div>}
162
160
  <button onClick={() => store.clear()}>Clear Selection</button>
163
161
  </div>
164
162
  )
@@ -167,13 +165,13 @@ function UserManager() {
167
165
 
168
166
  **Store API:**
169
167
 
170
- | Property | Type | Description |
171
- | --- | --- | --- |
172
- | `items` | `Signal<TValues[]>` | Cached list of items |
173
- | `selected` | `Signal<TValues \| null>` | Currently selected item |
174
- | `loading` | `Signal<boolean>` | Loading state |
168
+ | Property | Type | Description |
169
+ | ------------ | -------------------------------- | ------------------------------------------------- |
170
+ | `items` | `Signal<TValues[]>` | Cached list of items |
171
+ | `selected` | `Signal<TValues \| null>` | Currently selected item |
172
+ | `loading` | `Signal<boolean>` | Loading state |
175
173
  | `select(id)` | `(id: string \| number) => void` | Find and select an item by ID from the items list |
176
- | `clear()` | `() => void` | Clear the current selection |
174
+ | `clear()` | `() => void` | Clear the current selection |
177
175
 
178
176
  ## Pagination
179
177
 
@@ -190,15 +188,15 @@ function PaginatedUsers() {
190
188
  <p>Loading...</p>
191
189
  ) : (
192
190
  <ul>
193
- {data()!.map((u) => <li>{u.name}</li>)}
191
+ {data()!.map((u) => (
192
+ <li>{u.name}</li>
193
+ ))}
194
194
  </ul>
195
195
  )}
196
196
  <button onClick={() => page.set(page() - 1)} disabled={page() <= 1}>
197
197
  Previous
198
198
  </button>
199
- <button onClick={() => page.set(page() + 1)}>
200
- Next
201
- </button>
199
+ <button onClick={() => page.set(page() + 1)}>Next</button>
202
200
  </div>
203
201
  )
204
202
  }
@@ -206,13 +204,13 @@ function PaginatedUsers() {
206
204
 
207
205
  **ListOptions:**
208
206
 
209
- | Parameter | Type | Description |
210
- | --- | --- | --- |
211
- | `params?` | `Record<string, string \| number \| boolean>` | Additional query parameters |
212
- | `page?` | `number \| Signal<number>` | Page number (reactive or static) |
213
- | `pageSize?` | `number` | Items per page (defaults to 20 when `page` is set) |
214
- | `staleTime?` | `number` | Override stale time for this query |
215
- | `enabled?` | `boolean` | Enable/disable the query |
207
+ | Parameter | Type | Description |
208
+ | ------------ | --------------------------------------------- | -------------------------------------------------- |
209
+ | `params?` | `Record<string, string \| number \| boolean>` | Additional query parameters |
210
+ | `page?` | `number \| Signal<number>` | Page number (reactive or static) |
211
+ | `pageSize?` | `number` | Items per page (defaults to 20 when `page` is set) |
212
+ | `staleTime?` | `number` | Override stale time for this query |
213
+ | `enabled?` | `boolean` | Enable/disable the query |
216
214
 
217
215
  ## Edit Form (Auto-fetch)
218
216
 
@@ -241,14 +239,14 @@ function EditUser({ id }: { id: number }) {
241
239
 
242
240
  **FeatureFormOptions:**
243
241
 
244
- | Parameter | Type | Description |
245
- | --- | --- | --- |
246
- | `mode?` | `'create' \| 'edit'` | Form mode (default: `'create'`) |
247
- | `id?` | `string \| number` | Item ID for edit mode (triggers auto-fetch) |
248
- | `initialValues?` | `Partial<TValues>` | Override initial values |
249
- | `validateOn?` | `'blur' \| 'change' \| 'submit'` | Validation trigger (default: `'blur'`) |
250
- | `onSuccess?` | `(result: unknown) => void` | Called after successful submit |
251
- | `onError?` | `(error: unknown) => void` | Called on submit error |
242
+ | Parameter | Type | Description |
243
+ | ---------------- | -------------------------------- | ------------------------------------------- |
244
+ | `mode?` | `'create' \| 'edit'` | Form mode (default: `'create'`) |
245
+ | `id?` | `string \| number` | Item ID for edit mode (triggers auto-fetch) |
246
+ | `initialValues?` | `Partial<TValues>` | Override initial values |
247
+ | `validateOn?` | `'blur' \| 'change' \| 'submit'` | Validation trigger (default: `'blur'`) |
248
+ | `onSuccess?` | `(result: unknown) => void` | Called after successful submit |
249
+ | `onError?` | `(error: unknown) => void` | Called on submit error |
252
250
 
253
251
  ## Optimistic Updates
254
252
 
@@ -267,9 +265,7 @@ function UserRow({ user }: { user: User }) {
267
265
  <tr>
268
266
  <td>{user.name}</td>
269
267
  <td>
270
- <button onClick={toggleActive}>
271
- {user.active ? 'Deactivate' : 'Activate'}
272
- </button>
268
+ <button onClick={toggleActive}>{user.active ? 'Deactivate' : 'Activate'}</button>
273
269
  </td>
274
270
  </tr>
275
271
  )
@@ -295,7 +291,7 @@ const posts = defineFeature({
295
291
  schema: z.object({
296
292
  title: z.string(),
297
293
  body: z.string(),
298
- authorId: reference(users), // typed foreign key
294
+ authorId: reference(users), // typed foreign key
299
295
  }),
300
296
  api: '/api/posts',
301
297
  })
@@ -340,14 +336,14 @@ function AutoForm({ feature }: { feature: Feature<any> }) {
340
336
 
341
337
  **FieldInfo:**
342
338
 
343
- | Property | Type | Description |
344
- | --- | --- | --- |
345
- | `name` | `string` | Field name (key in the schema) |
346
- | `type` | `FieldType` | `'string'`, `'number'`, `'boolean'`, `'date'`, `'enum'`, `'array'`, `'object'`, `'reference'`, or `'unknown'` |
347
- | `optional` | `boolean` | Whether the field is optional |
348
- | `enumValues?` | `(string \| number)[]` | Allowed values for enum fields |
349
- | `referenceTo?` | `string` | Referenced feature name for reference fields |
350
- | `label` | `string` | Human-readable label (e.g., `firstName` becomes `First Name`) |
339
+ | Property | Type | Description |
340
+ | -------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------- |
341
+ | `name` | `string` | Field name (key in the schema) |
342
+ | `type` | `FieldType` | `'string'`, `'number'`, `'boolean'`, `'date'`, `'enum'`, `'array'`, `'object'`, `'reference'`, or `'unknown'` |
343
+ | `optional` | `boolean` | Whether the field is optional |
344
+ | `enumValues?` | `(string \| number)[]` | Allowed values for enum fields |
345
+ | `referenceTo?` | `string` | Referenced feature name for reference fields |
346
+ | `label` | `string` | Human-readable label (e.g., `firstName` becomes `First Name`) |
351
347
 
352
348
  ## Error Handling
353
349
 
@@ -367,9 +363,7 @@ function CreateUser() {
367
363
  {isError() && (
368
364
  <div>
369
365
  <p>{(error() as Error).message}</p>
370
- {(error() as any).errors?.email && (
371
- <p>Email: {(error() as any).errors.email}</p>
372
- )}
366
+ {(error() as any).errors?.email && <p>Email: {(error() as any).errors.email}</p>}
373
367
  </div>
374
368
  )}
375
369
  </div>
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["_useQuery","_useMutation","_useForm","_useTable"],"sources":["../src/schema.ts","../src/define-feature.ts"],"sourcesContent":["/**\n * Schema introspection utilities.\n *\n * Extracts field names, types, and metadata from Zod schemas at runtime\n * without importing Zod types directly (duck-typed).\n */\n\nexport interface FieldInfo {\n /** Field name (key in the schema object). */\n name: string\n /** Inferred type: 'string' | 'number' | 'boolean' | 'date' | 'enum' | 'array' | 'object' | 'reference' | 'unknown'. */\n type: FieldType\n /** Whether the field is optional. */\n optional: boolean\n /** For enum fields, the list of allowed values. */\n enumValues?: (string | number)[]\n /** For reference fields, the name of the referenced feature. */\n referenceTo?: string\n /** Human-readable label derived from field name. */\n label: string\n}\n\nexport type FieldType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"enum\"\n | \"array\"\n | \"object\"\n | \"reference\"\n | \"unknown\"\n\n/** Symbol used to tag reference schema objects. */\nconst REFERENCE_TAG = Symbol.for(\"pyreon:feature:reference\")\n\n/**\n * Metadata carried by a reference schema.\n */\nexport interface ReferenceSchema {\n /** Marker symbol for detection. */\n [key: symbol]: true\n /** Name of the referenced feature. */\n _featureName: string\n /** Duck-typed Zod-like interface: validates as string | number. */\n safeParse: (value: unknown) => {\n success: boolean\n error?: { issues: { message: string }[] }\n }\n /** Async variant for compatibility. */\n safeParseAsync: (\n value: unknown,\n ) => Promise<{ success: boolean; error?: { issues: { message: string }[] } }>\n /** Shape-like marker for schema introspection. */\n _def: { typeName: string }\n}\n\n/**\n * Check if a value is a reference schema created by `reference()`.\n */\nexport function isReference(value: unknown): value is ReferenceSchema {\n return (\n value !== null &&\n typeof value === \"object\" &&\n (value as Record<symbol, unknown>)[REFERENCE_TAG] === true\n )\n}\n\n/**\n * Create a reference field that links to another feature.\n *\n * Returns a Zod-compatible schema that validates as `string | number` and\n * carries metadata about the referenced feature for form dropdowns and table links.\n *\n * @example\n * ```ts\n * import { defineFeature, reference } from '@pyreon/feature'\n *\n * const posts = defineFeature({\n * name: 'posts',\n * schema: z.object({\n * title: z.string(),\n * authorId: reference(users),\n * }),\n * api: '/api/posts',\n * })\n * ```\n */\nexport function reference(feature: { name: string }): ReferenceSchema {\n const featureName = feature.name\n\n function validateRef(value: unknown): {\n success: boolean\n error?: { issues: { message: string }[] }\n } {\n if (typeof value === \"string\" || typeof value === \"number\") {\n return { success: true }\n }\n return {\n success: false,\n error: {\n issues: [\n {\n message: `Expected string or number reference to ${featureName}, got ${typeof value}`,\n },\n ],\n },\n }\n }\n\n return {\n [REFERENCE_TAG]: true,\n _featureName: featureName,\n safeParse: validateRef,\n safeParseAsync: async (value: unknown) => validateRef(value),\n _def: { typeName: \"ZodString\" },\n }\n}\n\n/**\n * Convert a field name to a human-readable label.\n * e.g., 'firstName' → 'First Name', 'created_at' → 'Created At'\n */\nfunction nameToLabel(name: string): string {\n return name\n .replace(/([a-z])([A-Z])/g, \"$1 $2\") // camelCase → camel Case\n .replace(/[_-]/g, \" \") // snake_case/kebab-case → spaces\n .replace(/\\b\\w/g, (c) => c.toUpperCase()) // capitalize words\n}\n\n/**\n * Detect the field type from a Zod schema shape entry.\n * Duck-typed — works with Zod v3 and v4 without importing Zod.\n */\nfunction detectFieldType(zodField: unknown): {\n type: FieldType\n optional: boolean\n enumValues?: (string | number)[]\n referenceTo?: string\n} {\n // Check for reference fields first\n if (isReference(zodField)) {\n return {\n type: \"reference\",\n optional: false,\n referenceTo: zodField._featureName,\n }\n }\n\n if (!zodField || typeof zodField !== \"object\") {\n return { type: \"unknown\", optional: false }\n }\n\n const field = zodField as Record<string, unknown>\n\n // Check for optional wrapper (ZodOptional or ZodNullable)\n let inner = field\n let optional = false\n\n // Zod v3: _def.typeName, Zod v4: _zod.def.type\n const getTypeName = (obj: Record<string, unknown>): string | undefined => {\n // v3 path\n const def = obj._def as Record<string, unknown> | undefined\n if (def?.typeName && typeof def.typeName === \"string\") {\n return def.typeName\n }\n // v4 path\n const zod = obj._zod as Record<string, unknown> | undefined\n const zodDef = zod?.def as Record<string, unknown> | undefined\n if (zodDef?.type && typeof zodDef.type === \"string\") {\n return zodDef.type\n }\n return undefined\n }\n\n const typeName = getTypeName(inner)\n\n // Unwrap optional/nullable\n if (\n typeName === \"ZodOptional\" ||\n typeName === \"ZodNullable\" ||\n typeName === \"optional\" ||\n typeName === \"nullable\"\n ) {\n optional = true\n const def = inner._def as Record<string, unknown> | undefined\n const innerType = def?.innerType ?? (inner._zod as Record<string, unknown>)?.def\n if (innerType && typeof innerType === \"object\") {\n inner = innerType as Record<string, unknown>\n }\n }\n\n const innerTypeName = getTypeName(inner) ?? typeName\n\n // Map Zod type names to our FieldType\n if (!innerTypeName) return { type: \"unknown\", optional }\n\n const typeMap: Record<string, FieldType> = {\n ZodString: \"string\",\n ZodNumber: \"number\",\n ZodBoolean: \"boolean\",\n ZodDate: \"date\",\n ZodEnum: \"enum\",\n ZodNativeEnum: \"enum\",\n ZodArray: \"array\",\n ZodObject: \"object\",\n // v4 names\n string: \"string\",\n number: \"number\",\n boolean: \"boolean\",\n date: \"date\",\n enum: \"enum\",\n array: \"array\",\n object: \"object\",\n }\n\n const type = typeMap[innerTypeName] ?? \"string\"\n\n // Extract enum values\n let enumValues: (string | number)[] | undefined\n if (type === \"enum\") {\n const def = inner._def as Record<string, unknown> | undefined\n if (def?.values && Array.isArray(def.values)) {\n enumValues = def.values as (string | number)[]\n }\n // v4 path\n const zodDef = (inner._zod as Record<string, unknown>)?.def as\n | Record<string, unknown>\n | undefined\n if (zodDef?.values && Array.isArray(zodDef.values)) {\n enumValues = zodDef.values as (string | number)[]\n }\n }\n\n return {\n type,\n optional,\n ...(enumValues != null ? { enumValues } : {}),\n }\n}\n\n/**\n * Extract field information from a Zod object schema.\n * Returns an array of FieldInfo objects describing each field.\n *\n * @example\n * ```ts\n * const schema = z.object({ name: z.string(), age: z.number().optional() })\n * const fields = extractFields(schema)\n * // [\n * // { name: 'name', type: 'string', optional: false, label: 'Name' },\n * // { name: 'age', type: 'number', optional: true, label: 'Age' },\n * // ]\n * ```\n */\nexport function extractFields(schema: unknown): FieldInfo[] {\n if (!schema || typeof schema !== \"object\") return []\n\n const s = schema as Record<string, unknown>\n\n // Get the shape object from the schema\n // Zod v3: schema._def.shape() or schema.shape\n // Zod v4: schema._zod.def.shape or schema.shape\n let shape: Record<string, unknown> | undefined\n\n // Try schema.shape (works for both v3 and v4)\n if (s.shape && typeof s.shape === \"object\") {\n shape = s.shape as Record<string, unknown>\n }\n\n // Try _def.shape (v3 — can be a function)\n if (!shape) {\n const def = s._def as Record<string, unknown> | undefined\n if (def?.shape) {\n shape =\n typeof def.shape === \"function\"\n ? (def.shape as () => Record<string, unknown>)()\n : (def.shape as Record<string, unknown>)\n }\n }\n\n // Try _zod.def.shape (v4)\n if (!shape) {\n const zod = s._zod as Record<string, unknown> | undefined\n const zodDef = zod?.def as Record<string, unknown> | undefined\n if (zodDef?.shape && typeof zodDef.shape === \"object\") {\n shape = zodDef.shape as Record<string, unknown>\n }\n }\n\n if (!shape) return []\n\n return Object.entries(shape).map(([name, fieldSchema]) => {\n const { type, optional, enumValues, referenceTo } = detectFieldType(fieldSchema)\n const info: FieldInfo = {\n name,\n type,\n optional,\n label: nameToLabel(name),\n }\n if (enumValues) info.enumValues = enumValues\n if (referenceTo) info.referenceTo = referenceTo\n return info\n })\n}\n\n/**\n * Generate default initial values from a schema's field types.\n */\nexport function defaultInitialValues(fields: FieldInfo[]): Record<string, unknown> {\n const values: Record<string, unknown> = {}\n for (const field of fields) {\n switch (field.type) {\n case \"string\":\n values[field.name] = \"\"\n break\n case \"number\":\n values[field.name] = 0\n break\n case \"boolean\":\n values[field.name] = false\n break\n case \"enum\":\n values[field.name] = field.enumValues?.[0] ?? \"\"\n break\n case \"date\":\n values[field.name] = \"\"\n break\n default:\n values[field.name] = \"\"\n }\n }\n return values\n}\n","import type { SchemaValidateFn } from \"@pyreon/form\"\nimport { useForm as _useForm } from \"@pyreon/form\"\nimport type { QueryKey } from \"@pyreon/query\"\nimport { useMutation as _useMutation, useQuery as _useQuery, useQueryClient } from \"@pyreon/query\"\nimport { batch, signal } from \"@pyreon/reactivity\"\nimport { defineStore } from \"@pyreon/store\"\nimport type { ColumnDef, SortingState } from \"@pyreon/table\"\nimport {\n useTable as _useTable,\n getCoreRowModel,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n} from \"@pyreon/table\"\nimport { zodSchema } from \"@pyreon/validation\"\nimport { defaultInitialValues, extractFields } from \"./schema\"\nimport type {\n Feature,\n FeatureConfig,\n FeatureFormOptions,\n FeatureStore,\n FeatureTableOptions,\n ListOptions,\n} from \"./types\"\n\n// ─── Fetch wrapper ────────────────────────────────────────────────────────────\n\nfunction createFetcher(baseFetcher: typeof fetch = fetch) {\n async function request<T>(url: string, init?: RequestInit): Promise<T> {\n const res = await baseFetcher(url, init)\n\n if (!res.ok) {\n let message = `${init?.method ?? \"GET\"} ${url} failed: ${res.status}`\n try {\n const body = await res.json()\n if (body?.message) message = body.message\n if (body?.errors) {\n throw Object.assign(new Error(message), {\n status: res.status,\n errors: body.errors,\n })\n }\n } catch (e) {\n if (e instanceof Error && \"errors\" in e) throw e\n }\n throw Object.assign(new Error(message), { status: res.status })\n }\n\n if (res.status === 204) return undefined as T\n return res.json()\n }\n\n return {\n list<T>(url: string, params?: Record<string, string | number | boolean>): Promise<T[]> {\n const query = params\n ? `?${new URLSearchParams(Object.entries(params).map(([k, v]) => [k, String(v)])).toString()}`\n : \"\"\n return request<T[]>(`${url}${query}`)\n },\n getById<T>(url: string, id: string | number): Promise<T> {\n return request<T>(`${url}/${id}`)\n },\n create<T>(url: string, data: unknown): Promise<T> {\n return request<T>(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(data),\n })\n },\n update<T>(url: string, id: string | number, data: unknown): Promise<T> {\n return request<T>(`${url}/${id}`, {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(data),\n })\n },\n delete(url: string, id: string | number): Promise<void> {\n return request<void>(`${url}/${id}`, { method: \"DELETE\" })\n },\n }\n}\n\n// ─── Schema validation ────────────────────────────────────────────────────────\n\nfunction createValidator<TValues extends Record<string, unknown>>(\n schema: unknown,\n customValidate?: SchemaValidateFn<TValues>,\n): SchemaValidateFn<TValues> | undefined {\n if (customValidate) return customValidate\n\n if (\n schema &&\n typeof schema === \"object\" &&\n \"safeParseAsync\" in schema &&\n typeof (schema as Record<string, unknown>).safeParseAsync === \"function\"\n ) {\n return zodSchema(schema as Parameters<typeof zodSchema>[0]) as SchemaValidateFn<TValues>\n }\n\n return undefined\n}\n\n// ─── Resolve page value ───────────────────────────────────────────────────────\n\nfunction resolvePageValue(page: number | (() => number) | undefined): number | undefined {\n if (page === undefined) return undefined\n if (typeof page === \"function\") return page()\n return page\n}\n\n// ─── defineFeature ────────────────────────────────────────────────────────────\n\n/**\n * Define a schema-driven feature with auto-generated CRUD hooks.\n *\n * @example\n * ```ts\n * import { defineFeature } from '@pyreon/feature'\n * import { z } from 'zod'\n *\n * const users = defineFeature({\n * name: 'users',\n * schema: z.object({\n * name: z.string().min(2),\n * email: z.string().email(),\n * role: z.enum(['admin', 'editor', 'viewer']),\n * }),\n * api: '/api/users',\n * })\n * ```\n */\nexport function defineFeature<TValues extends Record<string, unknown>>(\n config: FeatureConfig<TValues>,\n): Feature<TValues> {\n const { name, schema, api, fetcher: customFetcher } = config\n const http = createFetcher(customFetcher)\n\n // Introspect schema fields\n const fields = extractFields(schema)\n const autoInitialValues = defaultInitialValues(fields) as TValues\n const initialValues = config.initialValues\n ? { ...autoInitialValues, ...config.initialValues }\n : autoInitialValues\n\n const validate = createValidator<TValues>(schema, config.validate)\n\n const queryKeyBase = [name] as const\n const queryKey = (suffix?: string | number): QueryKey =>\n suffix !== undefined ? [name, suffix] : [name]\n\n // ─── Store definition ──────────────────────────────────────────────\n\n const useStoreHook = defineStore<FeatureStore<TValues>>(name, () => {\n const items = signal<TValues[]>([])\n const selected = signal<TValues | null>(null)\n const loading = signal(false)\n\n const select = (id: string | number) => {\n const found = items.peek().find((item) => {\n const record = item as Record<string, unknown>\n return record.id === id\n })\n selected.set(found ?? null)\n }\n\n const clear = () => {\n selected.set(null)\n }\n\n return { items, selected, loading, select, clear }\n })\n\n return {\n name,\n api,\n schema,\n fields,\n queryKey,\n\n // ─── Store ───────────────────────────────────────────────────────\n\n useStore: useStoreHook,\n\n // ─── Queries ────────────────────────────────────────────────────\n\n useList(options?: ListOptions) {\n return _useQuery(() => {\n const pageValue = resolvePageValue(options?.page)\n const pageSize = options?.pageSize ?? 20\n\n const params: Record<string, string | number | boolean> = {\n ...(options?.params ?? {}),\n }\n\n if (pageValue !== undefined) {\n params.page = pageValue\n params.pageSize = pageSize\n }\n\n const queryKeyParts: unknown[] = [...queryKeyBase, \"list\", params]\n\n return {\n queryKey: queryKeyParts as QueryKey,\n queryFn: () =>\n http.list<TValues>(api, Object.keys(params).length > 0 ? params : undefined),\n ...(options?.staleTime != null ? { staleTime: options.staleTime } : {}),\n ...(options?.enabled != null ? { enabled: options.enabled } : {}),\n }\n })\n },\n\n useById(id: string | number) {\n return _useQuery(() => ({\n queryKey: [name, id],\n queryFn: () => http.getById<TValues>(api, id),\n enabled: id !== undefined && id !== null,\n }))\n },\n\n useSearch(searchTerm, options?: ListOptions) {\n return _useQuery(() => ({\n queryKey: [...queryKeyBase, \"search\", searchTerm()],\n queryFn: () => http.list<TValues>(api, { ...options?.params, q: searchTerm() }),\n enabled: searchTerm().length > 0,\n ...(options?.staleTime != null ? { staleTime: options.staleTime } : {}),\n }))\n },\n\n // ─── Mutations ──────────────────────────────────────────────────\n\n useCreate() {\n const client = useQueryClient()\n return _useMutation({\n mutationFn: (data: Partial<TValues>) => http.create<TValues>(api, data),\n onSuccess: () => {\n client.invalidateQueries({\n queryKey: queryKeyBase as unknown as QueryKey,\n })\n },\n })\n },\n\n useUpdate() {\n type TVariables = { id: string | number; data: Partial<TValues> }\n const client = useQueryClient()\n return _useMutation<TValues, unknown, TVariables, { previous?: unknown }>({\n mutationFn: ({ id, data }: TVariables) => http.update<TValues>(api, id, data),\n onMutate: async (variables) => {\n await client.cancelQueries({ queryKey: [name, variables.id] })\n const previous = client.getQueryData([name, variables.id])\n client.setQueryData([name, variables.id], (old: unknown) => {\n if (old && typeof old === \"object\") {\n return { ...old, ...variables.data }\n }\n return variables.data\n })\n return { previous }\n },\n onError: (_err, variables, context) => {\n if (context?.previous) {\n client.setQueryData([name, variables.id], context.previous)\n }\n },\n onSuccess: (_data, variables) => {\n client.invalidateQueries({\n queryKey: queryKeyBase as unknown as QueryKey,\n })\n client.invalidateQueries({ queryKey: [name, variables.id] })\n },\n }) as ReturnType<Feature<TValues>[\"useUpdate\"]>\n },\n\n useDelete() {\n const client = useQueryClient()\n return _useMutation({\n mutationFn: (id: string | number) => http.delete(api, id),\n onSuccess: () => {\n client.invalidateQueries({\n queryKey: queryKeyBase as unknown as QueryKey,\n })\n },\n })\n },\n\n // ─── Form ───────────────────────────────────────────────────────\n\n useForm(options?: FeatureFormOptions<TValues>) {\n const mode = options?.mode ?? \"create\"\n const mergedInitial = {\n ...initialValues,\n ...(options?.initialValues ?? {}),\n } as TValues\n\n const form = _useForm<TValues>({\n initialValues: mergedInitial,\n ...(validate != null ? { schema: validate } : {}),\n validateOn: options?.validateOn ?? \"blur\",\n onSubmit: async (values) => {\n try {\n let result: unknown\n if (mode === \"edit\" && options?.id !== undefined) {\n result = await http.update<TValues>(api, options.id, values)\n } else {\n result = await http.create<TValues>(api, values)\n }\n options?.onSuccess?.(result)\n } catch (err) {\n options?.onError?.(err)\n throw err\n }\n },\n })\n\n // Auto-fetch in edit mode\n if (mode === \"edit\" && options?.id !== undefined) {\n form.isSubmitting.set(true)\n http.getById<TValues>(api, options.id).then(\n (data) => {\n batch(() => {\n for (const key of Object.keys(data)) {\n form.setFieldValue(\n key as keyof TValues & string,\n (data as Record<string, unknown>)[key] as TValues[keyof TValues],\n )\n }\n form.isSubmitting.set(false)\n })\n },\n () => {\n form.isSubmitting.set(false)\n },\n )\n }\n\n return form\n },\n\n // ─── Table ──────────────────────────────────────────────────────\n\n useTable(data: TValues[] | (() => TValues[]), options?: FeatureTableOptions<TValues>) {\n const visibleFields = options?.columns\n ? fields.filter((f) => options.columns!.includes(f.name as keyof TValues & string))\n : fields\n\n const columns: ColumnDef<TValues, unknown>[] = visibleFields.map((field) => ({\n accessorKey: field.name,\n header: field.label,\n ...(options?.columnOverrides?.[field.name as keyof TValues & string] ?? {}),\n }))\n\n const sorting = signal<SortingState>([])\n const globalFilter = signal(\"\")\n\n const table = _useTable(() => ({\n data: typeof data === \"function\" ? data() : data,\n columns,\n state: {\n sorting: sorting(),\n globalFilter: globalFilter(),\n },\n onSortingChange: (updater: unknown) => {\n sorting.set(\n typeof updater === \"function\"\n ? (updater as (prev: SortingState) => SortingState)(sorting())\n : (updater as SortingState),\n )\n },\n onGlobalFilterChange: (updater: unknown) => {\n globalFilter.set(\n typeof updater === \"function\"\n ? (updater as (prev: string) => string)(globalFilter())\n : (updater as string),\n )\n },\n getCoreRowModel: getCoreRowModel(),\n getSortedRowModel: getSortedRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n ...(options?.pageSize ? { getPaginationRowModel: getPaginationRowModel() } : {}),\n }))\n\n return {\n table,\n sorting,\n globalFilter,\n columns: visibleFields,\n }\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["_useQuery","_useMutation","_useForm","_useTable"],"sources":["../src/schema.ts","../src/define-feature.ts"],"sourcesContent":["/**\n * Schema introspection utilities.\n *\n * Extracts field names, types, and metadata from Zod schemas at runtime\n * without importing Zod types directly (duck-typed).\n */\n\nexport interface FieldInfo {\n /** Field name (key in the schema object). */\n name: string\n /** Inferred type: 'string' | 'number' | 'boolean' | 'date' | 'enum' | 'array' | 'object' | 'reference' | 'unknown'. */\n type: FieldType\n /** Whether the field is optional. */\n optional: boolean\n /** For enum fields, the list of allowed values. */\n enumValues?: (string | number)[]\n /** For reference fields, the name of the referenced feature. */\n referenceTo?: string\n /** Human-readable label derived from field name. */\n label: string\n}\n\nexport type FieldType =\n | 'string'\n | 'number'\n | 'boolean'\n | 'date'\n | 'enum'\n | 'array'\n | 'object'\n | 'reference'\n | 'unknown'\n\n/** Symbol used to tag reference schema objects. */\nconst REFERENCE_TAG = Symbol.for('pyreon:feature:reference')\n\n/**\n * Metadata carried by a reference schema.\n */\nexport interface ReferenceSchema {\n /** Marker symbol for detection. */\n [key: symbol]: true\n /** Name of the referenced feature. */\n _featureName: string\n /** Duck-typed Zod-like interface: validates as string | number. */\n safeParse: (value: unknown) => {\n success: boolean\n error?: { issues: { message: string }[] }\n }\n /** Async variant for compatibility. */\n safeParseAsync: (\n value: unknown,\n ) => Promise<{ success: boolean; error?: { issues: { message: string }[] } }>\n /** Shape-like marker for schema introspection. */\n _def: { typeName: string }\n}\n\n/**\n * Check if a value is a reference schema created by `reference()`.\n */\nexport function isReference(value: unknown): value is ReferenceSchema {\n return (\n value !== null &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[REFERENCE_TAG] === true\n )\n}\n\n/**\n * Create a reference field that links to another feature.\n *\n * Returns a Zod-compatible schema that validates as `string | number` and\n * carries metadata about the referenced feature for form dropdowns and table links.\n *\n * @example\n * ```ts\n * import { defineFeature, reference } from '@pyreon/feature'\n *\n * const posts = defineFeature({\n * name: 'posts',\n * schema: z.object({\n * title: z.string(),\n * authorId: reference(users),\n * }),\n * api: '/api/posts',\n * })\n * ```\n */\nexport function reference(feature: { name: string }): ReferenceSchema {\n const featureName = feature.name\n\n function validateRef(value: unknown): {\n success: boolean\n error?: { issues: { message: string }[] }\n } {\n if (typeof value === 'string' || typeof value === 'number') {\n return { success: true }\n }\n return {\n success: false,\n error: {\n issues: [\n {\n message: `Expected string or number reference to ${featureName}, got ${typeof value}`,\n },\n ],\n },\n }\n }\n\n return {\n [REFERENCE_TAG]: true,\n _featureName: featureName,\n safeParse: validateRef,\n safeParseAsync: async (value: unknown) => validateRef(value),\n _def: { typeName: 'ZodString' },\n }\n}\n\n/**\n * Convert a field name to a human-readable label.\n * e.g., 'firstName' → 'First Name', 'created_at' → 'Created At'\n */\nfunction nameToLabel(name: string): string {\n return name\n .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase → camel Case\n .replace(/[_-]/g, ' ') // snake_case/kebab-case → spaces\n .replace(/\\b\\w/g, (c) => c.toUpperCase()) // capitalize words\n}\n\n/**\n * Detect the field type from a Zod schema shape entry.\n * Duck-typed — works with Zod v3 and v4 without importing Zod.\n */\nfunction detectFieldType(zodField: unknown): {\n type: FieldType\n optional: boolean\n enumValues?: (string | number)[]\n referenceTo?: string\n} {\n // Check for reference fields first\n if (isReference(zodField)) {\n return {\n type: 'reference',\n optional: false,\n referenceTo: zodField._featureName,\n }\n }\n\n if (!zodField || typeof zodField !== 'object') {\n return { type: 'unknown', optional: false }\n }\n\n const field = zodField as Record<string, unknown>\n\n // Check for optional wrapper (ZodOptional or ZodNullable)\n let inner = field\n let optional = false\n\n // Zod v3: _def.typeName, Zod v4: _zod.def.type\n const getTypeName = (obj: Record<string, unknown>): string | undefined => {\n // v3 path\n const def = obj._def as Record<string, unknown> | undefined\n if (def?.typeName && typeof def.typeName === 'string') {\n return def.typeName\n }\n // v4 path\n const zod = obj._zod as Record<string, unknown> | undefined\n const zodDef = zod?.def as Record<string, unknown> | undefined\n if (zodDef?.type && typeof zodDef.type === 'string') {\n return zodDef.type\n }\n return undefined\n }\n\n const typeName = getTypeName(inner)\n\n // Unwrap optional/nullable\n if (\n typeName === 'ZodOptional' ||\n typeName === 'ZodNullable' ||\n typeName === 'optional' ||\n typeName === 'nullable'\n ) {\n optional = true\n const def = inner._def as Record<string, unknown> | undefined\n const innerType = def?.innerType ?? (inner._zod as Record<string, unknown>)?.def\n if (innerType && typeof innerType === 'object') {\n inner = innerType as Record<string, unknown>\n }\n }\n\n const innerTypeName = getTypeName(inner) ?? typeName\n\n // Map Zod type names to our FieldType\n if (!innerTypeName) return { type: 'unknown', optional }\n\n const typeMap: Record<string, FieldType> = {\n ZodString: 'string',\n ZodNumber: 'number',\n ZodBoolean: 'boolean',\n ZodDate: 'date',\n ZodEnum: 'enum',\n ZodNativeEnum: 'enum',\n ZodArray: 'array',\n ZodObject: 'object',\n // v4 names\n string: 'string',\n number: 'number',\n boolean: 'boolean',\n date: 'date',\n enum: 'enum',\n array: 'array',\n object: 'object',\n }\n\n const type = typeMap[innerTypeName] ?? 'string'\n\n // Extract enum values\n let enumValues: (string | number)[] | undefined\n if (type === 'enum') {\n const def = inner._def as Record<string, unknown> | undefined\n if (def?.values && Array.isArray(def.values)) {\n enumValues = def.values as (string | number)[]\n }\n // v4 path\n const zodDef = (inner._zod as Record<string, unknown>)?.def as\n | Record<string, unknown>\n | undefined\n if (zodDef?.values && Array.isArray(zodDef.values)) {\n enumValues = zodDef.values as (string | number)[]\n }\n }\n\n return {\n type,\n optional,\n ...(enumValues != null ? { enumValues } : {}),\n }\n}\n\n/**\n * Extract field information from a Zod object schema.\n * Returns an array of FieldInfo objects describing each field.\n *\n * @example\n * ```ts\n * const schema = z.object({ name: z.string(), age: z.number().optional() })\n * const fields = extractFields(schema)\n * // [\n * // { name: 'name', type: 'string', optional: false, label: 'Name' },\n * // { name: 'age', type: 'number', optional: true, label: 'Age' },\n * // ]\n * ```\n */\nexport function extractFields(schema: unknown): FieldInfo[] {\n if (!schema || typeof schema !== 'object') return []\n\n const s = schema as Record<string, unknown>\n\n // Get the shape object from the schema\n // Zod v3: schema._def.shape() or schema.shape\n // Zod v4: schema._zod.def.shape or schema.shape\n let shape: Record<string, unknown> | undefined\n\n // Try schema.shape (works for both v3 and v4)\n if (s.shape && typeof s.shape === 'object') {\n shape = s.shape as Record<string, unknown>\n }\n\n // Try _def.shape (v3 — can be a function)\n if (!shape) {\n const def = s._def as Record<string, unknown> | undefined\n if (def?.shape) {\n shape =\n typeof def.shape === 'function'\n ? (def.shape as () => Record<string, unknown>)()\n : (def.shape as Record<string, unknown>)\n }\n }\n\n // Try _zod.def.shape (v4)\n if (!shape) {\n const zod = s._zod as Record<string, unknown> | undefined\n const zodDef = zod?.def as Record<string, unknown> | undefined\n if (zodDef?.shape && typeof zodDef.shape === 'object') {\n shape = zodDef.shape as Record<string, unknown>\n }\n }\n\n if (!shape) return []\n\n return Object.entries(shape).map(([name, fieldSchema]) => {\n const { type, optional, enumValues, referenceTo } = detectFieldType(fieldSchema)\n const info: FieldInfo = {\n name,\n type,\n optional,\n label: nameToLabel(name),\n }\n if (enumValues) info.enumValues = enumValues\n if (referenceTo) info.referenceTo = referenceTo\n return info\n })\n}\n\n/**\n * Generate default initial values from a schema's field types.\n */\nexport function defaultInitialValues(fields: FieldInfo[]): Record<string, unknown> {\n const values: Record<string, unknown> = {}\n for (const field of fields) {\n switch (field.type) {\n case 'string':\n values[field.name] = ''\n break\n case 'number':\n values[field.name] = 0\n break\n case 'boolean':\n values[field.name] = false\n break\n case 'enum':\n values[field.name] = field.enumValues?.[0] ?? ''\n break\n case 'date':\n values[field.name] = ''\n break\n default:\n values[field.name] = ''\n }\n }\n return values\n}\n","import type { SchemaValidateFn } from '@pyreon/form'\nimport { useForm as _useForm } from '@pyreon/form'\nimport type { QueryKey } from '@pyreon/query'\nimport { useMutation as _useMutation, useQuery as _useQuery, useQueryClient } from '@pyreon/query'\nimport { batch, signal } from '@pyreon/reactivity'\nimport { defineStore } from '@pyreon/store'\nimport type { ColumnDef, SortingState } from '@pyreon/table'\nimport {\n useTable as _useTable,\n getCoreRowModel,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n} from '@pyreon/table'\nimport { zodSchema } from '@pyreon/validation'\nimport { defaultInitialValues, extractFields } from './schema'\nimport type {\n Feature,\n FeatureConfig,\n FeatureFormOptions,\n FeatureStore,\n FeatureTableOptions,\n ListOptions,\n} from './types'\n\n// ─── Fetch wrapper ────────────────────────────────────────────────────────────\n\nfunction createFetcher(baseFetcher: typeof fetch = fetch) {\n async function request<T>(url: string, init?: RequestInit): Promise<T> {\n const res = await baseFetcher(url, init)\n\n if (!res.ok) {\n let message = `${init?.method ?? 'GET'} ${url} failed: ${res.status}`\n try {\n const body = await res.json()\n if (body?.message) message = body.message\n if (body?.errors) {\n throw Object.assign(new Error(message), {\n status: res.status,\n errors: body.errors,\n })\n }\n } catch (e) {\n if (e instanceof Error && 'errors' in e) throw e\n }\n throw Object.assign(new Error(message), { status: res.status })\n }\n\n if (res.status === 204) return undefined as T\n return res.json()\n }\n\n return {\n list<T>(url: string, params?: Record<string, string | number | boolean>): Promise<T[]> {\n const query = params\n ? `?${new URLSearchParams(Object.entries(params).map(([k, v]) => [k, String(v)])).toString()}`\n : ''\n return request<T[]>(`${url}${query}`)\n },\n getById<T>(url: string, id: string | number): Promise<T> {\n return request<T>(`${url}/${id}`)\n },\n create<T>(url: string, data: unknown): Promise<T> {\n return request<T>(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(data),\n })\n },\n update<T>(url: string, id: string | number, data: unknown): Promise<T> {\n return request<T>(`${url}/${id}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(data),\n })\n },\n delete(url: string, id: string | number): Promise<void> {\n return request<void>(`${url}/${id}`, { method: 'DELETE' })\n },\n }\n}\n\n// ─── Schema validation ────────────────────────────────────────────────────────\n\nfunction createValidator<TValues extends Record<string, unknown>>(\n schema: unknown,\n customValidate?: SchemaValidateFn<TValues>,\n): SchemaValidateFn<TValues> | undefined {\n if (customValidate) return customValidate\n\n if (\n schema &&\n typeof schema === 'object' &&\n 'safeParseAsync' in schema &&\n typeof (schema as Record<string, unknown>).safeParseAsync === 'function'\n ) {\n return zodSchema(schema as Parameters<typeof zodSchema>[0]) as SchemaValidateFn<TValues>\n }\n\n return undefined\n}\n\n// ─── Resolve page value ───────────────────────────────────────────────────────\n\nfunction resolvePageValue(page: number | (() => number) | undefined): number | undefined {\n if (page === undefined) return undefined\n if (typeof page === 'function') return page()\n return page\n}\n\n// ─── defineFeature ────────────────────────────────────────────────────────────\n\n/**\n * Define a schema-driven feature with auto-generated CRUD hooks.\n *\n * @example\n * ```ts\n * import { defineFeature } from '@pyreon/feature'\n * import { z } from 'zod'\n *\n * const users = defineFeature({\n * name: 'users',\n * schema: z.object({\n * name: z.string().min(2),\n * email: z.string().email(),\n * role: z.enum(['admin', 'editor', 'viewer']),\n * }),\n * api: '/api/users',\n * })\n * ```\n */\nexport function defineFeature<TValues extends Record<string, unknown>>(\n config: FeatureConfig<TValues>,\n): Feature<TValues> {\n const { name, schema, api, fetcher: customFetcher } = config\n const http = createFetcher(customFetcher)\n\n // Introspect schema fields\n const fields = extractFields(schema)\n const autoInitialValues = defaultInitialValues(fields) as TValues\n const initialValues = config.initialValues\n ? { ...autoInitialValues, ...config.initialValues }\n : autoInitialValues\n\n const validate = createValidator<TValues>(schema, config.validate)\n\n const queryKeyBase = [name] as const\n const queryKey = (suffix?: string | number): QueryKey =>\n suffix !== undefined ? [name, suffix] : [name]\n\n // ─── Store definition ──────────────────────────────────────────────\n\n const useStoreHook = defineStore<FeatureStore<TValues>>(name, () => {\n const items = signal<TValues[]>([])\n const selected = signal<TValues | null>(null)\n const loading = signal(false)\n\n const select = (id: string | number) => {\n const found = items.peek().find((item) => {\n const record = item as Record<string, unknown>\n return record.id === id\n })\n selected.set(found ?? null)\n }\n\n const clear = () => {\n selected.set(null)\n }\n\n return { items, selected, loading, select, clear }\n })\n\n return {\n name,\n api,\n schema,\n fields,\n queryKey,\n\n // ─── Store ───────────────────────────────────────────────────────\n\n useStore: useStoreHook,\n\n // ─── Queries ────────────────────────────────────────────────────\n\n useList(options?: ListOptions) {\n return _useQuery(() => {\n const pageValue = resolvePageValue(options?.page)\n const pageSize = options?.pageSize ?? 20\n\n const params: Record<string, string | number | boolean> = {\n ...(options?.params ?? {}),\n }\n\n if (pageValue !== undefined) {\n params.page = pageValue\n params.pageSize = pageSize\n }\n\n const queryKeyParts: unknown[] = [...queryKeyBase, 'list', params]\n\n return {\n queryKey: queryKeyParts as QueryKey,\n queryFn: () =>\n http.list<TValues>(api, Object.keys(params).length > 0 ? params : undefined),\n ...(options?.staleTime != null ? { staleTime: options.staleTime } : {}),\n ...(options?.enabled != null ? { enabled: options.enabled } : {}),\n }\n })\n },\n\n useById(id: string | number) {\n return _useQuery(() => ({\n queryKey: [name, id],\n queryFn: () => http.getById<TValues>(api, id),\n enabled: id !== undefined && id !== null,\n }))\n },\n\n useSearch(searchTerm, options?: ListOptions) {\n return _useQuery(() => ({\n queryKey: [...queryKeyBase, 'search', searchTerm()],\n queryFn: () => http.list<TValues>(api, { ...options?.params, q: searchTerm() }),\n enabled: searchTerm().length > 0,\n ...(options?.staleTime != null ? { staleTime: options.staleTime } : {}),\n }))\n },\n\n // ─── Mutations ──────────────────────────────────────────────────\n\n useCreate() {\n const client = useQueryClient()\n return _useMutation({\n mutationFn: (data: Partial<TValues>) => http.create<TValues>(api, data),\n onSuccess: () => {\n client.invalidateQueries({\n queryKey: queryKeyBase as unknown as QueryKey,\n })\n },\n })\n },\n\n useUpdate() {\n type TVariables = { id: string | number; data: Partial<TValues> }\n const client = useQueryClient()\n return _useMutation<TValues, unknown, TVariables, { previous?: unknown }>({\n mutationFn: ({ id, data }: TVariables) => http.update<TValues>(api, id, data),\n onMutate: async (variables) => {\n await client.cancelQueries({ queryKey: [name, variables.id] })\n const previous = client.getQueryData([name, variables.id])\n client.setQueryData([name, variables.id], (old: unknown) => {\n if (old && typeof old === 'object') {\n return { ...old, ...variables.data }\n }\n return variables.data\n })\n return { previous }\n },\n onError: (_err, variables, context) => {\n if (context?.previous) {\n client.setQueryData([name, variables.id], context.previous)\n }\n },\n onSuccess: (_data, variables) => {\n client.invalidateQueries({\n queryKey: queryKeyBase as unknown as QueryKey,\n })\n client.invalidateQueries({ queryKey: [name, variables.id] })\n },\n }) as ReturnType<Feature<TValues>['useUpdate']>\n },\n\n useDelete() {\n const client = useQueryClient()\n return _useMutation({\n mutationFn: (id: string | number) => http.delete(api, id),\n onSuccess: () => {\n client.invalidateQueries({\n queryKey: queryKeyBase as unknown as QueryKey,\n })\n },\n })\n },\n\n // ─── Form ───────────────────────────────────────────────────────\n\n useForm(options?: FeatureFormOptions<TValues>) {\n const mode = options?.mode ?? 'create'\n const mergedInitial = {\n ...initialValues,\n ...(options?.initialValues ?? {}),\n } as TValues\n\n const form = _useForm<TValues>({\n initialValues: mergedInitial,\n ...(validate != null ? { schema: validate } : {}),\n validateOn: options?.validateOn ?? 'blur',\n onSubmit: async (values) => {\n try {\n let result: unknown\n if (mode === 'edit' && options?.id !== undefined) {\n result = await http.update<TValues>(api, options.id, values)\n } else {\n result = await http.create<TValues>(api, values)\n }\n options?.onSuccess?.(result)\n } catch (err) {\n options?.onError?.(err)\n throw err\n }\n },\n })\n\n // Auto-fetch in edit mode\n if (mode === 'edit' && options?.id !== undefined) {\n form.isSubmitting.set(true)\n http.getById<TValues>(api, options.id).then(\n (data) => {\n batch(() => {\n for (const key of Object.keys(data)) {\n form.setFieldValue(\n key as keyof TValues & string,\n (data as Record<string, unknown>)[key] as TValues[keyof TValues],\n )\n }\n form.isSubmitting.set(false)\n })\n },\n () => {\n form.isSubmitting.set(false)\n },\n )\n }\n\n return form\n },\n\n // ─── Table ──────────────────────────────────────────────────────\n\n useTable(data: TValues[] | (() => TValues[]), options?: FeatureTableOptions<TValues>) {\n const visibleFields = options?.columns\n ? fields.filter((f) => options.columns!.includes(f.name as keyof TValues & string))\n : fields\n\n const columns: ColumnDef<TValues, unknown>[] = visibleFields.map((field) => ({\n accessorKey: field.name,\n header: field.label,\n ...(options?.columnOverrides?.[field.name as keyof TValues & string] ?? {}),\n }))\n\n const sorting = signal<SortingState>([])\n const globalFilter = signal('')\n\n const table = _useTable(() => ({\n data: typeof data === 'function' ? data() : data,\n columns,\n state: {\n sorting: sorting(),\n globalFilter: globalFilter(),\n },\n onSortingChange: (updater: unknown) => {\n sorting.set(\n typeof updater === 'function'\n ? (updater as (prev: SortingState) => SortingState)(sorting())\n : (updater as SortingState),\n )\n },\n onGlobalFilterChange: (updater: unknown) => {\n globalFilter.set(\n typeof updater === 'function'\n ? (updater as (prev: string) => string)(globalFilter())\n : (updater as string),\n )\n },\n getCoreRowModel: getCoreRowModel(),\n getSortedRowModel: getSortedRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n ...(options?.pageSize ? { getPaginationRowModel: getPaginationRowModel() } : {}),\n }))\n\n return {\n table,\n sorting,\n globalFilter,\n columns: visibleFields,\n }\n },\n }\n}\n"],"mappings":"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"}
@@ -26,7 +26,7 @@ interface FieldInfo {
26
26
  /** Human-readable label derived from field name. */
27
27
  label: string;
28
28
  }
29
- type FieldType = "string" | "number" | "boolean" | "date" | "enum" | "array" | "object" | "reference" | "unknown";
29
+ type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'enum' | 'array' | 'object' | 'reference' | 'unknown';
30
30
  /**
31
31
  * Metadata carried by a reference schema.
32
32
  */
@@ -155,13 +155,13 @@ interface ListOptions {
155
155
  */
156
156
  interface FeatureFormOptions<TValues extends Record<string, unknown>> {
157
157
  /** 'create' (default) or 'edit'. Edit mode uses PUT instead of POST. */
158
- mode?: "create" | "edit";
158
+ mode?: 'create' | 'edit';
159
159
  /** Item ID — required when mode is 'edit'. Used to PUT to api/:id and auto-fetch data. */
160
160
  id?: string | number;
161
161
  /** Override initial values (merged with feature defaults). */
162
162
  initialValues?: Partial<TValues>;
163
163
  /** When to validate: 'blur' (default), 'change', or 'submit'. */
164
- validateOn?: "blur" | "change" | "submit";
164
+ validateOn?: 'blur' | 'change' | 'submit';
165
165
  /** Callback after successful create/update. */
166
166
  onSuccess?: (result: unknown) => void;
167
167
  /** Callback on submit error. */
package/package.json CHANGED
@@ -1,28 +1,25 @@
1
1
  {
2
2
  "name": "@pyreon/feature",
3
- "version": "0.11.4",
3
+ "version": "0.11.6",
4
4
  "description": "Schema-driven feature primitives — define once, get CRUD hooks, forms, tables, and stores",
5
+ "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/feature#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/pyreon/pyreon/issues"
8
+ },
5
9
  "license": "MIT",
6
10
  "repository": {
7
11
  "type": "git",
8
12
  "url": "https://github.com/pyreon/pyreon.git",
9
13
  "directory": "packages/fundamentals/feature"
10
14
  },
11
- "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/feature#readme",
12
- "bugs": {
13
- "url": "https://github.com/pyreon/pyreon/issues"
14
- },
15
- "publishConfig": {
16
- "access": "public"
17
- },
18
15
  "files": [
19
16
  "lib",
20
17
  "src",
21
18
  "README.md",
22
19
  "LICENSE"
23
20
  ],
24
- "sideEffects": false,
25
21
  "type": "module",
22
+ "sideEffects": false,
26
23
  "main": "./lib/index.js",
27
24
  "module": "./lib/index.js",
28
25
  "types": "./lib/types/index.d.ts",
@@ -33,29 +30,32 @@
33
30
  "types": "./lib/types/index.d.ts"
34
31
  }
35
32
  },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
36
  "scripts": {
37
37
  "build": "vl_rolldown_build",
38
38
  "dev": "vl_rolldown_build-watch",
39
39
  "test": "vitest run",
40
40
  "typecheck": "tsc --noEmit",
41
- "lint": "biome check ."
42
- },
43
- "peerDependencies": {
44
- "@pyreon/core": "^0.11.4",
45
- "@pyreon/reactivity": "^0.11.4"
41
+ "lint": "oxlint ."
46
42
  },
47
43
  "dependencies": {
48
- "@pyreon/form": "^0.11.4",
49
- "@pyreon/query": "^0.11.4",
50
- "@pyreon/store": "^0.11.4",
51
- "@pyreon/table": "^0.11.4",
52
- "@pyreon/validation": "^0.11.4"
44
+ "@pyreon/form": "^0.11.6",
45
+ "@pyreon/query": "^0.11.6",
46
+ "@pyreon/store": "^0.11.6",
47
+ "@pyreon/table": "^0.11.6",
48
+ "@pyreon/validation": "^0.11.6"
53
49
  },
54
50
  "devDependencies": {
55
51
  "@happy-dom/global-registrator": "^20.8.3",
56
- "@pyreon/core": "^0.11.4",
57
- "@pyreon/reactivity": "^0.11.4",
58
- "@pyreon/runtime-dom": "^0.11.4",
52
+ "@pyreon/core": "^0.11.6",
53
+ "@pyreon/reactivity": "^0.11.6",
54
+ "@pyreon/runtime-dom": "^0.11.6",
59
55
  "zod": "^4.3.6"
56
+ },
57
+ "peerDependencies": {
58
+ "@pyreon/core": "^0.11.6",
59
+ "@pyreon/reactivity": "^0.11.6"
60
60
  }
61
61
  }