// Centralised Zod schemas for API mutation routes.
//
// Pattern (in route handler):
//   const parsed = SomeSchema.safeParse(await req.json().catch(() => ({})))
//   if (!parsed.success) return apiError.invalidBody(parsed.error.issues)
//   const body = parsed.data
//
// Why centralise:
//   - One source of truth for shape + constraints (no drift between
//     routes that share the same body shape).
//   - Enables ADR-traceable change history when a contract evolves.
//   - Lets us derive TypeScript types via `z.infer<typeof Schema>`
//     instead of duplicate hand-written interfaces.

import { z } from 'zod'

// ── Common primitives ──────────────────────────────────────────────────

export const uuidSchema = z.string().uuid()
export const emailSchema = z.string().email().max(254)
export const slugSchema = z.string().regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/).min(2).max(64)

// ── User admin routes ─────────────────────────────────────────────────

export const UserIdBody = z.object({
  userId: uuidSchema,
})
export type UserIdBody = z.infer<typeof UserIdBody>

export const CreateUserBody = z.object({
  first_name: z.string().min(1).max(80),
  last_name:  z.string().min(1).max(80),
  email:      emailSchema,
  role_id:    uuidSchema,
  display_name: z.string().min(1).max(160).optional(),
  department_id: uuidSchema.optional(),
})
export type CreateUserBody = z.infer<typeof CreateUserBody>

export const UpdateUserEmailBody = z.object({
  userId: uuidSchema,
  email:  emailSchema,
})
export type UpdateUserEmailBody = z.infer<typeof UpdateUserEmailBody>

export const NotifyUserBody = z.object({
  template: z.enum([
    'account_created',
    'password_changed',
    'email_changed',
    'account_deactivated',
    'password_reset',
  ]),
  email: emailSchema,
  templateData: z.record(z.string(), z.string()).optional(),
})
export type NotifyUserBody = z.infer<typeof NotifyUserBody>

// ── Email queue admin ─────────────────────────────────────────────────

export const RetryEmailBody = z.object({
  id: uuidSchema,
})
export type RetryEmailBody = z.infer<typeof RetryEmailBody>

// ── v1/projects ───────────────────────────────────────────────────────

export const CreateProjectBody = z.object({
  project_code: z.string().min(1).max(40).optional(),
  user_id: uuidSchema.optional(),
  supplier_id: uuidSchema.optional(),
  supplier_name: z.string().min(1).max(160),
  responsibility_id: uuidSchema.nullable().optional(),
  project_lead_id: uuidSchema.nullable().optional(),
  status_id: uuidSchema.nullable().optional(),
  vehicle_project: z.string().max(160).nullable().optional(),
  product_line: z.string().max(160).nullable().optional(),
  delivery_scope: z.string().max(400).nullable().optional(),
  kpi_board_de: z.string().max(80).nullable().optional(),
  kpi_board_international: z.string().max(80).nullable().optional(),
  kifag_area_id: uuidSchema.nullable().optional(),
  qmt_value_id: uuidSchema.nullable().optional(),
  qmt_free_text: z.string().max(160).nullable().optional(),
  einkauf_value_id: uuidSchema.nullable().optional(),
  einkauf_free_text: z.string().max(160).nullable().optional(),
  cost_engineering_value_id: uuidSchema.nullable().optional(),
  cost_engineering_free_text: z.string().max(160).nullable().optional(),
  visit_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
  order_received_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
  expected_end_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
})
export type CreateProjectBody = z.infer<typeof CreateProjectBody>

// ── v1/suppliers ──────────────────────────────────────────────────────

export const CreateSupplierBody = z.object({
  supplier_number: z.string().min(1).max(40),
  supplier_name: z.string().min(1).max(160),
  supplier_location: z.string().max(160).optional(),
  plant: z.string().max(160).optional(),
  country: z.string().max(8).optional(),
  city: z.string().max(120).optional(),
  is_active: z.boolean().optional(),
})
export type CreateSupplierBody = z.infer<typeof CreateSupplierBody>

// ── Owner tenants ─────────────────────────────────────────────────────

