import { SchemaBase } from './dsl.js'; import { FrontAppSchema, ProjectApiSchema } from './project.js'; import type { DtoMessage } from './dto.js'; /** A backend RPC controller. Strong constraints: * - a backend module maps 1:1 to a frontend app (they are peers); * - a controller serves exactly one frontend app — no cross-module calls. */ export interface ControllerSchema extends SchemaBase { type: 'controller'; /** The backend api module this controller belongs to (shared instance from * project.config.ts apis). Controllers are always backend-side, so storage * is controller_schema/{api.name}/{app.name}/controller/. */ api: ProjectApiSchema; /** The frontend app this controller serves (shared instance from project.config). */ app: FrontAppSchema; /** RPC methods, keyed by method name (key === method.name, enforced by the builder). */ methods: Record; } export function defineController(options: { name: string; api: ProjectApiSchema; app: FrontAppSchema; /** Method declarations: type/schema/name are injected by this builder. */ methods: Record>; description?: string; }): ControllerSchema { if (!options.api.apps.includes(options.app)) { throw new Error(`controller ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`); } const schema: ControllerSchema = { type: 'controller', name: options.name, description: options.description, api: options.api, app: options.app, methods: {}, }; for (const [key, method] of Object.entries(options.methods)) { schema.methods[key] = { type: 'method', schema, name: key, ...method }; } return schema; } /** An RPC method exposed by a controller. Carries the shared API call * signature: one request DTO in, one response shape out. Referenced by * frontend page actions — both sides use the exact same instance, so * drift is impossible. */ export interface ControllerMethodSchema extends SchemaBase { type: 'method'; schema: ControllerSchema; args: DtoMessage; results: DtoMessage | number | boolean | string; }