@pyreon/feature 0.9.0 → 0.11.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.
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 =\n 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 } =\n 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(\n fields: FieldInfo[],\n): 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 {\n useMutation as _useMutation,\n useQuery as _useQuery,\n useQueryClient,\n} 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>(\n url: string,\n params?: Record<string, string | number | boolean>,\n ): 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(\n schema as Parameters<typeof zodSchema>[0],\n ) as SchemaValidateFn<TValues>\n }\n\n return undefined\n}\n\n// ─── Resolve page value ───────────────────────────────────────────────────────\n\nfunction resolvePageValue(\n page: number | (() => number) | undefined,\n): 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>(\n api,\n Object.keys(params).length > 0 ? params : undefined,\n ),\n ...(options?.staleTime != null\n ? { staleTime: options.staleTime }\n : {}),\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: () =>\n 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 {\n mutationFn: ({ id, data }: TVariables) =>\n 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 },\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>)[\n key\n ] 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(\n data: TValues[] | (() => TValues[]),\n options?: FeatureTableOptions<TValues>,\n ) {\n const visibleFields = options?.columns\n ? fields.filter((f) =>\n options.columns!.includes(f.name as keyof TValues & string),\n )\n : fields\n\n const columns: ColumnDef<TValues, unknown>[] = visibleFields.map(\n (field) => ({\n accessorKey: field.name,\n header: field.label,\n ...(options?.columnOverrides?.[\n field.name as keyof TValues & string\n ] ?? {}),\n }),\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\n ? { getPaginationRowModel: getPaginationRowModel() }\n : {}),\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
  */
@@ -106,14 +106,26 @@ declare function extractFields(schema: unknown): FieldInfo[];
106
106
  declare function defaultInitialValues(fields: FieldInfo[]): Record<string, unknown>;
107
107
  //#endregion
108
108
  //#region src/types.d.ts
109
+ /**
110
+ * Duck-typed schema inference. Matches Zod (`_output`), Valibot, ArkType
111
+ * without importing their types. Allows TypeScript to infer TValues from schema.
112
+ */
113
+ type InferSchemaValues<TSchema> = TSchema extends {
114
+ _output: infer T extends Record<string, unknown>;
115
+ } ? T : TSchema extends {
116
+ infer: infer T extends Record<string, unknown>;
117
+ } ? T : Record<string, unknown>;
109
118
  /**
110
119
  * Configuration for defining a feature.
111
120
  */
