Reference

Arkos Prisma Input

Available since v1.5.0-beta

A TypeScript utility type that simplifies Prisma relation operations by flattening nested create, connect, update, and delete operations into an intuitive array-based format with automatic operation detection.

Overview

When working with Prisma relations, you typically need to write verbose nested objects:

// Prisma's default way
const user: Prisma.UserCreateInput = {
  name: "John",
  posts: {
    create: [{ title: "Post 1" }],
    connect: [{ id: 1 }],
    update: [
      { 
        where: { id: 2 }, 
        data: { title: "Updated" } 
      }
    ]
  }
};

ArkosPrismaInput<T> transforms this into a simpler, more intuitive format:

import { ArkosPrismaInput } from "arkos/prisma";
import { Prisma } from "@prisma/client";

// Arkos way - cleaner and more intuitive
const user: ArkosPrismaInput<Prisma.UserCreateInput> = {
  name: "John",
  posts: [
    { title: "Post 1" },              // auto-detects: create
    { id: 1 },                         // auto-detects: connect
    { id: 2, title: "Updated" },      // auto-detects: update
    { id: 3, apiAction: "delete" }    // explicit: delete
  ]
};

Import

import { ArkosPrismaInput } from "arkos/prisma";

Type Signature

type ArkosPrismaInput<T> = FlattenRelations<T>;

Where T is any Prisma input type (e.g., Prisma.UserCreateInput, Prisma.PostUpdateInput).

How It Works

Automatic Operation Detection

The utility type analyzes the fields present in each relation object to determine the operation:

Fields PresentDetected OperationExample
Only create fieldscreate{ title: "New Post" }
Only unique identifiersconnect{ id: 1 }
Unique ID + data fieldsupdate{ id: 1, title: "Updated" }
apiAction: "delete"delete{ id: 1, apiAction: "delete" }

Explicit Operation Control

For ambiguous cases, use the apiAction discriminator:

const user: ArkosPrismaInput<Prisma.UserCreateInput> = {
  posts: [
    { id: 1 },                        // Ambiguous - could be connect or update
    { id: 2, apiAction: "connect" },  // Explicit connect
    { id: 3, apiAction: "update", title: "Updated" }  // Explicit update
  ]
};

Supported Operations

The type utility supports all Prisma relation operations:

  • create - Create new related records
  • connect - Link to existing records by unique identifier
  • update - Update existing related records
  • delete - Delete related records
  • disconnect - Remove relationship without deleting
  • deleteMany - Delete multiple related records

Usage Examples

One-to-Many Relations

import { ArkosPrismaInput } from "arkos/prisma";
import { Prisma } from "@prisma/client";

type CreateUserInput = ArkosPrismaInput<Prisma.UserCreateInput>;

const newUser: CreateUserInput = {
  name: "Alice",
  email: "alice@example.com",
  posts: [
    // Create new posts
    { title: "First Post", content: "Hello World" },
    { title: "Second Post", content: "Learning Arkos" },
    
    // Connect existing posts
    { id: 10 },
    { id: 15 },
    
    // Update existing post
    { id: 20, title: "Updated Title" }
  ]
};

One-to-One Relations

type UpdateUserInput = ArkosPrismaInput<Prisma.UserUpdateInput>;

const userUpdate: UpdateUserInput = {
  name: "Alice Updated",
  profile: {
    // For singular relations, can use either format
    bio: "Software Developer",
    avatar: "avatar.jpg"
  }
  // OR use apiAction for explicit control
  profile: {
    id: 1,
    apiAction: "update",
    bio: "Updated bio"
  }
};

Nested Relations

The type works recursively for deeply nested relations:

type CreatePostInput = ArkosPrismaInput<Prisma.PostCreateInput>;

const newPost: CreatePostInput = {
  title: "My Post",
  author: {
    id: 1  // Connect to existing author
  },
  comments: [
    {
      content: "Great post!",
      author: {
        id: 2  // Nested relation - connect to commenter
      }
    },
    {
      content: "Thanks for sharing",
      author: {
        name: "Anonymous",  // Nested relation - create new user
        email: "anon@example.com"
      }
    }
  ]
};

Explicit Operations

const userUpdate: ArkosPrismaInput<Prisma.UserUpdateInput> = {
  posts: [
    // Create
    { title: "New Post", apiAction: "create" },
    
    // Connect
    { id: 1, apiAction: "connect" },
    
    // Update
    { id: 2, title: "Updated", apiAction: "update" },
    
    // Delete
    { id: 3, apiAction: "delete" },
    
    // Disconnect (remove relation without deleting)
    { id: 4, apiAction: "disconnect" },
  ]
};

Integration with Arkos

With Interceptor Middlewares

Perfect for type-safe request body manipulation:

import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos";
import { Prisma } from "@prisma/client";
import { ArkosPrismaInput } from "arkos/prisma";

type CreateUserBody = ArkosPrismaInput<Prisma.UserCreateInput>;

export const beforeCreateOne = [
  async (
    req: ArkosRequest<any, any, CreateUserBody>,
    res: ArkosResponse,
    next: ArkosNextFunction
  ) => {
    // Type-safe access to flattened relations
    if (!req.body.profile) {
      req.body.profile = {
        bio: "New user",
        isPublic: true
      };
    }
    
    // Ensure all posts are created with draft status
    if (req.body.posts) {
      req.body.posts = req.body.posts.map(post => ({
        ...post,
        status: "draft"
      }));
    }
    
    next();
  }
];

With Validation Schemas

Combine with Zod or class-validator for complete type safety:

import z from "zod";
import { ArkosPrismaInput } from "arkos/prisma";
import { Prisma } from "@prisma/client";

