From c20de182e1bd239bfa92312b20bb333684d27b20 Mon Sep 17 00:00:00 2001 From: Ryth Azhur Date: Tue, 7 Apr 2026 12:09:35 -0400 Subject: [PATCH] Two-stage codegen: React + Vue + Svelte from one schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 1 (framework-agnostic): types.ts — OpenAPI interfaces contexts/.ts — fetchXxxContext(params) using mizanFetch mutations/.ts — callXxx(args) using mizanCall functions/.ts — callXxx(args) using mizanCall index.ts — re-exports Stage 2 (per framework): react.tsx — hooks + context providers + SSR hydration vue.ts — composables with provide/inject + ref/computed svelte.ts — writable/derived store factories New packages: mizan-runtime — the kernel (~200 lines, zero framework deps) configure(), initSession(), registerContext(), invalidate(), mizanFetch(), mizanCall(), MizanError mizan-vue — Vue adapter (package.json, codegen template) mizan-svelte — Svelte adapter (package.json, codegen template) CLI: mizan-generate --target react,vue,svelte Config: target: 'react' (default) in django.config.mjs Verified: codegen produces 33 functions across 2 contexts, 14 plain functions, 0 mutations, generating all three Stage 2 outputs from one schema fetch. Co-Authored-By: Claude Opus 4.6 (1M context) --- ....channels.hooks.tsx => channels.hooks.tsx} | 0 .../{generated.channels.ts => channels.ts} | 0 .../harness/src/api/contexts/global.ts | 15 + .../harness/src/api/contexts/local.ts | 17 + .../harness/src/api/functions/add.ts | 9 + .../harness/src/api/functions/buggyFn.ts | 9 + .../harness/src/api/functions/echo.ts | 9 + .../harness/src/api/functions/httpOnlyEcho.ts | 9 + .../harness/src/api/functions/jwtObtain.ts | 9 + .../harness/src/api/functions/jwtRefresh.ts | 9 + .../harness/src/api/functions/multiply.ts | 9 + .../src/api/functions/notImplementedFn.ts | 9 + .../src/api/functions/permissionCheckFn.ts | 9 + .../harness/src/api/functions/staffOnly.ts | 9 + .../src/api/functions/superuserOnly.ts | 9 + .../harness/src/api/functions/verifiedOnly.ts | 9 + .../harness/src/api/functions/whoami.ts | 9 + .../harness/src/api/functions/wsWhoami.ts | 9 + .../src/api/generated.channels.schema.json | 268 -- .../harness/src/api/generated.forms.ts | 226 -- .../harness/src/api/index.ts | 112 +- .../harness/src/api/react.tsx | 118 + .../harness/src/api/schema.json | 2678 +++++++++++++++++ .../harness/src/api/svelte.ts | 67 + .../harness/src/api/types.ts | 1908 ++++++++++++ .../django-react-site/harness/src/api/vue.ts | 96 + .../mizan-django/generate/generator/cli.mjs | 398 ++- .../generate/generator/lib/adapters/react.mjs | 160 + .../generator/lib/adapters/svelte.mjs | 97 + .../generate/generator/lib/adapters/vue.mjs | 105 + .../generate/generator/lib/stage1.mjs | 198 ++ packages/mizan-runtime/package.json | 11 + packages/mizan-runtime/src/index.ts | 204 ++ packages/mizan-svelte/package.json | 11 + packages/mizan-vue/package.json | 11 + 35 files changed, 6009 insertions(+), 817 deletions(-) rename examples/django-react-site/harness/src/api/{generated.channels.hooks.tsx => channels.hooks.tsx} (100%) rename examples/django-react-site/harness/src/api/{generated.channels.ts => channels.ts} (100%) create mode 100644 examples/django-react-site/harness/src/api/contexts/global.ts create mode 100644 examples/django-react-site/harness/src/api/contexts/local.ts create mode 100644 examples/django-react-site/harness/src/api/functions/add.ts create mode 100644 examples/django-react-site/harness/src/api/functions/buggyFn.ts create mode 100644 examples/django-react-site/harness/src/api/functions/echo.ts create mode 100644 examples/django-react-site/harness/src/api/functions/httpOnlyEcho.ts create mode 100644 examples/django-react-site/harness/src/api/functions/jwtObtain.ts create mode 100644 examples/django-react-site/harness/src/api/functions/jwtRefresh.ts create mode 100644 examples/django-react-site/harness/src/api/functions/multiply.ts create mode 100644 examples/django-react-site/harness/src/api/functions/notImplementedFn.ts create mode 100644 examples/django-react-site/harness/src/api/functions/permissionCheckFn.ts create mode 100644 examples/django-react-site/harness/src/api/functions/staffOnly.ts create mode 100644 examples/django-react-site/harness/src/api/functions/superuserOnly.ts create mode 100644 examples/django-react-site/harness/src/api/functions/verifiedOnly.ts create mode 100644 examples/django-react-site/harness/src/api/functions/whoami.ts create mode 100644 examples/django-react-site/harness/src/api/functions/wsWhoami.ts delete mode 100644 examples/django-react-site/harness/src/api/generated.channels.schema.json delete mode 100644 examples/django-react-site/harness/src/api/generated.forms.ts create mode 100644 examples/django-react-site/harness/src/api/react.tsx create mode 100644 examples/django-react-site/harness/src/api/schema.json create mode 100644 examples/django-react-site/harness/src/api/svelte.ts create mode 100644 examples/django-react-site/harness/src/api/types.ts create mode 100644 examples/django-react-site/harness/src/api/vue.ts create mode 100644 packages/mizan-django/generate/generator/lib/adapters/react.mjs create mode 100644 packages/mizan-django/generate/generator/lib/adapters/svelte.mjs create mode 100644 packages/mizan-django/generate/generator/lib/adapters/vue.mjs create mode 100644 packages/mizan-django/generate/generator/lib/stage1.mjs create mode 100644 packages/mizan-runtime/package.json create mode 100644 packages/mizan-runtime/src/index.ts create mode 100644 packages/mizan-svelte/package.json create mode 100644 packages/mizan-vue/package.json diff --git a/examples/django-react-site/harness/src/api/generated.channels.hooks.tsx b/examples/django-react-site/harness/src/api/channels.hooks.tsx similarity index 100% rename from examples/django-react-site/harness/src/api/generated.channels.hooks.tsx rename to examples/django-react-site/harness/src/api/channels.hooks.tsx diff --git a/examples/django-react-site/harness/src/api/generated.channels.ts b/examples/django-react-site/harness/src/api/channels.ts similarity index 100% rename from examples/django-react-site/harness/src/api/generated.channels.ts rename to examples/django-react-site/harness/src/api/channels.ts diff --git a/examples/django-react-site/harness/src/api/contexts/global.ts b/examples/django-react-site/harness/src/api/contexts/global.ts new file mode 100644 index 0000000..93c97f2 --- /dev/null +++ b/examples/django-react-site/harness/src/api/contexts/global.ts @@ -0,0 +1,15 @@ +// AUTO-GENERATED by mizan — do not edit + +import { mizanFetch } from '@mizan/runtime' + +import type { currentUserOutput } from '../types' + +export interface GlobalContextData { + current_user: currentUserOutput +} + +export type GlobalContextParams = Record + +export function fetchGlobalContext(params: GlobalContextParams): Promise { + return mizanFetch('global', params) +} diff --git a/examples/django-react-site/harness/src/api/contexts/local.ts b/examples/django-react-site/harness/src/api/contexts/local.ts new file mode 100644 index 0000000..6fb25a7 --- /dev/null +++ b/examples/django-react-site/harness/src/api/contexts/local.ts @@ -0,0 +1,17 @@ +// AUTO-GENERATED by mizan — do not edit + +import { mizanFetch } from '@mizan/runtime' + +import type { greetOutput } from '../types' + +export interface LocalContextData { + greet: greetOutput +} + +export interface LocalContextParams { + name: string +} + +export function fetchLocalContext(params: LocalContextParams): Promise { + return mizanFetch('local', params) +} diff --git a/examples/django-react-site/harness/src/api/functions/add.ts b/examples/django-react-site/harness/src/api/functions/add.ts new file mode 100644 index 0000000..5c14af1 --- /dev/null +++ b/examples/django-react-site/harness/src/api/functions/add.ts @@ -0,0 +1,9 @@ +// AUTO-GENERATED by mizan — do not edit + +import { mizanCall } from '@mizan/runtime' + +import type { addInput, addOutput } from '../types' + +export function callAdd(args: addInput): Promise { + return mizanCall('add', args) +} diff --git a/examples/django-react-site/harness/src/api/functions/buggyFn.ts b/examples/django-react-site/harness/src/api/functions/buggyFn.ts new file mode 100644 index 0000000..205dc76 --- /dev/null +++ b/examples/django-react-site/harness/src/api/functions/buggyFn.ts @@ -0,0 +1,9 @@ +// AUTO-GENERATED by mizan — do not edit + +import { mizanCall } from '@mizan/runtime' + +import type { buggyFnOutput } from '../types' + +export function callBuggyFn(): Promise { + return mizanCall('buggy_fn', {}) +} diff --git a/examples/django-react-site/harness/src/api/functions/echo.ts b/examples/django-react-site/harness/src/api/functions/echo.ts new file mode 100644 index 0000000..b054957 --- /dev/null +++ b/examples/django-react-site/harness/src/api/functions/echo.ts @@ -0,0 +1,9 @@ +// AUTO-GENERATED by mizan — do not edit + +import { mizanCall } from '@mizan/runtime' + +import type { echoInput, echoOutput } from '../types' + +export function callEcho(args: echoInput): Promise { + return mizanCall('echo', args) +} diff --git a/examples/django-react-site/harness/src/api/functions/httpOnlyEcho.ts b/examples/django-react-site/harness/src/api/functions/httpOnlyEcho.ts new file mode 100644 index 0000000..e0af195 --- /dev/null +++ b/examples/django-react-site/harness/src/api/functions/httpOnlyEcho.ts @@ -0,0 +1,9 @@ +// AUTO-GENERATED by mizan — do not edit + +import { mizanCall } from '@mizan/runtime' + +import type { httpOnlyEchoInput, httpOnlyEchoOutput } from '../types' + +export function callHttpOnlyEcho(args: httpOnlyEchoInput): Promise { + return mizanCall('http_only_echo', args) +} diff --git a/examples/django-react-site/harness/src/api/functions/jwtObtain.ts b/examples/django-react-site/harness/src/api/functions/jwtObtain.ts new file mode 100644 index 0000000..bccc183 --- /dev/null +++ b/examples/django-react-site/harness/src/api/functions/jwtObtain.ts @@ -0,0 +1,9 @@ +// AUTO-GENERATED by mizan — do not edit + +import { mizanCall } from '@mizan/runtime' + +import type { jwtObtainOutput } from '../types' + +export function callJwtObtain(): Promise { + return mizanCall('jwt_obtain', {}) +} diff --git a/examples/django-react-site/harness/src/api/functions/jwtRefresh.ts b/examples/django-react-site/harness/src/api/functions/jwtRefresh.ts new file mode 100644 index 0000000..c49d078 --- /dev/null +++ b/examples/django-react-site/harness/src/api/functions/jwtRefresh.ts @@ -0,0 +1,9 @@ +// AUTO-GENERATED by mizan — do not edit + +import { mizanCall } from '@mizan/runtime' + +import type { jwtRefreshInput, jwtRefreshOutput } from '../types' + +export function callJwtRefresh(args: jwtRefreshInput): Promise { + return mizanCall('jwt_refresh', args) +} diff --git a/examples/django-react-site/harness/src/api/functions/multiply.ts b/examples/django-react-site/harness/src/api/functions/multiply.ts new file mode 100644 index 0000000..4039a52 --- /dev/null +++ b/examples/django-react-site/harness/src/api/functions/multiply.ts @@ -0,0 +1,9 @@ +// AUTO-GENERATED by mizan — do not edit + +import { mizanCall } from '@mizan/runtime' + +import type { multiplyInput, multiplyOutput } from '../types' + +export function callMultiply(args: multiplyInput): Promise { + return mizanCall('multiply', args) +} diff --git a/examples/django-react-site/harness/src/api/functions/notImplementedFn.ts b/examples/django-react-site/harness/src/api/functions/notImplementedFn.ts new file mode 100644 index 0000000..2ae0e12 --- /dev/null +++ b/examples/django-react-site/harness/src/api/functions/notImplementedFn.ts @@ -0,0 +1,9 @@ +// AUTO-GENERATED by mizan — do not edit + +import { mizanCall } from '@mizan/runtime' + +import type { notImplementedFnOutput } from '../types' + +export function callNotImplementedFn(): Promise { + return mizanCall('not_implemented_fn', {}) +} diff --git a/examples/django-react-site/harness/src/api/functions/permissionCheckFn.ts b/examples/django-react-site/harness/src/api/functions/permissionCheckFn.ts new file mode 100644 index 0000000..82bfbeb --- /dev/null +++ b/examples/django-react-site/harness/src/api/functions/permissionCheckFn.ts @@ -0,0 +1,9 @@ +// AUTO-GENERATED by mizan — do not edit + +import { mizanCall } from '@mizan/runtime' + +import type { permissionCheckFnInput, permissionCheckFnOutput } from '../types' + +export function callPermissionCheckFn(args: permissionCheckFnInput): Promise { + return mizanCall('permission_check_fn', args) +} diff --git a/examples/django-react-site/harness/src/api/functions/staffOnly.ts b/examples/django-react-site/harness/src/api/functions/staffOnly.ts new file mode 100644 index 0000000..38c8c25 --- /dev/null +++ b/examples/django-react-site/harness/src/api/functions/staffOnly.ts @@ -0,0 +1,9 @@ +// AUTO-GENERATED by mizan — do not edit + +import { mizanCall } from '@mizan/runtime' + +import type { staffOnlyOutput } from '../types' + +export function callStaffOnly(): Promise { + return mizanCall('staff_only', {}) +} diff --git a/examples/django-react-site/harness/src/api/functions/superuserOnly.ts b/examples/django-react-site/harness/src/api/functions/superuserOnly.ts new file mode 100644 index 0000000..52f7167 --- /dev/null +++ b/examples/django-react-site/harness/src/api/functions/superuserOnly.ts @@ -0,0 +1,9 @@ +// AUTO-GENERATED by mizan — do not edit + +import { mizanCall } from '@mizan/runtime' + +import type { superuserOnlyOutput } from '../types' + +export function callSuperuserOnly(): Promise { + return mizanCall('superuser_only', {}) +} diff --git a/examples/django-react-site/harness/src/api/functions/verifiedOnly.ts b/examples/django-react-site/harness/src/api/functions/verifiedOnly.ts new file mode 100644 index 0000000..08a2520 --- /dev/null +++ b/examples/django-react-site/harness/src/api/functions/verifiedOnly.ts @@ -0,0 +1,9 @@ +// AUTO-GENERATED by mizan — do not edit + +import { mizanCall } from '@mizan/runtime' + +import type { verifiedOnlyOutput } from '../types' + +export function callVerifiedOnly(): Promise { + return mizanCall('verified_only', {}) +} diff --git a/examples/django-react-site/harness/src/api/functions/whoami.ts b/examples/django-react-site/harness/src/api/functions/whoami.ts new file mode 100644 index 0000000..0a57cc7 --- /dev/null +++ b/examples/django-react-site/harness/src/api/functions/whoami.ts @@ -0,0 +1,9 @@ +// AUTO-GENERATED by mizan — do not edit + +import { mizanCall } from '@mizan/runtime' + +import type { whoamiOutput } from '../types' + +export function callWhoami(): Promise { + return mizanCall('whoami', {}) +} diff --git a/examples/django-react-site/harness/src/api/functions/wsWhoami.ts b/examples/django-react-site/harness/src/api/functions/wsWhoami.ts new file mode 100644 index 0000000..891563f --- /dev/null +++ b/examples/django-react-site/harness/src/api/functions/wsWhoami.ts @@ -0,0 +1,9 @@ +// AUTO-GENERATED by mizan — do not edit + +import { mizanCall } from '@mizan/runtime' + +import type { wsWhoamiOutput } from '../types' + +export function callWsWhoami(): Promise { + return mizanCall('ws_whoami', {}) +} diff --git a/examples/django-react-site/harness/src/api/generated.channels.schema.json b/examples/django-react-site/harness/src/api/generated.channels.schema.json deleted file mode 100644 index b7e0acd..0000000 --- a/examples/django-react-site/harness/src/api/generated.channels.schema.json +++ /dev/null @@ -1,268 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "title": "mizan Channels", - "version": "1.0.0", - "description": "Auto-generated schema for mizan channels" - }, - "paths": { - "/channels/chat/params": { - "post": { - "operationId": "chatParams", - "summary": "Chat channel params", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BaseModel" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChatParams" - } - } - }, - "required": true - } - } - }, - "/channels/chat/react": { - "post": { - "operationId": "chatReactMessage", - "summary": "Chat React→Django message", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BaseModel" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChatReactMessage" - } - } - }, - "required": true - } - } - }, - "/channels/chat/django": { - "post": { - "operationId": "chatDjangoMessage", - "summary": "Chat Django→React message", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChatDjangoMessage" - } - } - } - } - } - } - }, - "/channels/notifications/django": { - "post": { - "operationId": "notificationsDjangoMessage", - "summary": "Notifications Django→React message", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotificationsDjangoMessage" - } - } - } - } - } - } - }, - "/channels/presence/django": { - "post": { - "operationId": "presenceDjangoMessage", - "summary": "Presence Django→React message", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresenceDjangoMessage" - } - } - } - } - } - } - }, - "/channels/private/django": { - "post": { - "operationId": "privateDjangoMessage", - "summary": "Private Django→React message", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PrivateDjangoMessage" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "BaseModel": { - "properties": {}, - "title": "BaseModel", - "type": "object" - }, - "ChatParams": { - "properties": { - "room": { - "title": "Room", - "type": "string" - } - }, - "required": [ - "room" - ], - "title": "ChatParams", - "type": "object" - }, - "ChatReactMessage": { - "properties": { - "text": { - "title": "Text", - "type": "string" - } - }, - "required": [ - "text" - ], - "title": "ChatReactMessage", - "type": "object" - }, - "ChatDjangoMessage": { - "properties": { - "text": { - "title": "Text", - "type": "string" - } - }, - "required": [ - "text" - ], - "title": "ChatDjangoMessage", - "type": "object" - }, - "NotificationsDjangoMessage": { - "properties": { - "text": { - "title": "Text", - "type": "string" - } - }, - "required": [ - "text" - ], - "title": "NotificationsDjangoMessage", - "type": "object" - }, - "PresenceDjangoMessage": { - "properties": { - "value": { - "title": "Value", - "type": "integer" - } - }, - "required": [ - "value" - ], - "title": "PresenceDjangoMessage", - "type": "object" - }, - "PrivateDjangoMessage": { - "properties": { - "text": { - "title": "Text", - "type": "string" - } - }, - "required": [ - "text" - ], - "title": "PrivateDjangoMessage", - "type": "object" - } - } - }, - "servers": [], - "x-mizan-channels": [ - { - "name": "chat", - "pascalName": "Chat", - "hasParams": true, - "hasReactMessage": true, - "hasDjangoMessage": true, - "paramsType": "ChatParams", - "reactMessageType": "ChatReactMessage", - "djangoMessageType": "ChatDjangoMessage" - }, - { - "name": "notifications", - "pascalName": "Notifications", - "hasParams": false, - "hasReactMessage": false, - "hasDjangoMessage": true, - "djangoMessageType": "NotificationsDjangoMessage" - }, - { - "name": "presence", - "pascalName": "Presence", - "hasParams": false, - "hasReactMessage": false, - "hasDjangoMessage": true, - "djangoMessageType": "PresenceDjangoMessage" - }, - { - "name": "private", - "pascalName": "Private", - "hasParams": false, - "hasReactMessage": false, - "hasDjangoMessage": true, - "djangoMessageType": "PrivateDjangoMessage" - } - ] -} \ No newline at end of file diff --git a/examples/django-react-site/harness/src/api/generated.forms.ts b/examples/django-react-site/harness/src/api/generated.forms.ts deleted file mode 100644 index bec9816..0000000 --- a/examples/django-react-site/harness/src/api/generated.forms.ts +++ /dev/null @@ -1,226 +0,0 @@ -'use client' - -// AUTO-GENERATED by mizan - do not edit manually -// Regenerate with: npm run schemas - -// Typed form hooks with Zod validation. -// Zod schemas are generated from Django form field definitions. -// Client-side validation matches Django constraints (required, max_length, email, etc.) - -import { z } from 'zod' -import { - useDjangoFormCore, - useDjangoFormsetCore, - type DjangoFormState, - type DjangoFormsetState, - type FormOptions, -} from 'mizan' - -// ============================================================================ -// Zod Schemas -// ============================================================================ - -/** - * Zod schema for login form - * Generated from Django form field definitions - */ -export const LoginSchema = z.object({ -}) - -/** - * Zod schema for signup form - * Generated from Django form field definitions - */ -export const SignupSchema = z.object({ -}) - -/** - * Zod schema for add_email form - * Generated from Django form field definitions - */ -export const AddEmailSchema = z.object({ -}) - -/** - * Zod schema for contact form - * Generated from Django form field definitions - */ -export const ContactSchema = z.object({ - name: z.string().max(100), - email: z.string().email('Invalid email address').max(320), - message: z.string(), -}) - -/** - * Zod schema for item form - * Generated from Django form field definitions - */ -export const ItemSchema = z.object({ - label: z.string().max(50), - quantity: z.number().int().min(1), -}) - -// ============================================================================ -// Form Data Types (inferred from Zod schemas) -// ============================================================================ - -/** Form data type for login, inferred from Zod schema */ -export type LoginFormData = z.infer - -/** Form data type for signup, inferred from Zod schema */ -export type SignupFormData = z.infer - -/** Form data type for add_email, inferred from Zod schema */ -export type AddEmailFormData = z.infer - -/** Form data type for contact, inferred from Zod schema */ -export type ContactFormData = z.infer - -/** Form data type for item, inferred from Zod schema */ -export type ItemFormData = z.infer - -// ============================================================================ -// Form Hooks -// ============================================================================ - -/** - * Typed form hook for login - * - * Features: - * - Full TypeScript inference for form fields - * - Client-side Zod validation (instant feedback) - * - Server-side Django validation (authoritative) - */ -export function useLoginForm( - options?: FormOptions -): DjangoFormState { - return useDjangoFormCore({ - name: 'login', - zodSchema: LoginSchema, - options, - }) -} - -/** - * Typed form hook for signup - * - * Features: - * - Full TypeScript inference for form fields - * - Client-side Zod validation (instant feedback) - * - Server-side Django validation (authoritative) - */ -export function useSignupForm( - options?: FormOptions -): DjangoFormState { - return useDjangoFormCore({ - name: 'signup', - zodSchema: SignupSchema, - options, - }) -} - -/** - * Typed form hook for add_email - * - * Features: - * - Full TypeScript inference for form fields - * - Client-side Zod validation (instant feedback) - * - Server-side Django validation (authoritative) - */ -export function useAddEmailForm( - options?: FormOptions -): DjangoFormState { - return useDjangoFormCore({ - name: 'add_email', - zodSchema: AddEmailSchema, - options, - }) -} - -/** - * Typed form hook for contact - * - * Features: - * - Full TypeScript inference for form fields - * - Client-side Zod validation (instant feedback) - * - Server-side Django validation (authoritative) - */ -export function useContactForm( - options?: FormOptions -): DjangoFormState { - return useDjangoFormCore({ - name: 'contact', - zodSchema: ContactSchema, - options, - }) -} - -/** - * Typed form hook for item - * - * Features: - * - Full TypeScript inference for form fields - * - Client-side Zod validation (instant feedback) - * - Server-side Django validation (authoritative) - */ -export function useItemForm( - options?: FormOptions -): DjangoFormState { - return useDjangoFormCore({ - name: 'item', - zodSchema: ItemSchema, - options, - }) -} - -/** - * Typed formset hook for item - */ -export function useItemFormset( - initialCount?: number, - liveValidation?: boolean -): DjangoFormsetState { - return useDjangoFormsetCore({ - name: 'item', - zodSchema: ItemSchema, - initialCount, - liveValidation, - }) -} - -// ============================================================================ -// Form Registry -// ============================================================================ - -export const MIZAN_FORMS = { - login: { - name: 'login', - schema: LoginSchema, - hook: 'useLoginForm', - hasFormset: false, - }, - signup: { - name: 'signup', - schema: SignupSchema, - hook: 'useSignupForm', - hasFormset: false, - }, - addEmail: { - name: 'add_email', - schema: AddEmailSchema, - hook: 'useAddEmailForm', - hasFormset: false, - }, - contact: { - name: 'contact', - schema: ContactSchema, - hook: 'useContactForm', - hasFormset: false, - }, - item: { - name: 'item', - schema: ItemSchema, - hook: 'useItemForm', - hasFormset: true, - }, -} as const diff --git a/examples/django-react-site/harness/src/api/index.ts b/examples/django-react-site/harness/src/api/index.ts index c0bb864..72a912f 100644 --- a/examples/django-react-site/harness/src/api/index.ts +++ b/examples/django-react-site/harness/src/api/index.ts @@ -1,97 +1,21 @@ -/** - * mizan API - Consolidated Exports - * - * Import everything from here: - * - * @example - * ```tsx - * import { - * MizanContext, - * useCurrentUser, - * useEcho, - * useChatChannel, - * } from '@/api' - * ``` - */ +// AUTO-GENERATED by mizan — do not edit -// AUTO-GENERATED by mizan - do not edit manually -// Regenerate with: npm run schemas +export * from './types' -// ============================================================================= -// mizan Provider & Hooks -// ============================================================================= +export { fetchGlobalContext, type GlobalContextData, type GlobalContextParams } from './contexts/global' +export { fetchLocalContext, type LocalContextData, type LocalContextParams } from './contexts/local' -export { - getMizanHydration, - getDjangoHydration, - type MizanHydrationData, - type DjangoHydration, -} from './generated.server' - -export { - // Provider - MizanContext, - type MizanContextProps, - DjangoContext, - type DjangoContextProps, - - // Global context hooks - useCurrentUser, - - // Refresh hooks - useMizanRefresh, - useDjangoRefresh, - - // Named context providers - LocalContext, - useGreet, - - // Function hooks - useEcho, - useAdd, - useWhoami, - useHttpOnlyEcho, - useStaffOnly, - useSuperuserOnly, - useVerifiedOnly, - useMultiply, - useNotImplementedFn, - useBuggyFn, - usePermissionCheckFn, - useWsWhoami, - useJwtObtain, - useJwtRefresh, - - // Re-exports from mizan library - useMizan, - useMizanStatus, - usePush, - DjangoError, - type ConnectionStatus, - type PushMessage, - type PushListener, -} from './generated.provider' - -// ============================================================================= -// Channel Hooks -// ============================================================================= - -export { - useChatChannel, - useNotificationsChannel, - usePresenceChannel, - usePrivateChannel, -} from './generated.channels.hooks' - -// ============================================================================= -// Channel Types -// ============================================================================= - -export type { - ChatParams, - ChatReactMessage, - ChatDjangoMessage, - NotificationsDjangoMessage, - PresenceDjangoMessage, - PrivateDjangoMessage, -} from './generated.channels' +export { callEcho } from './functions/echo' +export { callAdd } from './functions/add' +export { callWhoami } from './functions/whoami' +export { callHttpOnlyEcho } from './functions/httpOnlyEcho' +export { callStaffOnly } from './functions/staffOnly' +export { callSuperuserOnly } from './functions/superuserOnly' +export { callVerifiedOnly } from './functions/verifiedOnly' +export { callMultiply } from './functions/multiply' +export { callNotImplementedFn } from './functions/notImplementedFn' +export { callBuggyFn } from './functions/buggyFn' +export { callPermissionCheckFn } from './functions/permissionCheckFn' +export { callWsWhoami } from './functions/wsWhoami' +export { callJwtObtain } from './functions/jwtObtain' +export { callJwtRefresh } from './functions/jwtRefresh' diff --git a/examples/django-react-site/harness/src/api/react.tsx b/examples/django-react-site/harness/src/api/react.tsx new file mode 100644 index 0000000..86df1ae --- /dev/null +++ b/examples/django-react-site/harness/src/api/react.tsx @@ -0,0 +1,118 @@ +'use client' + +// AUTO-GENERATED by mizan — do not edit + +import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react' +import { registerContext, mizanCall, mizanFetch } from '@mizan/runtime' + +import { fetchGlobalContext, type GlobalContextData, type GlobalContextParams, fetchLocalContext, type LocalContextData, type LocalContextParams, callEcho, callAdd, callWhoami, callHttpOnlyEcho, callStaffOnly, callSuperuserOnly, callVerifiedOnly, callMultiply, callNotImplementedFn, callBuggyFn, callPermissionCheckFn, callWsWhoami, callJwtObtain, callJwtRefresh } from '../index' + +// Global context — fetched once at app init +const GlobalCtx = createContext(null) + +export function GlobalContextProvider({ children }: { children: ReactNode }) { + const [data, setData] = useState(() => { + if (typeof window === 'undefined') return null + const ssr = (window as any).__MIZAN_SSR_DATA__ + return ssr ?? null + }) + + const refetch = useCallback(async () => { + const result = await fetchGlobalContext({} as any) + setData(result) + }, []) + + useEffect(() => { if (!data) refetch() }, [data, refetch]) + useEffect(() => registerContext('global', {}, refetch), [refetch]) + + return {children} +} + +export function useCurrentUser(): currentUserOutput { + const ctx = useContext(GlobalCtx) + if (!ctx) throw new Error('useCurrentUser requires GlobalContextProvider') + return ctx.current_user +} + +// Local context +const LocalCtx = createContext(null) + +export function LocalContext({ children, ...params }: LocalContextParams & { children: ReactNode }) { + const [data, setData] = useState(() => { + if (typeof window === 'undefined') return null + const ssr = (window as any).__MIZAN_SSR_DATA__ + if (ssr?.greet !== undefined) return ssr + return null + }) + + const refetch = useCallback(async () => { + const result = await fetchLocalContext(params) + setData(result) + }, [params.name]) + + useEffect(() => { refetch() }, [refetch]) + useEffect(() => registerContext('local', params, refetch), [params.name, refetch]) + + return {children} +} + +export function useGreet(): greetOutput | null { + const ctx = useContext(LocalCtx) + return ctx?.greet ?? null +} + +export function useEcho() { + return useCallback((args: Parameters[0]) => callEcho(args), []) +} + +export function useAdd() { + return useCallback((args: Parameters[0]) => callAdd(args), []) +} + +export function useWhoami() { + return useCallback(() => callWhoami(), []) +} + +export function useHttpOnlyEcho() { + return useCallback((args: Parameters[0]) => callHttpOnlyEcho(args), []) +} + +export function useStaffOnly() { + return useCallback(() => callStaffOnly(), []) +} + +export function useSuperuserOnly() { + return useCallback(() => callSuperuserOnly(), []) +} + +export function useVerifiedOnly() { + return useCallback(() => callVerifiedOnly(), []) +} + +export function useMultiply() { + return useCallback((args: Parameters[0]) => callMultiply(args), []) +} + +export function useNotImplementedFn() { + return useCallback(() => callNotImplementedFn(), []) +} + +export function useBuggyFn() { + return useCallback(() => callBuggyFn(), []) +} + +export function usePermissionCheckFn() { + return useCallback((args: Parameters[0]) => callPermissionCheckFn(args), []) +} + +export function useWsWhoami() { + return useCallback(() => callWsWhoami(), []) +} + +export function useJwtObtain() { + return useCallback(() => callJwtObtain(), []) +} + +export function useJwtRefresh() { + return useCallback((args: Parameters[0]) => callJwtRefresh(args), []) +} diff --git a/examples/django-react-site/harness/src/api/schema.json b/examples/django-react-site/harness/src/api/schema.json new file mode 100644 index 0000000..08a27ab --- /dev/null +++ b/examples/django-react-site/harness/src/api/schema.json @@ -0,0 +1,2678 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "mizan Server Functions", + "version": "1.0.0", + "description": "Auto-generated schema for mizan server functions" + }, + "paths": { + "/mizan/echo": { + "post": { + "operationId": "echo", + "summary": "Call echo", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/echoOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/echoInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "websocket", + "isContext": false + } + } + }, + "/mizan/add": { + "post": { + "operationId": "add", + "summary": "Call add", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/addOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/addInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "websocket", + "isContext": false + } + } + }, + "/mizan/whoami": { + "post": { + "operationId": "whoami", + "summary": "Call whoami", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/whoamiOutput" + } + } + } + } + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/http_only_echo": { + "post": { + "operationId": "httpOnlyEcho", + "summary": "Call http_only_echo", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/httpOnlyEchoOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/httpOnlyEchoInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/login.schema": { + "post": { + "operationId": "loginSchema", + "summary": "Call login.schema", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/loginSchemaOutput" + } + } + } + } + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/login.validate": { + "post": { + "operationId": "loginValidate", + "summary": "Call login.validate", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/loginValidateOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/loginValidateInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/login.submit": { + "post": { + "operationId": "loginSubmit", + "summary": "Call login.submit", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/loginSubmitOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/loginSubmitInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/signup.schema": { + "post": { + "operationId": "signupSchema", + "summary": "Call signup.schema", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/signupSchemaOutput" + } + } + } + } + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/signup.validate": { + "post": { + "operationId": "signupValidate", + "summary": "Call signup.validate", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/signupValidateOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/signupValidateInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/signup.submit": { + "post": { + "operationId": "signupSubmit", + "summary": "Call signup.submit", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/signupSubmitOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/signupSubmitInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/add_email.schema": { + "post": { + "operationId": "addEmailSchema", + "summary": "Call add_email.schema", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/addEmailSchemaOutput" + } + } + } + } + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/add_email.validate": { + "post": { + "operationId": "addEmailValidate", + "summary": "Call add_email.validate", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/addEmailValidateOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/addEmailValidateInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/staff_only": { + "post": { + "operationId": "staffOnly", + "summary": "Call staff_only", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/staffOnlyOutput" + } + } + } + } + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/superuser_only": { + "post": { + "operationId": "superuserOnly", + "summary": "Call superuser_only", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/superuserOnlyOutput" + } + } + } + } + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/verified_only": { + "post": { + "operationId": "verifiedOnly", + "summary": "Call verified_only", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/verifiedOnlyOutput" + } + } + } + } + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/current_user": { + "post": { + "operationId": "currentUser", + "summary": "Call current_user", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/currentUserOutput" + } + } + } + } + }, + "x-mizan": { + "transport": "http", + "isContext": "global" + } + } + }, + "/mizan/greet": { + "post": { + "operationId": "greet", + "summary": "Call greet", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/greetOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/greetInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": "local" + } + } + }, + "/mizan/multiply": { + "post": { + "operationId": "multiply", + "summary": "Call multiply", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/multiplyOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/multiplyInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/not_implemented_fn": { + "post": { + "operationId": "notImplementedFn", + "summary": "Call not_implemented_fn", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/notImplementedFnOutput" + } + } + } + } + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/buggy_fn": { + "post": { + "operationId": "buggyFn", + "summary": "Call buggy_fn", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/buggyFnOutput" + } + } + } + } + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/permission_check_fn": { + "post": { + "operationId": "permissionCheckFn", + "summary": "Call permission_check_fn", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/permissionCheckFnOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/permissionCheckFnInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/ws_whoami": { + "post": { + "operationId": "wsWhoami", + "summary": "Call ws_whoami", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wsWhoamiOutput" + } + } + } + } + }, + "x-mizan": { + "transport": "websocket", + "isContext": false + } + } + }, + "/mizan/contact.schema": { + "post": { + "operationId": "contactSchema", + "summary": "Call contact.schema", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/contactSchemaOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/contactSchemaInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/contact.validate": { + "post": { + "operationId": "contactValidate", + "summary": "Call contact.validate", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/contactValidateOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/contactValidateInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/contact.submit": { + "post": { + "operationId": "contactSubmit", + "summary": "\nSubmit function handles both JSON and multipart/form-data.\n\nThe executor detects form functions and parses the request appropriately.\n", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/contactSubmitOutput" + } + } + } + } + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/item.schema": { + "post": { + "operationId": "itemSchema", + "summary": "Call item.schema", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/itemSchemaOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/itemSchemaInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/item.validate": { + "post": { + "operationId": "itemValidate", + "summary": "Call item.validate", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/itemValidateOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/itemValidateInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/item.submit": { + "post": { + "operationId": "itemSubmit", + "summary": "\nSubmit function handles both JSON and multipart/form-data.\n\nThe executor detects form functions and parses the request appropriately.\n", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/itemSubmitOutput" + } + } + } + } + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/item.formset.schema": { + "post": { + "operationId": "itemFormsetSchema", + "summary": "Call item.formset.schema", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/itemFormsetSchemaOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/itemFormsetSchemaInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/item.formset.validate": { + "post": { + "operationId": "itemFormsetValidate", + "summary": "Call item.formset.validate", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/itemFormsetValidateOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/itemFormsetValidateInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/item.formset.submit": { + "post": { + "operationId": "itemFormsetSubmit", + "summary": "Call item.formset.submit", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/itemFormsetSubmitOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/itemFormsetSubmitInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/jwt_obtain": { + "post": { + "operationId": "jwtObtain", + "summary": "\nObtain JWT tokens from an authenticated session.\n\nRequires session authentication (cookie or WebSocket session).\nReturns access and refresh tokens that can be used for stateless auth.\n\nThe tokens include user claims (is_staff, is_superuser) so that\nsubsequent JWT-authenticated requests don't need a database query.\n\nUsage:\n const { access_token, refresh_token } = await call('jwt_obtain')\n // Use access_token in Authorization: Bearer header\n", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/jwtObtainOutput" + } + } + } + } + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + }, + "/mizan/jwt_refresh": { + "post": { + "operationId": "jwtRefresh", + "summary": "\nRefresh JWT tokens using a refresh token.\n\nDoes not require session authentication - the refresh token itself\ncontains the session reference and is validated against the session store.\n\nIf the original session has been destroyed (user logged out), this fails.\n\nUsage:\n const { access_token, refresh_token } = await call('jwt_refresh', { refresh_token })\n", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/jwtRefreshOutput" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/jwtRefreshInput" + } + } + }, + "required": true + }, + "x-mizan": { + "transport": "http", + "isContext": false + } + } + } + }, + "components": { + "schemas": { + "echoOutput": { + "properties": { + "message": { + "title": "Message", + "type": "string" + } + }, + "required": [ + "message" + ], + "title": "echoOutput", + "type": "object" + }, + "echoInput": { + "properties": { + "text": { + "title": "Text", + "type": "string" + } + }, + "required": [ + "text" + ], + "title": "echoInput", + "type": "object" + }, + "addOutput": { + "properties": { + "result": { + "title": "Result", + "type": "integer" + } + }, + "required": [ + "result" + ], + "title": "addOutput", + "type": "object" + }, + "addInput": { + "properties": { + "a": { + "title": "A", + "type": "integer" + }, + "b": { + "title": "B", + "type": "integer" + } + }, + "required": [ + "a", + "b" + ], + "title": "addInput", + "type": "object" + }, + "whoamiOutput": { + "properties": { + "user_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "User Id" + }, + "email": { + "title": "Email", + "type": "string" + }, + "is_staff": { + "title": "Is Staff", + "type": "boolean" + } + }, + "required": [ + "user_id", + "email", + "is_staff" + ], + "title": "whoamiOutput", + "type": "object" + }, + "httpOnlyEchoOutput": { + "properties": { + "message": { + "title": "Message", + "type": "string" + } + }, + "required": [ + "message" + ], + "title": "httpOnlyEchoOutput", + "type": "object" + }, + "httpOnlyEchoInput": { + "properties": { + "text": { + "title": "Text", + "type": "string" + } + }, + "required": [ + "text" + ], + "title": "httpOnlyEchoInput", + "type": "object" + }, + "FormSchemaField": { + "description": "Schema for a single form field.", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "required": { + "title": "Required", + "type": "boolean" + }, + "label": { + "title": "Label", + "type": "string" + }, + "help_text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Help Text" + }, + "choices": { + "anyOf": [ + { + "items": { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "type": "array" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Choices" + }, + "initial": { + "title": "Initial" + } + }, + "required": [ + "name", + "type", + "required", + "label" + ], + "title": "FormSchemaField", + "type": "object" + }, + "loginSchemaOutput": { + "properties": { + "fields": { + "items": { + "$ref": "#/components/schemas/FormSchemaField" + }, + "title": "Fields", + "type": "array" + } + }, + "required": [ + "fields" + ], + "title": "loginSchemaOutput", + "type": "object" + }, + "loginValidateOutput": { + "properties": { + "valid": { + "title": "Valid", + "type": "boolean" + }, + "errors": { + "additionalProperties": { + "items": { + "type": "string" + }, + "type": "array" + }, + "title": "Errors", + "type": "object" + } + }, + "required": [ + "valid", + "errors" + ], + "title": "loginValidateOutput", + "type": "object" + }, + "loginValidateInput": { + "properties": { + "data": { + "additionalProperties": true, + "title": "Data", + "type": "object" + } + }, + "required": [ + "data" + ], + "title": "loginValidateInput", + "type": "object" + }, + "loginSubmitOutput": { + "properties": {}, + "title": "loginSubmitOutput", + "type": "object" + }, + "loginSubmitInput": { + "properties": { + "data": { + "additionalProperties": true, + "title": "Data", + "type": "object" + } + }, + "required": [ + "data" + ], + "title": "loginSubmitInput", + "type": "object" + }, + "signupSchemaOutput": { + "properties": { + "fields": { + "items": { + "$ref": "#/components/schemas/FormSchemaField" + }, + "title": "Fields", + "type": "array" + } + }, + "required": [ + "fields" + ], + "title": "signupSchemaOutput", + "type": "object" + }, + "signupValidateOutput": { + "properties": { + "valid": { + "title": "Valid", + "type": "boolean" + }, + "errors": { + "additionalProperties": { + "items": { + "type": "string" + }, + "type": "array" + }, + "title": "Errors", + "type": "object" + } + }, + "required": [ + "valid", + "errors" + ], + "title": "signupValidateOutput", + "type": "object" + }, + "signupValidateInput": { + "properties": { + "data": { + "additionalProperties": true, + "title": "Data", + "type": "object" + } + }, + "required": [ + "data" + ], + "title": "signupValidateInput", + "type": "object" + }, + "signupSubmitOutput": { + "properties": {}, + "title": "signupSubmitOutput", + "type": "object" + }, + "signupSubmitInput": { + "properties": { + "data": { + "additionalProperties": true, + "title": "Data", + "type": "object" + } + }, + "required": [ + "data" + ], + "title": "signupSubmitInput", + "type": "object" + }, + "addEmailSchemaOutput": { + "properties": { + "fields": { + "items": { + "$ref": "#/components/schemas/FormSchemaField" + }, + "title": "Fields", + "type": "array" + } + }, + "required": [ + "fields" + ], + "title": "addEmailSchemaOutput", + "type": "object" + }, + "addEmailValidateOutput": { + "properties": { + "valid": { + "title": "Valid", + "type": "boolean" + }, + "errors": { + "additionalProperties": { + "items": { + "type": "string" + }, + "type": "array" + }, + "title": "Errors", + "type": "object" + } + }, + "required": [ + "valid", + "errors" + ], + "title": "addEmailValidateOutput", + "type": "object" + }, + "addEmailValidateInput": { + "properties": { + "data": { + "additionalProperties": true, + "title": "Data", + "type": "object" + } + }, + "required": [ + "data" + ], + "title": "addEmailValidateInput", + "type": "object" + }, + "staffOnlyOutput": { + "properties": { + "message": { + "title": "Message", + "type": "string" + } + }, + "required": [ + "message" + ], + "title": "staffOnlyOutput", + "type": "object" + }, + "superuserOnlyOutput": { + "properties": { + "message": { + "title": "Message", + "type": "string" + } + }, + "required": [ + "message" + ], + "title": "superuserOnlyOutput", + "type": "object" + }, + "verifiedOnlyOutput": { + "properties": { + "message": { + "title": "Message", + "type": "string" + } + }, + "required": [ + "message" + ], + "title": "verifiedOnlyOutput", + "type": "object" + }, + "currentUserOutput": { + "properties": { + "authenticated": { + "title": "Authenticated", + "type": "boolean" + }, + "email": { + "title": "Email", + "type": "string" + }, + "is_staff": { + "title": "Is Staff", + "type": "boolean" + } + }, + "required": [ + "authenticated", + "email", + "is_staff" + ], + "title": "currentUserOutput", + "type": "object" + }, + "greetOutput": { + "properties": { + "greeting": { + "title": "Greeting", + "type": "string" + } + }, + "required": [ + "greeting" + ], + "title": "greetOutput", + "type": "object" + }, + "greetInput": { + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "greetInput", + "type": "object" + }, + "multiplyOutput": { + "properties": { + "product": { + "title": "Product", + "type": "integer" + } + }, + "required": [ + "product" + ], + "title": "multiplyOutput", + "type": "object" + }, + "multiplyInput": { + "properties": { + "x": { + "title": "X", + "type": "integer" + }, + "y": { + "title": "Y", + "type": "integer" + } + }, + "required": [ + "x", + "y" + ], + "title": "multiplyInput", + "type": "object" + }, + "notImplementedFnOutput": { + "properties": { + "message": { + "title": "Message", + "type": "string" + } + }, + "required": [ + "message" + ], + "title": "notImplementedFnOutput", + "type": "object" + }, + "buggyFnOutput": { + "properties": { + "message": { + "title": "Message", + "type": "string" + } + }, + "required": [ + "message" + ], + "title": "buggyFnOutput", + "type": "object" + }, + "permissionCheckFnOutput": { + "properties": { + "message": { + "title": "Message", + "type": "string" + } + }, + "required": [ + "message" + ], + "title": "permissionCheckFnOutput", + "type": "object" + }, + "permissionCheckFnInput": { + "properties": { + "secret": { + "title": "Secret", + "type": "string" + } + }, + "required": [ + "secret" + ], + "title": "permissionCheckFnInput", + "type": "object" + }, + "wsWhoamiOutput": { + "properties": { + "user_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "User Id" + }, + "email": { + "title": "Email", + "type": "string" + }, + "is_staff": { + "title": "Is Staff", + "type": "boolean" + } + }, + "required": [ + "user_id", + "email", + "is_staff" + ], + "title": "wsWhoamiOutput", + "type": "object" + }, + "FieldChoice": { + "properties": { + "value": { + "title": "Value", + "type": "string" + }, + "label": { + "title": "Label", + "type": "string" + } + }, + "required": [ + "value", + "label" + ], + "title": "FieldChoice", + "type": "object" + }, + "FieldSchema": { + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "label": { + "title": "Label", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "widget": { + "title": "Widget", + "type": "string" + }, + "required": { + "title": "Required", + "type": "boolean" + }, + "disabled": { + "title": "Disabled", + "type": "boolean" + }, + "help_text": { + "title": "Help Text", + "type": "string" + }, + "initial": { + "title": "Initial" + }, + "max_length": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Length" + }, + "min_length": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Min Length" + }, + "choices": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/FieldChoice" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Choices" + } + }, + "required": [ + "name", + "label", + "type", + "widget", + "required", + "disabled", + "help_text", + "initial", + "max_length", + "min_length", + "choices" + ], + "title": "FieldSchema", + "type": "object" + }, + "FormMeta": { + "description": "Metadata controlling frontend form behavior.\n\nAttributes:\n refetch_schema_on_validate: If True, frontend should refetch schema on each\n validation (useful for dynamic choice fields). Default False.\n live_validation: If False, frontend should disable live validation entirely.\n Useful for sensitive forms like login. Default True.\n live_form_errors: If True, show form-level errors during live validation.\n Form errors are things like \"Invalid credentials\" vs field errors like\n \"This field is required\". Default False for security.", + "properties": { + "refetch_schema_on_validate": { + "default": false, + "title": "Refetch Schema On Validate", + "type": "boolean" + }, + "live_validation": { + "default": true, + "title": "Live Validation", + "type": "boolean" + }, + "live_form_errors": { + "default": false, + "title": "Live Form Errors", + "type": "boolean" + } + }, + "title": "FormMeta", + "type": "object" + }, + "contactSchemaOutput": { + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "subtitle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Subtitle" + }, + "submit_label": { + "title": "Submit Label", + "type": "string" + }, + "fields": { + "items": { + "$ref": "#/components/schemas/FieldSchema" + }, + "title": "Fields", + "type": "array" + }, + "meta": { + "allOf": [ + { + "$ref": "#/components/schemas/FormMeta" + } + ], + "default": { + "refetch_schema_on_validate": false, + "live_validation": true, + "live_form_errors": false + } + } + }, + "required": [ + "name", + "title", + "subtitle", + "submit_label", + "fields" + ], + "title": "contactSchemaOutput", + "type": "object" + }, + "contactSchemaInput": { + "properties": { + "data": { + "additionalProperties": true, + "default": {}, + "title": "Data", + "type": "object" + } + }, + "title": "contactSchemaInput", + "type": "object" + }, + "FieldError": { + "properties": { + "message": { + "title": "Message", + "type": "string" + }, + "code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Code" + } + }, + "required": [ + "message", + "code" + ], + "title": "FieldError", + "type": "object" + }, + "FieldErrorList": { + "properties": { + "field": { + "title": "Field", + "type": "string" + }, + "errors": { + "items": { + "$ref": "#/components/schemas/FieldError" + }, + "title": "Errors", + "type": "array" + } + }, + "required": [ + "field", + "errors" + ], + "title": "FieldErrorList", + "type": "object" + }, + "contactValidateOutput": { + "properties": { + "errors": { + "items": { + "$ref": "#/components/schemas/FieldErrorList" + }, + "title": "Errors", + "type": "array" + } + }, + "required": [ + "errors" + ], + "title": "contactValidateOutput", + "type": "object" + }, + "contactValidateInput": { + "properties": { + "data": { + "additionalProperties": true, + "title": "Data", + "type": "object" + } + }, + "required": [ + "data" + ], + "title": "contactValidateInput", + "type": "object" + }, + "contactSubmitOutput": { + "properties": { + "success": { + "title": "Success", + "type": "boolean" + }, + "data": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Data" + } + }, + "required": [ + "success" + ], + "title": "contactSubmitOutput", + "type": "object" + }, + "itemSchemaOutput": { + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "subtitle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Subtitle" + }, + "submit_label": { + "title": "Submit Label", + "type": "string" + }, + "fields": { + "items": { + "$ref": "#/components/schemas/FieldSchema" + }, + "title": "Fields", + "type": "array" + }, + "meta": { + "allOf": [ + { + "$ref": "#/components/schemas/FormMeta" + } + ], + "default": { + "refetch_schema_on_validate": false, + "live_validation": true, + "live_form_errors": false + } + } + }, + "required": [ + "name", + "title", + "subtitle", + "submit_label", + "fields" + ], + "title": "itemSchemaOutput", + "type": "object" + }, + "itemSchemaInput": { + "properties": { + "data": { + "additionalProperties": true, + "default": {}, + "title": "Data", + "type": "object" + } + }, + "title": "itemSchemaInput", + "type": "object" + }, + "itemValidateOutput": { + "properties": { + "errors": { + "items": { + "$ref": "#/components/schemas/FieldErrorList" + }, + "title": "Errors", + "type": "array" + } + }, + "required": [ + "errors" + ], + "title": "itemValidateOutput", + "type": "object" + }, + "itemValidateInput": { + "properties": { + "data": { + "additionalProperties": true, + "title": "Data", + "type": "object" + } + }, + "required": [ + "data" + ], + "title": "itemValidateInput", + "type": "object" + }, + "itemSubmitOutput": { + "properties": { + "success": { + "title": "Success", + "type": "boolean" + }, + "data": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Data" + } + }, + "required": [ + "success" + ], + "title": "itemSubmitOutput", + "type": "object" + }, + "FormSchema": { + "description": "Schema returned by /schema endpoint with form metadata and fields.", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "subtitle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Subtitle" + }, + "submit_label": { + "title": "Submit Label", + "type": "string" + }, + "fields": { + "items": { + "$ref": "#/components/schemas/FieldSchema" + }, + "title": "Fields", + "type": "array" + }, + "meta": { + "allOf": [ + { + "$ref": "#/components/schemas/FormMeta" + } + ], + "default": { + "refetch_schema_on_validate": false, + "live_validation": true, + "live_form_errors": false + } + } + }, + "required": [ + "name", + "title", + "subtitle", + "submit_label", + "fields" + ], + "title": "FormSchema", + "type": "object" + }, + "itemFormsetSchemaOutput": { + "properties": { + "forms": { + "items": { + "$ref": "#/components/schemas/FormSchema" + }, + "title": "Forms", + "type": "array" + }, + "min_num": { + "title": "Min Num", + "type": "integer" + }, + "max_num": { + "title": "Max Num", + "type": "integer" + }, + "can_delete": { + "title": "Can Delete", + "type": "boolean" + }, + "can_order": { + "title": "Can Order", + "type": "boolean" + } + }, + "required": [ + "forms", + "min_num", + "max_num", + "can_delete", + "can_order" + ], + "title": "itemFormsetSchemaOutput", + "type": "object" + }, + "itemFormsetSchemaInput": { + "properties": { + "forms": { + "default": [], + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Forms", + "type": "array" + } + }, + "title": "itemFormsetSchemaInput", + "type": "object" + }, + "FormValidation": { + "properties": { + "errors": { + "items": { + "$ref": "#/components/schemas/FieldErrorList" + }, + "title": "Errors", + "type": "array" + } + }, + "required": [ + "errors" + ], + "title": "FormValidation", + "type": "object" + }, + "itemFormsetValidateOutput": { + "properties": { + "general": { + "items": { + "type": "string" + }, + "title": "General", + "type": "array" + }, + "per_form": { + "items": { + "$ref": "#/components/schemas/FormValidation" + }, + "title": "Per Form", + "type": "array" + } + }, + "required": [ + "general", + "per_form" + ], + "title": "itemFormsetValidateOutput", + "type": "object" + }, + "itemFormsetValidateInput": { + "properties": { + "forms": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Forms", + "type": "array" + } + }, + "required": [ + "forms" + ], + "title": "itemFormsetValidateInput", + "type": "object" + }, + "itemFormsetSubmitOutput": { + "properties": { + "success": { + "title": "Success", + "type": "boolean" + } + }, + "required": [ + "success" + ], + "title": "itemFormsetSubmitOutput", + "type": "object" + }, + "itemFormsetSubmitInput": { + "properties": { + "forms": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Forms", + "type": "array" + } + }, + "required": [ + "forms" + ], + "title": "itemFormsetSubmitInput", + "type": "object" + }, + "jwtObtainOutput": { + "properties": { + "access_token": { + "title": "Access Token", + "type": "string" + }, + "refresh_token": { + "title": "Refresh Token", + "type": "string" + }, + "expires_in": { + "title": "Expires In", + "type": "integer" + } + }, + "required": [ + "access_token", + "refresh_token", + "expires_in" + ], + "title": "jwtObtainOutput", + "type": "object" + }, + "jwtRefreshOutput": { + "properties": { + "access_token": { + "title": "Access Token", + "type": "string" + }, + "refresh_token": { + "title": "Refresh Token", + "type": "string" + }, + "expires_in": { + "title": "Expires In", + "type": "integer" + } + }, + "required": [ + "access_token", + "refresh_token", + "expires_in" + ], + "title": "jwtRefreshOutput", + "type": "object" + }, + "jwtRefreshInput": { + "properties": { + "refresh_token": { + "title": "Refresh Token", + "type": "string" + } + }, + "required": [ + "refresh_token" + ], + "title": "jwtRefreshInput", + "type": "object" + } + } + }, + "servers": [], + "x-mizan-functions": [ + { + "name": "echo", + "camelName": "echo", + "hasInput": true, + "inputType": "echoInput", + "outputType": "echoOutput", + "transport": "websocket", + "isContext": false, + "isForm": false, + "formName": null, + "formRole": null + }, + { + "name": "add", + "camelName": "add", + "hasInput": true, + "inputType": "addInput", + "outputType": "addOutput", + "transport": "websocket", + "isContext": false, + "isForm": false, + "formName": null, + "formRole": null + }, + { + "name": "whoami", + "camelName": "whoami", + "hasInput": false, + "inputType": null, + "outputType": "whoamiOutput", + "transport": "http", + "isContext": false, + "isForm": false, + "formName": null, + "formRole": null + }, + { + "name": "http_only_echo", + "camelName": "httpOnlyEcho", + "hasInput": true, + "inputType": "httpOnlyEchoInput", + "outputType": "httpOnlyEchoOutput", + "transport": "http", + "isContext": false, + "isForm": false, + "formName": null, + "formRole": null + }, + { + "name": "login.schema", + "camelName": "loginSchema", + "hasInput": false, + "inputType": null, + "outputType": "loginSchemaOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "login", + "formRole": "schema" + }, + { + "name": "login.validate", + "camelName": "loginValidate", + "hasInput": true, + "inputType": "loginValidateInput", + "outputType": "loginValidateOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "login", + "formRole": "validate" + }, + { + "name": "login.submit", + "camelName": "loginSubmit", + "hasInput": true, + "inputType": "loginSubmitInput", + "outputType": "loginSubmitOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "login", + "formRole": "submit" + }, + { + "name": "signup.schema", + "camelName": "signupSchema", + "hasInput": false, + "inputType": null, + "outputType": "signupSchemaOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "signup", + "formRole": "schema" + }, + { + "name": "signup.validate", + "camelName": "signupValidate", + "hasInput": true, + "inputType": "signupValidateInput", + "outputType": "signupValidateOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "signup", + "formRole": "validate" + }, + { + "name": "signup.submit", + "camelName": "signupSubmit", + "hasInput": true, + "inputType": "signupSubmitInput", + "outputType": "signupSubmitOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "signup", + "formRole": "submit" + }, + { + "name": "add_email.schema", + "camelName": "addEmailSchema", + "hasInput": false, + "inputType": null, + "outputType": "addEmailSchemaOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "add_email", + "formRole": "schema" + }, + { + "name": "add_email.validate", + "camelName": "addEmailValidate", + "hasInput": true, + "inputType": "addEmailValidateInput", + "outputType": "addEmailValidateOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "add_email", + "formRole": "validate" + }, + { + "name": "staff_only", + "camelName": "staffOnly", + "hasInput": false, + "inputType": null, + "outputType": "staffOnlyOutput", + "transport": "http", + "isContext": false, + "isForm": false, + "formName": null, + "formRole": null + }, + { + "name": "superuser_only", + "camelName": "superuserOnly", + "hasInput": false, + "inputType": null, + "outputType": "superuserOnlyOutput", + "transport": "http", + "isContext": false, + "isForm": false, + "formName": null, + "formRole": null + }, + { + "name": "verified_only", + "camelName": "verifiedOnly", + "hasInput": false, + "inputType": null, + "outputType": "verifiedOnlyOutput", + "transport": "http", + "isContext": false, + "isForm": false, + "formName": null, + "formRole": null + }, + { + "name": "current_user", + "camelName": "currentUser", + "hasInput": false, + "inputType": null, + "outputType": "currentUserOutput", + "transport": "http", + "isContext": "global", + "isForm": false, + "formName": null, + "formRole": null + }, + { + "name": "greet", + "camelName": "greet", + "hasInput": true, + "inputType": "greetInput", + "outputType": "greetOutput", + "transport": "http", + "isContext": "local", + "isForm": false, + "formName": null, + "formRole": null + }, + { + "name": "multiply", + "camelName": "multiply", + "hasInput": true, + "inputType": "multiplyInput", + "outputType": "multiplyOutput", + "transport": "http", + "isContext": false, + "isForm": false, + "formName": null, + "formRole": null + }, + { + "name": "not_implemented_fn", + "camelName": "notImplementedFn", + "hasInput": false, + "inputType": null, + "outputType": "notImplementedFnOutput", + "transport": "http", + "isContext": false, + "isForm": false, + "formName": null, + "formRole": null + }, + { + "name": "buggy_fn", + "camelName": "buggyFn", + "hasInput": false, + "inputType": null, + "outputType": "buggyFnOutput", + "transport": "http", + "isContext": false, + "isForm": false, + "formName": null, + "formRole": null + }, + { + "name": "permission_check_fn", + "camelName": "permissionCheckFn", + "hasInput": true, + "inputType": "permissionCheckFnInput", + "outputType": "permissionCheckFnOutput", + "transport": "http", + "isContext": false, + "isForm": false, + "formName": null, + "formRole": null + }, + { + "name": "ws_whoami", + "camelName": "wsWhoami", + "hasInput": false, + "inputType": null, + "outputType": "wsWhoamiOutput", + "transport": "websocket", + "isContext": false, + "isForm": false, + "formName": null, + "formRole": null + }, + { + "name": "contact.schema", + "camelName": "contactSchema", + "hasInput": true, + "inputType": "contactSchemaInput", + "outputType": "contactSchemaOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "contact", + "formRole": "schema", + "formFields": [ + { + "name": "name", + "required": true, + "constraints": { + "max": 100 + }, + "zodType": "string" + }, + { + "name": "email", + "required": true, + "constraints": { + "max": 320, + "email": true + }, + "zodType": "string" + }, + { + "name": "message", + "required": true, + "constraints": {}, + "zodType": "string" + } + ] + }, + { + "name": "contact.validate", + "camelName": "contactValidate", + "hasInput": true, + "inputType": "contactValidateInput", + "outputType": "contactValidateOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "contact", + "formRole": "validate" + }, + { + "name": "contact.submit", + "camelName": "contactSubmit", + "hasInput": false, + "inputType": null, + "outputType": "contactSubmitOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "contact", + "formRole": "submit" + }, + { + "name": "item.schema", + "camelName": "itemSchema", + "hasInput": true, + "inputType": "itemSchemaInput", + "outputType": "itemSchemaOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "item", + "formRole": "schema", + "formFields": [ + { + "name": "label", + "required": true, + "constraints": { + "max": 50 + }, + "zodType": "string" + }, + { + "name": "quantity", + "required": true, + "constraints": { + "int": true, + "min": 1 + }, + "zodType": "number" + } + ] + }, + { + "name": "item.validate", + "camelName": "itemValidate", + "hasInput": true, + "inputType": "itemValidateInput", + "outputType": "itemValidateOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "item", + "formRole": "validate" + }, + { + "name": "item.submit", + "camelName": "itemSubmit", + "hasInput": false, + "inputType": null, + "outputType": "itemSubmitOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "item", + "formRole": "submit" + }, + { + "name": "item.formset.schema", + "camelName": "itemFormsetSchema", + "hasInput": true, + "inputType": "itemFormsetSchemaInput", + "outputType": "itemFormsetSchemaOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "item", + "formRole": "formset_schema" + }, + { + "name": "item.formset.validate", + "camelName": "itemFormsetValidate", + "hasInput": true, + "inputType": "itemFormsetValidateInput", + "outputType": "itemFormsetValidateOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "item", + "formRole": "formset_validate" + }, + { + "name": "item.formset.submit", + "camelName": "itemFormsetSubmit", + "hasInput": true, + "inputType": "itemFormsetSubmitInput", + "outputType": "itemFormsetSubmitOutput", + "transport": "http", + "isContext": false, + "isForm": true, + "formName": "item", + "formRole": "formset_submit" + }, + { + "name": "jwt_obtain", + "camelName": "jwtObtain", + "hasInput": false, + "inputType": null, + "outputType": "jwtObtainOutput", + "transport": "http", + "isContext": false, + "isForm": false, + "formName": null, + "formRole": null + }, + { + "name": "jwt_refresh", + "camelName": "jwtRefresh", + "hasInput": true, + "inputType": "jwtRefreshInput", + "outputType": "jwtRefreshOutput", + "transport": "http", + "isContext": false, + "isForm": false, + "formName": null, + "formRole": null + } + ], + "x-mizan-contexts": { + "global": { + "functions": [ + "current_user" + ], + "params": {} + }, + "local": { + "functions": [ + "greet" + ], + "params": { + "name": { + "type": "string", + "sharedBy": [ + "greet" + ], + "required": true + } + } + } + } +} \ No newline at end of file diff --git a/examples/django-react-site/harness/src/api/svelte.ts b/examples/django-react-site/harness/src/api/svelte.ts new file mode 100644 index 0000000..9b87a8c --- /dev/null +++ b/examples/django-react-site/harness/src/api/svelte.ts @@ -0,0 +1,67 @@ +// AUTO-GENERATED by mizan — do not edit + +import { writable, derived, type Readable } from 'svelte/store' +import { registerContext } from '@mizan/runtime' + +import { fetchGlobalContext, type GlobalContextData, type GlobalContextParams, fetchLocalContext, type LocalContextData, type LocalContextParams, callEcho, callAdd, callWhoami, callHttpOnlyEcho, callStaffOnly, callSuperuserOnly, callVerifiedOnly, callMultiply, callNotImplementedFn, callBuggyFn, callPermissionCheckFn, callWsWhoami, callJwtObtain, callJwtRefresh } from '../index' + +// Global context +export function createGlobalContext() { + const data = writable(null) + const loading = writable(true) + + const refetch = async () => { + loading.set(true) + const result = await fetchGlobalContext({} as any) + data.set(result) + loading.set(false) + } + + refetch() + const unregister = registerContext('global', {}, refetch) + + return { + data, + loading, + currentUser: derived(data, $d => $d?.current_user ?? null) as Readable, + destroy: unregister, + } +} + +// Local context +export function createLocalContext(params: LocalContextParams) { + const data = writable(null) + const loading = writable(true) + + const refetch = async () => { + loading.set(true) + const result = await fetchLocalContext(params) + data.set(result) + loading.set(false) + } + + refetch() + const unregister = registerContext('local', params, refetch) + + return { + data, + loading, + greet: derived(data, $d => $d?.greet ?? null) as Readable, + destroy: unregister, + } +} + +export { callEcho } from '../functions/echo' +export { callAdd } from '../functions/add' +export { callWhoami } from '../functions/whoami' +export { callHttpOnlyEcho } from '../functions/httpOnlyEcho' +export { callStaffOnly } from '../functions/staffOnly' +export { callSuperuserOnly } from '../functions/superuserOnly' +export { callVerifiedOnly } from '../functions/verifiedOnly' +export { callMultiply } from '../functions/multiply' +export { callNotImplementedFn } from '../functions/notImplementedFn' +export { callBuggyFn } from '../functions/buggyFn' +export { callPermissionCheckFn } from '../functions/permissionCheckFn' +export { callWsWhoami } from '../functions/wsWhoami' +export { callJwtObtain } from '../functions/jwtObtain' +export { callJwtRefresh } from '../functions/jwtRefresh' diff --git a/examples/django-react-site/harness/src/api/types.ts b/examples/django-react-site/harness/src/api/types.ts new file mode 100644 index 0000000..7657976 --- /dev/null +++ b/examples/django-react-site/harness/src/api/types.ts @@ -0,0 +1,1908 @@ +// AUTO-GENERATED by mizan — do not edit + +export interface paths { + "/mizan/echo": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call echo */ + post: operations["echo"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/add": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call add */ + post: operations["add"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/whoami": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call whoami */ + post: operations["whoami"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/http_only_echo": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call http_only_echo */ + post: operations["httpOnlyEcho"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/login.schema": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call login.schema */ + post: operations["loginSchema"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/login.validate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call login.validate */ + post: operations["loginValidate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/login.submit": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call login.submit */ + post: operations["loginSubmit"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/signup.schema": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call signup.schema */ + post: operations["signupSchema"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/signup.validate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call signup.validate */ + post: operations["signupValidate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/signup.submit": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call signup.submit */ + post: operations["signupSubmit"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/add_email.schema": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call add_email.schema */ + post: operations["addEmailSchema"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/add_email.validate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call add_email.validate */ + post: operations["addEmailValidate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/staff_only": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call staff_only */ + post: operations["staffOnly"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/superuser_only": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call superuser_only */ + post: operations["superuserOnly"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/verified_only": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call verified_only */ + post: operations["verifiedOnly"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/current_user": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call current_user */ + post: operations["currentUser"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/greet": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call greet */ + post: operations["greet"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/multiply": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call multiply */ + post: operations["multiply"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/not_implemented_fn": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call not_implemented_fn */ + post: operations["notImplementedFn"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/buggy_fn": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call buggy_fn */ + post: operations["buggyFn"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/permission_check_fn": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call permission_check_fn */ + post: operations["permissionCheckFn"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/ws_whoami": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call ws_whoami */ + post: operations["wsWhoami"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/contact.schema": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call contact.schema */ + post: operations["contactSchema"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/contact.validate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call contact.validate */ + post: operations["contactValidate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/contact.submit": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Submit function handles both JSON and multipart/form-data. + * + * The executor detects form functions and parses the request appropriately. + */ + post: operations["contactSubmit"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/item.schema": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call item.schema */ + post: operations["itemSchema"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/item.validate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call item.validate */ + post: operations["itemValidate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/item.submit": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Submit function handles both JSON and multipart/form-data. + * + * The executor detects form functions and parses the request appropriately. + */ + post: operations["itemSubmit"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/item.formset.schema": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call item.formset.schema */ + post: operations["itemFormsetSchema"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/item.formset.validate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call item.formset.validate */ + post: operations["itemFormsetValidate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/item.formset.submit": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Call item.formset.submit */ + post: operations["itemFormsetSubmit"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/jwt_obtain": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Obtain JWT tokens from an authenticated session. + * + * Requires session authentication (cookie or WebSocket session). + * Returns access and refresh tokens that can be used for stateless auth. + * + * The tokens include user claims (is_staff, is_superuser) so that + * subsequent JWT-authenticated requests don't need a database query. + * + * Usage: + * const { access_token, refresh_token } = await call('jwt_obtain') + * // Use access_token in Authorization: Bearer header + */ + post: operations["jwtObtain"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/mizan/jwt_refresh": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Refresh JWT tokens using a refresh token. + * + * Does not require session authentication - the refresh token itself + * contains the session reference and is validated against the session store. + * + * If the original session has been destroyed (user logged out), this fails. + * + * Usage: + * const { access_token, refresh_token } = await call('jwt_refresh', { refresh_token }) + */ + post: operations["jwtRefresh"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** echoOutput */ + echoOutput: { + /** Message */ + message: string; + }; + /** echoInput */ + echoInput: { + /** Text */ + text: string; + }; + /** addOutput */ + addOutput: { + /** Result */ + result: number; + }; + /** addInput */ + addInput: { + /** A */ + a: number; + /** B */ + b: number; + }; + /** whoamiOutput */ + whoamiOutput: { + /** User Id */ + user_id: number | null; + /** Email */ + email: string; + /** Is Staff */ + is_staff: boolean; + }; + /** httpOnlyEchoOutput */ + httpOnlyEchoOutput: { + /** Message */ + message: string; + }; + /** httpOnlyEchoInput */ + httpOnlyEchoInput: { + /** Text */ + text: string; + }; + /** + * FormSchemaField + * @description Schema for a single form field. + */ + FormSchemaField: { + /** Name */ + name: string; + /** Type */ + type: string; + /** Required */ + required: boolean; + /** Label */ + label: string; + /** Help Text */ + help_text?: string | null; + /** Choices */ + choices?: [ + string, + string + ][] | null; + /** Initial */ + initial?: unknown; + }; + /** loginSchemaOutput */ + loginSchemaOutput: { + /** Fields */ + fields: components["schemas"]["FormSchemaField"][]; + }; + /** loginValidateOutput */ + loginValidateOutput: { + /** Valid */ + valid: boolean; + /** Errors */ + errors: { + [key: string]: string[]; + }; + }; + /** loginValidateInput */ + loginValidateInput: { + /** Data */ + data: { + [key: string]: unknown; + }; + }; + /** loginSubmitOutput */ + loginSubmitOutput: Record; + /** loginSubmitInput */ + loginSubmitInput: { + /** Data */ + data: { + [key: string]: unknown; + }; + }; + /** signupSchemaOutput */ + signupSchemaOutput: { + /** Fields */ + fields: components["schemas"]["FormSchemaField"][]; + }; + /** signupValidateOutput */ + signupValidateOutput: { + /** Valid */ + valid: boolean; + /** Errors */ + errors: { + [key: string]: string[]; + }; + }; + /** signupValidateInput */ + signupValidateInput: { + /** Data */ + data: { + [key: string]: unknown; + }; + }; + /** signupSubmitOutput */ + signupSubmitOutput: Record; + /** signupSubmitInput */ + signupSubmitInput: { + /** Data */ + data: { + [key: string]: unknown; + }; + }; + /** addEmailSchemaOutput */ + addEmailSchemaOutput: { + /** Fields */ + fields: components["schemas"]["FormSchemaField"][]; + }; + /** addEmailValidateOutput */ + addEmailValidateOutput: { + /** Valid */ + valid: boolean; + /** Errors */ + errors: { + [key: string]: string[]; + }; + }; + /** addEmailValidateInput */ + addEmailValidateInput: { + /** Data */ + data: { + [key: string]: unknown; + }; + }; + /** staffOnlyOutput */ + staffOnlyOutput: { + /** Message */ + message: string; + }; + /** superuserOnlyOutput */ + superuserOnlyOutput: { + /** Message */ + message: string; + }; + /** verifiedOnlyOutput */ + verifiedOnlyOutput: { + /** Message */ + message: string; + }; + /** currentUserOutput */ + currentUserOutput: { + /** Authenticated */ + authenticated: boolean; + /** Email */ + email: string; + /** Is Staff */ + is_staff: boolean; + }; + /** greetOutput */ + greetOutput: { + /** Greeting */ + greeting: string; + }; + /** greetInput */ + greetInput: { + /** Name */ + name: string; + }; + /** multiplyOutput */ + multiplyOutput: { + /** Product */ + product: number; + }; + /** multiplyInput */ + multiplyInput: { + /** X */ + x: number; + /** Y */ + y: number; + }; + /** notImplementedFnOutput */ + notImplementedFnOutput: { + /** Message */ + message: string; + }; + /** buggyFnOutput */ + buggyFnOutput: { + /** Message */ + message: string; + }; + /** permissionCheckFnOutput */ + permissionCheckFnOutput: { + /** Message */ + message: string; + }; + /** permissionCheckFnInput */ + permissionCheckFnInput: { + /** Secret */ + secret: string; + }; + /** wsWhoamiOutput */ + wsWhoamiOutput: { + /** User Id */ + user_id: number | null; + /** Email */ + email: string; + /** Is Staff */ + is_staff: boolean; + }; + /** FieldChoice */ + FieldChoice: { + /** Value */ + value: string; + /** Label */ + label: string; + }; + /** FieldSchema */ + FieldSchema: { + /** Name */ + name: string; + /** Label */ + label: string; + /** Type */ + type: string; + /** Widget */ + widget: string; + /** Required */ + required: boolean; + /** Disabled */ + disabled: boolean; + /** Help Text */ + help_text: string; + /** Initial */ + initial: unknown; + /** Max Length */ + max_length: number | null; + /** Min Length */ + min_length: number | null; + /** Choices */ + choices: components["schemas"]["FieldChoice"][] | null; + }; + /** + * FormMeta + * @description Metadata controlling frontend form behavior. + * + * Attributes: + * refetch_schema_on_validate: If True, frontend should refetch schema on each + * validation (useful for dynamic choice fields). Default False. + * live_validation: If False, frontend should disable live validation entirely. + * Useful for sensitive forms like login. Default True. + * live_form_errors: If True, show form-level errors during live validation. + * Form errors are things like "Invalid credentials" vs field errors like + * "This field is required". Default False for security. + */ + FormMeta: { + /** + * Refetch Schema On Validate + * @default false + */ + refetch_schema_on_validate: boolean; + /** + * Live Validation + * @default true + */ + live_validation: boolean; + /** + * Live Form Errors + * @default false + */ + live_form_errors: boolean; + }; + /** contactSchemaOutput */ + contactSchemaOutput: { + /** Name */ + name: string; + /** Title */ + title: string; + /** Subtitle */ + subtitle: string | null; + /** Submit Label */ + submit_label: string; + /** Fields */ + fields: components["schemas"]["FieldSchema"][]; + /** + * @default { + * "refetch_schema_on_validate": false, + * "live_validation": true, + * "live_form_errors": false + * } + */ + meta: components["schemas"]["FormMeta"]; + }; + /** contactSchemaInput */ + contactSchemaInput: { + /** + * Data + * @default {} + */ + data: { + [key: string]: unknown; + }; + }; + /** FieldError */ + FieldError: { + /** Message */ + message: string; + /** Code */ + code: string | null; + }; + /** FieldErrorList */ + FieldErrorList: { + /** Field */ + field: string; + /** Errors */ + errors: components["schemas"]["FieldError"][]; + }; + /** contactValidateOutput */ + contactValidateOutput: { + /** Errors */ + errors: components["schemas"]["FieldErrorList"][]; + }; + /** contactValidateInput */ + contactValidateInput: { + /** Data */ + data: { + [key: string]: unknown; + }; + }; + /** contactSubmitOutput */ + contactSubmitOutput: { + /** Success */ + success: boolean; + /** Data */ + data?: { + [key: string]: unknown; + } | null; + }; + /** itemSchemaOutput */ + itemSchemaOutput: { + /** Name */ + name: string; + /** Title */ + title: string; + /** Subtitle */ + subtitle: string | null; + /** Submit Label */ + submit_label: string; + /** Fields */ + fields: components["schemas"]["FieldSchema"][]; + /** + * @default { + * "refetch_schema_on_validate": false, + * "live_validation": true, + * "live_form_errors": false + * } + */ + meta: components["schemas"]["FormMeta"]; + }; + /** itemSchemaInput */ + itemSchemaInput: { + /** + * Data + * @default {} + */ + data: { + [key: string]: unknown; + }; + }; + /** itemValidateOutput */ + itemValidateOutput: { + /** Errors */ + errors: components["schemas"]["FieldErrorList"][]; + }; + /** itemValidateInput */ + itemValidateInput: { + /** Data */ + data: { + [key: string]: unknown; + }; + }; + /** itemSubmitOutput */ + itemSubmitOutput: { + /** Success */ + success: boolean; + /** Data */ + data?: { + [key: string]: unknown; + } | null; + }; + /** + * FormSchema + * @description Schema returned by /schema endpoint with form metadata and fields. + */ + FormSchema: { + /** Name */ + name: string; + /** Title */ + title: string; + /** Subtitle */ + subtitle: string | null; + /** Submit Label */ + submit_label: string; + /** Fields */ + fields: components["schemas"]["FieldSchema"][]; + /** + * @default { + * "refetch_schema_on_validate": false, + * "live_validation": true, + * "live_form_errors": false + * } + */ + meta: components["schemas"]["FormMeta"]; + }; + /** itemFormsetSchemaOutput */ + itemFormsetSchemaOutput: { + /** Forms */ + forms: components["schemas"]["FormSchema"][]; + /** Min Num */ + min_num: number; + /** Max Num */ + max_num: number; + /** Can Delete */ + can_delete: boolean; + /** Can Order */ + can_order: boolean; + }; + /** itemFormsetSchemaInput */ + itemFormsetSchemaInput: { + /** + * Forms + * @default [] + */ + forms: { + [key: string]: unknown; + }[]; + }; + /** FormValidation */ + FormValidation: { + /** Errors */ + errors: components["schemas"]["FieldErrorList"][]; + }; + /** itemFormsetValidateOutput */ + itemFormsetValidateOutput: { + /** General */ + general: string[]; + /** Per Form */ + per_form: components["schemas"]["FormValidation"][]; + }; + /** itemFormsetValidateInput */ + itemFormsetValidateInput: { + /** Forms */ + forms: { + [key: string]: unknown; + }[]; + }; + /** itemFormsetSubmitOutput */ + itemFormsetSubmitOutput: { + /** Success */ + success: boolean; + }; + /** itemFormsetSubmitInput */ + itemFormsetSubmitInput: { + /** Forms */ + forms: { + [key: string]: unknown; + }[]; + }; + /** jwtObtainOutput */ + jwtObtainOutput: { + /** Access Token */ + access_token: string; + /** Refresh Token */ + refresh_token: string; + /** Expires In */ + expires_in: number; + }; + /** jwtRefreshOutput */ + jwtRefreshOutput: { + /** Access Token */ + access_token: string; + /** Refresh Token */ + refresh_token: string; + /** Expires In */ + expires_in: number; + }; + /** jwtRefreshInput */ + jwtRefreshInput: { + /** Refresh Token */ + refresh_token: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + echo: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["echoInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["echoOutput"]; + }; + }; + }; + }; + add: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["addInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["addOutput"]; + }; + }; + }; + }; + whoami: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["whoamiOutput"]; + }; + }; + }; + }; + httpOnlyEcho: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["httpOnlyEchoInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["httpOnlyEchoOutput"]; + }; + }; + }; + }; + loginSchema: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["loginSchemaOutput"]; + }; + }; + }; + }; + loginValidate: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["loginValidateInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["loginValidateOutput"]; + }; + }; + }; + }; + loginSubmit: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["loginSubmitInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["loginSubmitOutput"]; + }; + }; + }; + }; + signupSchema: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["signupSchemaOutput"]; + }; + }; + }; + }; + signupValidate: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["signupValidateInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["signupValidateOutput"]; + }; + }; + }; + }; + signupSubmit: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["signupSubmitInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["signupSubmitOutput"]; + }; + }; + }; + }; + addEmailSchema: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["addEmailSchemaOutput"]; + }; + }; + }; + }; + addEmailValidate: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["addEmailValidateInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["addEmailValidateOutput"]; + }; + }; + }; + }; + staffOnly: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["staffOnlyOutput"]; + }; + }; + }; + }; + superuserOnly: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["superuserOnlyOutput"]; + }; + }; + }; + }; + verifiedOnly: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["verifiedOnlyOutput"]; + }; + }; + }; + }; + currentUser: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["currentUserOutput"]; + }; + }; + }; + }; + greet: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["greetInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["greetOutput"]; + }; + }; + }; + }; + multiply: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["multiplyInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["multiplyOutput"]; + }; + }; + }; + }; + notImplementedFn: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["notImplementedFnOutput"]; + }; + }; + }; + }; + buggyFn: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["buggyFnOutput"]; + }; + }; + }; + }; + permissionCheckFn: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["permissionCheckFnInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["permissionCheckFnOutput"]; + }; + }; + }; + }; + wsWhoami: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["wsWhoamiOutput"]; + }; + }; + }; + }; + contactSchema: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["contactSchemaInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["contactSchemaOutput"]; + }; + }; + }; + }; + contactValidate: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["contactValidateInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["contactValidateOutput"]; + }; + }; + }; + }; + contactSubmit: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["contactSubmitOutput"]; + }; + }; + }; + }; + itemSchema: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["itemSchemaInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["itemSchemaOutput"]; + }; + }; + }; + }; + itemValidate: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["itemValidateInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["itemValidateOutput"]; + }; + }; + }; + }; + itemSubmit: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["itemSubmitOutput"]; + }; + }; + }; + }; + itemFormsetSchema: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["itemFormsetSchemaInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["itemFormsetSchemaOutput"]; + }; + }; + }; + }; + itemFormsetValidate: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["itemFormsetValidateInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["itemFormsetValidateOutput"]; + }; + }; + }; + }; + itemFormsetSubmit: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["itemFormsetSubmitInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["itemFormsetSubmitOutput"]; + }; + }; + }; + }; + jwtObtain: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["jwtObtainOutput"]; + }; + }; + }; + }; + jwtRefresh: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["jwtRefreshInput"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["jwtRefreshOutput"]; + }; + }; + }; + }; +} + + +// Convenience type exports +export type echoOutput = components["schemas"]["echoOutput"] +export type echoInput = components["schemas"]["echoInput"] +export type addOutput = components["schemas"]["addOutput"] +export type addInput = components["schemas"]["addInput"] +export type whoamiOutput = components["schemas"]["whoamiOutput"] +export type httpOnlyEchoOutput = components["schemas"]["httpOnlyEchoOutput"] +export type httpOnlyEchoInput = components["schemas"]["httpOnlyEchoInput"] +export type FormSchemaField = components["schemas"]["FormSchemaField"] +export type loginSchemaOutput = components["schemas"]["loginSchemaOutput"] +export type loginValidateOutput = components["schemas"]["loginValidateOutput"] +export type loginValidateInput = components["schemas"]["loginValidateInput"] +export type loginSubmitOutput = components["schemas"]["loginSubmitOutput"] +export type loginSubmitInput = components["schemas"]["loginSubmitInput"] +export type signupSchemaOutput = components["schemas"]["signupSchemaOutput"] +export type signupValidateOutput = components["schemas"]["signupValidateOutput"] +export type signupValidateInput = components["schemas"]["signupValidateInput"] +export type signupSubmitOutput = components["schemas"]["signupSubmitOutput"] +export type signupSubmitInput = components["schemas"]["signupSubmitInput"] +export type addEmailSchemaOutput = components["schemas"]["addEmailSchemaOutput"] +export type addEmailValidateOutput = components["schemas"]["addEmailValidateOutput"] +export type addEmailValidateInput = components["schemas"]["addEmailValidateInput"] +export type staffOnlyOutput = components["schemas"]["staffOnlyOutput"] +export type superuserOnlyOutput = components["schemas"]["superuserOnlyOutput"] +export type verifiedOnlyOutput = components["schemas"]["verifiedOnlyOutput"] +export type currentUserOutput = components["schemas"]["currentUserOutput"] +export type greetOutput = components["schemas"]["greetOutput"] +export type greetInput = components["schemas"]["greetInput"] +export type multiplyOutput = components["schemas"]["multiplyOutput"] +export type multiplyInput = components["schemas"]["multiplyInput"] +export type notImplementedFnOutput = components["schemas"]["notImplementedFnOutput"] +export type buggyFnOutput = components["schemas"]["buggyFnOutput"] +export type permissionCheckFnOutput = components["schemas"]["permissionCheckFnOutput"] +export type permissionCheckFnInput = components["schemas"]["permissionCheckFnInput"] +export type wsWhoamiOutput = components["schemas"]["wsWhoamiOutput"] +export type FieldChoice = components["schemas"]["FieldChoice"] +export type FieldSchema = components["schemas"]["FieldSchema"] +export type FormMeta = components["schemas"]["FormMeta"] +export type contactSchemaOutput = components["schemas"]["contactSchemaOutput"] +export type contactSchemaInput = components["schemas"]["contactSchemaInput"] +export type FieldError = components["schemas"]["FieldError"] +export type FieldErrorList = components["schemas"]["FieldErrorList"] +export type contactValidateOutput = components["schemas"]["contactValidateOutput"] +export type contactValidateInput = components["schemas"]["contactValidateInput"] +export type contactSubmitOutput = components["schemas"]["contactSubmitOutput"] +export type itemSchemaOutput = components["schemas"]["itemSchemaOutput"] +export type itemSchemaInput = components["schemas"]["itemSchemaInput"] +export type itemValidateOutput = components["schemas"]["itemValidateOutput"] +export type itemValidateInput = components["schemas"]["itemValidateInput"] +export type itemSubmitOutput = components["schemas"]["itemSubmitOutput"] +export type FormSchema = components["schemas"]["FormSchema"] +export type itemFormsetSchemaOutput = components["schemas"]["itemFormsetSchemaOutput"] +export type itemFormsetSchemaInput = components["schemas"]["itemFormsetSchemaInput"] +export type FormValidation = components["schemas"]["FormValidation"] +export type itemFormsetValidateOutput = components["schemas"]["itemFormsetValidateOutput"] +export type itemFormsetValidateInput = components["schemas"]["itemFormsetValidateInput"] +export type itemFormsetSubmitOutput = components["schemas"]["itemFormsetSubmitOutput"] +export type itemFormsetSubmitInput = components["schemas"]["itemFormsetSubmitInput"] +export type jwtObtainOutput = components["schemas"]["jwtObtainOutput"] +export type jwtRefreshOutput = components["schemas"]["jwtRefreshOutput"] +export type jwtRefreshInput = components["schemas"]["jwtRefreshInput"] diff --git a/examples/django-react-site/harness/src/api/vue.ts b/examples/django-react-site/harness/src/api/vue.ts new file mode 100644 index 0000000..2dfaac6 --- /dev/null +++ b/examples/django-react-site/harness/src/api/vue.ts @@ -0,0 +1,96 @@ +// AUTO-GENERATED by mizan — do not edit + +import { ref, computed, watch, onMounted, onUnmounted, provide, inject, type Ref, type ComputedRef, type InjectionKey } from 'vue' +import { registerContext } from '@mizan/runtime' + +import { fetchGlobalContext, type GlobalContextData, type GlobalContextParams, fetchLocalContext, type LocalContextData, type LocalContextParams, callEcho, callAdd, callWhoami, callHttpOnlyEcho, callStaffOnly, callSuperuserOnly, callVerifiedOnly, callMultiply, callNotImplementedFn, callBuggyFn, callPermissionCheckFn, callWsWhoami, callJwtObtain, callJwtRefresh } from '../index' + +// Global context +const GlobalKey: InjectionKey<{ data: Ref, loading: Ref }> = Symbol('global') + +export function provideGlobalContext() { + const data = ref(null) + const loading = ref(true) + + const refetch = async () => { + loading.value = true + try { + data.value = await fetchGlobalContext({} as any) + } catch (e) { console.error('[mizan] global fetch failed:', e) } + loading.value = false + } + + let unregister: (() => void) | null = null + onMounted(() => { + refetch() + unregister = registerContext('global', {}, refetch) + }) + onUnmounted(() => { unregister?.() }) + + provide(GlobalKey, { data, loading }) +} + +export function useCurrentUser(): ComputedRef { + const ctx = inject(GlobalKey) + if (!ctx) throw new Error('useCurrentUser requires provideGlobalContext in a parent') + return computed(() => ctx.data.value?.current_user ?? null) +} + +// Local context +const LocalKey: InjectionKey<{ data: Ref, loading: Ref }> = Symbol('local') + +export function provideLocalContext(params: { name: string }) { + const data = ref(null) + const loading = ref(true) + + const refetch = async () => { + loading.value = true + try { + data.value = await fetchLocalContext(params as any) + } catch (e) { console.error('[mizan] local fetch failed:', e) } + loading.value = false + } + + let unregister: (() => void) | null = null + onMounted(() => { + refetch() + unregister = registerContext('local', params, refetch) + }) + onUnmounted(() => { unregister?.() }) + + provide(LocalKey, { data, loading }) +} + +export function useGreet(): ComputedRef { + const ctx = inject(LocalKey) + if (!ctx) throw new Error('useGreet requires provideLocalContext in a parent') + return computed(() => ctx.data.value?.greet ?? null) +} + +export const useEcho = callEcho + +export const useAdd = callAdd + +export const useWhoami = callWhoami + +export const useHttpOnlyEcho = callHttpOnlyEcho + +export const useStaffOnly = callStaffOnly + +export const useSuperuserOnly = callSuperuserOnly + +export const useVerifiedOnly = callVerifiedOnly + +export const useMultiply = callMultiply + +export const useNotImplementedFn = callNotImplementedFn + +export const useBuggyFn = callBuggyFn + +export const usePermissionCheckFn = callPermissionCheckFn + +export const useWsWhoami = callWsWhoami + +export const useJwtObtain = callJwtObtain + +export const useJwtRefresh = callJwtRefresh diff --git a/packages/mizan-django/generate/generator/cli.mjs b/packages/mizan-django/generate/generator/cli.mjs index 56cf42a..f7cb625 100755 --- a/packages/mizan-django/generate/generator/cli.mjs +++ b/packages/mizan-django/generate/generator/cli.mjs @@ -2,282 +2,222 @@ /** * mizan Code Generator CLI * - * Generate TypeScript types, React provider, and hooks from Django schemas. + * Two-stage codegen: + * Stage 1: Framework-agnostic types + fetch/mutation functions + * Stage 2: Framework-specific wrappers (React hooks, Vue composables, Svelte stores) * * Usage: - * npx mizan-generate # Run once - * npx mizan-generate --watch # Watch mode + * npx mizan-generate # React (default) + * npx mizan-generate --target vue # Vue + * npx mizan-generate --target react,vue,svelte # All three */ import { promises as fs } from 'fs' import path from 'path' import { fetchChannelsSchema, fetchMizanSchema } from './lib/fetch.mjs' -import { generateMizanFiles } from './lib/mizan.mjs' +import { generateTypes, generateContextFile, generateMutationFile, generateFunctionFile, generateStage1Index } from './lib/stage1.mjs' +import { generateReactAdapter } from './lib/adapters/react.mjs' +import { generateVueAdapter } from './lib/adapters/vue.mjs' +import { generateSvelteAdapter } from './lib/adapters/svelte.mjs' import { generateChannelsFiles } from './lib/channels.mjs' -import { generateIndex } from './lib/index.mjs' -// Use cwd — the script runs via `npx mizan-generate` from the frontend root const frontendDir = process.cwd() -/** - * Load configuration from django.config.mjs - */ async function loadConfig(configPath) { - const fullPath = path.resolve(frontendDir, configPath) - - try { - await fs.access(fullPath) - } catch { - throw new Error(`Config file not found: ${fullPath}`) - } - - // Convert to file:// URL for Windows compatibility - const fileUrl = new URL(`file://${fullPath.replace(/\\/g, '/')}`) - - if (configPath.endsWith('.mjs') || configPath.endsWith('.js')) { + const fullPath = path.resolve(frontendDir, configPath) + try { await fs.access(fullPath) } catch { throw new Error(`Config not found: ${fullPath}`) } + const fileUrl = new URL(`file://${fullPath.replace(/\\/g, '/')}`) const module = await import(fileUrl) return module.default - } - - if (configPath.endsWith('.ts')) { - try { - const module = await import(fileUrl) - return module.default - } catch { - throw new Error( - `Cannot load TypeScript config directly. Either:\n` + - ` 1. Install tsx: npm install -D tsx\n` + - ` 2. Use django.config.mjs instead` - ) - } - } - - throw new Error(`Unsupported config file format: ${configPath}`) } -/** - * Write generated code to file. - */ async function writeOutput(filePath, content) { - const dir = path.dirname(filePath) - await fs.mkdir(dir, { recursive: true }) - await fs.writeFile(filePath, content, 'utf8') + const dir = path.dirname(filePath) + await fs.mkdir(dir, { recursive: true }) + await fs.writeFile(filePath, content, 'utf8') +} + +function pascalCase(str) { + return str.split(/[.\-_]/).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('') } -/** - * Run schema generation. - */ async function generate(config, options = {}) { - const { output } = options + const { output, target: targetFlag } = options + const outputDir = output || config.output || 'src/api' + const targets = (targetFlag || config.target || 'react').split(',').map(t => t.trim()) - console.log('[mizan] Starting schema generation...') + console.log(`[mizan] Starting generation (targets: ${targets.join(', ')})...`) - const outputPath = output || config.output || 'src/api/generated.ts' + const fullOutputDir = path.resolve(frontendDir, outputDir) + let mizanSchema = null + let channelsSchema = null - let channelsSchema = null - let mizanSchema = null + // ── Channels (React-only for now) ─────────────────────────────────── - // Fetch and generate channels if available - try { - console.log('[mizan] Fetching channels schema...') - channelsSchema = await fetchChannelsSchema(config.source, frontendDir) - - const channelCount = channelsSchema['x-mizan-channels']?.length || 0 - if (channelCount > 0) { - console.log(`[mizan] Found ${channelCount} channels`) - - const channelsTypesPath = outputPath.replace(/\.ts$/, '.channels.ts') - const fullChannelsTypesPath = path.resolve(frontendDir, channelsTypesPath) - const channelsHooksPath = outputPath.replace(/\.ts$/, '.channels.hooks.tsx') - const fullChannelsHooksPath = path.resolve(frontendDir, channelsHooksPath) - const channelsSchemaPath = outputPath.replace(/\.ts$/, '.channels.schema.json') - const fullChannelsSchemaPath = path.resolve(frontendDir, channelsSchemaPath) - - const { types: channelsTypes, hooks: channelsHooks } = await generateChannelsFiles(channelsSchema) - - console.log(`[mizan] Generating -> ${channelsTypesPath}`) - await writeOutput(fullChannelsTypesPath, channelsTypes) - - if (channelsHooks) { - console.log(`[mizan] Generating -> ${channelsHooksPath}`) - await writeOutput(fullChannelsHooksPath, channelsHooks) - } - - console.log(`[mizan] Generating -> ${channelsSchemaPath}`) - await writeOutput(fullChannelsSchemaPath, JSON.stringify(channelsSchema, null, 2)) - } else { - console.log('[mizan] No channels registered, skipping channels generation') - } - } catch (err) { - console.log(`[mizan] Channels schema not available: ${err.message}`) - } - - // Fetch and generate mizan files - try { - console.log('[mizan] Fetching mizan schema...') - mizanSchema = await fetchMizanSchema(config.source, frontendDir) - - const functionCount = mizanSchema['x-mizan-functions']?.length || 0 - if (functionCount > 0) { - console.log(`[mizan] Found ${functionCount} mizan functions`) - - const mizanTypesPath = outputPath.replace(/\.ts$/, '.mizan.ts') - const fullMizanTypesPath = path.resolve(frontendDir, mizanTypesPath) - const mizanProviderPath = outputPath.replace(/\.ts$/, '.provider.tsx') - const fullMizanProviderPath = path.resolve(frontendDir, mizanProviderPath) - const mizanServerPath = outputPath.replace(/\.ts$/, '.server.ts') - const fullMizanServerPath = path.resolve(frontendDir, mizanServerPath) - const mizanFormsPath = outputPath.replace(/\.ts$/, '.forms.ts') - const fullMizanFormsPath = path.resolve(frontendDir, mizanFormsPath) - const mizanSchemaPath = outputPath.replace(/\.ts$/, '.mizan.schema.json') - const fullMizanSchemaPath = path.resolve(frontendDir, mizanSchemaPath) - - const hasChannels = (channelsSchema?.['x-mizan-channels']?.length || 0) > 0 - const { types: mizanTypes, provider: mizanProvider, server: mizanServer, forms: mizanForms } = await generateMizanFiles(mizanSchema, { hasChannels }) - - console.log(`[mizan] Generating -> ${mizanTypesPath}`) - await writeOutput(fullMizanTypesPath, mizanTypes) - - if (mizanProvider) { - console.log(`[mizan] Generating -> ${mizanProviderPath}`) - await writeOutput(fullMizanProviderPath, mizanProvider) - } - - if (mizanServer) { - console.log(`[mizan] Generating -> ${mizanServerPath}`) - await writeOutput(fullMizanServerPath, mizanServer) - } - - if (mizanForms) { - console.log(`[mizan] Generating -> ${mizanFormsPath}`) - await writeOutput(fullMizanFormsPath, mizanForms) - } - - console.log(`[mizan] Generating -> ${mizanSchemaPath}`) - await writeOutput(fullMizanSchemaPath, JSON.stringify(mizanSchema, null, 2)) - } else { - console.log('[mizan] No mizan functions registered, skipping mizan generation') - } - } catch (err) { - console.log(`[mizan] mizan schema not available: ${err.message}`) - } - - // Generate consolidated index.ts - const indexPath = path.dirname(outputPath) + '/index.ts' - const fullIndexPath = path.resolve(frontendDir, indexPath) - - console.log(`[mizan] Generating -> ${indexPath}`) - const indexContent = generateIndex({ - channelsSchema, - mizanSchema, - }) - await writeOutput(fullIndexPath, indexContent) - - console.log('[mizan] Generation complete!') -} - -/** - * Watch for changes and regenerate. - */ -async function watch(config, options) { - const debounce = config.watch?.debounce || 1000 - let timeout = null - let running = false - - async function runGenerate() { - if (running) { - timeout = setTimeout(runGenerate, debounce) - return - } - - running = true try { - await generate(config, options) + console.log('[mizan] Fetching channels schema...') + channelsSchema = await fetchChannelsSchema(config.source, frontendDir) + const channelCount = channelsSchema['x-mizan-channels']?.length || 0 + if (channelCount > 0 && targets.includes('react')) { + console.log(`[mizan] Found ${channelCount} channels`) + const { types, hooks } = await generateChannelsFiles(channelsSchema) + await writeOutput(path.join(fullOutputDir, 'channels.ts'), types) + if (hooks) await writeOutput(path.join(fullOutputDir, 'channels.hooks.tsx'), hooks) + } } catch (err) { - console.error('[mizan] Generation failed:', err.message) - } finally { - running = false + console.log(`[mizan] Channels not available: ${err.message}`) } - } - await runGenerate() + // ── Mizan functions ───────────────────────────────────────────────── - console.log('[mizan] Watching for changes (press Ctrl+C to stop)...') + try { + console.log('[mizan] Fetching mizan schema...') + mizanSchema = await fetchMizanSchema(config.source, frontendDir) - if (config.source.django) { - const { watch: chokidarWatch } = await import('chokidar') - const djangoDir = path.resolve(frontendDir, path.dirname(config.source.django.managePath)) + const functions = mizanSchema['x-mizan-functions'] || [] + const contextGroups = mizanSchema['x-mizan-contexts'] || {} - const watcher = chokidarWatch([ - path.join(djangoDir, '**/*.py'), - ], { - ignored: [ - '**/node_modules/**', - '**/__pycache__/**', - '**/migrations/**', - '**/.venv/**', - ], - ignoreInitial: true, - }) + if (functions.length === 0) { + console.log('[mizan] No functions registered') + return + } - watcher.on('change', (filePath) => { - console.log(`[mizan] Detected change: ${path.relative(djangoDir, filePath)}`) - if (timeout) clearTimeout(timeout) - timeout = setTimeout(runGenerate, debounce) - }) - } + console.log(`[mizan] Found ${functions.length} functions`) - process.on('SIGINT', () => { - console.log('\n[mizan] Stopping watch mode...') - process.exit(0) - }) + // ── Stage 1: Framework-agnostic ───────────────────────────────── + + // Types + const types = await generateTypes(mizanSchema) + await writeOutput(path.join(fullOutputDir, 'types.ts'), types) + console.log('[mizan] Stage 1 -> types.ts') + + // Context files + await fs.mkdir(path.join(fullOutputDir, 'contexts'), { recursive: true }) + for (const [ctxName, ctxMeta] of Object.entries(contextGroups)) { + const content = generateContextFile(ctxName, ctxMeta, functions) + await writeOutput(path.join(fullOutputDir, 'contexts', `${ctxName}.ts`), content) + console.log(`[mizan] Stage 1 -> contexts/${ctxName}.ts`) + } + + // Mutation + function files + const regularFns = functions.filter(fn => !fn.isContext && !fn.isForm) + if (regularFns.length > 0) { + await fs.mkdir(path.join(fullOutputDir, 'mutations'), { recursive: true }) + await fs.mkdir(path.join(fullOutputDir, 'functions'), { recursive: true }) + + for (const fn of regularFns) { + const dir = fn.affects ? 'mutations' : 'functions' + const content = fn.affects ? generateMutationFile(fn) : generateFunctionFile(fn) + await writeOutput(path.join(fullOutputDir, dir, `${fn.camelName}.ts`), content) + console.log(`[mizan] Stage 1 -> ${dir}/${fn.camelName}.ts`) + } + } + + // Stage 1 index + const stage1Index = generateStage1Index(mizanSchema) + await writeOutput(path.join(fullOutputDir, 'index.ts'), stage1Index) + console.log('[mizan] Stage 1 -> index.ts') + + // ── Stage 2: Framework-specific ───────────────────────────────── + + for (const target of targets) { + let content + let filename + + switch (target) { + case 'react': + content = generateReactAdapter(mizanSchema) + filename = 'react.tsx' + break + case 'vue': + content = generateVueAdapter(mizanSchema) + filename = 'vue.ts' + break + case 'svelte': + content = generateSvelteAdapter(mizanSchema) + filename = 'svelte.ts' + break + default: + console.warn(`[mizan] Unknown target: ${target}`) + continue + } + + if (content) { + await writeOutput(path.join(fullOutputDir, filename), content) + console.log(`[mizan] Stage 2 -> ${filename}`) + } + } + + // Schema JSON + await writeOutput( + path.join(fullOutputDir, 'schema.json'), + JSON.stringify(mizanSchema, null, 2), + ) + + } catch (err) { + console.log(`[mizan] Schema not available: ${err.message}`) + } + + console.log('[mizan] Generation complete!') } -/** - * Main entry point. - */ async function main() { - const args = process.argv.slice(2) + const args = process.argv.slice(2) - let configPath = 'django.config.mjs' - let watchMode = false - let output = null + let configPath = 'django.config.mjs' + let watchMode = false + let output = null + let target = null - for (let i = 0; i < args.length; i++) { - if (args[i] === '--config' || args[i] === '-c') { - configPath = args[++i] - } else if (args[i] === '--watch' || args[i] === '-w') { - watchMode = true - } else if (args[i] === '--output' || args[i] === '-o') { - output = args[++i] - } else if (args[i] === '--help' || args[i] === '-h') { - console.log(` -mizan Code Generator - Generate TypeScript from Django schemas + for (let i = 0; i < args.length; i++) { + if (args[i] === '--config' || args[i] === '-c') configPath = args[++i] + else if (args[i] === '--watch' || args[i] === '-w') watchMode = true + else if (args[i] === '--output' || args[i] === '-o') output = args[++i] + else if (args[i] === '--target' || args[i] === '-t') target = args[++i] + else if (args[i] === '--help' || args[i] === '-h') { + console.log(` +mizan Code Generator Usage: npx mizan-generate [options] Options: - -c, --config Config file path (default: django.config.mjs) - -w, --watch Watch mode - regenerate on changes - -o, --output Output file path (overrides config) - -h, --help Show this help message - `) - process.exit(0) + -c, --config Config file (default: django.config.mjs) + -t, --target Comma-separated: react,vue,svelte (default: react) + -o, --output Output directory (default: src/api) + -w, --watch Watch mode + -h, --help Show help +`) + process.exit(0) + } } - } - const config = await loadConfig(configPath) - const options = { output } + const config = await loadConfig(configPath) + const options = { output, target } - if (watchMode) { - await watch(config, options) - } else { - await generate(config, options) - } + if (watchMode) { + await generate(config, options) + console.log('[mizan] Watching for changes...') + const { watch: chokidarWatch } = await import('chokidar') + if (config.source.django) { + const djangoDir = path.resolve(frontendDir, path.dirname(config.source.django.managePath)) + let timeout = null + const watcher = chokidarWatch([path.join(djangoDir, '**/*.py')], { + ignored: ['**/node_modules/**', '**/__pycache__/**', '**/migrations/**'], + ignoreInitial: true, + }) + watcher.on('change', () => { + if (timeout) clearTimeout(timeout) + timeout = setTimeout(() => generate(config, options), 1000) + }) + } + process.on('SIGINT', () => process.exit(0)) + } else { + await generate(config, options) + } } main().catch(err => { - console.error('[mizan] Error:', err.message) - process.exit(1) + console.error('[mizan] Error:', err.message) + process.exit(1) }) diff --git a/packages/mizan-django/generate/generator/lib/adapters/react.mjs b/packages/mizan-django/generate/generator/lib/adapters/react.mjs new file mode 100644 index 0000000..ca7bda1 --- /dev/null +++ b/packages/mizan-django/generate/generator/lib/adapters/react.mjs @@ -0,0 +1,160 @@ +/** + * React Stage 2 — Generates hooks + context providers from Stage 1 output. + */ + +function pascalCase(str) { + return str.split(/[.\-_]/).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('') +} + +export function generateReactAdapter(schema) { + const functions = schema['x-mizan-functions'] || [] + const contextGroups = schema['x-mizan-contexts'] || {} + const namedContexts = Object.entries(contextGroups).filter(([n]) => n !== 'global') + const globalContexts = functions.filter(fn => fn.isContext === 'global') + const mutations = functions.filter(fn => !fn.isContext && !fn.isForm && fn.affects) + const plainFns = functions.filter(fn => !fn.isContext && !fn.isForm && !fn.affects) + + const lines = [ + "'use client'", + '', + '// AUTO-GENERATED by mizan — do not edit', + '', + "import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'", + "import { registerContext, mizanCall, mizanFetch } from '@mizan/runtime'", + '', + ] + + // Import from Stage 1 + const stage1Imports = [] + for (const [ctxName] of Object.entries(contextGroups)) { + const p = pascalCase(ctxName) + stage1Imports.push(`fetch${p}Context`, `type ${p}ContextData`, `type ${p}ContextParams`) + } + for (const fn of [...mutations, ...plainFns]) { + stage1Imports.push(`call${pascalCase(fn.camelName)}`) + } + if (stage1Imports.length > 0) { + lines.push(`import { ${stage1Imports.join(', ')} } from '../index'`) + lines.push('') + } + + // ── Global context hooks ──────────────────────────────────────────── + + if (globalContexts.length > 0) { + const p = pascalCase('global') + + lines.push(`// Global context — fetched once at app init`) + lines.push(`const GlobalCtx = createContext<${p}ContextData | null>(null)`) + lines.push('') + + lines.push(`export function GlobalContextProvider({ children }: { children: ReactNode }) {`) + lines.push(` const [data, setData] = useState<${p}ContextData | null>(() => {`) + lines.push(` if (typeof window === 'undefined') return null`) + lines.push(` const ssr = (window as any).__MIZAN_SSR_DATA__`) + lines.push(` return ssr ?? null`) + lines.push(` })`) + lines.push('') + lines.push(` const refetch = useCallback(async () => {`) + lines.push(` const result = await fetch${p}Context({} as any)`) + lines.push(` setData(result)`) + lines.push(` }, [])`) + lines.push('') + lines.push(` useEffect(() => { if (!data) refetch() }, [data, refetch])`) + lines.push(` useEffect(() => registerContext('global', {}, refetch), [refetch])`) + lines.push('') + lines.push(` return {children}`) + lines.push('}') + lines.push('') + + for (const fn of globalContexts) { + const hookPascal = pascalCase(fn.camelName) + lines.push(`export function use${hookPascal}(): ${fn.outputType} {`) + lines.push(` const ctx = useContext(GlobalCtx)`) + lines.push(` if (!ctx) throw new Error('use${hookPascal} requires GlobalContextProvider')`) + lines.push(` return ctx.${fn.name}`) + lines.push('}') + lines.push('') + } + } + + // ── Named context providers ───────────────────────────────────────── + + for (const [ctxName, ctxMeta] of namedContexts) { + const p = pascalCase(ctxName) + const ctxFunctions = functions.filter(fn => fn.isContext === ctxName) + const paramEntries = Object.entries(ctxMeta.params || {}) + + lines.push(`// ${p} context`) + lines.push(`const ${p}Ctx = createContext<${p}ContextData | null>(null)`) + lines.push('') + + // Provider + lines.push(`export function ${p}Context({ children, ...params }: ${p}ContextParams & { children: ReactNode }) {`) + lines.push(` const [data, setData] = useState<${p}ContextData | null>(() => {`) + lines.push(` if (typeof window === 'undefined') return null`) + lines.push(` const ssr = (window as any).__MIZAN_SSR_DATA__`) + if (ctxFunctions.length > 0) { + lines.push(` if (ssr?.${ctxFunctions[0].name} !== undefined) return ssr`) + } + lines.push(` return null`) + lines.push(` })`) + lines.push('') + lines.push(` const refetch = useCallback(async () => {`) + lines.push(` const result = await fetch${p}Context(params)`) + lines.push(` setData(result)`) + + const deps = paramEntries.map(([pName]) => `params.${pName}`) + lines.push(` }, [${deps.join(', ')}])`) + lines.push('') + lines.push(` useEffect(() => { refetch() }, [refetch])`) + lines.push(` useEffect(() => registerContext('${ctxName}', params, refetch), [${deps.join(', ')}, refetch])`) + lines.push('') + lines.push(` return <${p}Ctx.Provider value={data}>{children}`) + lines.push('}') + lines.push('') + + // Hooks + for (const fn of ctxFunctions) { + const hookPascal = pascalCase(fn.camelName) + lines.push(`export function use${hookPascal}(): ${fn.outputType} | null {`) + lines.push(` const ctx = useContext(${p}Ctx)`) + lines.push(` return ctx?.${fn.name} ?? null`) + lines.push('}') + lines.push('') + } + } + + // ── Mutation hooks ────────────────────────────────────────────────── + + for (const fn of mutations) { + const p = pascalCase(fn.camelName) + if (fn.hasInput) { + lines.push(`export function use${p}() {`) + lines.push(` return useCallback((args: Parameters[0]) => call${p}(args), [])`) + lines.push('}') + } else { + lines.push(`export function use${p}() {`) + lines.push(` return useCallback(() => call${p}(), [])`) + lines.push('}') + } + lines.push('') + } + + // ── Plain function hooks ──────────────────────────────────────────── + + for (const fn of plainFns) { + const p = pascalCase(fn.camelName) + if (fn.hasInput) { + lines.push(`export function use${p}() {`) + lines.push(` return useCallback((args: Parameters[0]) => call${p}(args), [])`) + lines.push('}') + } else { + lines.push(`export function use${p}() {`) + lines.push(` return useCallback(() => call${p}(), [])`) + lines.push('}') + } + lines.push('') + } + + return lines.join('\n') +} diff --git a/packages/mizan-django/generate/generator/lib/adapters/svelte.mjs b/packages/mizan-django/generate/generator/lib/adapters/svelte.mjs new file mode 100644 index 0000000..9269865 --- /dev/null +++ b/packages/mizan-django/generate/generator/lib/adapters/svelte.mjs @@ -0,0 +1,97 @@ +/** + * Svelte Stage 2 — Generates stores from Stage 1 output. + */ + +function pascalCase(str) { + return str.split(/[.\-_]/).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('') +} + +export function generateSvelteAdapter(schema) { + const functions = schema['x-mizan-functions'] || [] + const contextGroups = schema['x-mizan-contexts'] || {} + const mutations = functions.filter(fn => !fn.isContext && !fn.isForm && fn.affects) + const plainFns = functions.filter(fn => !fn.isContext && !fn.isForm && !fn.affects) + + const lines = [ + '// AUTO-GENERATED by mizan — do not edit', + '', + "import { writable, derived, type Readable } from 'svelte/store'", + "import { registerContext } from '@mizan/runtime'", + '', + ] + + // Stage 1 imports + const stage1Imports = [] + for (const [ctxName] of Object.entries(contextGroups)) { + const p = pascalCase(ctxName) + stage1Imports.push(`fetch${p}Context`, `type ${p}ContextData`, `type ${p}ContextParams`) + } + for (const fn of [...mutations, ...plainFns]) { + stage1Imports.push(`call${pascalCase(fn.camelName)}`) + } + if (stage1Imports.length > 0) { + lines.push(`import { ${stage1Imports.join(', ')} } from '../index'`) + lines.push('') + } + + // ── Context stores ────────────────────────────────────────────────── + + for (const [ctxName, ctxMeta] of Object.entries(contextGroups)) { + const p = pascalCase(ctxName) + const ctxFunctions = functions.filter(fn => fn.isContext === ctxName) + const paramEntries = Object.entries(ctxMeta.params || {}) + + lines.push(`// ${p} context`) + + if (paramEntries.length > 0) { + lines.push(`export function create${p}Context(params: ${p}ContextParams) {`) + } else { + lines.push(`export function create${p}Context() {`) + } + + lines.push(` const data = writable<${p}ContextData | null>(null)`) + lines.push(` const loading = writable(true)`) + lines.push('') + lines.push(` const refetch = async () => {`) + lines.push(` loading.set(true)`) + if (paramEntries.length > 0) { + lines.push(` const result = await fetch${p}Context(params)`) + } else { + lines.push(` const result = await fetch${p}Context({} as any)`) + } + lines.push(` data.set(result)`) + lines.push(` loading.set(false)`) + lines.push(` }`) + lines.push('') + lines.push(` refetch()`) + if (paramEntries.length > 0) { + lines.push(` const unregister = registerContext('${ctxName}', params, refetch)`) + } else { + lines.push(` const unregister = registerContext('${ctxName}', {}, refetch)`) + } + lines.push('') + + // Derived stores for each function + lines.push(` return {`) + lines.push(` data,`) + lines.push(` loading,`) + for (const fn of ctxFunctions) { + const camel = fn.camelName + lines.push(` ${camel}: derived(data, $d => $d?.${fn.name} ?? null) as Readable<${fn.outputType} | null>,`) + } + lines.push(` destroy: unregister,`) + lines.push(` }`) + lines.push('}') + lines.push('') + } + + // ── Mutation + function exports ───────────────────────────────────── + + for (const fn of [...mutations, ...plainFns]) { + const p = pascalCase(fn.camelName) + lines.push(`export { call${p} } from '../${fn.affects ? 'mutations' : 'functions'}/${fn.camelName}'`) + } + lines.push('') + + return lines.join('\n') +} diff --git a/packages/mizan-django/generate/generator/lib/adapters/vue.mjs b/packages/mizan-django/generate/generator/lib/adapters/vue.mjs new file mode 100644 index 0000000..853e949 --- /dev/null +++ b/packages/mizan-django/generate/generator/lib/adapters/vue.mjs @@ -0,0 +1,105 @@ +/** + * Vue Stage 2 — Generates composables from Stage 1 output. + */ + +function pascalCase(str) { + return str.split(/[.\-_]/).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('') +} + +export function generateVueAdapter(schema) { + const functions = schema['x-mizan-functions'] || [] + const contextGroups = schema['x-mizan-contexts'] || {} + const mutations = functions.filter(fn => !fn.isContext && !fn.isForm && fn.affects) + const plainFns = functions.filter(fn => !fn.isContext && !fn.isForm && !fn.affects) + + const lines = [ + '// AUTO-GENERATED by mizan — do not edit', + '', + "import { ref, computed, watch, onMounted, onUnmounted, provide, inject, type Ref, type ComputedRef, type InjectionKey } from 'vue'", + "import { registerContext } from '@mizan/runtime'", + '', + ] + + // Stage 1 imports + const stage1Imports = [] + for (const [ctxName] of Object.entries(contextGroups)) { + const p = pascalCase(ctxName) + stage1Imports.push(`fetch${p}Context`, `type ${p}ContextData`, `type ${p}ContextParams`) + } + for (const fn of [...mutations, ...plainFns]) { + stage1Imports.push(`call${pascalCase(fn.camelName)}`) + } + if (stage1Imports.length > 0) { + lines.push(`import { ${stage1Imports.join(', ')} } from '../index'`) + lines.push('') + } + + // ── Context composables ───────────────────────────────────────────── + + for (const [ctxName, ctxMeta] of Object.entries(contextGroups)) { + const p = pascalCase(ctxName) + const ctxFunctions = functions.filter(fn => fn.isContext === ctxName) + const paramEntries = Object.entries(ctxMeta.params || {}) + + lines.push(`// ${p} context`) + lines.push(`const ${p}Key: InjectionKey<{ data: Ref<${p}ContextData | null>, loading: Ref }> = Symbol('${ctxName}')`) + lines.push('') + + // Provider composable + if (paramEntries.length > 0) { + lines.push(`export function provide${p}Context(params: { ${paramEntries.map(([k, v]) => `${k}: ${v.type === 'integer' || v.type === 'number' ? 'number' : 'string'}`).join(', ')} }) {`) + } else { + lines.push(`export function provide${p}Context() {`) + } + lines.push(` const data = ref<${p}ContextData | null>(null)`) + lines.push(` const loading = ref(true)`) + lines.push('') + lines.push(` const refetch = async () => {`) + lines.push(` loading.value = true`) + lines.push(` try {`) + if (paramEntries.length > 0) { + lines.push(` data.value = await fetch${p}Context(params as any)`) + } else { + lines.push(` data.value = await fetch${p}Context({} as any)`) + } + lines.push(` } catch (e) { console.error('[mizan] ${ctxName} fetch failed:', e) }`) + lines.push(` loading.value = false`) + lines.push(` }`) + lines.push('') + lines.push(` let unregister: (() => void) | null = null`) + lines.push(` onMounted(() => {`) + lines.push(` refetch()`) + if (paramEntries.length > 0) { + lines.push(` unregister = registerContext('${ctxName}', params, refetch)`) + } else { + lines.push(` unregister = registerContext('${ctxName}', {}, refetch)`) + } + lines.push(` })`) + lines.push(` onUnmounted(() => { unregister?.() })`) + lines.push('') + lines.push(` provide(${p}Key, { data, loading })`) + lines.push('}') + lines.push('') + + // Consumer composables + for (const fn of ctxFunctions) { + const hookPascal = pascalCase(fn.camelName) + lines.push(`export function use${hookPascal}(): ComputedRef<${fn.outputType} | null> {`) + lines.push(` const ctx = inject(${p}Key)`) + lines.push(` if (!ctx) throw new Error('use${hookPascal} requires provide${p}Context in a parent')`) + lines.push(` return computed(() => ctx.data.value?.${fn.name} ?? null)`) + lines.push('}') + lines.push('') + } + } + + // ── Mutation composables ──────────────────────────────────────────── + + for (const fn of [...mutations, ...plainFns]) { + const p = pascalCase(fn.camelName) + lines.push(`export const use${p} = call${p}`) + lines.push('') + } + + return lines.join('\n') +} diff --git a/packages/mizan-django/generate/generator/lib/stage1.mjs b/packages/mizan-django/generate/generator/lib/stage1.mjs new file mode 100644 index 0000000..588246c --- /dev/null +++ b/packages/mizan-django/generate/generator/lib/stage1.mjs @@ -0,0 +1,198 @@ +/** + * Stage 1 Codegen — Framework-agnostic TypeScript output. + * + * Produces: + * types.ts — interfaces from OpenAPI schema + * contexts/.ts — fetchXxxContext(params) per context group + * mutations/.ts — callXxx(args) per mutation + * functions/.ts — callXxx(args) per plain function + * index.ts — re-exports + */ + +import openapiTS, { astToString } from 'openapi-typescript' + +// ─── Helpers ──────────────────────────────────────────────────────────────── + +function pascalCase(str) { + return str + .split(/[.\-_]/) + .map(part => part.charAt(0).toUpperCase() + part.slice(1)) + .join('') +} + +function camelCase(str) { + const p = pascalCase(str) + return p.charAt(0).toLowerCase() + p.slice(1) +} + +// TypeScript SyntaxKind values for openapi-typescript AST +const SyntaxKind = { + InterfaceDeclaration: 265, + PropertySignature: 172, + Identifier: 80, +} + +function idName(node) { + return node?.kind === SyntaxKind.Identifier ? node.escapedText : undefined +} + +function getSchemaNamesFromAst(ast) { + if (!Array.isArray(ast)) return [] + const componentsNode = ast.find( + n => n?.kind === SyntaxKind.InterfaceDeclaration && idName(n?.name) === 'components' + ) + if (!componentsNode?.members) return [] + const schemasProp = componentsNode.members.find( + m => m?.kind === SyntaxKind.PropertySignature && idName(m?.name) === 'schemas' && Array.isArray(m?.type?.members) + ) + if (!schemasProp) return [] + return schemasProp.type.members + .map(m => m?.kind === SyntaxKind.PropertySignature ? idName(m.name) : undefined) + .filter(n => typeof n === 'string') +} + +// ─── Types ────────────────────────────────────────────────────────────────── + +export async function generateTypes(schema) { + const ast = await openapiTS(schema) + const schemaNames = getSchemaNamesFromAst(ast) + const typesCode = astToString(ast) + + const lines = [ + '// AUTO-GENERATED by mizan — do not edit', + '', + typesCode, + '', + '// Convenience type exports', + ...schemaNames.map(name => `export type ${name} = components["schemas"]["${name}"]`), + '', + ] + + return lines.join('\n') +} + +// ─── Context Files ────────────────────────────────────────────────────────── + +export function generateContextFile(ctxName, ctxMeta, functions) { + const pascal = pascalCase(ctxName) + const ctxFunctions = functions.filter(fn => fn.isContext === ctxName) + + const lines = [ + '// AUTO-GENERATED by mizan — do not edit', + '', + "import { mizanFetch } from '@mizan/runtime'", + '', + ] + + // Import output types + const typeImports = ctxFunctions.map(fn => fn.outputType).filter(Boolean) + if (typeImports.length > 0) { + lines.push(`import type { ${[...new Set(typeImports)].join(', ')} } from '../types'`) + lines.push('') + } + + // Data interface + lines.push(`export interface ${pascal}ContextData {`) + for (const fn of ctxFunctions) { + lines.push(` ${fn.name}: ${fn.outputType}`) + } + lines.push('}') + lines.push('') + + // Params interface (from x-mizan-contexts) + const params = ctxMeta?.params || {} + const paramEntries = Object.entries(params) + + if (paramEntries.length > 0) { + lines.push(`export interface ${pascal}ContextParams {`) + for (const [pName, pMeta] of paramEntries) { + const tsType = pMeta.type === 'integer' || pMeta.type === 'number' ? 'number' : pMeta.type === 'boolean' ? 'boolean' : 'string' + const optional = pMeta.required ? '' : '?' + lines.push(` ${pName}${optional}: ${tsType}`) + } + lines.push('}') + } else { + lines.push(`export type ${pascal}ContextParams = Record`) + } + lines.push('') + + // Fetch function + lines.push(`export function fetch${pascal}Context(params: ${pascal}ContextParams): Promise<${pascal}ContextData> {`) + lines.push(` return mizanFetch('${ctxName}', params)`) + lines.push('}') + lines.push('') + + return lines.join('\n') +} + +// ─── Mutation Files ───────────────────────────────────────────────────────── + +export function generateMutationFile(fn) { + const pascal = pascalCase(fn.camelName) + + const lines = [ + '// AUTO-GENERATED by mizan — do not edit', + '', + "import { mizanCall } from '@mizan/runtime'", + '', + ] + + // Import types + const typeImports = [] + if (fn.hasInput && fn.inputType) typeImports.push(fn.inputType) + if (fn.outputType) typeImports.push(fn.outputType) + if (typeImports.length > 0) { + lines.push(`import type { ${[...new Set(typeImports)].join(', ')} } from '../types'`) + lines.push('') + } + + // Call function + if (fn.hasInput) { + lines.push(`export function call${pascal}(args: ${fn.inputType}): Promise<${fn.outputType}> {`) + } else { + lines.push(`export function call${pascal}(): Promise<${fn.outputType}> {`) + } + lines.push(` return mizanCall('${fn.name}', ${fn.hasInput ? 'args' : '{}'})`) + lines.push('}') + lines.push('') + + return lines.join('\n') +} + +// ─── Function Files (plain, no context, no affects) ───────────────────────── + +export function generateFunctionFile(fn) { + // Same shape as mutation, just different semantics + return generateMutationFile(fn) +} + +// ─── Index ────────────────────────────────────────────────────────────────── + +export function generateStage1Index(schema) { + const functions = schema['x-mizan-functions'] || [] + const contextGroups = schema['x-mizan-contexts'] || {} + + const lines = [ + '// AUTO-GENERATED by mizan — do not edit', + '', + "export * from './types'", + '', + ] + + // Context exports + for (const ctxName of Object.keys(contextGroups)) { + const pascal = pascalCase(ctxName) + lines.push(`export { fetch${pascal}Context, type ${pascal}ContextData, type ${pascal}ContextParams } from './contexts/${ctxName}'`) + } + if (Object.keys(contextGroups).length > 0) lines.push('') + + // Mutation + function exports + const regularFns = functions.filter(fn => !fn.isContext && !fn.isForm) + for (const fn of regularFns) { + const pascal = pascalCase(fn.camelName) + lines.push(`export { call${pascal} } from './${fn.affects ? 'mutations' : 'functions'}/${fn.camelName}'`) + } + if (regularFns.length > 0) lines.push('') + + return lines.join('\n') +} diff --git a/packages/mizan-runtime/package.json b/packages/mizan-runtime/package.json new file mode 100644 index 0000000..30b5cd9 --- /dev/null +++ b/packages/mizan-runtime/package.json @@ -0,0 +1,11 @@ +{ + "name": "@mizan/runtime", + "version": "0.1.0", + "description": "Mizan client runtime — context registry, invalidation, fetch. Zero framework dependencies.", + "type": "module", + "main": "src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "license": "MIT" +} diff --git a/packages/mizan-runtime/src/index.ts b/packages/mizan-runtime/src/index.ts new file mode 100644 index 0000000..7fdf99d --- /dev/null +++ b/packages/mizan-runtime/src/index.ts @@ -0,0 +1,204 @@ +/** + * @mizan/runtime — The client state kernel. + * + * Zero framework dependencies. React, Vue, Svelte — all import from here. + * + * Four concerns: + * 1. Configuration — baseUrl, auth headers, CSRF + * 2. Context registry — mounted providers register for invalidation + * 3. Invalidation — microtask-batched, scoped or broad + * 4. Fetch — mizanFetch (GET context bundles) + mizanCall (POST mutations) + */ + +// === Error === + +export class MizanError extends Error { + constructor(public status: number, public body: string) { + super(`Mizan call failed (${status})`) + } +} + +// === Configuration === + +interface MizanConfig { + baseUrl: string + getHeaders: () => Record | Promise> + csrfCookieName: string + csrfHeaderName: string +} + +const config: MizanConfig = { + baseUrl: '/api/mizan', + getHeaders: () => ({}), + csrfCookieName: 'csrftoken', + csrfHeaderName: 'X-CSRFToken', +} + +export function configure(opts: Partial): void { + Object.assign(config, opts) +} + +export function getConfig(): Readonly { + return config +} + +// === CSRF === + +function getCSRFToken(): string | null { + if (typeof document === 'undefined') return null + const match = document.cookie.match(new RegExp(`${config.csrfCookieName}=([^;]+)`)) + return match?.[1] ?? null +} + +// === Session Init === + +let _sessionReady: Promise | null = null + +/** + * Initialize a session (fetches CSRF cookie from GET /session/). + * Called automatically on first fetch if not called explicitly. + * No-op if a CSRF cookie already exists. + */ +export function initSession(): Promise { + if (_sessionReady) return _sessionReady + + _sessionReady = (async () => { + // If we already have a CSRF token, skip + if (getCSRFToken()) return + + try { + await fetch(`${config.baseUrl}/session/`, { credentials: 'include' }) + } catch (e) { + console.error('[mizan] Session init failed:', e) + } + })() + + return _sessionReady +} + +// === Context Registry === + +export type RefetchFn = () => void +type ParamKey = string + +interface ContextEntry { + params: Record + refetch: RefetchFn +} + +const contexts: Map> = new Map() + +export function registerContext( + name: string, + params: Record, + refetch: RefetchFn, +): () => void { + if (!contexts.has(name)) contexts.set(name, new Map()) + const key = JSON.stringify(params) + contexts.get(name)!.set(key, { params, refetch }) + return () => contexts.get(name)!.delete(key) +} + +// === Invalidation === + +const pending: Set = new Set() +const pendingScoped: Map> = new Map() +let scheduled = false + +export function invalidate(context: string, params?: Record): void { + if (params) { + pendingScoped.set(context, params) + } else { + pending.add(context) + } + if (!scheduled) { + scheduled = true + queueMicrotask(flush) + } +} + +function flush(): void { + for (const name of pending) { + const entries = contexts.get(name) + if (entries) entries.forEach(entry => entry.refetch()) + } + + for (const [name, params] of pendingScoped) { + if (pending.has(name)) continue + const entries = contexts.get(name) + if (!entries) continue + const key = JSON.stringify(params) + const entry = entries.get(key) + if (entry) entry.refetch() + } + + pending.clear() + pendingScoped.clear() + scheduled = false +} + +// === Fetch === + +async function resolveHeaders(): Promise> { + await initSession() + + const custom = await config.getHeaders() + const csrf = getCSRFToken() + + return { + ...custom, + ...(csrf ? { [config.csrfHeaderName]: csrf } : {}), + 'Accept': 'application/json', + } +} + +export async function mizanFetch( + contextName: string, + params?: Record, +): Promise { + const url = new URL( + `${config.baseUrl}/ctx/${contextName}/`, + typeof globalThis.location !== 'undefined' ? globalThis.location.origin : 'http://localhost', + ) + if (params) { + for (const [k, v] of Object.entries(params)) { + url.searchParams.set(k, String(v)) + } + } + + const headers = await resolveHeaders() + const res = await fetch(url.toString(), { headers, credentials: 'same-origin' }) + if (!res.ok) throw new MizanError(res.status, await res.text()) + return res.json() +} + +export async function mizanCall( + functionName: string, + args: Record, +): Promise { + const headers = await resolveHeaders() + headers['Content-Type'] = 'application/json' + + const res = await fetch(`${config.baseUrl}/call/`, { + method: 'POST', + headers, + credentials: 'same-origin', + body: JSON.stringify({ fn: functionName, args }), + }) + if (!res.ok) throw new MizanError(res.status, await res.text()) + + const data = await res.json() + + // Server-driven invalidation + if (data.invalidate) { + for (const entry of data.invalidate) { + if (typeof entry === 'string') { + invalidate(entry) + } else { + invalidate(entry.context, entry.params) + } + } + } + + return data.result +} diff --git a/packages/mizan-svelte/package.json b/packages/mizan-svelte/package.json new file mode 100644 index 0000000..478b410 --- /dev/null +++ b/packages/mizan-svelte/package.json @@ -0,0 +1,11 @@ +{ + "name": "@mizan/svelte", + "version": "0.1.0", + "description": "Mizan Svelte adapter — stores generated from @mizan/runtime.", + "type": "module", + "peerDependencies": { + "svelte": ">=4", + "@mizan/runtime": ">=0.1.0" + }, + "license": "MIT" +} diff --git a/packages/mizan-vue/package.json b/packages/mizan-vue/package.json new file mode 100644 index 0000000..acfae61 --- /dev/null +++ b/packages/mizan-vue/package.json @@ -0,0 +1,11 @@ +{ + "name": "@mizan/vue", + "version": "0.1.0", + "description": "Mizan Vue adapter — composables generated from @mizan/runtime.", + "type": "module", + "peerDependencies": { + "vue": ">=3", + "@mizan/runtime": ">=0.1.0" + }, + "license": "MIT" +}