/** * Composable for TerminusDB: * - Load schema by ID via GraphQL introspection * - Convert schema to FormKit format */ import type { FormKitSchemaNode } from '@formkit/core' // Types for TerminusDB GraphQL introspection interface TerminusField { name: string type: { name: string | null kind: string ofType?: { name: string | null kind: string enumValues?: Array<{ name: string }> } enumValues?: Array<{ name: string }> } } interface TerminusType { name: string kind: string description: string | null fields: TerminusField[] | null enumValues: Array<{ name: string }> | null } // Map GraphQL scalar types to FormKit types const graphqlToFormKit: Record = { 'String': 'text', 'Int': 'number', 'Float': 'number', 'Boolean': 'checkbox', 'DateTime': 'datetime-local', 'Date': 'date', 'BigInt': 'number', 'Decimal': 'number', } export function useTerminus() { const config = useRuntimeConfig() // TerminusDB GraphQL endpoint const getEndpoint = (): string => { return (config.public.terminusGraphql as string) || 'https://terminus.optovia.ru/api/graphql/admin/optovia' } // Basic auth header const getAuthHeader = () => { // admin:optovia_admin_2024 in base64 return 'Basic ' + btoa('admin:optovia_admin_2024') } /** * Load schema from TerminusDB via GraphQL introspection */ async function getSchema(schemaId: string): Promise { try { const endpoint = getEndpoint() // GraphQL introspection query for a specific type const query = ` query IntrospectionQuery { __type(name: "${schemaId}") { name kind description fields { name type { name kind ofType { name kind enumValues { name } } enumValues { name } } } } } ` const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': getAuthHeader(), }, body: JSON.stringify({ query }), }) const result = await response.json() as { data: { __type: TerminusType | null } } return result.data?.__type || null } catch (error) { console.error('Failed to load schema from TerminusDB:', error) return null } } /** * Fetch all enums from schema via introspection */ async function getEnums(): Promise> { try { const endpoint = getEndpoint() // Get all types and filter enums const query = ` query IntrospectionQuery { __schema { types { name kind enumValues { name } } } } ` const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': getAuthHeader(), }, body: JSON.stringify({ query }), }) const result = await response.json() as { data: { __schema: { types: TerminusType[] } } } const enums: Record = {} for (const type of result.data?.__schema?.types || []) { // Keep only our enums (exclude system __) if (type.kind === 'ENUM' && !type.name.startsWith('__') && type.enumValues) { enums[type.name] = type.enumValues.map(e => e.name) } } return enums } catch (error) { console.error('Failed to load enums from TerminusDB:', error) return {} } } /** * Convert TerminusDB type to FormKit schema */ function schemaToFormKit( terminusType: TerminusType, enums: Record ): FormKitSchemaNode[] { const formkitSchema: FormKitSchemaNode[] = [] if (!terminusType.fields) return formkitSchema // System fields to skip (TerminusDB internal fields start with _) const skipFields = ['id', '_id', '_type', '_json'] // Also skip path fields like _path_to_Cocoa, _path_to_Coffee, etc. const isPathField = (name: string) => name.startsWith('_path_to_') for (const field of terminusType.fields) { if (skipFields.includes(field.name) || isPathField(field.name)) continue const formkitField = parseFieldType(field, enums) if (formkitField) { formkitSchema.push(formkitField) } } return formkitSchema } /** * Parse field type and build FormKit node */ function parseFieldType( field: TerminusField, enums: Record ): FormKitSchemaNode | null { const { name, type } = field // Determine actual type (may be wrapped in NON_NULL or LIST) let actualType = type let required = false if (type.kind === 'NON_NULL') { required = true actualType = type.ofType as typeof type } // Enum? if (actualType.kind === 'ENUM' && actualType.name) { const enumValues = actualType.enumValues?.map(e => e.name) || enums[actualType.name] || [] return createSelectField(name, enumValues, required) } // Scalar type if (actualType.kind === 'SCALAR' && actualType.name) { return createInputField(name, actualType.name, required) } // LIST -> textarea if (actualType.kind === 'LIST') { return { $formkit: 'textarea', name, label: formatLabel(name), help: 'Enter values separated by comma', validation: required ? 'required' : undefined, } } return null } /** * Create input field */ function createInputField( fieldName: string, graphqlType: string, required: boolean ): FormKitSchemaNode { const formkitType = graphqlToFormKit[graphqlType] || 'text' const field: FormKitSchemaNode = { $formkit: formkitType, name: fieldName, label: formatLabel(fieldName), validation: required ? 'required' : undefined, } // For Float/Decimal add step if (graphqlType === 'Float' || graphqlType === 'Decimal') { (field as Record).step = '0.01' } return field } /** * Create select field from enum */ function createSelectField( fieldName: string, options: string[], required: boolean ): FormKitSchemaNode { return { $formkit: 'select', name: fieldName, label: formatLabel(fieldName), validation: required ? 'required' : undefined, placeholder: 'Select...', options: options.map(opt => ({ value: opt, label: formatLabel(opt), })), } } /** * Format field name to human-readable label */ function formatLabel(fieldName: string): string { return fieldName .replace(/_/g, ' ') .replace(/([A-Z])/g, ' $1') .replace(/^\w/, c => c.toUpperCase()) .trim() } return { getSchema, getEnums, schemaToFormKit, formatLabel, getEndpoint, } }