export const CreateTenantBody = z.object({
  slug: slugSchema,
  company_name: z.string().min(2).max(160),
  contact_email: emailSchema,
  plan_id: uuidSchema.optional(),
  tenant_type: z.enum(['production', 'trial', 'demo', 'sandbox', 'internal']).optional(),
})
export type CreateTenantBody = z.infer<typeof CreateTenantBody>

export const UpdateTenantBody = CreateTenantBody.partial().extend({
  vercel_deployment_url: z.string().url().optional(),
  status: z.enum(['DRAFT','PROVISIONING','ACTIVE','SUSPENDED','ARCHIVED','PROVISIONING_FAILED']).optional(),
})
export type UpdateTenantBody = z.infer<typeof UpdateTenantBody>

export const InviteAdminBody = z.object({
  email: emailSchema.optional(),
  first_name: z.string().min(1).max(80).optional(),
  last_name:  z.string().min(1).max(80).optional(),
})
export type InviteAdminBody = z.infer<typeof InviteAdminBody>

// ── Planning ──────────────────────────────────────────────────────────

export const ConsultantPatchBody = z.object({
  action: z.enum([
    'approve','reject','deactivate','archive','reactivate',
    'mark_left_department','mark_left_company',
  ]).optional(),
  first_name: z.string().min(1).max(80).optional(),
  last_name:  z.string().min(1).max(80).optional(),
  display_name: z.string().min(1).max(160).optional(),
  team_code: z.string().min(1).max(20).optional(),
  is_active: z.boolean().optional(),
  capacity_hours_per_day: z.number().min(0).max(24).optional(),
})
export type ConsultantPatchBody = z.infer<typeof ConsultantPatchBody>

// ── If-Match header for optimistic concurrency ────────────────────────
// updated_at as ISO string; client sends `If-Match: <updated_at>` on UPDATE.
// Server confirms the row's updated_at has not advanced before applying.