// Define Zod schema
const CreateUserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  posts: z.array(z.object({
    title: z.string(),
    content: z.string(),
    apiAction: z.enum(["create", "connect", "update"]).optional()
  })).optional()
});

// Merge with Prisma types
type CreateUserInput = z.infer<typeof CreateUserSchema> & 
  ArkosPrismaInput<Prisma.UserCreateInput>;

// Use in interceptor
export const addDefaults = async (
  req: ArkosRequest<any, any, CreateUserInput>,
  res: ArkosResponse,
  next: ArkosNextFunction
) => {
  // Full type safety from both schema and Prisma
  if (!req.body.posts) {
    req.body.posts = [];
  }
  next();
};

With Custom Controllers

import { ArkosRequest, ArkosResponse } from "arkos";
import { Prisma } from "@prisma/client";
import { ArkosPrismaInput } from "arkos/prisma";
import userService from "./user.service";

type CreateUserBody = ArkosPrismaInput<Prisma.UserCreateInput>;

class UserController {
  async createWithPosts(
    req: ArkosRequest<any, any, CreateUserBody>,
    res: ArkosResponse
  ) {
    const { name, email, posts } = req.body;
    
    // Type-safe body access
    const user = await userService.createOne({
      name,
      email,
      posts  // Arkos auto-handles the flattened format
    });
    
    res.json(user);
  }
}

Automatic Relation Handling

This type utility pairs perfectly with Arkos's built-in relation handling that's been available since the beginning. When you use ArkosPrismaInput with Arkos services, the framework automatically converts the flattened format into proper Prisma operations.

// In your controller or interceptor
const userData: ArkosPrismaInput<Prisma.UserCreateInput> = {
  name: "John",
  posts: [
    { title: "Post 1" },      // Arkos converts to: { create: { title: "Post 1" } }
    { id: 1 }                  // Arkos converts to: { connect: { id: 1 } }
  ]
};

// BaseService automatically handles the conversion
await userService.createOne(userData);

For more details on how Arkos handles relations, see Handling Prisma Relation Fields.

Type Safety Features

Mutual Exclusivity for Singular Relations

For one-to-one relations, the type enforces mutual exclusivity between flattened and Prisma formats:

type UpdateUserInput = ArkosPrismaInput<Prisma.UserUpdateInput>;

const update: UpdateUserInput = {
  profile: {
    bio: "New bio"  // ✅ Flattened format
  }
};

// OR

const update2: UpdateUserInput = {
  profile: {
    update: {       // ✅ Prisma format
      where: { id: 1 },
      data: { bio: "New bio" }
    }
  }
};

// But NOT mixed
const invalid: UpdateUserInput = {
  profile: {
    bio: "New bio",  // ❌ Can't mix both formats
    update: { ... }
  }
};

Non-Relation Fields Preserved

The type utility only affects relation fields - scalar fields remain unchanged:

type CreateUserInput = ArkosPrismaInput<Prisma.UserCreateInput>;

const user: CreateUserInput = {
  // Scalar fields - unchanged
  name: "John",
  email: "john@example.com",
  age: 30,
  
  // Relation fields - flattened
  posts: [
    { title: "Post 1" }
  ]
};

Advanced Usage

Dynamic Operation Types

Use TypeScript discriminated unions for runtime type narrowing:

type PostOperation = 
  | { action: "create"; title: string; content: string }
  | { action: "connect"; id: number }
  | { action: "update"; id: number; title?: string }
  | { action: "delete"; id: number };

const createUser = (operations: PostOperation[]) => {
  const userData: ArkosPrismaInput<Prisma.UserCreateInput> = {
    name: "User",
    posts: operations.map(op => {
      switch (op.action) {
        case "create":
          return { title: op.title, content: op.content };
        case "connect":
          return { id: op.id };
        case "update":
          return { id: op.id, title: op.title, apiAction: "update" };
        case "delete":
          return { id: op.id, apiAction: "delete" };
      }
    })
  };
  
  return userService.createOne(userData);
};

Type Guards

Create type guards for safer runtime checks:

function isCreateOperation(rel: any): rel is { title: string } {
  return 'title' in rel && !('id' in rel);
}

function isConnectOperation(rel: any): rel is { id: number } {
  return 'id' in rel && !('title' in rel) && !('apiAction' in rel);
}

function isUpdateOperation(rel: any): rel is { id: number; title?: string; apiAction: "update" } {
  return 'id' in rel && ('title' in rel || rel.apiAction === 'update');
}

// Use in your code
const processRelation = (rel: any) => {
  if (isCreateOperation(rel)) {
    console.log("Creating:", rel.title);
  } else if (isConnectOperation(rel)) {
    console.log("Connecting:", rel.id);
  } else if (isUpdateOperation(rel)) {
    console.log("Updating:", rel.id);
  }
};

Limitations

  1. No Runtime Transformation: This is a type-only utility. The actual runtime transformation is handled by Arkos's relation handling system when using BaseService methods.

  2. Requires Arkos Services: For the flattened format to work, you must use Arkos's BaseService or custom services that extend it. Direct Prisma Client calls require traditional Prisma format.

  3. Ambiguous Cases: When an object could be either connect or update, explicitly use apiAction to disambiguate.

Best Practices

  1. Use with Interceptors: Combine with interceptor middlewares for type-safe request manipulation
  2. Explicit Actions: When in doubt, use apiAction for clarity
  3. Validation Integration: Pair with Zod/class-validator schemas for complete type safety
  4. Leverage Auto-Detection: Let the type utility auto-detect operations when possible
  5. Document Complex Relations: Add comments for complex nested relation operations