Mutations
Mutations modify data on the server. Zeus provides full type safety for all mutation operations.
Basic Mutation
import { Chain } from './zeus';
const chain = Chain('https://api.com/graphql');
const result = await chain('mutation')({
createUser: [
{
input: {
name: 'Zeus',
email: 'zeus@olympus.com',
},
},
{
id: true,
name: true,
email: true,
},
],
});
console.log('Created user:', result.createUser.id);Mutation Syntax
Mutations use the same syntax as queries:
await chain('mutation')({
mutationName: [
{
/* input arguments */
},
{
/* return fields */
},
],
});Create Operations
Simple Create
const result = await chain('mutation')({
createPost: [
{
input: {
title: 'GraphQL Zeus',
content: 'Type-safe GraphQL client',
authorId: '123',
},
},
{
id: true,
title: true,
createdAt: true,
},
],
});Create with Nested Data
const result = await chain('mutation')({
createUser: [
{
input: {
name: 'Zeus',
email: 'zeus@olympus.com',
profile: {
bio: 'King of the Gods',
location: 'Mount Olympus',
},
posts: [
{
title: 'First Post',
content: 'Hello World',
},
{
title: 'Second Post',
content: 'GraphQL is awesome',
},
],
},
},
{
id: true,
name: true,
profile: {
bio: true,
location: true,
},
posts: {
id: true,
title: true,
},
},
],
});Create Multiple
const result = await chain('mutation')({
createUsers: [
{
input: [
{ name: 'Zeus', email: 'zeus@olympus.com' },
{ name: 'Athena', email: 'athena@olympus.com' },
{ name: 'Apollo', email: 'apollo@olympus.com' },
],
},
{
count: true,
users: {
id: true,
name: true,
},
},
],
});
console.log(`Created ${result.createUsers.count} users`);Update Operations
Update by ID
const result = await chain('mutation')({
updateUser: [
{
id: '123',
input: {
name: 'Zeus Updated',
email: 'zeus.new@olympus.com',
},
},
{
id: true,
name: true,
email: true,
updatedAt: true,
},
],
});Partial Update
// Only update specific fields
const result = await chain('mutation')({
updateUser: [
{
id: '123',
input: {
name: 'Zeus', // Only updating name
},
},
{
id: true,
name: true,
},
],
});Update with Where Clause
const result = await chain('mutation')({
updateUsers: [
{
where: {
role: 'USER',
active: false,
},
input: {
active: true,
},
},
{
count: true,
users: {
id: true,
active: true,
},
},
],
});
console.log(`Updated ${result.updateUsers.count} users`);Upsert
Create if doesn’t exist, update if exists:
const result = await chain('mutation')({
upsertUser: [
{
where: {
email: 'zeus@olympus.com',
},
create: {
name: 'Zeus',
email: 'zeus@olympus.com',
role: 'ADMIN',
},
update: {
lastLogin: new Date().toISOString(),
},
},
{
id: true,
name: true,
email: true,
},
],
});Delete Operations
Delete by ID
const result = await chain('mutation')({
deleteUser: [
{
id: '123',
},
{
id: true,
name: true,
},
],
});
console.log('Deleted user:', result.deleteUser.name);Delete with Where Clause
const result = await chain('mutation')({
deleteUsers: [
{
where: {
active: false,
createdAt_lt: '2023-01-01',
},
},
{
count: true,
},
],
});
console.log(`Deleted ${result.deleteUsers.count} inactive users`);Soft Delete
// Mark as deleted instead of removing
const result = await chain('mutation')({
updateUser: [
{
id: '123',
input: {
deletedAt: new Date().toISOString(),
},
},
{
id: true,
deletedAt: true,
},
],
});Nested Mutations
Update Nested Relations
const result = await chain('mutation')({
updateUser: [
{
id: '123',
input: {
name: 'Zeus',
profile: {
update: {
bio: 'Updated bio',
},
},
posts: {
create: [
{
title: 'New Post',
content: 'Content here',
},
],
update: [
{
where: { id: 'post-1' },
data: { title: 'Updated Title' },
},
],
delete: ['post-2', 'post-3'],
},
},
},
{
id: true,
name: true,
profile: {
bio: true,
},
posts: {
id: true,
title: true,
},
},
],
});Connect Existing Relations
const result = await chain('mutation')({
updatePost: [
{
id: 'post-1',
input: {
categories: {
connect: ['cat-1', 'cat-2'], // Link existing categories
},
tags: {
create: [{ name: 'GraphQL' }, { name: 'TypeScript' }],
},
},
},
{
id: true,
categories: {
id: true,
name: true,
},
tags: {
id: true,
name: true,
},
},
],
});Disconnect Relations
const result = await chain('mutation')({
updatePost: [
{
id: 'post-1',
input: {
categories: {
disconnect: ['cat-1'], // Remove category link
},
},
},
{
id: true,
categories: {
id: true,
name: true,
},
},
],
});Multiple Mutations
Execute multiple mutations in one request:
const result = await chain('mutation')({
createUser: [
{
input: {
name: 'Zeus',
email: 'zeus@olympus.com',
},
},
{
id: true,
name: true,
},
],
createPost: [
{
input: {
title: 'First Post',
authorId: 'will-be-replaced',
},
},
{
id: true,
title: true,
},
],
updateSettings: [
{
input: {
theme: 'dark',
},
},
{
theme: true,
},
],
});
// Access all results
console.log('Created user:', result.createUser.name);
console.log('Created post:', result.createPost.title);
console.log('Updated theme:', result.updateSettings.theme);File Uploads
Single File
const file = document.querySelector('input[type="file"]').files[0];
const result = await chain('mutation')({
uploadFile: [
{
file: file, // File object
},
{
id: true,
url: true,
filename: true,
},
],
});
console.log('Uploaded:', result.uploadFile.url);Multiple Files
const files = Array.from(document.querySelector('input[type="file"]').files);
const result = await chain('mutation')({
uploadFiles: [
{
files: files,
},
{
id: true,
url: true,
filename: true,
},
],
});
result.uploadFiles.forEach((file) => {
console.log('Uploaded:', file.url);
});With Metadata
const result = await chain('mutation')({
createPost: [
{
input: {
title: 'Post with Image',
content: 'Content here',
coverImage: file, // File upload in nested input
},
},
{
id: true,
title: true,
coverImage: {
url: true,
filename: true,
},
},
],
});Optimistic Updates
Update UI before server response:
// Optimistically update local state
const optimisticUser = {
id: 'temp-id',
name: newName,
email: currentEmail,
};
setUser(optimisticUser); // Update UI immediately
try {
// Send mutation
const result = await chain('mutation')({
updateUser: [
{
id: userId,
input: { name: newName },
},
{
id: true,
name: true,
email: true,
},
],
});
setUser(result.updateUser); // Update with real data
} catch (error) {
setUser(previousUser); // Rollback on error
console.error('Update failed:', error);
}Batching Mutations
Sequential Mutations
Execute mutations one after another:
async function createUserWithPosts(userData: any, posts: any[]) {
// 1. Create user
const userResult = await chain('mutation')({
createUser: [{ input: userData }, { id: true, name: true }],
});
const userId = userResult.createUser.id;
// 2. Create posts for user
const postsResult = await chain('mutation')({
createPosts: [
{
input: posts.map((post) => ({
...post,
authorId: userId,
})),
},
{
id: true,
title: true,
},
],
});
return {
user: userResult.createUser,
posts: postsResult.createPosts,
};
}Parallel Mutations
Execute independent mutations in parallel:
const [user, settings, preferences] = await Promise.all([
chain('mutation')({
updateUser: [
{ id: '123', input: { name: 'Zeus' } },
{ id: true, name: true },
],
}),
chain('mutation')({
updateSettings: [{ input: { theme: 'dark' } }, { theme: true }],
}),
chain('mutation')({
updatePreferences: [{ input: { notifications: true } }, { notifications: true }],
}),
]);
console.log(user.updateUser.name);
console.log(settings.updateSettings.theme);
console.log(preferences.updatePreferences.notifications);Transaction-Like Behavior
Some servers support transactions:
const result = await chain('mutation')({
transaction: [
{
operations: [
{
createUser: {
name: 'Zeus',
email: 'zeus@olympus.com',
},
},
{
createPost: {
title: 'First Post',
authorEmail: 'zeus@olympus.com',
},
},
],
},
{
success: true,
user: {
id: true,
name: true,
},
post: {
id: true,
title: true,
},
},
],
});Error Handling
Basic Error Handling
try {
const result = await chain('mutation')({
createUser: [
{
input: {
name: 'Zeus',
email: 'zeus@olympus.com',
},
},
{
id: true,
name: true,
},
],
});
console.log('Success:', result.createUser);
} catch (error) {
console.error('Mutation failed:', error);
}Validation Errors
try {
await chain('mutation')({
createUser: [
{
input: {
name: 'Ze', // Too short
email: 'invalid-email', // Invalid format
},
},
{
id: true,
},
],
});
} catch (error: any) {
if (error.message.includes('validation')) {
console.error('Validation errors:', error.message);
// Show validation errors to user
}
}Conflict Handling
try {
await chain('mutation')({
createUser: [
{
input: {
email: 'existing@email.com', // Already exists
},
},
{
id: true,
},
],
});
} catch (error: any) {
if (error.message.includes('unique constraint')) {
console.error('Email already exists');
// Suggest login or password reset
}
}Type-Safe Inputs
Zeus enforces input types:
import { InputType, GraphQLTypes } from './zeus';
type CreateUserInput = InputType<GraphQLTypes['CreateUserInput']>;
// Type-safe input object
const input: CreateUserInput = {
name: 'Zeus',
email: 'zeus@olympus.com',
age: 3000,
role: 'ADMIN', // Validated against enum
};
const result = await chain('mutation')({
createUser: [{ input }, { id: true, name: true }],
});Reusable Mutations
Function Wrapper
async function createUser(input: CreateUserInput) {
return chain('mutation')({
createUser: [
{ input },
{
id: true,
name: true,
email: true,
createdAt: true,
},
],
});
}
async function updateUser(id: string, input: UpdateUserInput) {
return chain('mutation')({
updateUser: [
{ id, input },
{
id: true,
name: true,
email: true,
updatedAt: true,
},
],
});
}
async function deleteUser(id: string) {
return chain('mutation')({
deleteUser: [
{ id },
{
id: true,
name: true,
},
],
});
}
// Usage
const newUser = await createUser({
name: 'Zeus',
email: 'zeus@olympus.com',
});
await updateUser(newUser.createUser.id, {
name: 'Zeus Updated',
});
await deleteUser(newUser.createUser.id);Best Practices
1. Return Useful Fields
Return fields you’ll need after mutation:
// ✅ Good - return updated fields
const result = await chain('mutation')({
updateUser: [
{ id: '123', input: { name: 'Zeus' } },
{
id: true,
name: true,
updatedAt: true, // Know when it was updated
version: true, // For optimistic locking
},
],
});
// ❌ Bad - only return id
const result = await chain('mutation')({
updateUser: [
{ id: '123', input: { name: 'Zeus' } },
{ id: true }, // Have to refetch to get updated data
],
});2. Handle Errors Gracefully
async function safeCreateUser(input: CreateUserInput) {
try {
return await chain('mutation')({
createUser: [{ input }, { id: true, name: true }],
});
} catch (error) {
console.error('Failed to create user:', error);
// Return null or throw custom error
return null;
}
}3. Use TypeScript Types
import { ModelTypes, InputType, GraphQLTypes } from './zeus';
type User = ModelTypes['User'];
type CreateUserInput = InputType<GraphQLTypes['CreateUserInput']>;
async function createUser(input: CreateUserInput): Promise<User> {
const result = await chain('mutation')({
createUser: [{ input }, { id: true, name: true, email: true }],
});
return result.createUser;
}Next Steps
- Variables - Dynamic mutation inputs
- Selectors - Reusable field selections
- Thunder Client - Custom fetch implementation
Last updated on