export const IfMatchHeader = z.string()
  .regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
  .or(z.string().regex(/^"[^"]+"$/))  // ETag-like quoted form

// ── Wertstrom (VSM) — app/api/wertstrom/route.ts + [id]/route.ts ──────
// Wertstrom P0 (KAR-878): these routes used to do plain TypeScript `as`
// casts with zero runtime validation. Node/Connection shapes mirror
// VsmNode/VsmConnection (lib/vsm-types.ts) exactly, but are NOT derived
// from them mechanically — z.infer<> types are asserted structurally
// compatible in schemas.test.ts instead, since a hand-written zod schema
// reads far more clearly than one generated from a TS type.
//
// `.passthrough()` everywhere: later QVS phases keep extending the node/
// layout model (see lib/vsm-types.ts JSDoc), and an existing, already-saved
// Wertstrom must remain loadable/saveable through this validation — never
// reject a payload just because it carries a field this schema doesn't
// know about yet.

const vsmNodeTypeSchema = z.enum(['process', 'machine', 'inventory', 'transport', 'customer', 'supplier', 'timevalue'])
const vsmProcessTypeSchema = z.enum(['manual', 'robot', 'human_robot', 'machine'])
const vsmVaClassSchema = z.enum(['va', 'nnva', 'nva', 'unknown'])
const vsmFieldStatusSchema = z.enum(['imported', 'modified'])
// Wertstrom P2 (KAR-878/KAR-986) — see lib/vsm-types.ts ConnectionKind/
// InventoryKind/ValueProvenance/ScenarioKind for the "absence = prior
// semantics" discipline these mirror; all three are `.nullish()` below,
// same posture as vsmVaClassSchema/vsmFieldStatusSchema.
const vsmConnectionKindSchema = z.enum(['materialFlow', 'information'])
const vsmInventoryKindSchema = z.enum(['fifo', 'supermarket', 'push'])
const vsmProvenanceSchema = z.enum(['planned', 'measured', 'calculated', 'imported', 'assumed'])
// Wertstrom P8.2a (Baustein 3, KAR-878/KAR-986) — see lib/vsm-types.ts
// FlowControl for field docs. NOT the same axis as vsmInventoryKindSchema's
// own 'push' member above (that one is an INVENTORY node's storage
// sub-type; this is a CONNECTION's control mechanism) — same word, two
// distinct, both fachlich-legitimate VSM concepts.
const vsmFlowControlSchema = z.enum(['push', 'pull', 'kanban'])

// QAF-Import-Herkunft (schemas/value-stream-process-source.schema.json,
// VsmNodeQafSource in lib/vsm-types.ts) — same tolerant/passthrough posture.
const vsmQafSourceFieldSchema = z.object({
  cell: z.string().nullish(),
  original: z.union([z.string(), z.number(), z.null()]),
  rawText: z.string().nullish(),
  unit: z.string().nullish(),
  confidence: z.number().nullish(),
  mappedTo: z.string().nullish(),
}).passthrough()

const vsmQafSourceSchema = z.object({
  importId: z.string().min(1),
  sheet: z.string().nullish(),
  rowIndex: z.number(),
  positionNumber: z.string().nullish(),
  manufacturingStepId: z.string().nullish(),
  fields: z.record(z.string(), vsmQafSourceFieldSchema).optional(),
  sequenceConfidence: z.enum(['explicit', 'row_order', 'ambiguous']).nullish(),
  variantContext: z.object({
    shared: z.boolean(),
    variantKeys: z.array(z.string()),
  }).nullish(),
}).passthrough()

/**
 * VsmNode (lib/vsm-types.ts). Zeiten/Mengen >= 0, oee/scrapRate 0–100.
 * Numeric/boolean/string fields are `.nullish()` (null OR undefined OR
 * absent), not just `.optional()` — the editor never sends `null` itself,
 * but an existing saved Wertstrom or another future client might, and none
 * of these are meaningfully different from "not set" here.
 */
export const VsmNodeSchema = z.object({
  id: z.string().min(1),
  type: vsmNodeTypeSchema,
  x: z.number(),
  y: z.number(),
  name: z.string(),
  cycleTimeSec: z.number().min(0).nullish(),
  machineTimeSec: z.number().min(0).nullish(),
  manualTimeSec: z.number().min(0).nullish(),
  setupTimeSec: z.number().min(0).nullish(),
  waitTimeSec: z.number().min(0).nullish(),
  isValueAdded: z.boolean().nullish(),
  capacityPerHour: z.number().min(0).nullish(),
  notes: z.string().nullish(),
  oee: z.number().min(0).max(100).nullish(),
  quantity: z.number().min(0).nullish(),
  distance: z.number().min(0).nullish(),
  transportTimeSec: z.number().min(0).nullish(),
  demand: z.number().min(0).nullish(),
  numWorkers: z.number().min(0).nullish(),
  processType: vsmProcessTypeSchema.nullish(),
  machineType: z.string().nullish(),
  location: z.string().nullish(),
  currency: z.string().nullish(),
  partsPerCycle: z.number().min(0).nullish(),
  scrapRate: z.number().min(0).max(100).nullish(),
  scrapCostPerUnit: z.number().min(0).nullish(),
  machineHourRate: z.number().min(0).nullish(),
  laborHourRate: z.number().min(0).nullish(),
  setupCostPerUnit: z.number().min(0).nullish(),
  costPerUnit: z.number().min(0).nullish(),
  vaClass: vsmVaClassSchema.nullish(),
  variantTags: z.array(z.string()).nullish(),
  qafSource: vsmQafSourceSchema.nullish(),
  fieldStatus: z.record(z.string(), vsmFieldStatusSchema).nullish(),
  // Wertstrom P2 (A8/A12) — see lib/vsm-types.ts VsmNode for field docs.
  inventoryKind: vsmInventoryKindSchema.nullish(),
  inventoryMaxQuantity: z.number().min(0).nullish(),
  provenance: vsmProvenanceSchema.nullish(),
  // Wertstrom P4 (B3/B4) — see lib/vsm-types.ts VsmNode for field docs.
  availabilityPct: z.number().min(0).max(100).nullish(),
  mtbfMin: z.number().min(0).nullish(),
  mttrMin: z.number().min(0).nullish(),
  transportFrequency: z.string().nullish(),
  // Wertstrom P8.1 (A18, §15.5) — see lib/vsm-types.ts VsmNode for field
  // docs. measureRefs entries are workshop_actions.id (uuid) strings, but
  // kept as a plain non-empty string (not uuidSchema) — same posture `id`
  // at the top of this schema already takes, tolerant of a future
  // non-uuid id source.
  kaizenNote: z.string().nullish(),
  measureRefs: z.array(z.string().min(1)).nullish(),
  // Wertstrom P8.2a (Baustein 2) — see lib/vsm-types.ts VsmNode for field
  // docs. `z.literal(true)` (not `z.boolean()`): the field is deliberately
  // `true`-only at the TYPE level (never an explicit `false`) — this is a
  // BRAND NEW field with no pre-existing saved data, so (unlike an older
  // field) there is no legacy-payload-tolerance concern in being strict here.
  isPps: z.literal(true).nullish(),
}).passthrough()
export type VsmNodeBody = z.infer<typeof VsmNodeSchema>

/**
 * VsmConnection (lib/vsm-types.ts). transportTimeSec/batchSize >= 0.
 * Referential integrity (fromNodeId/toNodeId must resolve within the SAME
 * payload) is enforced by UpdateValueStreamMapBody's own `.refine()` below,
 * not here — a lone connection has no sibling nodes array to check against.
 */
export const VsmConnectionSchema = z
  .object({
    id: z.string().min(1),
    fromNodeId: z.string().min(1),
    toNodeId: z.string().min(1),
    label: z.string().nullish(),
    transportTimeSec: z.number().min(0).nullish(),
    batchSize: z.number().min(0).nullish(),
    // Wertstrom P2 (A7) — see lib/vsm-types.ts VsmConnection for field docs.
    kind: vsmConnectionKindSchema.nullish(),
    frequency: z.string().nullish(),
    // Wertstrom P8.2a (Baustein 3) — see lib/vsm-types.ts VsmConnection for
    // field docs. Cross-field validity (materialFlow-only) is enforced by
    // the `.refine()` below, not here.
    flowControl: vsmFlowControlSchema.nullish(),
  })
  .passthrough()
  .refine((c) => !c.flowControl || c.kind !== 'information', {
    message: 'flowControl ist nur an materialFlow-Kanten zulässig (kind !== "information").',
    path: ['flowControl'],
  })
export type VsmConnectionBody = z.infer<typeof VsmConnectionSchema>

const VsmViewportSchema = z.object({
  x: z.number(),
  y: z.number(),
  zoom: z.number(),
}).passthrough()

/**
 * Wertstrom P4 (B2, Capability-Matrix, KAR-878/KAR-986) — the "einfaches
 * Arbeitszeitmodell" (§8.2 Standard input), persisted in `layout.shiftModel`
 * rather than a new DB column (Nicht-Scope: no migration) — same jsonb-bucket
 * approach `viewport` already established. Mirrors `lib/vsm-engine`'s own
 * `ShiftModelInput` shape exactly (see lib/vsm-engine/internal/work-time.ts)
 * so no adapter is needed between what the editor persists and what
 * `computeNetAvailableTimePerDay`/`computeTimelineLadder` consume.
 * `.min(0)` for non-negativity, same boundary-inclusive convention the rest
 * of this schema uses — a genuinely INCOMPLETE shift model (e.g. breaks
 * consuming the whole shift) is still the ENGINE's "nicht berechenbar" job
 * to surface, not a 400 at the API boundary.
 *
 * Review-Fix F9 (adversarial review, PR #355): `.max()` added on every
 * field — a POSITIVE but implausible value (e.g. `hoursPerShift: 80`, a
 * plausible typo for `8`) used to sail through both this schema AND
 * `computeNetAvailableTimePerDay` (whose own `invalid` check only tests
 * `> 0`, never an upper bound) and silently produce a wrong-but-plausible-
 * looking Bestandszeit/Verfügbarkeit with no warning anywhere. Bounds are
 * generous absolute ceilings (a real day has 24h/1440min, a shift count
 * above 6 is not a real single-day roster), not a business-rule guess —
 * still leaves the ENGINE responsible for "makes sense together" checks
 * (e.g. Pausen ≥ Schichtzeit) the way the paragraph above describes.
 */
const VsmShiftModelSchema = z.object({
  hoursPerShift: z.number().min(0).max(24),
  shiftsPerDay: z.number().min(0).max(6),
  breakMinPerShift: z.number().min(0).max(1440),
  plannedDowntimeMinPerShift: z.number().min(0).max(1440).optional(),
}).passthrough()

/**
 * value_stream_maps.layout (jsonb) — freeform; `viewport` (Fit-View,
 * Wertstrom P0 KAR-878), `shiftModel` (Wertstrom P4, B2) and `timelineOffsetY`
 * (VSM-Standard-Visualisierung, Kais-Live-Feedback 23.07., KAR-878) are the
 * only known shapes today. Everything else passes through untouched —
 * forward-compat, and tolerant of the pre-existing `{}` DB default and of
 * other, unrelated shapes already sitting in the column (e.g. a demo seed's
 * own flat `{ zoom, panX, panY }`).
 */
export const VsmLayoutSchema = z.object({
  viewport: VsmViewportSchema.optional(),
  // Review-Fix F3+F5 (adversarial review, PR #355): `.nullable()` added to
  // `.optional()` — a client now has THREE distinct things it can send, not
  // two. Key absent (`undefined`, JSON has no `undefined` literal so this
  // can only mean the key was never in the request body) = this save
  // doesn't touch shiftModel, leave whatever is already persisted alone.
  // Explicit `null` = delete the persisted shiftModel. A shiftModel object =
  // replace it. Before this, `undefined` was the ONLY way to say "don't
  // touch it", so a client could never express "delete" — see route.ts's
  // merge logic for the three-way handling this enables.
  shiftModel: VsmShiftModelSchema.nullable().optional(),
  // Baustein 2 (Canvas-integrierte Standard-Zeitlinie, Kais-Live-Feedback
  // 23.07., KAR-878): the band's user-adjustable vertical nudge, in px
  // (world-space) — additive `layout` key, same "no migration" pattern as
  // `viewport`/`shiftModel`. Only `.optional()` (no `.nullable()` three-way
  // like shiftModel) — 0 is always a perfectly valid resting value, there is
  // no meaningful "explicit delete" distinct from "reset to 0" the way an
  // INCOMPLETE shiftModel form has (see vsm-geometry.ts
  // `resolveTimelineOffsetY`'s own doc). Key absent = this save doesn't
  // touch it, same "leave whatever is persisted alone" posture as
  // shiftModel's absent case — route.ts's merge spreads `existing.layout`
  // forward. `z.number()` (not `.finite()`) matches `VsmViewportSchema`'s
  // own plain-number fields above; a non-finite value is caught defensively
  // by `parseTimelineOffsetYFromLayout` on read, same posture as
  // `parseViewportFromLayout`'s own `Number.isFinite` re-check.
  //
  // K4-Fix (Referenzwertstrom-Fixrunde 2, KAR-878): before this, an
  // unbounded value could be pushed arbitrarily far below any viewport
  // (major finding) with nothing anywhere — client, schema, or server —
  // ever bringing it back down. Two DELIBERATELY DIFFERENT bounds, same
  // "sanity ceiling at the boundary, real semantic clamp at the merge"
  // split VsmShiftModelSchema's own Review-Fix F9 `.max()`s already
  // established for this file (generous absolute ceilings, "not a
  // business-rule guess" — still leaves something ELSE responsible for the
  // tighter, meaningful bound):
  //   - HERE: a generous ±1,000,000 sanity ceiling — rejects only a
  //     genuinely pathological/adversarial payload (an old-client "giant
  //     value" like the finding's own `100000` example is very much
  //     WITHIN this range and must NOT 400 the entire save just because of
  //     one stale UI-preference field).
  //   - route.ts's merge additionally routes the value through
  //     `resolveTimelineOffsetY` (components/wertstrom/vsm-geometry.ts)
  //     itself before persisting — THAT is the real, tight semantic clamp
  //     (its own floor/ceiling, -12/2000), applied as a graceful
  //     self-healing clamp rather than a 400, precisely so an old client
  //     resending a stale huge value (or a non-browser caller that never
  //     picked up the client-side clamp) still gets to save everything
  //     ELSE in its payload — only this one UI-preference field is
  //     silently normalized, never the reason a whole save is rejected.
  // Kept as literal numbers rather than importing vsm-geometry.ts's
  // constants, matching this file's own existing convention (zero
  // non-`zod` imports anywhere in this file).
  timelineOffsetY: z.number().min(-1_000_000).max(1_000_000).optional(),
  // Baustein 5 (Preferred Future State, KAR-878/KAR-986/KAR-987, P8.2b):
  // §24 "A preferred future state can be selected". Lebt am layout des
  // PARENT-Wertstroms (scenario_kind === 'current'), NICHT am Szenario
  // selbst — verweist per uuid auf eines seiner Kind-Szenarien
  // (parent_value_stream_id). Genau EINES pro Parent: das Feld ist ein
  // einzelner Skalar (kein Array), ein neues Markieren überschreibt das
  // alte automatisch, keine zusätzliche Logik nötig.
  // Drei-Wege-Unterscheidung wie shiftModel oben (Brief: "umsetzbar/
  // entfernbar"): Key fehlt = unangetastet lassen, explizites `null` =
  // Markierung entfernen, ein uuid-Wert = setzen/ersetzen. KEINE
  // referentielle Prüfung hier (dass die id tatsächlich ein Kind-Szenario
  // dieses Parents ist) — der einzige Caller (vsm-list-client.tsx) sendet
  // sie ausschließlich aus einem bereits geladenen Kind-Szenario-Kontext,
  // s. Abschlussbericht für die bewusste Grenze.
  preferredScenarioId: uuidSchema.nullable().optional(),
}).passthrough()

// Wertstrom P2 (A9): `parent_value_stream_id`/`scenario_kind` are deliberately
// NOT client-settable fields on either body below — POST /api/wertstrom
// always creates a top-level map (`scenario_kind: 'current'`, the DB
// default; `parent_value_stream_id: null`), and PUT never changes either
// column. Only POST /api/wertstrom/[id]/duplicate sets them, hardcoded
// server-side from the source row's id — never trusted from request input.

export const CreateValueStreamMapBody = z.object({
  title: z.string().trim().min(1),
  description: z.string().nullish(),
  project_id: uuidSchema.nullish(),
}).passthrough()
export type CreateValueStreamMapBody = z.infer<typeof CreateValueStreamMapBody>

/**
 * PUT /api/wertstrom/[id] body. Referential integrity: every
 * connection.fromNodeId/toNodeId must resolve to a node.id in the SAME
 * payload — only checked when `nodes` is present (a PUT that omits `nodes`
 * entirely because it isn't touching the graph at all — e.g. a title-only
 * rename — has no payload-local id set to check against, and is left
 * as-is). A `connections` array WITHOUT `nodes` is rejected outright (see
 * Review-Fix F5 below) rather than silently skipped — the old "only check
 * when both are present" wording let a connections-only payload bypass the
 * check entirely.
 *
 * Optimistic concurrency (Review-Fix F6): this body used to also carry an
 * `expectedUpdatedAt` field for a hand-rolled lock. That field was
 * unreleased (introduced in this same PR, never shipped) and has been
 * removed — the route now uses the repo's existing If-Match/guardIfMatch
 * header convention (lib/api/concurrency.ts), the same one
 * app/api/v1/assignments/[id]/route.ts already uses.
 */
export const UpdateValueStreamMapBody = z.object({
  title: z.string().trim().min(1).optional(),
  description: z.string().nullish(),
  project_id: uuidSchema.nullish(),
  nodes: z.array(VsmNodeSchema).optional(),
  connections: z.array(VsmConnectionSchema).optional(),
  layout: VsmLayoutSchema.optional(),
})
  .passthrough()
  .superRefine((body, ctx) => {
    // Review-Fix F5: connections-only payload (no nodes at all) used to
    // skip the referential check entirely — nothing to resolve fromNodeId/
    // toNodeId against, but that's a reason to REJECT the payload, not to
    // wave it through. The editor always sends both together; this only
    // ever fires for a hand-crafted/buggy client payload.
    if (body.connections !== undefined && body.nodes === undefined) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'connections requires nodes',
        path: ['connections'],
      })
      return
    }
    if (!body.nodes || !body.connections) return
    const nodeIds = new Set(body.nodes.map((n) => n.id))
    const hasDangling = body.connections.some((c) => !nodeIds.has(c.fromNodeId) || !nodeIds.has(c.toNodeId))
    if (hasDangling) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'Eine Verbindung verweist auf einen Knoten, der nicht im selben Speichervorgang enthalten ist.',
        path: ['connections'],
      })
    }
  })
export type UpdateValueStreamMapBody = z.infer<typeof UpdateValueStreamMapBody>

