nyxcore-systems
5 min read

Prisma's Nested Connect: Unlocking Our Hetzner Migration Action Plan (and a Crucial Gotcha)

Diving back into our Hetzner migration, I hit a classic Prisma 'gotcha' when creating action points. Learn how a subtle syntax difference in nested `connect` operations can make or break your database operations.

PrismaTypeScriptDatabase MigrationHetznerDeveloper ExperienceORMPostgreSQL

It’s always satisfying to check off a critical task, especially one that unblocks a much larger initiative. This past session was all about getting our ducks in a row for the upcoming Hetzner migration. Specifically, I tackled the task of programmatically generating a comprehensive set of action points, linked to our main "Migrate to Hetzner" note, within our project management system.

What seemed like a straightforward script quickly revealed a subtle, yet potent, Prisma connect gotcha. Let's dive in.

The Mission: Formalizing Our Hetzner Migration

Our goal is to move our core services to Hetzner. This isn't a small feat; it touches almost every part of our infrastructure and application. To break down this elephant, we needed a clear, actionable list of tasks. My objective was to create a one-off script that would:

  1. Identify the main "Core: Migrate to Hetzner" note.
  2. Generate 10 specific action points covering critical areas like secrets, CI/CD, Dockerization, database migration, multi-tenant routing, and more.
  3. Link each of these new action points back to the main migration note.
  4. Ensure they were correctly associated with the relevant project.

This approach ensures that every aspect of the migration is tracked, assigned, and visible within our existing workflow.

The Hurdle: Prisma's Nested connect Conundrum

I started crafting scripts/create-hetzner-note.ts. The script needed to create ActionPoint records, each linked to our main Note (the Hetzner migration note) and to our Project.

My initial attempt at linking the ActionPoint to the Note looked like this:

typescript
// Inside prisma.actionPoint.create({ data: { ... } })
prisma.actionPoint.create({
  data: {
    // ... other fields
    sourceNoteId: note.id, // Attempting to use scalar FK
    project: {
      connect: { id: projectId }, // Using nested connect for project
    },
    // ...
  },
});

I've used scalar foreign keys (sourceNoteId) many times with Prisma, and it usually works fine. However, when I ran the script, Prisma threw an error:

Unknown argument 'sourceNoteId'. Did you mean 'sourceNote'?

This was perplexing at first. sourceNoteId is clearly defined in my Prisma schema as a foreign key. Why was it complaining?

The "Aha!" Moment: Prisma's Input Type XOR

After some head-scratching and a quick mental check of Prisma's documentation (and an internal note I'd made about this very issue!), I remembered the crucial detail: Prisma XORs between checked and unchecked input types.

What does that mean? When you're creating a record and you have other nested connect relations in the same create operation, Prisma expects all relation inputs to use the nested connect syntax, even for simple one-to-many relationships where a scalar foreign key might otherwise suffice.

You cannot mix a scalar foreign key (sourceNoteId: note.id) with a nested connect (project: { connect: { id: projectId } }) within the same create or update operation. It's one or the other for relations.

The Fix: Consistent Nested connect

The workaround was straightforward: change the sourceNoteId to use the consistent nested connect syntax, just like the project relation.

typescript
// Inside prisma.actionPoint.create({ data: { ... } })
prisma.actionPoint.create({
  data: {
    // ... other fields
    sourceNote: {
      connect: { id: note.id }, // Corrected: Using nested connect for sourceNote
    },
    project: {
      connect: { id: projectId }, // Consistent nested connect for project
    },
    // ...
  },
});

With this change, the script ran flawlessly! All 10 action points were created, correctly linked to the main Hetzner migration note and our project.

Lesson Learned: Prisma Relation Input Consistency

This is a classic "gotcha" that I've encountered before and even documented internally. The key takeaway:

When working with Prisma create or update operations, if you use any nested connect (or create) syntax for a relation, you must use nested connect (or create) for all other relations within that same data object, even if a scalar foreign key would otherwise be valid.

It forces consistency in how you interact with relational data in complex mutations, which can prevent ambiguity but definitely trips you up if you're not aware of it.

Victory Lap: Action Points & Cleanup

With the Prisma syntax resolved, the script successfully generated the following action points, all linked to our "Core: Migrate to Hetzner" note:

  • Secrets Management Strategy (Hetzner)
  • CI/CD Pipeline Adaptation (Hetzner)
  • Docker Image Optimization & Registry (Hetzner)
  • Database Migration Plan & Execution (Hetzner)
  • Multi-Tenant Routing & DNS Setup (Hetzner)
  • NextAuth Audit for Hetzner Environment
  • RLS Verification & Policy Application (Hetzner)
  • Logging & Metrics Integration (Hetzner)
  • Graceful Shutdown Implementation (Hetzner)
  • Environment Variable Validation (Hetzner)

This comprehensive list now gives us a solid roadmap. With the one-off script having served its purpose, I promptly deleted scripts/create-hetzner-note.ts. Clean code is happy code!

What's Next?

With the Hetzner migration now properly tracked, our immediate focus shifts to:

  1. Applying RLS Policies: We still need to run psql -f prisma/rls.sql to apply the persona_evaluations Row-Level Security policy to our database. This is a critical security step.
  2. AI Hardening: Our Cael persona needs its system prompt hardened against jailbreak attacks. We've seen some concerning scores (attack_1 scored 29, attack_2 scored 10) that need immediate attention.
  3. Performance Benchmarking: Running temperature and degradation tests on Cael, and then benchmarks on other personas (Lee, Morgan, Sage), to populate our trend charts and ensure consistent performance.
  4. Beginning Hetzner Migration Work: Now that the plan is in place, it's time to start executing on those action points!

It was a productive session, transforming a high-level goal into concrete, trackable tasks, and learning a valuable Prisma lesson along the way. Onwards to the migration!


json
{
  "thingsDone": [
    "Fixed Prisma syntax error for nested connect relations",
    "Created 10 Hetzner migration action points in the database",
    "Linked all action points to the main 'Core: Migrate to Hetzner' note",
    "Deleted the one-off script 'scripts/create-hetzner-note.ts'"
  ],
  "pains": [
    "Encountered 'Unknown argument 'sourceNoteId'. Did you mean 'sourceNote'?' error when mixing scalar FKs with nested connect syntax in Prisma create operations."
  ],
  "successes": [
    "Successfully identified and implemented the required consistent nested connect syntax for all relations.",
    "Achieved full programmatic creation of migration action points, unblocking the Hetzner migration planning.",
    "Cleaned up temporary script, maintaining a lean codebase."
  ],
  "techStack": [
    "Prisma",
    "TypeScript",
    "PostgreSQL",
    "Hetzner",
    "Docker",
    "NextAuth",
    "CI/CD"
  ]
}