Aller au contenu

Mutations

The CRUD resolver automatically exposes mutation endpoints for create, update, delete, and upsert operations. All mutations include built-in authorization, validation, and context filtering.

This guide will help you understand:

  • ✅ Available mutation operations
  • ✅ How to create, update, and delete records
  • ✅ How to use upsert operations
  • ✅ How to handle translations in mutations
  • ✅ How to use custom input DTOs

The CRUD resolver automatically exposes mutation endpoints. The endpoint names are derived from the name provided to @ObjectType or the class name.

The following examples are based on this ProductDTO:

import { EnhancedField, IDField } from '@lib/type-graphql-query';
import { ObjectType, ID, GraphQLISODateTime, Field } from '@lib/type-graphql';
@ObjectType('Product')
export class ProductDTO {
@IDField(() => ID)
id!: string;
@EnhancedField()
name!: string;
@EnhancedField()
price!: number;
@Field(() => GraphQLISODateTime)
created!: Date;
@Field(() => GraphQLISODateTime)
updated!: Date;
}

For the Product DTO, the following mutations are automatically generated:

Mutation Description Use Case
createOneProduct Create a single record Single record creation
createManyProducts Create multiple records Bulk creation
updateOneProduct Update a single record by ID Single record update
updateManyProducts Update multiple records with filter Bulk update
upsertOneProduct Create or update a single record Idempotent operations
upsertManyProducts Create or update multiple records Bulk upsert
deleteOneProduct Delete one record by ID Single record deletion
deleteManyProducts Delete multiple records with filter Bulk deletion

Create a single product:

GraphQL:

mutation {
createOneProduct(input: { product: { name: "Premium Eggs", price: 15.99 } }) {
id
name
price
created
updated
}
}

Response:

{
"data": {
"createOneProduct": {
"id": "1",
"name": "Premium Eggs",
"price": 15.99,
"created": "2020-01-14T09:01:35.834Z",
"updated": "2020-01-14T09:01:35.834Z"
}
}
}

Create multiple products at once:

GraphQL:

mutation {
createManyProducts(
input: {
products: [{ name: "Organic Eggs", price: 18.99 }, { name: "Free Range Eggs", price: 20.99 }]
}
) {
id
name
price
created
updated
}
}

Response:

{
"data": {
"createManyProducts": [
{
"id": "2",
"name": "Organic Eggs",
"price": 18.99,
"created": "2020-01-14T09:01:55.110Z",
"updated": "2020-01-14T09:01:55.110Z"
},
{
"id": "3",
"name": "Free Range Eggs",
"price": 20.99,
"created": "2020-01-14T09:01:55.110Z",
"updated": "2020-01-14T09:01:55.110Z"
}
]
}
}

Update a single product by ID:

GraphQL:

mutation {
updateOneProduct(input: { id: "1", update: { price: 16.99 } }) {
id
name
price
created
updated
}
}

Response:

{
"data": {
"updateOneProduct": {
"id": "1",
"name": "Premium Eggs",
"price": 16.99,
"created": "2020-01-14T07:00:31.763Z",
"updated": "2020-01-14T09:02:28.167Z"
}
}
}

Update multiple products using a filter:

Important

The filter CANNOT be an empty object. This prevents accidental updating of all records.

GraphQL:

mutation {
updateManyProducts(input: { filter: { id: { in: ["1", "2"] } }, update: { price: 17.99 } }) {
updatedCount
}
}

Response:

{
"data": {
"updateManyProducts": {
"updatedCount": 2
}
}
}

Create or update a single product (useful for idempotent operations):

GraphQL:

mutation {
upsertOneProduct(input: { product: { id: "1", name: "Premium Eggs", price: 15.99 } }) {
id
name
price
created
updated
}
}

Response:

{
"data": {
"upsertOneProduct": {
"id": "1",
"name": "Premium Eggs",
"price": 15.99,
"created": "2020-01-14T07:00:31.763Z",
"updated": "2020-01-14T09:03:15.234Z"
}
}
}

Create or update multiple products:

GraphQL:

mutation {
upsertManyProducts(
input: {
products: [
{ id: "1", name: "Premium Eggs", price: 15.99 }
{ id: "2", name: "Organic Eggs", price: 18.99 }
]
}
) {
id
name
price
created
updated
}
}

When using upsertOne mutations, you can automatically manage related records. The system supports two types of relations:

  • “One” relations (many-to-one, one-to-one): Stored via foreign key in main table
  • “Many” relations (one-to-many, many-to-many): Stored in separate relation table

GraphQL:

mutation {
upsertOneOrganization(
input: {
organization: {
id: "org-123"
name: "My Organization"
logo: { id: "file-456" }
memberships: [{ userId: "user-1", type: OWNER }, { userId: "user-2", type: MEMBER }]
}
}
) {
id
name
logo {
id
name
}
memberships {
userId
type
}
}
}

Behavior:

  • Creates or updates the organization
  • Sets the logo (one relation) by updating logoFileId
  • Creates, updates, or deletes memberships (many relation) based on the input