112
121
  interface FeatureConfig<TValues extends Record<string, unknown>> {
113
122
  /** Unique feature name — used for store ID and query key namespace. */
114
123
  name: string;
115
- /** Validation schema (Zod, Valibot, or ArkType). Duck-typed — must have `safeParseAsync` for auto-validation. */
116
- schema: unknown;
124
+ /** Validation schema (Zod, Valibot, or ArkType). Duck-typed — must have `safeParseAsync` for auto-validation.
125
+ * Zod schemas carry `_output` for automatic TValues inference. */
126
+ schema: {
127
+ _output?: TValues;
128
+ } & Record<never, never>;
117
129
  /** Custom schema-level validation function. If provided, overrides auto-detection from schema. */
118
130
  validate?: SchemaValidateFn<TValues>;
119
131
  /** API base path (e.g., '/api/users'). */
@@ -143,13 +155,13 @@ interface ListOptions {
143
155
  */
144
156
  interface FeatureFormOptions<TValues extends Record<string, unknown>> {
145
157
  /** 'create' (default) or 'edit'. Edit mode uses PUT instead of POST. */
146
- mode?: 'create' | 'edit';
158
+ mode?: "create" | "edit";
147
159
  /** Item ID — required when mode is 'edit'. Used to PUT to api/:id and auto-fetch data. */
148
160
  id?: string | number;
149
161
  /** Override initial values (merged with feature defaults). */
150
162
  initialValues?: Partial<TValues>;
151
163
  /** When to validate: 'blur' (default), 'change', or 'submit'. */
152
- validateOn?: 'blur' | 'change' | 'submit';
164
+ validateOn?: "blur" | "change" | "submit";
153
165
  /** Callback after successful create/update. */
154
166
  onSuccess?: (result: unknown) => void;
155
167
  /** Callback on submit error. */
@@ -255,5 +267,5 @@ interface Feature<TValues extends Record<string, unknown>> {
255
267
  */
256
268
  declare function defineFeature<TValues extends Record<string, unknown>>(config: FeatureConfig<TValues>): Feature<TValues>;
257
269
  //#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 };
270
+ export { type Feature, type FeatureConfig, type FeatureFormOptions, type FeatureStore, type FeatureTableOptions, type FeatureTableResult, type FieldInfo, type FieldType, type InferSchemaValues, type ListOptions, type ReferenceSchema, defaultInitialValues, defineFeature, extractFields, isReference, reference };
259
271
  //# sourceMappingURL=index2.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/schema.ts","../../../src/types.ts","../../../src/define-feature.ts"],"mappings":";;;;;;;;;;;;;;UAOiB,SAAA;;EAEf,IAAA;EAFwB;EAIxB,IAAA,EAAM,SAAA;EAAS;EAEf,QAAA;EAFA;EAIA,UAAA;EAFA;EAIA,WAAA;EAAA;EAEA,KAAA;AAAA;AAAA,KAGU,SAAA;AAAZ;;;AAAA,UAiBiB,eAAA;EAjBI;EAAA,CAmBlB,GAAA;EAF6B;EAI9B,YAAA;EASY;EAPZ,SAAA,GAAY,KAAA;IACV,OAAA;IACA,KAAA;MAAU,MAAA;QAAU,OAAA;MAAA;IAAA;EAAA;EAGtB;EAAA,cAAA,GACE,KAAA,cACG,OAAA;IAAU,OAAA;IAAkB,KAAA;MAAU,MAAA;QAAU,OAAA;MAAA;IAAA;EAAA;EAErC;EAAhB,IAAA;IAAQ,QAAA;EAAA;AAAA;;;;iBAMM,WAAA,CAAY,KAAA,YAAiB,KAAA,IAAS,eAAA;;;AA4BtD;;;;;;;;;AAwKA;;;;;AAuDA;;;;iBA/NgB,SAAA,CAAU,OAAA;EAAW,IAAA;AAAA,IAAiB,eAAA;;;;;;AC9EtD;;;;;;;;;iBDsPgB,aAAA,CAAc,MAAA,YAAkB,SAAA;;;;iBAuDhC,oBAAA,CACd,MAAA,EAAQ,SAAA,KACP,MAAA;;;;;AAlTH;UCGiB,aAAA,iBAA8B,MAAA;;EAE7C,IAAA;EDHA;ECKA,MAAA;EDHM;ECKN,QAAA,GAAW,gBAAA,CAAiB,OAAA;EDD5B;ECGA,GAAA;EDCA;ECCA,aAAA,GAAgB,OAAA,CAAQ,OAAA;EDDnB;ECGL,OAAA,UAAiB,KAAA;AAAA;;;;UAMF,WAAA;EDWe;ECT9B,MAAA,GAAS,MAAA;EDsBG;ECpBZ,IAAA,YAAgB,MAAA;EDWhB;ECTA,QAAA;EDWY;ECTZ,SAAA;EDWE;ECTF,OAAA;AAAA;;;;UAMe,kBAAA,iBAAmC,MAAA;EDQjB;ECNjC,IAAA;EDMqD;ECJrD,EAAA;EDMQ;ECJR,aAAA,GAAgB,OAAA,CAAQ,OAAA;EDIR;ECFhB,UAAA;EDQyB;ECNzB,SAAA,IAAa,MAAA;EDMsD;ECJnE,OAAA,IAAW,KAAA;AAAA;;;;UAMI,mBAAA,iBAAoC,MAAA;ED0B5B;ECxBvB,OAAA,UAAiB,OAAA;EDwBkD;ECtBnE,eAAA,GAAkB,OAAA,CAChB,MAAA,OAAa,OAAA,WAAkB,MAAA;EDqBT;EClBxB,QAAA;AAAA;;AD0LF;;UCpLiB,kBAAA,iBAAmC,MAAA;EDoLtB;EClL5B,KAAA,EAAO,QAAA,CAA8C,cAAA,CAAb,KAAA,CAAM,OAAA;EDyOhC;ECvOd,OAAA,EAAS,MAAA,CAAO,YAAA;;EAEhB,YAAA,EAAc,MAAA;EDsON;ECpOR,OAAA,EAAS,SAAA;AAAA;;;;UAMM,YAAA,iBAA6B,MAAA;;EAE5C,KAAA,EAAO,MAAA,CAAO,OAAA;EAlFc;EAoF5B,QAAA,EAAU,MAAA,CAAO,OAAA;EApF4B;EAsF7C,OAAA,EAAS,MAAA;EAhFE;EAkFX,MAAA,GAAS,EAAA;EA9EO;EAgFhB,KAAA;EA9EsB;EAAA,CAgFrB,GAAA;AAAA;;;;UAMc,OAAA,iBAAwB,MAAA;EA5F5B;EA8FX,IAAA;EA5FA;EA8FA,GAAA;EA5FgB;EA8FhB,MAAA;EA5FA;EA8FA,MAAA,EAAQ,SAAA;EA9Fc;EAiGtB,OAAA,GAAU,OAAA,GAAU,WAAA,KAAgB,cAAA,CAAe,OAAA;EA3FpC;EA8Ff,OAAA,GAAU,EAAA,sBAAwB,cAAA,CAAe,OAAA;;EAGjD,SAAA,GACE,UAAA,EAAY,MAAA,UACZ,OAAA,GAAU,WAAA,KACP,cAAA,CAAe,OAAA;EAlGpB;EAqGA,SAAA,QAAiB,iBAAA,CAAkB,OAAA,WAAkB,OAAA,CAAQ,OAAA;EAnG7D;EAsGA,SAAA,QAAiB,iBAAA,CACf,OAAA;IAEE,EAAA;IAAqB,IAAA,EAAM,OAAA,CAAQ,OAAA;EAAA;EAnGhC;EAuGP,SAAA,QAAiB,iBAAA;EAjGF;EAoGf,OAAA,GAAU,OAAA,GAAU,kBAAA,CAAmB,OAAA,MAAa,SAAA,CAAU,OAAA;EApG7B;EAuGjC,QAAA,GACE,IAAA,EAAM,OAAA,YAAmB,OAAA,KACzB,OAAA,GAAU,mBAAA,CAAoB,OAAA,MAC3B,kBAAA,CAAmB,OAAA;EApGA;EAuGxB,QAAA,QAAgB,QAAA,CAAS,YAAA,CAAa,OAAA;EAvGf;EA0GvB,QAAA,GAAW,MAAA,uBAA6B,QAAA;AAAA;;;;;;;;;;ADrJ1C;;;;;;;;;;;;iBEuIgB,aAAA,iBAA8B,MAAA,kBAAA,CAC5C,MAAA,EAAQ,aAAA,CAAc,OAAA,IACrB,OAAA,CAAQ,OAAA"}
1
+ {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/schema.ts","../../../src/types.ts","../../../src/define-feature.ts"],"mappings":"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"}
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@pyreon/feature",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Schema-driven feature primitives — define once, get CRUD hooks, forms, tables, and stores",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/pyreon/fundamentals.git",
9
- "directory": "packages/feature"
8
+ "url": "https://github.com/pyreon/pyreon.git",
9
+ "directory": "packages/fundamentals/feature"
10
10
  },
11
11
  "homepage": "https://github.com/pyreon/fundamentals/tree/main/packages/feature#readme",
12
12
  "bugs": {
13
- "url": "https://github.com/pyreon/fundamentals/issues"
13
+ "url": "https://github.com/pyreon/pyreon/issues"
14
14
  },
15
15
  "publishConfig": {
16
16
  "access": "public"
@@ -37,17 +37,25 @@
37
37
  "build": "vl_rolldown_build",
38
38
  "dev": "vl_rolldown_build-watch",
39
39
  "test": "vitest run",
40
- "typecheck": "tsc --noEmit"
40
+ "typecheck": "tsc --noEmit",
41
+ "lint": "biome check ."
41
42
  },
42
43
  "peerDependencies": {
43
- "@pyreon/core": ">=0.7.0 <0.8.0",
44
- "@pyreon/reactivity": ">=0.7.0 <0.8.0"
44
+ "@pyreon/core": "^0.11.0",
45
+ "@pyreon/reactivity": "^0.11.0"
45
46
  },
46
47
  "dependencies": {
47
- "@pyreon/form": "^0.9.0",
48
- "@pyreon/query": "^0.9.0",
49
- "@pyreon/store": "^0.9.0",
50
- "@pyreon/table": "^0.9.0",
51
- "@pyreon/validation": "^0.9.0"
48
+ "@pyreon/form": "^0.11.0",
49
+ "@pyreon/query": "^0.11.0",
50
+ "@pyreon/store": "^0.11.0",
51
+ "@pyreon/table": "^0.11.0",
52
+ "@pyreon/validation": "^0.11.0"
53
+ },
54
+ "devDependencies": {
55
+ "@happy-dom/global-registrator": "^20.8.3",
56
+ "@pyreon/core": "^0.11.0",
57
+ "@pyreon/reactivity": "^0.11.0",
58
+ "@pyreon/runtime-dom": "^0.11.0",
59
+ "zod": "^4.3.6"
52
60
  }
53
61
  }
@@ -1,23 +1,19 @@
1
- import type { SchemaValidateFn } from '@pyreon/form'
2
- import { useForm as _useForm } from '@pyreon/form'
3
- import type { QueryKey } from '@pyreon/query'
4
- import {
5
- useMutation as _useMutation,
6
- useQuery as _useQuery,
7
- useQueryClient,
8
- } from '@pyreon/query'
9
- import { batch, signal } from '@pyreon/reactivity'
10
- import { defineStore } from '@pyreon/store'
11
- import type { ColumnDef, SortingState } from '@pyreon/table'
1
+ import type { SchemaValidateFn } from "@pyreon/form"
2
+ import { useForm as _useForm } from "@pyreon/form"
3
+ import type { QueryKey } from "@pyreon/query"
4
+ import { useMutation as _useMutation, useQuery as _useQuery, useQueryClient } from "@pyreon/query"
5
+ import { batch, signal } from "@pyreon/reactivity"
6
+ import { defineStore } from "@pyreon/store"
7
+ import type { ColumnDef, SortingState } from "@pyreon/table"
12
8
  import {
13
9
  useTable as _useTable,
14
10
  getCoreRowModel,
15
11
  getFilteredRowModel,
16
12
  getPaginationRowModel,
17
13
  getSortedRowModel,
18
- } from '@pyreon/table'
19
- import { zodSchema } from '@pyreon/validation'
20
- import { defaultInitialValues, extractFields } from './schema'
14
+ } from "@pyreon/table"
15
+ import { zodSchema } from "@pyreon/validation"
16
+ import { defaultInitialValues, extractFields } from "./schema"
21
17
  import type {
22
18
  Feature,
23
19
  FeatureConfig,
@@ -25,7 +21,7 @@ import type {
25
21
  FeatureStore,
26
22
  FeatureTableOptions,
27
23
  ListOptions,
28
- } from './types'
24
+ } from "./types"
29
25
 
30
26
  // ─── Fetch wrapper ────────────────────────────────────────────────────────────
31
27
 
@@ -34,7 +30,7 @@ function createFetcher(baseFetcher: typeof fetch = fetch) {
34
30
  const res = await baseFetcher(url, init)
35
31
 
36
32
  if (!res.ok) {
37
- let message = `${init?.method ?? 'GET'} ${url} failed: ${res.status}`
33
+ let message = `${init?.method ?? "GET"} ${url} failed: ${res.status}`
38
34
  try {
39
35
  const body = await res.json()
40
36
  if (body?.message) message = body.message
@@ -45,7 +41,7 @@ function createFetcher(baseFetcher: typeof fetch = fetch) {
45
41
  })
46
42
  }
47
43
  } catch (e) {
48
- if (e instanceof Error && 'errors' in e) throw e
44
+ if (e instanceof Error && "errors" in e) throw e
49
45
  }
50
46
  throw Object.assign(new Error(message), { status: res.status })
51
47
  }
@@ -55,13 +51,10 @@ function createFetcher(baseFetcher: typeof fetch = fetch) {
55
51
  }
56
52
 
57
53
  return {
58
- list<T>(
59
- url: string,
60
- params?: Record<string, string | number | boolean>,
61
- ): Promise<T[]> {
54
+ list<T>(url: string, params?: Record<string, string | number | boolean>): Promise<T[]> {
62
55
  const query = params
63
56
  ? `?${new URLSearchParams(Object.entries(params).map(([k, v]) => [k, String(v)])).toString()}`
64
- : ''
57
+ : ""
65
58
  return request<T[]>(`${url}${query}`)
66
59
  },
67
60
  getById<T>(url: string, id: string | number): Promise<T> {
@@ -69,20 +62,20 @@ function createFetcher(baseFetcher: typeof fetch = fetch) {
69
62
  },
70
63
  create<T>(url: string, data: unknown): Promise<T> {
71
64
  return request<T>(url, {
72
- method: 'POST',
73
- headers: { 'Content-Type': 'application/json' },
65
+ method: "POST",
66
+ headers: { "Content-Type": "application/json" },
74
67
  body: JSON.stringify(data),
75
68
  })
76
69
  },
77
70
  update<T>(url: string, id: string | number, data: unknown): Promise<T> {
78
71
  return request<T>(`${url}/${id}`, {
79
- method: 'PUT',
80
- headers: { 'Content-Type': 'application/json' },
72
+ method: "PUT",
73
+ headers: { "Content-Type": "application/json" },
81
74
  body: JSON.stringify(data),
82
75
  })
83
76
  },
84
77
  delete(url: string, id: string | number): Promise<void> {
85
- return request<void>(`${url}/${id}`, { method: 'DELETE' })
78
+ return request<void>(`${url}/${id}`, { method: "DELETE" })
86
79
  },
87
80
  }
88
81
  }
@@ -97,13 +90,11 @@ function createValidator<TValues extends Record<string, unknown>>(
97
90
 
98
91
  if (
99
92
  schema &&
100
- typeof schema === 'object' &&
101
- 'safeParseAsync' in schema &&
102
- typeof (schema as Record<string, unknown>).safeParseAsync === 'function'
93
+ typeof schema === "object" &&
94
+ "safeParseAsync" in schema &&
95
+ typeof (schema as Record<string, unknown>).safeParseAsync === "function"
103
96
  ) {
104
- return zodSchema(
105
- schema as Parameters<typeof zodSchema>[0],
106
- ) as SchemaValidateFn<TValues>
97
+ return zodSchema(schema as Parameters<typeof zodSchema>[0]) as SchemaValidateFn<TValues>
107
98
  }
108
99
 
109
100
  return undefined
@@ -111,11 +102,9 @@ function createValidator<TValues extends Record<string, unknown>>(
111
102
 
112
103
  // ─── Resolve page value ───────────────────────────────────────────────────────
113
104
 
114
- function resolvePageValue(
115
- page: number | (() => number) | undefined,
116
- ): number | undefined {
105
+ function resolvePageValue(page: number | (() => number) | undefined): number | undefined {
117
106
  if (page === undefined) return undefined
118
- if (typeof page === 'function') return page()
107
+ if (typeof page === "function") return page()
119
108
  return page
120
109
  }
121
110
 
@@ -208,18 +197,13 @@ export function defineFeature<TValues extends Record<string, unknown>>(
208
197
  params.pageSize = pageSize
209
198
  }
210
199
 
211
- const queryKeyParts: unknown[] = [...queryKeyBase, 'list', params]
200
+ const queryKeyParts: unknown[] = [...queryKeyBase, "list", params]
212
201
 
213
202
  return {
214
203
  queryKey: queryKeyParts as QueryKey,
215
204
  queryFn: () =>
216
- http.list<TValues>(
217
- api,
218
- Object.keys(params).length > 0 ? params : undefined,
219
- ),
220
- ...(options?.staleTime != null
221
- ? { staleTime: options.staleTime }
222
- : {}),
205
+ http.list<TValues>(api, Object.keys(params).length > 0 ? params : undefined),
206
+ ...(options?.staleTime != null ? { staleTime: options.staleTime } : {}),
223
207
  ...(options?.enabled != null ? { enabled: options.enabled } : {}),
224
208
  }
225
209
  })
@@ -235,9 +219,8 @@ export function defineFeature<TValues extends Record<string, unknown>>(
235
219
 
236
220
  useSearch(searchTerm, options?: ListOptions) {
237
221
  return _useQuery(() => ({
238
- queryKey: [...queryKeyBase, 'search', searchTerm()],
239
- queryFn: () =>
240
- http.list<TValues>(api, { ...options?.params, q: searchTerm() }),
222
+ queryKey: [...queryKeyBase, "search", searchTerm()],
223
+ queryFn: () => http.list<TValues>(api, { ...options?.params, q: searchTerm() }),
241
224
  enabled: searchTerm().length > 0,
242
225
  ...(options?.staleTime != null ? { staleTime: options.staleTime } : {}),
243
226
  }))
@@ -260,34 +243,31 @@ export function defineFeature<TValues extends Record<string, unknown>>(
260
243
  useUpdate() {
261
244
  type TVariables = { id: string | number; data: Partial<TValues> }
262
245
  const client = useQueryClient()
263
- return _useMutation<TValues, unknown, TVariables, { previous?: unknown }>(
264
- {
265
- mutationFn: ({ id, data }: TVariables) =>
266
- http.update<TValues>(api, id, data),
267
- onMutate: async (variables) => {
268
- await client.cancelQueries({ queryKey: [name, variables.id] })
269
- const previous = client.getQueryData([name, variables.id])
270
- client.setQueryData([name, variables.id], (old: unknown) => {
271
- if (old && typeof old === 'object') {
272
- return { ...old, ...variables.data }
273
- }
274
- return variables.data
275
- })
276
- return { previous }
277
- },
278
- onError: (_err, variables, context) => {
279
- if (context?.previous) {
280
- client.setQueryData([name, variables.id], context.previous)
246
+ return _useMutation<TValues, unknown, TVariables, { previous?: unknown }>({
247
+ mutationFn: ({ id, data }: TVariables) => http.update<TValues>(api, id, data),
248
+ onMutate: async (variables) => {
249
+ await client.cancelQueries({ queryKey: [name, variables.id] })
250
+ const previous = client.getQueryData([name, variables.id])
251
+ client.setQueryData([name, variables.id], (old: unknown) => {
252
+ if (old && typeof old === "object") {
253
+ return { ...old, ...variables.data }
281
254
  }
282
- },
283
- onSuccess: (_data, variables) => {
284
- client.invalidateQueries({
285
- queryKey: queryKeyBase as unknown as QueryKey,
286
- })
287
- client.invalidateQueries({ queryKey: [name, variables.id] })
288
- },
255
+ return variables.data
256
+ })
257
+ return { previous }
289
258
  },
290
- ) as ReturnType<Feature<TValues>['useUpdate']>
259
+ onError: (_err, variables, context) => {
260
+ if (context?.previous) {
261
+ client.setQueryData([name, variables.id], context.previous)
262
+ }
263
+ },
264
+ onSuccess: (_data, variables) => {
265
+ client.invalidateQueries({
266
+ queryKey: queryKeyBase as unknown as QueryKey,
267
+ })
268
+ client.invalidateQueries({ queryKey: [name, variables.id] })
269
+ },
270
+ }) as ReturnType<Feature<TValues>["useUpdate"]>
291
271
  },
292
272
 
293
273
  useDelete() {
@@ -305,7 +285,7 @@ export function defineFeature<TValues extends Record<string, unknown>>(
305
285
  // ─── Form ───────────────────────────────────────────────────────
306
286
 
307
287
  useForm(options?: FeatureFormOptions<TValues>) {
308
- const mode = options?.mode ?? 'create'
288
+ const mode = options?.mode ?? "create"
309
289
  const mergedInitial = {
310
290
  ...initialValues,
311
291
  ...(options?.initialValues ?? {}),
@@ -314,11 +294,11 @@ export function defineFeature<TValues extends Record<string, unknown>>(
314
294
  const form = _useForm<TValues>({
315
295
  initialValues: mergedInitial,
316
296
  ...(validate != null ? { schema: validate } : {}),
317
- validateOn: options?.validateOn ?? 'blur',
297
+ validateOn: options?.validateOn ?? "blur",
318
298
  onSubmit: async (values) => {
319
299
  try {
320
300
  let result: unknown
321
- if (mode === 'edit' && options?.id !== undefined) {
301
+ if (mode === "edit" && options?.id !== undefined) {
322
302
  result = await http.update<TValues>(api, options.id, values)
323
303
  } else {
324
304
  result = await http.create<TValues>(api, values)
@@ -332,7 +312,7 @@ export function defineFeature<TValues extends Record<string, unknown>>(
332
312
  })
333
313
 
334
314
  // Auto-fetch in edit mode
335
- if (mode === 'edit' && options?.id !== undefined) {
315
+ if (mode === "edit" && options?.id !== undefined) {
336
316
  form.isSubmitting.set(true)
337
317
  http.getById<TValues>(api, options.id).then(
338
318
  (data) => {
@@ -340,9 +320,7 @@ export function defineFeature<TValues extends Record<string, unknown>>(
340
320
  for (const key of Object.keys(data)) {
341
321
  form.setFieldValue(
342
322
  key as keyof TValues & string,
343
- (data as Record<string, unknown>)[
344
- key
345
- ] as TValues[keyof TValues],
323
+ (data as Record<string, unknown>)[key] as TValues[keyof TValues],
346
324
  )
347
325
  }
348
326
  form.isSubmitting.set(false)
@@ -359,31 +337,22 @@ export function defineFeature<TValues extends Record<string, unknown>>(
359
337
 
360
338
  // ─── Table ──────────────────────────────────────────────────────
361
339
 
362
- useTable(
363
- data: TValues[] | (() => TValues[]),
364
- options?: FeatureTableOptions<TValues>,
365
- ) {
340
+ useTable(data: TValues[] | (() => TValues[]), options?: FeatureTableOptions<TValues>) {
366
341
  const visibleFields = options?.columns
367
- ? fields.filter((f) =>
368
- options.columns!.includes(f.name as keyof TValues & string),
369
- )
342
+ ? fields.filter((f) => options.columns!.includes(f.name as keyof TValues & string))
370
343
  : fields
371
344
 
372
- const columns: ColumnDef<TValues, unknown>[] = visibleFields.map(
373
- (field) => ({
374
- accessorKey: field.name,
375
- header: field.label,
376
- ...(options?.columnOverrides?.[
377
- field.name as keyof TValues & string
378
- ] ?? {}),
379
- }),
380
- )
345
+ const columns: ColumnDef<TValues, unknown>[] = visibleFields.map((field) => ({
346
+ accessorKey: field.name,
347
+ header: field.label,
348
+ ...(options?.columnOverrides?.[field.name as keyof TValues & string] ?? {}),
349
+ }))
381
350
 
382
351
  const sorting = signal<SortingState>([])
383
- const globalFilter = signal('')
352
+ const globalFilter = signal("")
384
353
 
385
354
  const table = _useTable(() => ({
386
- data: typeof data === 'function' ? data() : data,
355
+ data: typeof data === "function" ? data() : data,
387
356
  columns,
388
357
  state: {
389
358
  sorting: sorting(),
@@ -391,14 +360,14 @@ export function defineFeature<TValues extends Record<string, unknown>>(
391
360
  },
392
361
  onSortingChange: (updater: unknown) => {
393
362
  sorting.set(
394
- typeof updater === 'function'
363
+ typeof updater === "function"
395
364
  ? (updater as (prev: SortingState) => SortingState)(sorting())
396
365
  : (updater as SortingState),
397
366
  )
398
367
  },
399
368
  onGlobalFilterChange: (updater: unknown) => {
400
369
  globalFilter.set(
401
- typeof updater === 'function'
370
+ typeof updater === "function"
402
371
  ? (updater as (prev: string) => string)(globalFilter())
403
372
  : (updater as string),
404
373
  )
@@ -406,9 +375,7 @@ export function defineFeature<TValues extends Record<string, unknown>>(
406
375
  getCoreRowModel: getCoreRowModel(),
407
376
  getSortedRowModel: getSortedRowModel(),
408
377
  getFilteredRowModel: getFilteredRowModel(),
409
- ...(options?.pageSize
410
- ? { getPaginationRowModel: getPaginationRowModel() }
411
- : {}),
378
+ ...(options?.pageSize ? { getPaginationRowModel: getPaginationRowModel() } : {}),
412
379
  }))
413
380
 
414
381
  return {