@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 +1 -1
- package/lib/types/index.d.ts +18 -6
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +20 -12
- package/src/define-feature.ts +71 -104
- package/src/index.ts +5 -4
- package/src/schema.ts +59 -63
- package/src/tests/feature.test.tsx +408 -473
- package/src/types.ts +25 -14
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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"}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ interface FieldInfo {
|
|
|
26
26
|
/** Human-readable label derived from field name. */
|
|
27
27
|
label: string;
|
|
28
28
|
}
|
|
29
|
-
type FieldType =
|
|
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
|
-
|
|
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?:
|
|
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?:
|
|
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
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -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;;;;;;;;;
|
|
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.
|
|
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/
|
|
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/
|
|
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": "
|
|
44
|
-
"@pyreon/reactivity": "
|
|
44
|
+
"@pyreon/core": "^0.11.0",
|
|
45
|
+
"@pyreon/reactivity": "^0.11.0"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
|
-
"@pyreon/form": "^0.
|
|
48
|
-
"@pyreon/query": "^0.
|
|
49
|
-
"@pyreon/store": "^0.
|
|
50
|
-
"@pyreon/table": "^0.
|
|
51
|
-
"@pyreon/validation": "^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
|
}
|
package/src/define-feature.ts
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
import type { SchemaValidateFn } from
|
|
2
|
-
import { useForm as _useForm } from
|
|
3
|
-
import type { QueryKey } from
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
19
|
-
import { zodSchema } from
|
|
20
|
-
import { defaultInitialValues, extractFields } from
|
|
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
|
|
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 ??
|
|
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 &&
|
|
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:
|
|
73
|
-
headers: {
|
|
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:
|
|
80
|
-
headers: {
|
|
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:
|
|
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 ===
|
|
101
|
-
|
|
102
|
-
typeof (schema as Record<string, unknown>).safeParseAsync ===
|
|
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 ===
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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 ??
|
|
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 ??
|
|
297
|
+
validateOn: options?.validateOn ?? "blur",
|
|
318
298
|
onSubmit: async (values) => {
|
|
319
299
|
try {
|
|
320
300
|
let result: unknown
|
|
321
|
-
if (mode ===
|
|
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 ===
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 {
|