295 lines
7.1 KiB
TypeScript
295 lines
7.1 KiB
TypeScript
/**
|
|
* 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, string> = {
|
|
'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<TerminusType | null> {
|
|
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<Record<string, string[]>> {
|
|
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<string, string[]> = {}
|
|
|
|
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<string, string[]>
|
|
): 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<string, string[]>
|
|
): 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<string, unknown>).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,
|
|
}
|
|
}
|