|
|
|
|
@@ -108,7 +108,7 @@ export async function generateMizanTypes(schema) {
|
|
|
|
|
const functions = schema['x-mizan-functions'] || []
|
|
|
|
|
|
|
|
|
|
if (functions.length > 0) {
|
|
|
|
|
lines.push('export const DJANGO_FUNCTIONS = {')
|
|
|
|
|
lines.push('export const MIZAN_FUNCTIONS = {')
|
|
|
|
|
for (const fn of functions) {
|
|
|
|
|
lines.push(` ${fn.camelName}: {`)
|
|
|
|
|
lines.push(` name: '${fn.name}',`)
|
|
|
|
|
@@ -119,7 +119,7 @@ export async function generateMizanTypes(schema) {
|
|
|
|
|
}
|
|
|
|
|
lines.push('} as const')
|
|
|
|
|
} else {
|
|
|
|
|
lines.push('export const DJANGO_FUNCTIONS = {} as const')
|
|
|
|
|
lines.push('export const MIZAN_FUNCTIONS = {} as const')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lines.push('')
|
|
|
|
|
@@ -127,25 +127,57 @@ export async function generateMizanTypes(schema) {
|
|
|
|
|
return lines.join('\n')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extract unique context names from an affects array.
|
|
|
|
|
* Both context-level and function-level affects resolve to context names.
|
|
|
|
|
*/
|
|
|
|
|
function getAffectedContexts(affects) {
|
|
|
|
|
const contexts = new Set()
|
|
|
|
|
for (const target of affects) {
|
|
|
|
|
if (target.type === 'context') {
|
|
|
|
|
contexts.add(target.name)
|
|
|
|
|
} else if (target.type === 'function' && target.context) {
|
|
|
|
|
contexts.add(target.context)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return [...contexts]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map JSON schema type string to TypeScript type.
|
|
|
|
|
*/
|
|
|
|
|
function jsonTypeToTS(type) {
|
|
|
|
|
if (type === 'integer' || type === 'number') return 'number'
|
|
|
|
|
if (type === 'boolean') return 'boolean'
|
|
|
|
|
return 'string'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate the React provider that wraps MizanProvider with typed hooks.
|
|
|
|
|
*
|
|
|
|
|
* The generated provider:
|
|
|
|
|
* - Wraps MizanProvider (from mizan library)
|
|
|
|
|
* - Passes context names for auto-fetch
|
|
|
|
|
* - Provides typed hooks for contexts and functions
|
|
|
|
|
* - MizanContext: Root provider with global context bundled fetch
|
|
|
|
|
* - Named context providers: <UserContext user_id={...}>
|
|
|
|
|
* - Mutation hooks with auto-invalidation
|
|
|
|
|
* - Plain function hooks
|
|
|
|
|
*/
|
|
|
|
|
export function generateMizanProvider(schema, options = {}) {
|
|
|
|
|
const { hasChannels = false } = options
|
|
|
|
|
const functions = schema['x-mizan-functions'] || []
|
|
|
|
|
const contextGroups = schema['x-mizan-contexts'] || {}
|
|
|
|
|
|
|
|
|
|
if (functions.length === 0) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Separate contexts from regular functions
|
|
|
|
|
const contexts = functions.filter(fn => fn.isContext)
|
|
|
|
|
// Partition functions
|
|
|
|
|
const globalContexts = functions.filter(fn => fn.isContext === 'global')
|
|
|
|
|
const regularFunctions = functions.filter(fn => !fn.isContext && !fn.isForm)
|
|
|
|
|
const mutationFunctions = regularFunctions.filter(fn => fn.affects)
|
|
|
|
|
const plainFunctions = regularFunctions.filter(fn => !fn.affects)
|
|
|
|
|
|
|
|
|
|
// Named context groups (everything except 'global')
|
|
|
|
|
const namedContextEntries = Object.entries(contextGroups).filter(([name]) => name !== 'global')
|
|
|
|
|
|
|
|
|
|
// Collect type imports
|
|
|
|
|
const typeImports = []
|
|
|
|
|
@@ -166,10 +198,11 @@ export function generateMizanProvider(schema, options = {}) {
|
|
|
|
|
'// Regenerate with: npm run schemas',
|
|
|
|
|
'',
|
|
|
|
|
'// This file provides typed wrappers around the mizan library.',
|
|
|
|
|
'// - DjangoContext: Typed provider wrapping MizanProvider',
|
|
|
|
|
'// - Typed hooks: useAuthStatus(), useUser(), etc.',
|
|
|
|
|
'// - MizanContext: Root provider with global context',
|
|
|
|
|
'// - Named context providers: <UserContext user_id={...}>',
|
|
|
|
|
'// - Typed hooks with auto-invalidation',
|
|
|
|
|
'',
|
|
|
|
|
"import { type ReactNode, useCallback } from 'react'",
|
|
|
|
|
"import { type ReactNode, useCallback, useState, useEffect, useRef, createContext, useContext } from 'react'",
|
|
|
|
|
"import {",
|
|
|
|
|
" MizanProvider,",
|
|
|
|
|
" useMizan,",
|
|
|
|
|
@@ -180,7 +213,6 @@ export function generateMizanProvider(schema, options = {}) {
|
|
|
|
|
"} from 'mizan'",
|
|
|
|
|
...(hasChannels ? [
|
|
|
|
|
"import { ChannelProvider, ChannelConnection } from 'mizan/channels'",
|
|
|
|
|
"import { useRef } from 'react'",
|
|
|
|
|
] : []),
|
|
|
|
|
'',
|
|
|
|
|
]
|
|
|
|
|
@@ -191,7 +223,7 @@ export function generateMizanProvider(schema, options = {}) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Hydration types
|
|
|
|
|
// Hydration types (global contexts only)
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
@@ -199,20 +231,19 @@ export function generateMizanProvider(schema, options = {}) {
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
if (contexts.length > 0) {
|
|
|
|
|
lines.push('/** Typed hydration data for SSR */')
|
|
|
|
|
lines.push('export interface DjangoHydration {')
|
|
|
|
|
for (const ctx of contexts) {
|
|
|
|
|
if (globalContexts.length > 0) {
|
|
|
|
|
lines.push('/** Typed hydration data for SSR (global contexts only) */')
|
|
|
|
|
lines.push('export interface MizanHydrationData {')
|
|
|
|
|
for (const ctx of globalContexts) {
|
|
|
|
|
lines.push(` ${ctx.camelName}?: ${ctx.outputType}`)
|
|
|
|
|
}
|
|
|
|
|
lines.push('}')
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
lines.push('/** Convert typed hydration to mizan format */')
|
|
|
|
|
lines.push('function toMizanHydration(hydration?: DjangoHydration): MizanHydration | undefined {')
|
|
|
|
|
lines.push('function toMizanHydration(hydration?: MizanHydrationData): MizanHydration | undefined {')
|
|
|
|
|
lines.push(' if (!hydration) return undefined')
|
|
|
|
|
lines.push(' const result: MizanHydration = {}')
|
|
|
|
|
for (const ctx of contexts) {
|
|
|
|
|
for (const ctx of globalContexts) {
|
|
|
|
|
lines.push(` if (hydration.${ctx.camelName} !== undefined) result['${ctx.name}'] = hydration.${ctx.camelName}`)
|
|
|
|
|
}
|
|
|
|
|
lines.push(' return result')
|
|
|
|
|
@@ -221,50 +252,81 @@ export function generateMizanProvider(schema, options = {}) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Provider
|
|
|
|
|
// Global Context Loader (inner component, fetches GET /ctx/global/)
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
if (globalContexts.length > 0) {
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('// Global Context Loader')
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('')
|
|
|
|
|
lines.push('function GlobalContextLoader({ children }: { children: ReactNode }) {')
|
|
|
|
|
lines.push(' const mizan = useMizan()')
|
|
|
|
|
lines.push(' const loaded = useRef(false)')
|
|
|
|
|
lines.push('')
|
|
|
|
|
lines.push(' useEffect(() => {')
|
|
|
|
|
lines.push(' if (loaded.current) return')
|
|
|
|
|
lines.push(' loaded.current = true')
|
|
|
|
|
lines.push('')
|
|
|
|
|
lines.push(' ;(async () => {')
|
|
|
|
|
lines.push(' await mizan.whenReady')
|
|
|
|
|
lines.push(' try {')
|
|
|
|
|
lines.push(" const response = await mizan.request('GET', `${mizan.baseUrl}/ctx/global/`)")
|
|
|
|
|
lines.push(' const result = await response.json()')
|
|
|
|
|
lines.push(' if (!result.error) {')
|
|
|
|
|
lines.push(' for (const [name, data] of Object.entries(result.data)) {')
|
|
|
|
|
lines.push(' mizan.setContextData(name, data)')
|
|
|
|
|
lines.push(' }')
|
|
|
|
|
lines.push(' }')
|
|
|
|
|
lines.push(' } catch (e) {')
|
|
|
|
|
lines.push(" console.error('[MizanContext] Global context fetch failed:', e)")
|
|
|
|
|
lines.push(' }')
|
|
|
|
|
lines.push(' })()')
|
|
|
|
|
lines.push(' }, [mizan])')
|
|
|
|
|
lines.push('')
|
|
|
|
|
lines.push(' return <>{children}</>')
|
|
|
|
|
lines.push('}')
|
|
|
|
|
lines.push('')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Root Provider (MizanContext)
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('// Provider')
|
|
|
|
|
lines.push('// Root Provider')
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
lines.push('export interface DjangoContextProps {')
|
|
|
|
|
lines.push('export interface MizanContextProps {')
|
|
|
|
|
lines.push(' children: ReactNode')
|
|
|
|
|
if (contexts.length > 0) {
|
|
|
|
|
lines.push(' /** SSR hydration data */')
|
|
|
|
|
lines.push(' hydration?: DjangoHydration')
|
|
|
|
|
if (globalContexts.length > 0) {
|
|
|
|
|
lines.push(' /** SSR hydration data (global contexts only) */')
|
|
|
|
|
lines.push(' hydration?: MizanHydrationData')
|
|
|
|
|
}
|
|
|
|
|
lines.push(' /** WebSocket URL for RPC calls (default: /ws/) */')
|
|
|
|
|
lines.push(' wsUrl?: string')
|
|
|
|
|
lines.push(' /** Base URL for HTTP fallback (default: /api/mizan) */')
|
|
|
|
|
lines.push(' /** Base URL for HTTP calls (default: /api/mizan) */')
|
|
|
|
|
lines.push(' baseUrl?: string')
|
|
|
|
|
lines.push('}')
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
// Context names array for MizanProvider
|
|
|
|
|
const contextNames = contexts.map(ctx => `'${ctx.name}'`).join(', ')
|
|
|
|
|
|
|
|
|
|
lines.push('/**')
|
|
|
|
|
lines.push(' * Typed Django context provider.')
|
|
|
|
|
lines.push(' *')
|
|
|
|
|
lines.push(' * Wraps MizanProvider with:')
|
|
|
|
|
lines.push(' * - Typed hydration')
|
|
|
|
|
lines.push(' * - Auto-fetch for registered contexts')
|
|
|
|
|
lines.push(' * Root mizan provider. Mount at your app root.')
|
|
|
|
|
lines.push(' *')
|
|
|
|
|
lines.push(' * Usage:')
|
|
|
|
|
lines.push(' * <DjangoContext hydration={hydration}>')
|
|
|
|
|
lines.push(' * <MizanContext hydration={hydration}>')
|
|
|
|
|
lines.push(' * <App />')
|
|
|
|
|
lines.push(' * </DjangoContext>')
|
|
|
|
|
lines.push(' * </MizanContext>')
|
|
|
|
|
lines.push(' */')
|
|
|
|
|
lines.push('export function DjangoContext({')
|
|
|
|
|
lines.push('export function MizanContext({')
|
|
|
|
|
lines.push(' children,')
|
|
|
|
|
if (contexts.length > 0) {
|
|
|
|
|
if (globalContexts.length > 0) {
|
|
|
|
|
lines.push(' hydration,')
|
|
|
|
|
}
|
|
|
|
|
lines.push(' wsUrl,')
|
|
|
|
|
lines.push(' baseUrl,')
|
|
|
|
|
lines.push('}: DjangoContextProps) {')
|
|
|
|
|
lines.push('}: MizanContextProps) {')
|
|
|
|
|
|
|
|
|
|
if (hasChannels) {
|
|
|
|
|
lines.push(' const connectionRef = useRef<ChannelConnection | null>(null)')
|
|
|
|
|
@@ -274,11 +336,11 @@ export function generateMizanProvider(schema, options = {}) {
|
|
|
|
|
lines.push('')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build the JSX tree
|
|
|
|
|
lines.push(' return (')
|
|
|
|
|
lines.push(' <MizanProvider')
|
|
|
|
|
if (contexts.length > 0) {
|
|
|
|
|
if (globalContexts.length > 0) {
|
|
|
|
|
lines.push(' hydration={toMizanHydration(hydration)}')
|
|
|
|
|
lines.push(` contexts={[${contextNames}]}`)
|
|
|
|
|
}
|
|
|
|
|
lines.push(' wsUrl={wsUrl}')
|
|
|
|
|
lines.push(' baseUrl={baseUrl}')
|
|
|
|
|
@@ -287,12 +349,18 @@ export function generateMizanProvider(schema, options = {}) {
|
|
|
|
|
}
|
|
|
|
|
lines.push(' >')
|
|
|
|
|
|
|
|
|
|
// Inner content: GlobalContextLoader wraps children if needed
|
|
|
|
|
let innerContent = '{children}'
|
|
|
|
|
if (globalContexts.length > 0) {
|
|
|
|
|
innerContent = `<GlobalContextLoader>{children}</GlobalContextLoader>`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasChannels) {
|
|
|
|
|
lines.push(' <ChannelProvider connection={connectionRef.current} autoConnect={true}>')
|
|
|
|
|
lines.push(' {children}')
|
|
|
|
|
lines.push(' </ChannelProvider>')
|
|
|
|
|
lines.push(` <ChannelProvider connection={connectionRef.current} autoConnect={true}>`)
|
|
|
|
|
lines.push(` ${innerContent}`)
|
|
|
|
|
lines.push(` </ChannelProvider>`)
|
|
|
|
|
} else {
|
|
|
|
|
lines.push(' {children}')
|
|
|
|
|
lines.push(` ${innerContent}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lines.push(' </MizanProvider>')
|
|
|
|
|
@@ -300,78 +368,200 @@ export function generateMizanProvider(schema, options = {}) {
|
|
|
|
|
lines.push('}')
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
// Legacy alias
|
|
|
|
|
lines.push('/** @deprecated Use MizanContext instead */')
|
|
|
|
|
lines.push('export const DjangoContext = MizanContext')
|
|
|
|
|
lines.push('/** @deprecated Use MizanContextProps instead */')
|
|
|
|
|
lines.push('export type DjangoContextProps = MizanContextProps')
|
|
|
|
|
if (globalContexts.length > 0) {
|
|
|
|
|
lines.push('/** @deprecated Use MizanHydrationData instead */')
|
|
|
|
|
lines.push('export type DjangoHydration = MizanHydrationData')
|
|
|
|
|
}
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Context Hooks
|
|
|
|
|
// Global Context Hooks
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
if (contexts.length > 0) {
|
|
|
|
|
if (globalContexts.length > 0) {
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('// Context Hooks (typed wrappers)')
|
|
|
|
|
lines.push('// Global Context Hooks')
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
for (const ctx of contexts) {
|
|
|
|
|
for (const ctx of globalContexts) {
|
|
|
|
|
const pascal = pascalCase(ctx.camelName)
|
|
|
|
|
lines.push(`/**`)
|
|
|
|
|
lines.push(` * Get ${ctx.name} context data.`)
|
|
|
|
|
lines.push(` * @throws if context not loaded yet`)
|
|
|
|
|
lines.push(` */`)
|
|
|
|
|
lines.push(`/** Get ${ctx.name} context data. @throws if not loaded yet */`)
|
|
|
|
|
lines.push(`export function use${pascal}(): ${ctx.outputType} {`)
|
|
|
|
|
lines.push(` const data = useMizanContext<${ctx.outputType}>('${ctx.name}')`)
|
|
|
|
|
lines.push(` if (data === undefined) {`)
|
|
|
|
|
lines.push(` throw new Error('use${pascal}: context not loaded yet')`)
|
|
|
|
|
lines.push(` }`)
|
|
|
|
|
lines.push(` if (data === undefined) throw new Error('use${pascal}: context not loaded yet')`)
|
|
|
|
|
lines.push(` return data`)
|
|
|
|
|
lines.push(`}`)
|
|
|
|
|
lines.push('')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Refresh hooks
|
|
|
|
|
lines.push('/**')
|
|
|
|
|
lines.push(' * Get context refresh functions without subscribing to data changes.')
|
|
|
|
|
lines.push(' * Use this in components that only need to trigger refreshes.')
|
|
|
|
|
lines.push(' */')
|
|
|
|
|
lines.push('export function useDjangoRefresh() {')
|
|
|
|
|
lines.push(' const { refreshContext, refreshAllContexts } = useMizan()')
|
|
|
|
|
lines.push('/** Refresh functions for global contexts. */')
|
|
|
|
|
lines.push('export function useMizanRefresh() {')
|
|
|
|
|
lines.push(' const { invalidateContext } = useMizan()')
|
|
|
|
|
lines.push(' return {')
|
|
|
|
|
for (const ctx of contexts) {
|
|
|
|
|
for (const ctx of globalContexts) {
|
|
|
|
|
const pascal = pascalCase(ctx.camelName)
|
|
|
|
|
lines.push(` refresh${pascal}: () => refreshContext('${ctx.name}'),`)
|
|
|
|
|
lines.push(` refresh${pascal}: () => invalidateContext('${ctx.name}'),`)
|
|
|
|
|
}
|
|
|
|
|
lines.push(' refreshAll: refreshAllContexts,')
|
|
|
|
|
lines.push(' }')
|
|
|
|
|
lines.push('}')
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
// Legacy alias
|
|
|
|
|
lines.push('/** @deprecated Use useMizanRefresh instead */')
|
|
|
|
|
lines.push('export const useDjangoRefresh = useMizanRefresh')
|
|
|
|
|
lines.push('')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Function Hooks
|
|
|
|
|
// Named Context Providers
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
if (regularFunctions.length > 0) {
|
|
|
|
|
if (namedContextEntries.length > 0) {
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('// Function Hooks (typed wrappers)')
|
|
|
|
|
lines.push('// Named Context Providers')
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
for (const fn of regularFunctions) {
|
|
|
|
|
for (const [ctxName, ctxMeta] of namedContextEntries) {
|
|
|
|
|
const ctxPascal = toPascalCase(ctxName)
|
|
|
|
|
const ctxFunctions = functions.filter(fn => fn.isContext === ctxName)
|
|
|
|
|
const params = ctxMeta.params || {}
|
|
|
|
|
const paramEntries = Object.entries(params)
|
|
|
|
|
|
|
|
|
|
// Internal React context type
|
|
|
|
|
lines.push(`const ${ctxPascal}ContextInternal = createContext<{`)
|
|
|
|
|
for (const fn of ctxFunctions) {
|
|
|
|
|
lines.push(` ${fn.name}: ${fn.outputType}`)
|
|
|
|
|
}
|
|
|
|
|
lines.push(`} | null>(null)`)
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
// Props interface
|
|
|
|
|
lines.push(`export interface ${ctxPascal}ContextProps {`)
|
|
|
|
|
lines.push(` children: ReactNode`)
|
|
|
|
|
for (const [pName, pMeta] of paramEntries) {
|
|
|
|
|
const tsType = jsonTypeToTS(pMeta.type)
|
|
|
|
|
const optional = pMeta.required ? '' : '?'
|
|
|
|
|
lines.push(` ${pName}${optional}: ${tsType}`)
|
|
|
|
|
}
|
|
|
|
|
lines.push(`}`)
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
// Provider component
|
|
|
|
|
lines.push(`export function ${ctxPascal}Context({ children, ...params }: ${ctxPascal}ContextProps) {`)
|
|
|
|
|
lines.push(` const mizan = useMizan()`)
|
|
|
|
|
lines.push(` const [data, setData] = useState<{`)
|
|
|
|
|
for (const fn of ctxFunctions) {
|
|
|
|
|
lines.push(` ${fn.name}: ${fn.outputType}`)
|
|
|
|
|
}
|
|
|
|
|
lines.push(` } | null>(null)`)
|
|
|
|
|
lines.push('')
|
|
|
|
|
lines.push(` const refetch = useCallback(async () => {`)
|
|
|
|
|
lines.push(` await mizan.whenReady`)
|
|
|
|
|
lines.push(` const qs = new URLSearchParams()`)
|
|
|
|
|
for (const [pName] of paramEntries) {
|
|
|
|
|
lines.push(` if (params.${pName} !== undefined) qs.set('${pName}', String(params.${pName}))`)
|
|
|
|
|
}
|
|
|
|
|
lines.push(` const resp = await mizan.request('GET', \`\${mizan.baseUrl}/ctx/${ctxName}/?\${qs}\`)`)
|
|
|
|
|
lines.push(` const result = await resp.json()`)
|
|
|
|
|
lines.push(` if (!result.error) setData(result.data)`)
|
|
|
|
|
|
|
|
|
|
// Dependency array: mizan + each param
|
|
|
|
|
const deps = ['mizan', ...paramEntries.map(([pName]) => `params.${pName}`)]
|
|
|
|
|
lines.push(` }, [${deps.join(', ')}])`)
|
|
|
|
|
lines.push('')
|
|
|
|
|
lines.push(` useEffect(() => { refetch() }, [refetch])`)
|
|
|
|
|
lines.push(` useEffect(() => mizan.registerContextProvider('${ctxName}', refetch), [mizan, refetch])`)
|
|
|
|
|
lines.push('')
|
|
|
|
|
lines.push(` return <${ctxPascal}ContextInternal value={data}>{children}</${ctxPascal}ContextInternal>`)
|
|
|
|
|
lines.push(`}`)
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
// Individual data hooks
|
|
|
|
|
for (const fn of ctxFunctions) {
|
|
|
|
|
const hookPascal = pascalCase(fn.camelName)
|
|
|
|
|
lines.push(`export function use${hookPascal}(): ${fn.outputType} {`)
|
|
|
|
|
lines.push(` const ctx = useContext(${ctxPascal}ContextInternal)`)
|
|
|
|
|
lines.push(` if (!ctx) throw new Error('use${hookPascal} must be used within ${ctxPascal}Context')`)
|
|
|
|
|
lines.push(` return ctx.${fn.name}`)
|
|
|
|
|
lines.push(`}`)
|
|
|
|
|
lines.push('')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Mutation Hooks (with auto-invalidation)
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
if (mutationFunctions.length > 0) {
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('// Mutation Hooks (auto-invalidate on success)')
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
for (const fn of mutationFunctions) {
|
|
|
|
|
const pascal = pascalCase(fn.camelName)
|
|
|
|
|
const transport = fn.transport || 'http'
|
|
|
|
|
const affectedContexts = getAffectedContexts(fn.affects)
|
|
|
|
|
|
|
|
|
|
lines.push(`/** Call ${fn.name}. Auto-invalidates: ${affectedContexts.join(', ')} */`)
|
|
|
|
|
lines.push(`export function use${pascal}() {`)
|
|
|
|
|
lines.push(` const mizan = useMizan()`)
|
|
|
|
|
|
|
|
|
|
if (fn.hasInput) {
|
|
|
|
|
lines.push(` return useCallback(async (input: ${fn.inputType}) => {`)
|
|
|
|
|
lines.push(` const result = await mizan.call<${fn.inputType}, ${fn.outputType}>('${fn.name}', input, '${transport}')`)
|
|
|
|
|
} else {
|
|
|
|
|
lines.push(` return useCallback(async () => {`)
|
|
|
|
|
lines.push(` const result = await mizan.call<void, ${fn.outputType}>('${fn.name}', undefined, '${transport}')`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Invalidation
|
|
|
|
|
if (affectedContexts.length === 1) {
|
|
|
|
|
lines.push(` await mizan.invalidateContext('${affectedContexts[0]}')`)
|
|
|
|
|
} else if (affectedContexts.length > 1) {
|
|
|
|
|
lines.push(` await Promise.all([`)
|
|
|
|
|
for (const ctx of affectedContexts) {
|
|
|
|
|
lines.push(` mizan.invalidateContext('${ctx}'),`)
|
|
|
|
|
}
|
|
|
|
|
lines.push(` ])`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lines.push(` return result`)
|
|
|
|
|
lines.push(` }, [mizan])`)
|
|
|
|
|
lines.push(`}`)
|
|
|
|
|
lines.push('')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Plain Function Hooks
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
if (plainFunctions.length > 0) {
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('// Function Hooks')
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
for (const fn of plainFunctions) {
|
|
|
|
|
const pascal = pascalCase(fn.camelName)
|
|
|
|
|
// Transport is known at generation time - pass it directly
|
|
|
|
|
const transport = fn.transport || 'http'
|
|
|
|
|
|
|
|
|
|
if (fn.hasInput) {
|
|
|
|
|
lines.push(`/**`)
|
|
|
|
|
lines.push(` * Call ${fn.name} server function.`)
|
|
|
|
|
lines.push(` * Transport: ${transport}`)
|
|
|
|
|
lines.push(` */`)
|
|
|
|
|
lines.push(`/** Call ${fn.name}. Transport: ${transport} */`)
|
|
|
|
|
lines.push(`export function use${pascal}() {`)
|
|
|
|
|
lines.push(` return useMizanCall<${fn.inputType}, ${fn.outputType}>('${fn.name}', '${transport}')`)
|
|
|
|
|
lines.push(`}`)
|
|
|
|
|
} else {
|
|
|
|
|
lines.push(`/**`)
|
|
|
|
|
lines.push(` * Call ${fn.name} server function.`)
|
|
|
|
|
lines.push(` * Transport: ${transport}`)
|
|
|
|
|
lines.push(` */`)
|
|
|
|
|
lines.push(`/** Call ${fn.name}. Transport: ${transport} */`)
|
|
|
|
|
lines.push(`export function use${pascal}() {`)
|
|
|
|
|
lines.push(` return useMizanCall<void, ${fn.outputType}>('${fn.name}', '${transport}')`)
|
|
|
|
|
lines.push(`}`)
|
|
|
|
|
@@ -401,14 +591,14 @@ export function generateMizanProvider(schema, options = {}) {
|
|
|
|
|
*/
|
|
|
|
|
export function generateMizanServer(schema) {
|
|
|
|
|
const functions = schema['x-mizan-functions'] || []
|
|
|
|
|
const contexts = functions.filter(fn => fn.isContext)
|
|
|
|
|
const globalContexts = functions.filter(fn => fn.isContext === 'global')
|
|
|
|
|
|
|
|
|
|
if (contexts.length === 0) {
|
|
|
|
|
if (globalContexts.length === 0) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Collect type imports for contexts
|
|
|
|
|
const typeImports = contexts.map(ctx => ctx.outputType).filter(Boolean)
|
|
|
|
|
// Collect type imports for global contexts
|
|
|
|
|
const typeImports = globalContexts.map(ctx => ctx.outputType).filter(Boolean)
|
|
|
|
|
const uniqueTypeImports = [...new Set(typeImports)].sort()
|
|
|
|
|
|
|
|
|
|
const lines = [
|
|
|
|
|
@@ -430,55 +620,54 @@ export function generateMizanServer(schema) {
|
|
|
|
|
lines.push('// Hydration Types')
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('')
|
|
|
|
|
lines.push('/** Typed hydration data for SSR */')
|
|
|
|
|
lines.push('export interface DjangoHydration {')
|
|
|
|
|
for (const ctx of contexts) {
|
|
|
|
|
lines.push('/** Typed hydration data for SSR (global contexts only) */')
|
|
|
|
|
lines.push('export interface MizanHydrationData {')
|
|
|
|
|
for (const ctx of globalContexts) {
|
|
|
|
|
lines.push(` ${ctx.camelName}?: ${ctx.outputType}`)
|
|
|
|
|
}
|
|
|
|
|
lines.push('}')
|
|
|
|
|
lines.push('')
|
|
|
|
|
lines.push('/** @deprecated Use MizanHydrationData instead */')
|
|
|
|
|
lines.push('export type DjangoHydration = MizanHydrationData')
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
// SSR Hydration Helper
|
|
|
|
|
// SSR Hydration Helper — single bundled GET
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('// SSR Hydration Helper')
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('')
|
|
|
|
|
lines.push('/**')
|
|
|
|
|
lines.push(' * Fetch hydration data for SSR.')
|
|
|
|
|
lines.push(' * Fetch hydration data for SSR via bundled context endpoint.')
|
|
|
|
|
lines.push(' *')
|
|
|
|
|
lines.push(' * Call this in your server component:')
|
|
|
|
|
lines.push(' * const hydration = await getDjangoHydration(client)')
|
|
|
|
|
lines.push(' * return <DjangoContext hydration={hydration}>...</DjangoContext>')
|
|
|
|
|
lines.push(' * const hydration = await getMizanHydration(client)')
|
|
|
|
|
lines.push(' * return <MizanContext hydration={hydration}>...</MizanContext>')
|
|
|
|
|
lines.push(' */')
|
|
|
|
|
lines.push('export async function getDjangoHydration(')
|
|
|
|
|
lines.push('export async function getMizanHydration(')
|
|
|
|
|
lines.push(" client: { request: (method: string, url: string, body?: unknown) => Promise<Response> }")
|
|
|
|
|
lines.push('): Promise<DjangoHydration> {')
|
|
|
|
|
lines.push(' const hydration: DjangoHydration = {}')
|
|
|
|
|
lines.push('): Promise<MizanHydrationData> {')
|
|
|
|
|
lines.push(' const hydration: MizanHydrationData = {}')
|
|
|
|
|
lines.push('')
|
|
|
|
|
lines.push(' const results = await Promise.allSettled([')
|
|
|
|
|
for (const ctx of contexts) {
|
|
|
|
|
lines.push(` client.request('POST', '/api/mizan/call/', { fn: '${ctx.name}', args: {} }),`)
|
|
|
|
|
lines.push(' try {')
|
|
|
|
|
lines.push(" const response = await client.request('GET', '/api/mizan/ctx/global/')")
|
|
|
|
|
lines.push(' const result = await response.json()')
|
|
|
|
|
lines.push(' if (!result.error) {')
|
|
|
|
|
for (const ctx of globalContexts) {
|
|
|
|
|
lines.push(` if (result.data?.${ctx.name} !== undefined) hydration.${ctx.camelName} = result.data.${ctx.name}`)
|
|
|
|
|
}
|
|
|
|
|
lines.push(' ])')
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
contexts.forEach((ctx, i) => {
|
|
|
|
|
lines.push(` if (results[${i}].status === 'fulfilled') {`)
|
|
|
|
|
lines.push(` const data = await (results[${i}] as PromiseFulfilledResult<Response>).value.json()`)
|
|
|
|
|
lines.push(` if (data.error) {`)
|
|
|
|
|
lines.push(` console.error('[getDjangoHydration] ${ctx.name} failed:', data.code, data.message)`)
|
|
|
|
|
lines.push(` } else {`)
|
|
|
|
|
lines.push(` hydration.${ctx.camelName} = data.data`)
|
|
|
|
|
lines.push(` }`)
|
|
|
|
|
lines.push(` } else {`)
|
|
|
|
|
lines.push(` console.error('[getDjangoHydration] ${ctx.name} request failed:', (results[${i}] as PromiseRejectedResult).reason)`)
|
|
|
|
|
lines.push(' }')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
lines.push(' } else {')
|
|
|
|
|
lines.push(" console.error('[getMizanHydration] Global context fetch failed:', result.code, result.message)")
|
|
|
|
|
lines.push(' }')
|
|
|
|
|
lines.push(' } catch (e) {')
|
|
|
|
|
lines.push(" console.error('[getMizanHydration] Request failed:', e)")
|
|
|
|
|
lines.push(' }')
|
|
|
|
|
lines.push('')
|
|
|
|
|
lines.push(' return hydration')
|
|
|
|
|
lines.push('}')
|
|
|
|
|
lines.push('')
|
|
|
|
|
lines.push('/** @deprecated Use getMizanHydration instead */')
|
|
|
|
|
lines.push('export const getDjangoHydration = getMizanHydration')
|
|
|
|
|
lines.push('')
|
|
|
|
|
|
|
|
|
|
return lines.join('\n')
|
|
|
|
|
}
|
|
|
|
|
@@ -658,7 +847,7 @@ export function generateMizanForms(schema) {
|
|
|
|
|
lines.push('// Form Registry')
|
|
|
|
|
lines.push('// ============================================================================')
|
|
|
|
|
lines.push('')
|
|
|
|
|
lines.push('export const DJANGO_FORMS = {')
|
|
|
|
|
lines.push('export const MIZAN_FORMS = {')
|
|
|
|
|
for (const [formName, group] of formGroups) {
|
|
|
|
|
if (!group.schema) continue
|
|
|
|
|
const pascalName = toPascalCase(formName)
|