Relation management is configured using decorator options on relation fields:

@ObjectType()
@Relation('logo', () => File, {
nullable: true,
foreignKeyFieldName: 'logoFileId', // Optional: auto-detected
})
@UnPagedRelation('memberships', () => OrganizationMembership, {
nullable: true,
preserveExisting: false, // If true, preserves existing records not in input
conflictColumns: ['userId', 'organizationId'], // Optional: auto-detected
})
export class Organization {
// ...
}

Learn more: Managing Relations in Mutations

Delete a single product by ID:

GraphQL:

mutation {
deleteOneProduct(input: { id: "1" }) {
id
name
price
created
updated
}
}

Delete multiple products using a filter:

GraphQL:

mutation {
deleteManyProducts(input: { filter: { name: { like: "Premium%" } } }) {
deletedCount
}
}

Response:

{
"data": {
"deleteManyProducts": {
"deletedCount": 2
}
}
}

The @ContextFilter() decorator automatically applies filters to mutations, ensuring users can only create, update, or delete records they have access to.

Learn more: ContextFilter Documentation

When creating or updating records with localizable fields, you can provide translations using the translations field.

import { IDField, TranslationInputType } from '@lib/type-graphql-query';
import { ObjectType, ID, InputType, Field } from '@lib/type-graphql';
import { EnhancedField } from '@lib/type-graphql-query';
import { Translation } from '@lib/type-graphql-query';
@ObjectType('Product')
export class Product {
@IDField(() => ID)
id!: string;
@EnhancedField({ localizable: true })
name!: string;
@EnhancedField({ localizable: true })
description!: string;
@EnhancedField()
price!: number;
}
// Get the translation input type automatically generated from Product
const ProductTranslationInput = TranslationInputType(Product);
@InputType()
export class ProductInput {
@Field({ nullable: true })
name?: string;
@Field({ nullable: true })
description?: string;
@Field({ nullable: true })
price?: number;
@Field(() => [ProductTranslationInput], { nullable: true, defaultValue: [] })
translations?: Translation<Product, 'name' | 'description'>[];
}

GraphQL:

mutation {
createOneProduct(
input: {
product: {
price: 15.99
translations: [
{ locale: "fr", name: "Œufs Premium", description: "Œufs de haute qualité" }
{ locale: "en", name: "Premium Eggs", description: "High quality eggs" }
{ locale: "mg", name: "Atody Premium", description: "Atody kalitao avo lenta" }
]
}
}
) {
id
name
description
price
}
}

All mutations accept an optional locale parameter that specifies the default locale:

GraphQL:

mutation {
createOneProduct(input: { product: { name: "Premium Eggs", price: 15.99 } }, locale: "fr") {
id
name
price
}
}

When locale is provided, it will be used as the default locale for any localizable fields that don’t have explicit translations.

You can specify custom input DTOs for create and update operations:

import { EnhancedField, IDField } from '@lib/type-graphql-query';
import { ObjectType, ID, InputType, Field } from '@lib/type-graphql';
import { Translation } from '@lib/type-graphql-query';
@ObjectType('Product')
export class ProductDTO {
@IDField(() => ID)
id!: string;
@EnhancedField()
name!: string;
@EnhancedField()
price!: number;
}
// Get the translation input type
const ProductTranslationInput = TranslationInputType(Product);
@InputType()
export class CreateProductInput {
@Field()
name!: string;
@Field()
price!: number;
@Field(() => [ProductTranslationInput], { nullable: true, defaultValue: [] })
translations?: Translation<Product, 'name' | 'description'>[];
}
@InputType()
export class UpdateProductInput {
@Field({ nullable: true })
name?: string;
@Field({ nullable: true })
price?: number;
@Field(() => [ProductTranslationInput], { nullable: true, defaultValue: [] })
translations?: Translation<Product, 'name' | 'description'>[];
}

Then specify these in your resolver options:

import { buildTypeDefsAndResolvers } from '@lib/type-graphql-query';
import { Product } from './dto/product.type';
import { CreateProductInput } from './dto/create-product.input';
import { UpdateProductInput } from './dto/update-product.input';
import { product as productTable } from '@lib/db';
const { resolvers, typeDefs } = await buildTypeDefsAndResolvers({
resolvers: [
{
DTOClass: Product,
EntityClass: productTable,
CreateDTOClass: CreateProductInput,
UpdateDTOClass: UpdateProductInput,
},
],
});
Operation Use Case Documentation
createOne Create a single record This guide
createMany Create multiple records This guide
updateOne Update a single record This guide
updateMany Update multiple records This guide
upsertOne Create or update This guide
upsertMany Bulk upsert This guide
deleteOne Delete a single record This guide
deleteMany Delete multiple records This guide

Continue learning:

  1. 🎯 ContextFilter - Automatic filtering for queries and mutations
  2. 🔍 Queries - Query operations
  3. 🔐 Authorization - Authorization and permissions
  4. 🌍 Translations - Multi-language support