import { z } from "zod";
import type { BlockMdxConfig } from "../types.js";
/**
* Pure (React-free) part of the PLAN-SPECIFIC `data-model` block: its data schema
* and MDX round-trip config. Shared by the server MDX adapter (`plan-mdx.ts` via
* `plan-block-registry.ts`) and the client spec (`planBlocks.tsx`). Keeping this
* React-free means importing it into a server module never pulls React into the
* Nitro/SSR bundle.
*
* The block renders a dbdiagram / Prisma-style entity-relationship diagram: a set
* of entity cards (each a small table of fields with PK / FK / nullable flags)
* plus optional explicit relations. The Read renderer makes foreign keys
* interactive — hovering / clicking an FK highlights and scrolls to the
* referenced entity — which is why this is a custom block, not a plain table.
*
* The schema MUST stay data-compatible with the `data-model` branch of
* `planBlockSchema` (`plan-content.ts`), and the MDX `tag` (`DataModel`) +
* attribute shape MUST match the inline planBlockSchema member so stored `.mdx`
* round-trips: the whole `entities` and `relations` arrays are JSON props
* (``).
*/
/** Cardinality of a relation between two entities. */
export type DataModelRelationKind = "1-1" | "1-n" | "n-n";
export const DATA_MODEL_RELATION_KINDS: DataModelRelationKind[] = [
"1-1",
"1-n",
"n-n",
];
/**
* Diff status of an entity or field, used to render before/after change chips
* on a data model. Shares the SAME vocabulary as the `file-tree` block's
* `FileTreeChange` so change chips look consistent across dev-doc blocks.
*/
export type DataModelChange = "added" | "modified" | "removed" | "renamed";
export const DATA_MODEL_CHANGES: DataModelChange[] = [
"added",
"modified",
"removed",
"renamed",
];
/** One column of an entity. `fk` is a string like `"User.id"`. */
export interface DataModelField {
name: string;
type?: string;
pk?: boolean;
/** Foreign-key target, e.g. `"User.id"` (entity name/id + optional field). */
fk?: string;
nullable?: boolean;
default?: string;
note?: string;
/** Diff status of this field, driving its change chip. */
change?: DataModelChange;
/** Prior value when `change === "modified"` (e.g. the old column type). */
was?: string;
}
/** One table / model. `id` is referenced by relations (`from`/`to`). */
export interface DataModelEntity {
id: string;
name: string;
note?: string;
/** Diff status of the whole table (added / removed / renamed / modified). */
change?: DataModelChange;
fields: DataModelField[];
}
/** An explicit relation between two entities, by `id` (or `name`). */
export interface DataModelRelation {
from: string;
to: string;
kind?: DataModelRelationKind;
label?: string;
}
export interface DataModelData {
entities: DataModelEntity[];
relations?: DataModelRelation[];
}
/** Diff-status enum, shared with the inline `data-model` schema's `change`. */
const changeSchema = z.enum(["added", "modified", "removed", "renamed"]);
const fieldSchema = z.object({
name: z.string().trim().min(1).max(160),
type: z.string().trim().max(120).optional(),
pk: z.boolean().optional(),
fk: z.string().trim().max(200).optional(),
nullable: z.boolean().optional(),
default: z.string().trim().max(400).optional(),
note: z.string().trim().max(600).optional(),
change: changeSchema.optional(),
was: z.string().trim().max(400).optional(),
}) as z.ZodType;
const entitySchema = z.object({
id: z.string().trim().min(1).max(120),
name: z.string().trim().min(1).max(160),
note: z.string().trim().max(600).optional(),
change: changeSchema.optional(),
fields: z.array(fieldSchema).max(80),
}) as z.ZodType;
const relationSchema = z.object({
from: z.string().trim().min(1).max(120),
to: z.string().trim().min(1).max(120),
kind: z.enum(["1-1", "1-n", "n-n"]).optional(),
label: z.string().trim().max(160).optional(),
}) as z.ZodType;
/**
* Data-compatible with the inline `data-model` member of `planBlockSchema`
* (`plan-content.ts`). At least one entity is required; `relations` is optional
* (the Read renderer can infer simple `1-n` relations from `fk` fields when it is
* omitted) so a fresh model validates from a single entity with a couple fields.
*/
export const dataModelSchema = z.object({
entities: z.array(entitySchema).min(1).max(60),
relations: z.array(relationSchema).max(200).optional(),
}) as unknown as z.ZodType;
/**
* MDX config: the whole `entities` and `relations` arrays are serialized as JSON
* props on a self-closing element — the `` form. `toAttrs` emits `entities` then `relations` in a STABLE
* order (the shared `prop()` encoder drops `relations` when it is undefined).
* Because `entities` is one JSON prop, each entity's `change` and each field's
* `change` / `was` diff attributes ride along inside it and round-trip verbatim
* (export → import) with no per-attribute handling, mirroring how `file-tree`
* encodes its per-entry `change` inside the `entries` JSON prop.
*
* `fromAttrs` tolerates missing/partial attributes for backward-compat: a missing
* `entities` decodes to `[]` and a missing `relations` decodes to `undefined` so
* a plan written before this block existed still parses.
*/
export const dataModelMdx: BlockMdxConfig = {
tag: "DataModel",
toAttrs: (data) => ({
entities: data.entities,
relations: data.relations,
}),
fromAttrs: (attrs) => ({
entities: attrs.array("entities") ?? [],
relations: attrs.array("relations"),
}),
};